@inshapardaz/likhari-react 0.1.18-dev.148 → 0.1.18-dev.152
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/blocks/PoetryNode.d.ts +11 -8
- package/dist/blocks/PoetryNode.d.ts.map +1 -1
- package/dist/blocks/PoetryNode.js +11 -8
- package/dist/blocks/poetryActions.d.ts +59 -28
- package/dist/blocks/poetryActions.d.ts.map +1 -1
- package/dist/blocks/poetryActions.js +208 -95
- package/dist/theme/editor.css +14 -3
- package/package.json +1 -1
|
@@ -7,14 +7,17 @@ export type SerializedPoetryBlockNode = Spread<{
|
|
|
7
7
|
width?: number;
|
|
8
8
|
}, SerializedElementNode>;
|
|
9
9
|
/**
|
|
10
|
-
* A
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
10
|
+
* A poetry *section*: one or more couplets (two-line verse units) sharing
|
|
11
|
+
* one layout — single-column stacks each couplet's two misras directly as
|
|
12
|
+
* paragraph children (two per couplet, in order), two-column gives each
|
|
13
|
+
* couplet its own LayoutContainerNode row (built on the columns primitive,
|
|
14
|
+
* §4.9) — see blocks/poetryActions.ts's $getCouplets for how either shape
|
|
15
|
+
* is read, and $setPoetryLayout/$exitPoetryOnEnter for how children are
|
|
16
|
+
* restructured/grown between couplets and layouts. `layout` and `align`
|
|
17
|
+
* apply to the whole section, not per couplet; both mutate in place via
|
|
18
|
+
* poetryActions so converting a section between layouts doesn't lose
|
|
19
|
+
* selection/undo coherence. One extensible primitive in place of four
|
|
20
|
+
* fixed templates, per requirements doc §4.11.
|
|
18
21
|
*/
|
|
19
22
|
export declare class PoetryBlockNode extends ElementNode {
|
|
20
23
|
__layout: PoetryLayout;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PoetryNode.d.ts","sourceRoot":"","sources":["../../src/blocks/PoetryNode.tsx"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,OAAO,EACZ,KAAK,qBAAqB,EAC1B,KAAK,MAAM,EACX,WAAW,EACZ,MAAM,SAAS,CAAC;AAEjB,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,YAAY,CAAC;AACnD,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC;AAwBjE,MAAM,MAAM,yBAAyB,GAAG,MAAM,CAAC;IAAE,MAAM,EAAE,YAAY,CAAC;IAAC,KAAK,EAAE,WAAW,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,qBAAqB,CAAC,CAAC;AAEpI
|
|
1
|
+
{"version":3,"file":"PoetryNode.d.ts","sourceRoot":"","sources":["../../src/blocks/PoetryNode.tsx"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,OAAO,EACZ,KAAK,qBAAqB,EAC1B,KAAK,MAAM,EACX,WAAW,EACZ,MAAM,SAAS,CAAC;AAEjB,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,YAAY,CAAC;AACnD,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC;AAwBjE,MAAM,MAAM,yBAAyB,GAAG,MAAM,CAAC;IAAE,MAAM,EAAE,YAAY,CAAC;IAAC,KAAK,EAAE,WAAW,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,qBAAqB,CAAC,CAAC;AAEpI;;;;;;;;;;;;GAYG;AACH,qBAAa,eAAgB,SAAQ,WAAW;IAC9C,QAAQ,EAAE,YAAY,CAAC;IACvB,OAAO,EAAE,WAAW,CAAC;IACrB;8DAC0D;IAC1D,OAAO,CAAC,EAAE,MAAM,CAAC;gBAEL,MAAM,GAAE,YAAuB,EAAE,KAAK,GAAE,WAAuB,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO;IAO1G,MAAM,CAAC,OAAO,IAAI,MAAM;IAIxB,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,eAAe,GAAG,eAAe;IAIpD,SAAS,IAAI,YAAY;IAIzB,QAAQ,IAAI,WAAW;IAIvB,QAAQ,IAAI,MAAM,GAAG,SAAS;IAI9B,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAMzC;;gEAE4D;IAC5D,kBAAkB,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI;IAM9C,QAAQ,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI;IAMlC,MAAM,CAAC,UAAU,CAAC,cAAc,EAAE,yBAAyB,GAAG,eAAe;IAI7E,UAAU,IAAI,yBAAyB;IAYvC,MAAM,CAAC,SAAS,IAAI,gBAAgB,GAAG,IAAI;IAe3C,SAAS,IAAI,eAAe;IAW5B,SAAS,CAAC,MAAM,EAAE,YAAY,GAAG,WAAW;IAa5C,SAAS,CAAC,QAAQ,EAAE,eAAe,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO;IAerF,UAAU,IAAI,KAAK;IAInB,YAAY,IAAI,OAAO;CAGxB;AAED,wBAAgB,sBAAsB,CAAC,MAAM,GAAE,YAAuB,EAAE,KAAK,GAAE,WAAuB,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,eAAe,CAEvI;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,IAAI,eAAe,CAEhG"}
|
|
@@ -22,14 +22,17 @@ function applyWidthStyle(element, width) {
|
|
|
22
22
|
element.style.maxWidth = `${width}px`;
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
|
-
* A
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
25
|
+
* A poetry *section*: one or more couplets (two-line verse units) sharing
|
|
26
|
+
* one layout — single-column stacks each couplet's two misras directly as
|
|
27
|
+
* paragraph children (two per couplet, in order), two-column gives each
|
|
28
|
+
* couplet its own LayoutContainerNode row (built on the columns primitive,
|
|
29
|
+
* §4.9) — see blocks/poetryActions.ts's $getCouplets for how either shape
|
|
30
|
+
* is read, and $setPoetryLayout/$exitPoetryOnEnter for how children are
|
|
31
|
+
* restructured/grown between couplets and layouts. `layout` and `align`
|
|
32
|
+
* apply to the whole section, not per couplet; both mutate in place via
|
|
33
|
+
* poetryActions so converting a section between layouts doesn't lose
|
|
34
|
+
* selection/undo coherence. One extensible primitive in place of four
|
|
35
|
+
* fixed templates, per requirements doc §4.11.
|
|
33
36
|
*/
|
|
34
37
|
export class PoetryBlockNode extends ElementNode {
|
|
35
38
|
constructor(layout = 'single', align = 'justify', width, key) {
|
|
@@ -1,51 +1,82 @@
|
|
|
1
1
|
import { type ParagraphNode } from 'lexical';
|
|
2
2
|
import { type PoetryAlign, type PoetryLayout, type PoetryBlockNode } from './PoetryNode.js';
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/** One couplet's two misra paragraphs, in order. */
|
|
4
|
+
export type Couplet = [ParagraphNode, ParagraphNode];
|
|
5
|
+
/**
|
|
6
|
+
* A poetry block is a *section* of one or more couplets sharing one layout
|
|
7
|
+
* (single-column stacks each couplet's two misras directly as paragraph
|
|
8
|
+
* children, two deep per couplet; two-column gives each couplet its own
|
|
9
|
+
* LayoutContainerNode row). This walks that structure and returns each
|
|
10
|
+
* couplet as a pair, in document order — the one place that knows how to
|
|
11
|
+
* read either shape, so nothing else needs to.
|
|
12
|
+
*/
|
|
13
|
+
export declare function $getCouplets(node: PoetryBlockNode): Couplet[];
|
|
14
|
+
/** Every misra in the block, flattened across all its couplets — for
|
|
15
|
+
* operations (like $setPoetryLayout) that don't care about couplet
|
|
16
|
+
* boundaries, just the full set of lines. */
|
|
5
17
|
export declare function $getMisraParagraphs(node: PoetryBlockNode): ParagraphNode[];
|
|
6
18
|
/** The PoetryBlockNode the current selection is inside, or null. */
|
|
7
19
|
export declare function $getPoetryBlockFromSelection(): PoetryBlockNode | null;
|
|
8
20
|
/**
|
|
9
|
-
* Converts
|
|
10
|
-
* two-column (built on the columns primitive, §4.9) in place —
|
|
11
|
-
* two misra paragraphs out of whichever shape
|
|
12
|
-
* rebuilds the other shape around the same
|
|
13
|
-
*
|
|
21
|
+
* Converts every couplet in the block between single-column (two stacked
|
|
22
|
+
* misras) and two-column (built on the columns primitive, §4.9) in place —
|
|
23
|
+
* pulls each couplet's two misra paragraphs out of whichever shape
|
|
24
|
+
* currently holds them and rebuilds the other shape around the same
|
|
25
|
+
* paragraph nodes, preserving couplet order, so content, selection and
|
|
26
|
+
* undo history all survive the conversion.
|
|
14
27
|
*/
|
|
15
28
|
export declare function $setPoetryLayout(node: PoetryBlockNode, layout: PoetryLayout): void;
|
|
16
|
-
/** A
|
|
29
|
+
/** A block at the very end of the document would otherwise leave no
|
|
17
30
|
* editable block to click or arrow down into — appends one empty paragraph
|
|
18
|
-
* if the
|
|
31
|
+
* if the block doesn't already have a next sibling. Idempotent: once a
|
|
19
32
|
* sibling exists (that paragraph, or anything else), this is a no-op, so
|
|
20
33
|
* it's safe to call from both the insert command and a node transform that
|
|
21
34
|
* keeps the invariant even after later edits (e.g. deleting the trailing
|
|
22
35
|
* paragraph, or JSON content loaded without one). */
|
|
23
36
|
export declare function $ensureTrailingParagraph(node: PoetryBlockNode): void;
|
|
24
|
-
/**
|
|
25
|
-
*
|
|
37
|
+
/**
|
|
38
|
+
* Inserts a poetry couplet at the caret. If the caret is already inside a
|
|
39
|
+
* poetry block, appends a new couplet to *that* block (in its existing
|
|
40
|
+
* layout — "one poetry block can contain one or more couplets, in one or
|
|
41
|
+
* two column layout", so a block's layout is fixed once it has couplets,
|
|
42
|
+
* not chosen per couplet) and focuses its first misra. Otherwise creates a
|
|
43
|
+
* new block (in `layout`/`align`) with one couplet, after the selection's
|
|
44
|
+
* top-level element.
|
|
45
|
+
*/
|
|
26
46
|
export declare function $insertPoetryCouplet(layout: PoetryLayout, align: PoetryAlign): boolean;
|
|
27
|
-
/** Removes the
|
|
28
|
-
*
|
|
29
|
-
*
|
|
47
|
+
/** Removes the couplet the selection is inside. If it's the block's only
|
|
48
|
+
* couplet, removes the whole block (an empty poetry section serves no
|
|
49
|
+
* purpose) — the explicit, always-available counterpart to
|
|
50
|
+
* $deletePoetryOnBackspace's empty-couplet shortcut (mirrors the table
|
|
51
|
+
* menu's own "Delete table" action). */
|
|
30
52
|
export declare function $deletePoetryCouplet(): boolean;
|
|
31
53
|
/**
|
|
32
|
-
* Backspace at the very start of an empty couplet
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
54
|
+
* Backspace at the very start of an empty couplet removes just that
|
|
55
|
+
* couplet — merging back into the previous one's end, or (if it's the
|
|
56
|
+
* block's only couplet) removing the whole block, since an empty poetry
|
|
57
|
+
* section serves no purpose. Otherwise there was no way to get rid of an
|
|
58
|
+
* empty couplet created by mistake (its misra paragraphs each have their
|
|
59
|
+
* own default canBeEmpty, so Lexical's usual "delete the empty block"
|
|
60
|
+
* handling never reaches the couplet itself). Returns false for every
|
|
61
|
+
* other caret position, including a non-empty couplet, so normal Backspace
|
|
62
|
+
* handling (e.g. deleting within text) proceeds.
|
|
39
63
|
*/
|
|
40
64
|
export declare function $deletePoetryOnBackspace(): boolean;
|
|
41
65
|
/**
|
|
42
|
-
* Enter
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
66
|
+
* Enter inside a poetry block never grows a couplet past its fixed two
|
|
67
|
+
* misras. The block's *last* couplet being completely empty is always the
|
|
68
|
+
* exit trigger, regardless of which of its two (both-empty) misras the
|
|
69
|
+
* caret happens to be on — pressing Enter once on a non-empty last couplet
|
|
70
|
+
* appends a fresh empty one to continue into ("new couplet after last"),
|
|
71
|
+
* and pressing Enter again on that still-untouched couplet exits ("double
|
|
72
|
+
* enter to exit the block"): the same empty-item convention most editors
|
|
73
|
+
* use for lists. Outside that case: pressed in a couplet's first misra,
|
|
74
|
+
* Enter moves the caret to the second (already there; nothing inserted —
|
|
75
|
+
* this is what stops ParagraphNode's default insertNewAfter from growing
|
|
76
|
+
* the couplet, which would otherwise insert the new paragraph as a sibling
|
|
77
|
+
* of wherever the caret was, i.e. still inside the couplet); pressed in
|
|
78
|
+
* the last misra of a couplet that isn't the block's last, it moves to the
|
|
79
|
+
* next couplet's first misra.
|
|
49
80
|
*/
|
|
50
81
|
export declare function $exitPoetryOnEnter(): boolean;
|
|
51
82
|
//# sourceMappingURL=poetryActions.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"poetryActions.d.ts","sourceRoot":"","sources":["../../src/blocks/poetryActions.ts"],"names":[],"mappings":"AACA,OAAO,EAA4F,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AAEvI,OAAO,EAA8C,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAErI;
|
|
1
|
+
{"version":3,"file":"poetryActions.d.ts","sourceRoot":"","sources":["../../src/blocks/poetryActions.ts"],"names":[],"mappings":"AACA,OAAO,EAA4F,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AAEvI,OAAO,EAA8C,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAErI,oDAAoD;AACpD,MAAM,MAAM,OAAO,GAAG,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC;AAErD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,EAAE,CAgB7D;AAED;;6CAE6C;AAC7C,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,eAAe,GAAG,aAAa,EAAE,CAE1E;AAED,oEAAoE;AACpE,wBAAgB,4BAA4B,IAAI,eAAe,GAAG,IAAI,CAKrE;AA2BD;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,eAAe,EAAE,MAAM,EAAE,YAAY,GAAG,IAAI,CAmBlF;AAED;;;;;;qDAMqD;AACrD,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,eAAe,GAAG,IAAI,CAEpE;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAmBtF;AAED;;;;wCAIwC;AACxC,wBAAgB,oBAAoB,IAAI,OAAO,CAwB9C;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,IAAI,OAAO,CAqClD;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,IAAI,OAAO,CAgD5C"}
|
|
@@ -2,20 +2,38 @@ import { $findMatchingParent } from '@lexical/utils';
|
|
|
2
2
|
import { $createParagraphNode, $getSelection, $isElementNode, $isParagraphNode, $isRangeSelection } from 'lexical';
|
|
3
3
|
import { $createLayoutContainerNode, $createLayoutItemNode, $isLayoutContainerNode, $isLayoutItemNode } from './LayoutNode.js';
|
|
4
4
|
import { $createPoetryBlockNode, $isPoetryBlockNode } from './PoetryNode.js';
|
|
5
|
-
/**
|
|
6
|
-
*
|
|
7
|
-
|
|
5
|
+
/**
|
|
6
|
+
* A poetry block is a *section* of one or more couplets sharing one layout
|
|
7
|
+
* (single-column stacks each couplet's two misras directly as paragraph
|
|
8
|
+
* children, two deep per couplet; two-column gives each couplet its own
|
|
9
|
+
* LayoutContainerNode row). This walks that structure and returns each
|
|
10
|
+
* couplet as a pair, in document order — the one place that knows how to
|
|
11
|
+
* read either shape, so nothing else needs to.
|
|
12
|
+
*/
|
|
13
|
+
export function $getCouplets(node) {
|
|
8
14
|
if (node.getLayout() === 'single') {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
return
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
.filter($
|
|
15
|
+
const paragraphs = node.getChildren().filter($isParagraphNode);
|
|
16
|
+
const couplets = [];
|
|
17
|
+
for (let i = 0; i + 1 < paragraphs.length; i += 2)
|
|
18
|
+
couplets.push([paragraphs[i], paragraphs[i + 1]]);
|
|
19
|
+
return couplets;
|
|
20
|
+
}
|
|
21
|
+
const containers = node.getChildren().filter($isLayoutContainerNode);
|
|
22
|
+
const couplets = [];
|
|
23
|
+
for (const container of containers) {
|
|
24
|
+
const items = container.getChildren().filter($isLayoutItemNode);
|
|
25
|
+
const a = items[0]?.getFirstChild();
|
|
26
|
+
const b = items[1]?.getFirstChild();
|
|
27
|
+
if ($isParagraphNode(a) && $isParagraphNode(b))
|
|
28
|
+
couplets.push([a, b]);
|
|
29
|
+
}
|
|
30
|
+
return couplets;
|
|
31
|
+
}
|
|
32
|
+
/** Every misra in the block, flattened across all its couplets — for
|
|
33
|
+
* operations (like $setPoetryLayout) that don't care about couplet
|
|
34
|
+
* boundaries, just the full set of lines. */
|
|
35
|
+
export function $getMisraParagraphs(node) {
|
|
36
|
+
return $getCouplets(node).flat();
|
|
19
37
|
}
|
|
20
38
|
/** The PoetryBlockNode the current selection is inside, or null. */
|
|
21
39
|
export function $getPoetryBlockFromSelection() {
|
|
@@ -25,36 +43,65 @@ export function $getPoetryBlockFromSelection() {
|
|
|
25
43
|
const match = $findMatchingParent(selection.anchor.getNode(), $isPoetryBlockNode);
|
|
26
44
|
return match ?? null;
|
|
27
45
|
}
|
|
46
|
+
/** Which couplet (and which of its two misras, 0 or 1) a node sits inside,
|
|
47
|
+
* or null if it's in neither — e.g. on the block's own boundary. */
|
|
48
|
+
function $findCoupletPosition(couplets, target) {
|
|
49
|
+
for (let c = 0; c < couplets.length; c++) {
|
|
50
|
+
for (const misraIndex of [0, 1]) {
|
|
51
|
+
const misra = couplets[c][misraIndex];
|
|
52
|
+
if (misra.getKey() === target.getKey() || misra.isParentOf(target))
|
|
53
|
+
return { coupletIndex: c, misraIndex };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
function $appendCoupletTo(node) {
|
|
59
|
+
const misraA = $createParagraphNode();
|
|
60
|
+
const misraB = $createParagraphNode();
|
|
61
|
+
if (node.getLayout() === 'single') {
|
|
62
|
+
node.append(misraA, misraB);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
const container = $createLayoutContainerNode('repeat(2, 1fr)');
|
|
66
|
+
container.append($createLayoutItemNode().append(misraA), $createLayoutItemNode().append(misraB));
|
|
67
|
+
node.append(container);
|
|
68
|
+
}
|
|
69
|
+
return [misraA, misraB];
|
|
70
|
+
}
|
|
28
71
|
/**
|
|
29
|
-
* Converts
|
|
30
|
-
* two-column (built on the columns primitive, §4.9) in place —
|
|
31
|
-
* two misra paragraphs out of whichever shape
|
|
32
|
-
* rebuilds the other shape around the same
|
|
33
|
-
*
|
|
72
|
+
* Converts every couplet in the block between single-column (two stacked
|
|
73
|
+
* misras) and two-column (built on the columns primitive, §4.9) in place —
|
|
74
|
+
* pulls each couplet's two misra paragraphs out of whichever shape
|
|
75
|
+
* currently holds them and rebuilds the other shape around the same
|
|
76
|
+
* paragraph nodes, preserving couplet order, so content, selection and
|
|
77
|
+
* undo history all survive the conversion.
|
|
34
78
|
*/
|
|
35
79
|
export function $setPoetryLayout(node, layout) {
|
|
36
80
|
if (node.getLayout() === layout)
|
|
37
81
|
return;
|
|
38
|
-
const
|
|
39
|
-
for (const
|
|
40
|
-
|
|
82
|
+
const couplets = $getCouplets(node);
|
|
83
|
+
for (const [a, b] of couplets) {
|
|
84
|
+
a.remove();
|
|
85
|
+
b.remove();
|
|
86
|
+
}
|
|
41
87
|
for (const leftover of node.getChildren())
|
|
42
88
|
leftover.remove();
|
|
43
89
|
if (layout === 'single') {
|
|
44
|
-
for (const
|
|
45
|
-
node.append(
|
|
90
|
+
for (const [a, b] of couplets)
|
|
91
|
+
node.append(a, b);
|
|
46
92
|
}
|
|
47
93
|
else {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
container.append($createLayoutItemNode().append(
|
|
51
|
-
|
|
94
|
+
for (const [a, b] of couplets) {
|
|
95
|
+
const container = $createLayoutContainerNode('repeat(2, 1fr)');
|
|
96
|
+
container.append($createLayoutItemNode().append(a), $createLayoutItemNode().append(b));
|
|
97
|
+
node.append(container);
|
|
98
|
+
}
|
|
52
99
|
}
|
|
53
100
|
node.setLayoutAttribute(layout);
|
|
54
101
|
}
|
|
55
|
-
/** A
|
|
102
|
+
/** A block at the very end of the document would otherwise leave no
|
|
56
103
|
* editable block to click or arrow down into — appends one empty paragraph
|
|
57
|
-
* if the
|
|
104
|
+
* if the block doesn't already have a next sibling. Idempotent: once a
|
|
58
105
|
* sibling exists (that paragraph, or anything else), this is a no-op, so
|
|
59
106
|
* it's safe to call from both the insert command and a node transform that
|
|
60
107
|
* keeps the invariant even after later edits (e.g. deleting the trailing
|
|
@@ -63,107 +110,173 @@ export function $ensureTrailingParagraph(node) {
|
|
|
63
110
|
if (node.getNextSibling() === null)
|
|
64
111
|
node.insertAfter($createParagraphNode());
|
|
65
112
|
}
|
|
66
|
-
/**
|
|
67
|
-
*
|
|
113
|
+
/**
|
|
114
|
+
* Inserts a poetry couplet at the caret. If the caret is already inside a
|
|
115
|
+
* poetry block, appends a new couplet to *that* block (in its existing
|
|
116
|
+
* layout — "one poetry block can contain one or more couplets, in one or
|
|
117
|
+
* two column layout", so a block's layout is fixed once it has couplets,
|
|
118
|
+
* not chosen per couplet) and focuses its first misra. Otherwise creates a
|
|
119
|
+
* new block (in `layout`/`align`) with one couplet, after the selection's
|
|
120
|
+
* top-level element.
|
|
121
|
+
*/
|
|
68
122
|
export function $insertPoetryCouplet(layout, align) {
|
|
69
123
|
const selection = $getSelection();
|
|
70
124
|
if (!$isRangeSelection(selection))
|
|
71
125
|
return false;
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const anchorTopLevel = $getPoetryBlockFromSelection() ?? selection.anchor.getNode().getTopLevelElementOrThrow();
|
|
78
|
-
const node = $createPoetryBlockNode(layout, align);
|
|
79
|
-
const misraA = $createParagraphNode();
|
|
80
|
-
const misraB = $createParagraphNode();
|
|
81
|
-
if (layout === 'single') {
|
|
82
|
-
node.append(misraA, misraB);
|
|
83
|
-
}
|
|
84
|
-
else {
|
|
85
|
-
const container = $createLayoutContainerNode('repeat(2, 1fr)');
|
|
86
|
-
container.append($createLayoutItemNode().append(misraA), $createLayoutItemNode().append(misraB));
|
|
87
|
-
node.append(container);
|
|
126
|
+
const existing = $getPoetryBlockFromSelection();
|
|
127
|
+
if (existing) {
|
|
128
|
+
const [misraA] = $appendCoupletTo(existing);
|
|
129
|
+
misraA.selectStart();
|
|
130
|
+
return true;
|
|
88
131
|
}
|
|
132
|
+
const anchorTopLevel = selection.anchor.getNode().getTopLevelElementOrThrow();
|
|
133
|
+
const node = $createPoetryBlockNode(layout, align);
|
|
134
|
+
const [misraA] = $appendCoupletTo(node);
|
|
89
135
|
anchorTopLevel.insertAfter(node);
|
|
90
136
|
$ensureTrailingParagraph(node);
|
|
91
137
|
misraA.selectStart();
|
|
92
138
|
return true;
|
|
93
139
|
}
|
|
94
|
-
/** Removes the
|
|
95
|
-
*
|
|
96
|
-
*
|
|
140
|
+
/** Removes the couplet the selection is inside. If it's the block's only
|
|
141
|
+
* couplet, removes the whole block (an empty poetry section serves no
|
|
142
|
+
* purpose) — the explicit, always-available counterpart to
|
|
143
|
+
* $deletePoetryOnBackspace's empty-couplet shortcut (mirrors the table
|
|
144
|
+
* menu's own "Delete table" action). */
|
|
97
145
|
export function $deletePoetryCouplet() {
|
|
98
|
-
const
|
|
99
|
-
if (!
|
|
146
|
+
const block = $getPoetryBlockFromSelection();
|
|
147
|
+
if (!block)
|
|
100
148
|
return false;
|
|
101
|
-
|
|
149
|
+
const selection = $getSelection();
|
|
150
|
+
const couplets = $getCouplets(block);
|
|
151
|
+
const anchorNode = $isRangeSelection(selection) ? selection.anchor.getNode() : null;
|
|
152
|
+
const position = anchorNode ? $findCoupletPosition(couplets, anchorNode) : null;
|
|
153
|
+
const target = position ? couplets[position.coupletIndex] : couplets[couplets.length - 1];
|
|
154
|
+
if (!target)
|
|
155
|
+
return false;
|
|
156
|
+
if (couplets.length <= 1) {
|
|
157
|
+
block.remove();
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
const [a, b] = target;
|
|
161
|
+
const container = $findMatchingParent(a, $isLayoutContainerNode);
|
|
162
|
+
if (container)
|
|
163
|
+
container.remove();
|
|
164
|
+
else {
|
|
165
|
+
a.remove();
|
|
166
|
+
b.remove();
|
|
167
|
+
}
|
|
102
168
|
return true;
|
|
103
169
|
}
|
|
104
170
|
/**
|
|
105
|
-
* Backspace at the very start of an empty couplet
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
171
|
+
* Backspace at the very start of an empty couplet removes just that
|
|
172
|
+
* couplet — merging back into the previous one's end, or (if it's the
|
|
173
|
+
* block's only couplet) removing the whole block, since an empty poetry
|
|
174
|
+
* section serves no purpose. Otherwise there was no way to get rid of an
|
|
175
|
+
* empty couplet created by mistake (its misra paragraphs each have their
|
|
176
|
+
* own default canBeEmpty, so Lexical's usual "delete the empty block"
|
|
177
|
+
* handling never reaches the couplet itself). Returns false for every
|
|
178
|
+
* other caret position, including a non-empty couplet, so normal Backspace
|
|
179
|
+
* handling (e.g. deleting within text) proceeds.
|
|
112
180
|
*/
|
|
113
181
|
export function $deletePoetryOnBackspace() {
|
|
114
182
|
const selection = $getSelection();
|
|
115
183
|
if (!$isRangeSelection(selection) || !selection.isCollapsed())
|
|
116
184
|
return false;
|
|
117
|
-
const
|
|
118
|
-
if (!
|
|
119
|
-
return false;
|
|
120
|
-
const misras = $getMisraParagraphs(couplet);
|
|
121
|
-
const firstMisra = misras[0];
|
|
122
|
-
if (!firstMisra)
|
|
185
|
+
const block = $getPoetryBlockFromSelection();
|
|
186
|
+
if (!block)
|
|
123
187
|
return false;
|
|
124
|
-
|
|
188
|
+
const couplets = $getCouplets(block);
|
|
189
|
+
const anchorNode = selection.anchor.getNode();
|
|
190
|
+
const position = $findCoupletPosition(couplets, anchorNode);
|
|
191
|
+
if (!position || position.misraIndex !== 0)
|
|
125
192
|
return false;
|
|
193
|
+
const [firstMisra] = couplets[position.coupletIndex];
|
|
126
194
|
const anchor = selection.anchor;
|
|
127
195
|
const isAtStart = anchor.key === firstMisra.getKey() && anchor.offset === 0;
|
|
128
196
|
if (!isAtStart)
|
|
129
197
|
return false;
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
198
|
+
const [a, b] = couplets[position.coupletIndex];
|
|
199
|
+
if (!a.isEmpty() || !b.isEmpty())
|
|
200
|
+
return false;
|
|
201
|
+
if (couplets.length <= 1) {
|
|
202
|
+
const previous = block.getPreviousSibling();
|
|
203
|
+
block.remove();
|
|
204
|
+
if ($isElementNode(previous))
|
|
205
|
+
previous.selectEnd();
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
const container = $findMatchingParent(a, $isLayoutContainerNode);
|
|
209
|
+
if (container)
|
|
210
|
+
container.remove();
|
|
211
|
+
else {
|
|
212
|
+
a.remove();
|
|
213
|
+
b.remove();
|
|
214
|
+
}
|
|
215
|
+
const remaining = $getCouplets(block);
|
|
216
|
+
const previousCouplet = remaining[position.coupletIndex - 1] ?? remaining[0];
|
|
217
|
+
previousCouplet?.[1].selectEnd();
|
|
134
218
|
return true;
|
|
135
219
|
}
|
|
136
220
|
/**
|
|
137
|
-
* Enter
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
221
|
+
* Enter inside a poetry block never grows a couplet past its fixed two
|
|
222
|
+
* misras. The block's *last* couplet being completely empty is always the
|
|
223
|
+
* exit trigger, regardless of which of its two (both-empty) misras the
|
|
224
|
+
* caret happens to be on — pressing Enter once on a non-empty last couplet
|
|
225
|
+
* appends a fresh empty one to continue into ("new couplet after last"),
|
|
226
|
+
* and pressing Enter again on that still-untouched couplet exits ("double
|
|
227
|
+
* enter to exit the block"): the same empty-item convention most editors
|
|
228
|
+
* use for lists. Outside that case: pressed in a couplet's first misra,
|
|
229
|
+
* Enter moves the caret to the second (already there; nothing inserted —
|
|
230
|
+
* this is what stops ParagraphNode's default insertNewAfter from growing
|
|
231
|
+
* the couplet, which would otherwise insert the new paragraph as a sibling
|
|
232
|
+
* of wherever the caret was, i.e. still inside the couplet); pressed in
|
|
233
|
+
* the last misra of a couplet that isn't the block's last, it moves to the
|
|
234
|
+
* next couplet's first misra.
|
|
144
235
|
*/
|
|
145
236
|
export function $exitPoetryOnEnter() {
|
|
146
237
|
const selection = $getSelection();
|
|
147
238
|
if (!$isRangeSelection(selection) || !selection.isCollapsed())
|
|
148
239
|
return false;
|
|
149
|
-
const
|
|
150
|
-
if (!
|
|
151
|
-
return false;
|
|
152
|
-
const misras = $getMisraParagraphs(couplet);
|
|
153
|
-
const lastMisra = misras[misras.length - 1];
|
|
154
|
-
if (!lastMisra)
|
|
240
|
+
const block = $getPoetryBlockFromSelection();
|
|
241
|
+
if (!block)
|
|
155
242
|
return false;
|
|
156
|
-
const
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
if (!isAtEnd)
|
|
243
|
+
const couplets = $getCouplets(block);
|
|
244
|
+
const position = $findCoupletPosition(couplets, selection.anchor.getNode());
|
|
245
|
+
if (!position)
|
|
160
246
|
return false;
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
const
|
|
165
|
-
if (
|
|
166
|
-
couplet
|
|
167
|
-
|
|
247
|
+
const { coupletIndex, misraIndex } = position;
|
|
248
|
+
const [a, b] = couplets[coupletIndex];
|
|
249
|
+
const isLastCouplet = coupletIndex === couplets.length - 1;
|
|
250
|
+
const coupletEmpty = a.isEmpty() && b.isEmpty();
|
|
251
|
+
if (isLastCouplet && coupletEmpty) {
|
|
252
|
+
// Drop the couplet we ourselves auto-created on the previous Enter —
|
|
253
|
+
// unless it's the block's only couplet, in which case leave the
|
|
254
|
+
// section as the user's (empty) content rather than deleting it.
|
|
255
|
+
if (couplets.length > 1) {
|
|
256
|
+
const container = $findMatchingParent(a, $isLayoutContainerNode);
|
|
257
|
+
if (container)
|
|
258
|
+
container.remove();
|
|
259
|
+
else {
|
|
260
|
+
a.remove();
|
|
261
|
+
b.remove();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const existingNext = block.getNextSibling();
|
|
265
|
+
const paragraph = $isParagraphNode(existingNext) ? existingNext : $createParagraphNode();
|
|
266
|
+
if (paragraph !== existingNext)
|
|
267
|
+
block.insertAfter(paragraph);
|
|
268
|
+
paragraph.select();
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
if (misraIndex === 0) {
|
|
272
|
+
b.selectEnd();
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
if (!isLastCouplet) {
|
|
276
|
+
couplets[coupletIndex + 1][0].selectEnd();
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
const [newA] = $appendCoupletTo(block);
|
|
280
|
+
newA.selectStart();
|
|
168
281
|
return true;
|
|
169
282
|
}
|
package/dist/theme/editor.css
CHANGED
|
@@ -406,13 +406,18 @@
|
|
|
406
406
|
|
|
407
407
|
/* Misra lines sit tight against each other within a couplet — the generic
|
|
408
408
|
.likhari-paragraph margin would otherwise add the same gap between a
|
|
409
|
-
couplet's own two lines as between separate couplets
|
|
410
|
-
|
|
411
|
-
|
|
409
|
+
couplet's own two lines as between separate couplets within the same
|
|
410
|
+
block, losing the pairing entirely. Re-added below on every *second*
|
|
411
|
+
line (nth-child(2n)) — one couplet's worth of lines — so the extra space
|
|
412
|
+
falls after every couplet, not after every single line. */
|
|
412
413
|
.likhari-poetry .likhari-paragraph {
|
|
413
414
|
margin: 0;
|
|
414
415
|
}
|
|
415
416
|
|
|
417
|
+
.likhari-poetry--single .likhari-paragraph:nth-child(2n):not(:last-child) {
|
|
418
|
+
margin-bottom: 1em;
|
|
419
|
+
}
|
|
420
|
+
|
|
416
421
|
.likhari-poetry--two-column .likhari-layout-container {
|
|
417
422
|
margin: 0;
|
|
418
423
|
/* No grid gap — the divider below sits exactly on the track boundary, so
|
|
@@ -421,6 +426,12 @@
|
|
|
421
426
|
gap: 0;
|
|
422
427
|
}
|
|
423
428
|
|
|
429
|
+
/* One row per couplet in two-column layout — same "extra space after every
|
|
430
|
+
couplet" rhythm as the single-column rule above. */
|
|
431
|
+
.likhari-poetry--two-column .likhari-layout-container:not(:last-child) {
|
|
432
|
+
margin-bottom: 1em;
|
|
433
|
+
}
|
|
434
|
+
|
|
424
435
|
.likhari-poetry--two-column .likhari-layout-item {
|
|
425
436
|
text-align: inherit;
|
|
426
437
|
}
|