1
- import { printTree } from 'tree-dump/lib/printTree' ;
2
1
import { Cursor } from './Cursor' ;
3
- import { stringify } from '../../../json-text/stringify ' ;
4
- import { CursorAnchor , SliceBehavior , SliceHeaderMask , SliceHeaderShift } from '../slice/constants ' ;
2
+ import { Anchor } from '../rga/constants ' ;
3
+ import { formatType } from '../slice/util ' ;
5
4
import { EditorSlices } from './EditorSlices' ;
6
5
import { next , prev } from 'sonic-forest/lib/util' ;
7
- import { isLetter , isPunctuation , isWhitespace } from './util' ;
8
- import { Anchor } from '../rga/constants' ;
9
- import { MarkerOverlayPoint } from '../overlay/MarkerOverlayPoint' ;
10
- import { UndefEndIter , type UndefIterator } from '../../../util/iterator' ;
6
+ import { printTree } from 'tree-dump/lib/printTree' ;
7
+ import { createRegistry } from '../registry/registry' ;
11
8
import { PersistedSlice } from '../slice/PersistedSlice' ;
9
+ import { stringify } from '../../../json-text/stringify' ;
10
+ import { CommonSliceType , type SliceTypeSteps , type SliceType } from '../slice' ;
11
+ import { isLetter , isPunctuation , isWhitespace , stepsEqual } from './util' ;
12
12
import { ValueSyncStore } from '../../../util/events/sync-store' ;
13
- import { formatType } from '../slice/util ' ;
14
- import { CommonSliceType , type SliceType } from '../slice ' ;
13
+ import { MarkerOverlayPoint } from '../overlay/MarkerOverlayPoint ' ;
14
+ import { UndefEndIter , type UndefIterator } from '../../../util/iterator ' ;
15
15
import { tick , Timespan , type ITimespanStruct } from '../../../json-crdt-patch' ;
16
- import type { ChunkSlice } from '../util/ChunkSlice' ;
17
- import type { Peritext } from '../Peritext' ;
16
+ import { CursorAnchor , SliceBehavior , SliceHeaderMask , SliceHeaderShift , SliceTypeCon } from '../slice/constants' ;
18
17
import type { Point } from '../rga/Point' ;
19
18
import type { Range } from '../rga/Range' ;
20
- import type { CharIterator , CharPredicate , Position , TextRangeUnit , ViewStyle , ViewRange , ViewSlice } from './types' ;
21
19
import type { Printable } from 'tree-dump' ;
20
+ import type { Peritext } from '../Peritext' ;
21
+ import type { ChunkSlice } from '../util/ChunkSlice' ;
22
22
import type { MarkerSlice } from '../slice/MarkerSlice' ;
23
+ import type { SliceRegistry } from '../registry/SliceRegistry' ;
24
+ import type { CharIterator , CharPredicate , Position , TextRangeUnit , ViewStyle , ViewRange , ViewSlice } from './types' ;
23
25
24
26
/**
25
27
* For inline boolean ("Overwrite") slices, both range endpoints should be
@@ -51,6 +53,8 @@ export class Editor<T = string> implements Printable {
51
53
*/
52
54
public readonly pending = new ValueSyncStore < Map < CommonSliceType | string | number , unknown > > ( new Map ( ) ) ;
53
55
56
+ public registry : SliceRegistry = createRegistry ( ) ;
57
+
54
58
constructor ( public readonly txt : Peritext < T > ) {
55
59
this . saved = new EditorSlices ( txt , txt . savedSlices ) ;
56
60
this . extra = new EditorSlices ( txt , txt . extraSlices ) ;
@@ -590,6 +594,45 @@ export class Editor<T = string> implements Printable {
590
594
591
595
// --------------------------------------------------------------- formatting
592
596
597
+ public eraseFormatting ( store : EditorSlices < T > = this . saved ) : void {
598
+ const overlay = this . txt . overlay ;
599
+ for ( let i = this . cursors0 ( ) , cursor = i ( ) ; cursor ; cursor = i ( ) ) {
600
+ overlay . refresh ( ) ;
601
+ const contained = overlay . findContained ( cursor ) ;
602
+ for ( const slice of contained ) {
603
+ if ( slice instanceof PersistedSlice ) {
604
+ switch ( slice . behavior ) {
605
+ case SliceBehavior . One :
606
+ case SliceBehavior . Many :
607
+ case SliceBehavior . Erase :
608
+ slice . del ( ) ;
609
+ }
610
+ }
611
+ }
612
+ overlay . refresh ( ) ;
613
+ const overlapping = overlay . findOverlapping ( cursor ) ;
614
+ for ( const slice of overlapping ) {
615
+ switch ( slice . behavior ) {
616
+ case SliceBehavior . One :
617
+ case SliceBehavior . Many : {
618
+ store . insErase ( slice . type ) ;
619
+ }
620
+ }
621
+ }
622
+ }
623
+ }
624
+
625
+ public clearFormatting ( store : EditorSlices < T > = this . saved ) : void {
626
+ const overlay = this . txt . overlay ;
627
+ overlay . refresh ( ) ;
628
+ for ( let i = this . cursors0 ( ) , cursor = i ( ) ; cursor ; cursor = i ( ) ) {
629
+ const overlapping = overlay . findOverlapping ( cursor ) ;
630
+ for ( const slice of overlapping ) store . del ( slice . id ) ;
631
+ }
632
+ }
633
+
634
+ // -------------------------------------------------------- inline formatting
635
+
593
636
protected toggleRangeExclFmt (
594
637
range : Range < T > ,
595
638
type : CommonSliceType | string | number ,
@@ -645,54 +688,7 @@ export class Editor<T = string> implements Printable {
645
688
}
646
689
}
647
690
648
- public eraseFormatting ( store : EditorSlices < T > = this . saved ) : void {
649
- const overlay = this . txt . overlay ;
650
- for ( let i = this . cursors0 ( ) , cursor = i ( ) ; cursor ; cursor = i ( ) ) {
651
- overlay . refresh ( ) ;
652
- const contained = overlay . findContained ( cursor ) ;
653
- for ( const slice of contained ) {
654
- if ( slice instanceof PersistedSlice ) {
655
- switch ( slice . behavior ) {
656
- case SliceBehavior . One :
657
- case SliceBehavior . Many :
658
- case SliceBehavior . Erase :
659
- slice . del ( ) ;
660
- }
661
- }
662
- }
663
- overlay . refresh ( ) ;
664
- const overlapping = overlay . findOverlapping ( cursor ) ;
665
- for ( const slice of overlapping ) {
666
- switch ( slice . behavior ) {
667
- case SliceBehavior . One :
668
- case SliceBehavior . Many : {
669
- store . insErase ( slice . type ) ;
670
- }
671
- }
672
- }
673
- }
674
- }
675
-
676
- public clearFormatting ( store : EditorSlices < T > = this . saved ) : void {
677
- const overlay = this . txt . overlay ;
678
- overlay . refresh ( ) ;
679
- for ( let i = this . cursors0 ( ) , cursor = i ( ) ; cursor ; cursor = i ( ) ) {
680
- const overlapping = overlay . findOverlapping ( cursor ) ;
681
- for ( const slice of overlapping ) store . del ( slice . id ) ;
682
- }
683
- }
684
-
685
- public split ( type ?: SliceType , data ?: unknown , slices : EditorSlices < T > = this . saved ) : void {
686
- for ( let i = this . cursors0 ( ) , cursor = i ( ) ; cursor ; cursor = i ( ) ) {
687
- this . collapseCursor ( cursor ) ;
688
- if ( type === void 0 ) {
689
- // TODO: detect current block type
690
- type = CommonSliceType . p ;
691
- }
692
- slices . insMarker ( type , data ) ;
693
- cursor . move ( 1 ) ;
694
- }
695
- }
691
+ // --------------------------------------------------------- block formatting
696
692
697
693
/**
698
694
* Returns block split marker of the block inside which the point is located.
@@ -704,6 +700,21 @@ export class Editor<T = string> implements Printable {
704
700
return this . txt . overlay . getOrNextLowerMarker ( point ) ?. marker ;
705
701
}
706
702
703
+ /**
704
+ * Returns the block type at the given point. Block type is a nested array of
705
+ * tags, e.g. `['p']`, `['blockquote', 'p']`, `['ul', 'li', 'p']`, etc.
706
+ *
707
+ * @param point The point to get the block type at.
708
+ * @returns Current block type at the point.
709
+ */
710
+ public getBlockType ( point : Point < T > ) : [ type : SliceTypeSteps , marker ?: MarkerSlice < T > | undefined ] {
711
+ const marker = this . getMarker ( point ) ;
712
+ if ( ! marker ) return [ [ SliceTypeCon . p ] ] ;
713
+ let steps = marker ?. type ?? [ SliceTypeCon . p ] ;
714
+ if ( ! Array . isArray ( steps ) ) steps = [ steps ] ;
715
+ return [ steps , marker ] ;
716
+ }
717
+
707
718
/**
708
719
* Insert a block split at the start of the document. The start of the
709
720
* document is defined as immediately after all deleted characters starting
@@ -739,6 +750,92 @@ export class Editor<T = string> implements Printable {
739
750
return this . insStartMarker ( type ) ;
740
751
}
741
752
753
+ public getContainerPath ( steps : SliceTypeSteps ) : SliceTypeSteps {
754
+ const registry = this . registry ;
755
+ const length = steps . length ;
756
+ for ( let i = length - 1 ; i >= 0 ; i -- ) {
757
+ const step = steps [ i ] ;
758
+ const tag = Array . isArray ( step ) ? step [ 0 ] : step ;
759
+ const isContainer = registry . isContainer ( tag ) ;
760
+ if ( isContainer ) return steps . slice ( 0 , i + 1 ) ;
761
+ }
762
+ return [ ] ;
763
+ }
764
+
765
+ public getDeepestCommonContainer ( steps1 : SliceTypeSteps , steps2 : SliceTypeSteps ) : number {
766
+ const length1 = steps1 . length ;
767
+ const length2 = steps2 . length ;
768
+ const min = Math . min ( length1 , length2 ) ;
769
+ for ( let i = 0 ; i < min ; i ++ ) {
770
+ const step1 = steps1 [ i ] ;
771
+ const step2 = steps2 [ i ] ;
772
+ const tag1 = Array . isArray ( step1 ) ? step1 [ 0 ] : step1 ;
773
+ const tag2 = Array . isArray ( step2 ) ? step2 [ 0 ] : step2 ;
774
+ const disc1 = Array . isArray ( step1 ) ? step1 [ 1 ] : 0 ;
775
+ const disc2 = Array . isArray ( step2 ) ? step2 [ 1 ] : 0 ;
776
+ if ( tag1 !== tag2 || disc1 !== disc2 ) return i - 1 ;
777
+ if ( ! this . registry . isContainer ( tag1 ) ) return i - 1 ;
778
+ }
779
+ return min ;
780
+ }
781
+
782
+ /**
783
+ * @param at Point at which split block split happens.
784
+ * @param slices The slices set to use.
785
+ * @returns True if a marker was inserted, false if it was updated.
786
+ */
787
+ public splitAt ( at : Point < T > , slices : EditorSlices < T > = this . saved ) : boolean {
788
+ const [ type , marker ] = this . getBlockType ( at ) ;
789
+ const prevMarker = marker ? this . getMarker ( marker . start . copy ( ( p ) => p . halfstep ( - 1 ) ) ) : void 0 ;
790
+ if ( marker && prevMarker ) {
791
+ const rangeFromMarker = this . txt . range ( marker . start , at ) ;
792
+ const noLeadingText = rangeFromMarker . length ( ) <= 1 ;
793
+ if ( noLeadingText ) {
794
+ const markerSteps = marker . typeSteps ( ) ;
795
+ const prevMarkerSteps = prevMarker . typeSteps ( ) ;
796
+ if ( markerSteps . length > 1 ) {
797
+ const areMarkerTypesEqual = stepsEqual ( markerSteps , prevMarkerSteps ) ;
798
+ if ( areMarkerTypesEqual ) {
799
+ const i = this . getDeepestCommonContainer ( markerSteps , prevMarkerSteps ) ;
800
+ if ( i >= 0 ) {
801
+ const newType = [ ...markerSteps ] ;
802
+ const step = newType [ i ] ;
803
+ const tag = Array . isArray ( step ) ? step [ 0 ] : step ;
804
+ const disc = Array . isArray ( step ) ? step [ 1 ] : 0 ;
805
+ newType [ i ] = [ tag , ( disc + 1 ) % 8 ] ;
806
+ marker . update ( { type : newType } ) ;
807
+ return false ;
808
+ }
809
+ }
810
+ }
811
+ }
812
+ }
813
+ const containerPath = this . getContainerPath ( type ) ;
814
+ const newType = containerPath . concat ( [ CommonSliceType . p ] ) ;
815
+ slices . insMarker ( newType ) ;
816
+ return true ;
817
+ }
818
+
819
+ public split ( type ?: SliceType , data ?: unknown , slices : EditorSlices < T > = this . saved ) : void {
820
+ if ( type === void 0 ) {
821
+ for ( let i = this . cursors0 ( ) , cursor = i ( ) ; cursor ; cursor = i ( ) ) {
822
+ this . collapseCursor ( cursor ) ;
823
+ const didInsertMarker = this . splitAt ( cursor . start , slices ) ;
824
+ if ( didInsertMarker ) cursor . move ( 1 ) ;
825
+ }
826
+ } else {
827
+ for ( let i = this . cursors0 ( ) , cursor = i ( ) ; cursor ; cursor = i ( ) ) {
828
+ this . collapseCursor ( cursor ) ;
829
+ if ( type === void 0 ) {
830
+ // TODO: detect current block type
831
+ type = CommonSliceType . p ;
832
+ }
833
+ slices . insMarker ( type , data ) ;
834
+ cursor . move ( 1 ) ;
835
+ }
836
+ }
837
+ }
838
+
742
839
// ---------------------------------------------------------- export / import
743
840
744
841
public export ( range : Range < T > ) : ViewRange {
0 commit comments