@inshapardaz/likhari-react 0.1.18 → 0.1.19
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/EditorRoot.d.ts.map +1 -1
- package/dist/EditorRoot.js +3 -1
- package/dist/blocks/PoetryNode.d.ts +70 -0
- package/dist/blocks/PoetryNode.d.ts.map +1 -0
- package/dist/blocks/PoetryNode.js +198 -0
- package/dist/blocks/PoetryResizer.d.ts +11 -0
- package/dist/blocks/PoetryResizer.d.ts.map +1 -0
- package/dist/blocks/PoetryResizer.js +94 -0
- package/dist/blocks/poetryActions.d.ts +117 -0
- package/dist/blocks/poetryActions.d.ts.map +1 -0
- package/dist/blocks/poetryActions.js +481 -0
- package/dist/components/Toolbar.d.ts.map +1 -1
- package/dist/components/Toolbar.js +80 -8
- package/dist/i18n/strings.d.ts +21 -1
- package/dist/i18n/strings.d.ts.map +1 -1
- package/dist/i18n/strings.js +60 -3
- package/dist/nodes.d.ts.map +1 -1
- package/dist/nodes.js +2 -0
- package/dist/plugins/PoetryPlugin.d.ts +19 -0
- package/dist/plugins/PoetryPlugin.d.ts.map +1 -0
- package/dist/plugins/PoetryPlugin.js +54 -0
- package/dist/theme/editor.css +163 -0
- package/dist/theme/editorTheme.d.ts.map +1 -1
- package/dist/theme/editorTheme.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
import { $findMatchingParent } from '@lexical/utils';
|
|
2
|
+
import { $createParagraphNode, $getSelection, $isElementNode, $isParagraphNode, $isRangeSelection, } from 'lexical';
|
|
3
|
+
import { $createLayoutContainerNode, $createLayoutItemNode, $isLayoutContainerNode, $isLayoutItemNode } from './LayoutNode.js';
|
|
4
|
+
import { $createPoetryBlockNode, $isPoetryBlockNode, POETRY_GUTTERS, POETRY_SPACINGS, POETRY_STAGGERS, } from './PoetryNode.js';
|
|
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) {
|
|
14
|
+
if (node.getLayout() !== 'two-column') {
|
|
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
|
+
if (items.length === 1) {
|
|
26
|
+
const [a, b] = items[0].getChildren();
|
|
27
|
+
if ($isParagraphNode(a) && $isParagraphNode(b))
|
|
28
|
+
couplets.push([a, b]);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const a = items[0]?.getFirstChild();
|
|
32
|
+
const b = items[1]?.getFirstChild();
|
|
33
|
+
if ($isParagraphNode(a) && $isParagraphNode(b))
|
|
34
|
+
couplets.push([a, b]);
|
|
35
|
+
}
|
|
36
|
+
return couplets;
|
|
37
|
+
}
|
|
38
|
+
/** Every misra in the block, flattened across all its couplets — for
|
|
39
|
+
* operations (like $setPoetryLayout) that don't care about couplet
|
|
40
|
+
* boundaries, just the full set of lines. */
|
|
41
|
+
export function $getMisraParagraphs(node) {
|
|
42
|
+
return $getCouplets(node).flat();
|
|
43
|
+
}
|
|
44
|
+
/** The PoetryBlockNode the current selection is inside, or null. */
|
|
45
|
+
export function $getPoetryBlockFromSelection() {
|
|
46
|
+
const selection = $getSelection();
|
|
47
|
+
if (!$isRangeSelection(selection))
|
|
48
|
+
return null;
|
|
49
|
+
const match = $findMatchingParent(selection.anchor.getNode(), $isPoetryBlockNode);
|
|
50
|
+
return match ?? null;
|
|
51
|
+
}
|
|
52
|
+
/** Which couplet (and which of its two misras, 0 or 1) a node sits inside,
|
|
53
|
+
* or null if it's in neither — e.g. on the block's own boundary. */
|
|
54
|
+
function $findCoupletPosition(couplets, target) {
|
|
55
|
+
for (let c = 0; c < couplets.length; c++) {
|
|
56
|
+
for (const misraIndex of [0, 1]) {
|
|
57
|
+
const misra = couplets[c][misraIndex];
|
|
58
|
+
if (misra.getKey() === target.getKey() || misra.isParentOf(target))
|
|
59
|
+
return { coupletIndex: c, misraIndex };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
function $appendCoupletTo(node) {
|
|
65
|
+
const misraA = $createParagraphNode();
|
|
66
|
+
const misraB = $createParagraphNode();
|
|
67
|
+
if (node.getLayout() !== 'two-column') {
|
|
68
|
+
node.append(misraA, misraB);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
const container = $createLayoutContainerNode('repeat(2, 1fr)');
|
|
72
|
+
container.append($createLayoutItemNode().append(misraA), $createLayoutItemNode().append(misraB));
|
|
73
|
+
node.append(container);
|
|
74
|
+
}
|
|
75
|
+
return [misraA, misraB];
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Inserts a new, empty couplet immediately before or after an existing one —
|
|
79
|
+
* the table-row-insert equivalent for a poetry block ("add/remove couplets
|
|
80
|
+
* just like adding/removing table rows"). `existingFirstMisra` is that
|
|
81
|
+
* couplet's first misra, used to locate its wrapper (nothing, in single
|
|
82
|
+
* layout — just its two sibling paragraphs; a LayoutContainerNode row in
|
|
83
|
+
* two-column) so the new couplet can be spliced in at the right spot
|
|
84
|
+
* regardless of layout.
|
|
85
|
+
*/
|
|
86
|
+
function $insertCoupletRelativeTo(existingFirstMisra, layout, position) {
|
|
87
|
+
const misraA = $createParagraphNode();
|
|
88
|
+
const misraB = $createParagraphNode();
|
|
89
|
+
if (layout !== 'two-column') {
|
|
90
|
+
if (position === 'before') {
|
|
91
|
+
existingFirstMisra.insertBefore(misraA);
|
|
92
|
+
misraA.insertAfter(misraB);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
const existingSecondMisra = existingFirstMisra.getNextSibling() ?? existingFirstMisra;
|
|
96
|
+
existingSecondMisra.insertAfter(misraA);
|
|
97
|
+
misraA.insertAfter(misraB);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
const container = $findMatchingParent(existingFirstMisra, $isLayoutContainerNode);
|
|
102
|
+
const newContainer = $createLayoutContainerNode('repeat(2, 1fr)');
|
|
103
|
+
newContainer.append($createLayoutItemNode().append(misraA), $createLayoutItemNode().append(misraB));
|
|
104
|
+
if (position === 'before')
|
|
105
|
+
container.insertBefore(newContainer);
|
|
106
|
+
else
|
|
107
|
+
container.insertAfter(newContainer);
|
|
108
|
+
}
|
|
109
|
+
return [misraA, misraB];
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The explicit "Insert couplet above/below" menu actions — mirrors the
|
|
113
|
+
* table menu's "Insert row before/after". Targets the couplet the caret is
|
|
114
|
+
* currently in, falling back to the block's last couplet (so the toolbar's
|
|
115
|
+
* always-available insert button has somewhere sensible to act on even
|
|
116
|
+
* without a precise caret position).
|
|
117
|
+
*/
|
|
118
|
+
export function $insertCoupletRelativeToSelection(position) {
|
|
119
|
+
const block = $getPoetryBlockFromSelection();
|
|
120
|
+
if (!block)
|
|
121
|
+
return false;
|
|
122
|
+
const selection = $getSelection();
|
|
123
|
+
const couplets = $getCouplets(block);
|
|
124
|
+
const anchorNode = $isRangeSelection(selection) ? selection.anchor.getNode() : null;
|
|
125
|
+
const found = anchorNode ? $findCoupletPosition(couplets, anchorNode) : null;
|
|
126
|
+
const index = found ? found.coupletIndex : couplets.length - 1;
|
|
127
|
+
const target = couplets[index];
|
|
128
|
+
if (!target)
|
|
129
|
+
return false;
|
|
130
|
+
const [newA] = $insertCoupletRelativeTo(target[0], block.getLayout(), position);
|
|
131
|
+
newA.selectStart();
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Converts every couplet in the block between single-column (two stacked
|
|
136
|
+
* misras) and two-column (built on the columns primitive, §4.9) in place —
|
|
137
|
+
* pulls each couplet's two misra paragraphs out of whichever shape
|
|
138
|
+
* currently holds them and rebuilds the other shape around the same
|
|
139
|
+
* paragraph nodes, preserving couplet order, so content, selection and
|
|
140
|
+
* undo history all survive the conversion.
|
|
141
|
+
*/
|
|
142
|
+
export function $setPoetryLayout(node, layout) {
|
|
143
|
+
if (node.getLayout() === layout)
|
|
144
|
+
return;
|
|
145
|
+
const couplets = $getCouplets(node);
|
|
146
|
+
for (const [a, b] of couplets) {
|
|
147
|
+
a.remove();
|
|
148
|
+
b.remove();
|
|
149
|
+
}
|
|
150
|
+
for (const leftover of node.getChildren())
|
|
151
|
+
leftover.remove();
|
|
152
|
+
if (layout !== 'two-column') {
|
|
153
|
+
for (const [a, b] of couplets)
|
|
154
|
+
node.append(a, b);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
for (const [a, b] of couplets) {
|
|
158
|
+
const container = $createLayoutContainerNode('repeat(2, 1fr)');
|
|
159
|
+
container.append($createLayoutItemNode().append(a), $createLayoutItemNode().append(b));
|
|
160
|
+
node.append(container);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
node.setLayoutAttribute(layout);
|
|
164
|
+
}
|
|
165
|
+
/** A block at the very end of the document would otherwise leave no
|
|
166
|
+
* editable block to click or arrow down into — appends one empty paragraph
|
|
167
|
+
* if the block doesn't already have a next sibling. Idempotent: once a
|
|
168
|
+
* sibling exists (that paragraph, or anything else), this is a no-op, so
|
|
169
|
+
* it's safe to call from both the insert command and a node transform that
|
|
170
|
+
* keeps the invariant even after later edits (e.g. deleting the trailing
|
|
171
|
+
* paragraph, or JSON content loaded without one). */
|
|
172
|
+
export function $ensureTrailingParagraph(node) {
|
|
173
|
+
if (node.getNextSibling() === null)
|
|
174
|
+
node.insertAfter($createParagraphNode());
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Inserts a poetry couplet at the caret. If the caret is already inside a
|
|
178
|
+
* poetry block, inserts a new couplet right after the one the caret is in
|
|
179
|
+
* (same as the "Insert couplet below" menu action — table-row-insert
|
|
180
|
+
* semantics, in its existing layout: "one poetry block can contain one or
|
|
181
|
+
* more couplets, in one or two column layout", so a block's layout is
|
|
182
|
+
* fixed once it has couplets, not chosen per couplet) and focuses its first
|
|
183
|
+
* misra. Otherwise creates a new block (in `layout`) with one
|
|
184
|
+
* couplet, after the selection's top-level element.
|
|
185
|
+
*/
|
|
186
|
+
export function $insertPoetryCouplet(layout) {
|
|
187
|
+
const selection = $getSelection();
|
|
188
|
+
if (!$isRangeSelection(selection))
|
|
189
|
+
return false;
|
|
190
|
+
const existing = $getPoetryBlockFromSelection();
|
|
191
|
+
if (existing) {
|
|
192
|
+
return $insertCoupletRelativeToSelection('after');
|
|
193
|
+
}
|
|
194
|
+
const anchorTopLevel = selection.anchor.getNode().getTopLevelElementOrThrow();
|
|
195
|
+
const node = $createPoetryBlockNode(layout);
|
|
196
|
+
const [misraA] = $appendCoupletTo(node);
|
|
197
|
+
anchorTopLevel.insertAfter(node);
|
|
198
|
+
$ensureTrailingParagraph(node);
|
|
199
|
+
misraA.selectStart();
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
/** Removes the couplet the selection is inside. If it's the block's only
|
|
203
|
+
* couplet, removes the whole block (an empty poetry section serves no
|
|
204
|
+
* purpose) — the explicit, always-available counterpart to
|
|
205
|
+
* $deletePoetryOnBackspace's empty-couplet shortcut (mirrors the table
|
|
206
|
+
* menu's own "Delete table" action). */
|
|
207
|
+
export function $deletePoetryCouplet() {
|
|
208
|
+
const block = $getPoetryBlockFromSelection();
|
|
209
|
+
if (!block)
|
|
210
|
+
return false;
|
|
211
|
+
const selection = $getSelection();
|
|
212
|
+
const couplets = $getCouplets(block);
|
|
213
|
+
const anchorNode = $isRangeSelection(selection) ? selection.anchor.getNode() : null;
|
|
214
|
+
const position = anchorNode ? $findCoupletPosition(couplets, anchorNode) : null;
|
|
215
|
+
const target = position ? couplets[position.coupletIndex] : couplets[couplets.length - 1];
|
|
216
|
+
if (!target)
|
|
217
|
+
return false;
|
|
218
|
+
if (couplets.length <= 1) {
|
|
219
|
+
block.remove();
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
const [a, b] = target;
|
|
223
|
+
const container = $findMatchingParent(a, $isLayoutContainerNode);
|
|
224
|
+
if (container)
|
|
225
|
+
container.remove();
|
|
226
|
+
else {
|
|
227
|
+
a.remove();
|
|
228
|
+
b.remove();
|
|
229
|
+
}
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
/** Whether a point sits at the very start (or, with `atEnd`, very end) of a paragraph. */
|
|
233
|
+
function $isPointAt(paragraph, point, atEnd) {
|
|
234
|
+
if (point.key === paragraph.getKey())
|
|
235
|
+
return point.offset === (atEnd ? paragraph.getChildrenSize() : 0);
|
|
236
|
+
const edge = atEnd ? paragraph.getLastDescendant() : paragraph.getFirstDescendant();
|
|
237
|
+
if (point.getNode() !== edge)
|
|
238
|
+
return false;
|
|
239
|
+
return point.offset === (atEnd ? point.getNode().getTextContent().length : 0);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Backspace at the start of a misra never merges it into its neighbour: the
|
|
243
|
+
* block's lines are paired into couplets, so merging one line away would
|
|
244
|
+
* shift every couplet after it (and take the previous couplet's last line
|
|
245
|
+
* with it). Instead, Backspace at the start of an empty couplet removes that
|
|
246
|
+
* couplet, and at the start of any other line it moves the caret to the end
|
|
247
|
+
* of the previous line. Other caret positions (deleting within text) fall
|
|
248
|
+
* through to normal handling.
|
|
249
|
+
*/
|
|
250
|
+
export function $deletePoetryOnBackspace() {
|
|
251
|
+
const selection = $getSelection();
|
|
252
|
+
if (!$isRangeSelection(selection) || !selection.isCollapsed())
|
|
253
|
+
return false;
|
|
254
|
+
const block = $getPoetryBlockFromSelection();
|
|
255
|
+
if (!block)
|
|
256
|
+
return false;
|
|
257
|
+
const couplets = $getCouplets(block);
|
|
258
|
+
const position = $findCoupletPosition(couplets, selection.anchor.getNode());
|
|
259
|
+
if (!position)
|
|
260
|
+
return false;
|
|
261
|
+
const { coupletIndex, misraIndex } = position;
|
|
262
|
+
const [a, b] = couplets[coupletIndex];
|
|
263
|
+
const misra = couplets[coupletIndex][misraIndex];
|
|
264
|
+
if (!$isPointAt(misra, selection.anchor, false))
|
|
265
|
+
return false;
|
|
266
|
+
if (a.isEmpty() && b.isEmpty()) {
|
|
267
|
+
if (couplets.length <= 1) {
|
|
268
|
+
const previous = block.getPreviousSibling();
|
|
269
|
+
block.remove();
|
|
270
|
+
if ($isElementNode(previous))
|
|
271
|
+
previous.selectEnd();
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
const container = $findMatchingParent(a, $isLayoutContainerNode);
|
|
275
|
+
if (container)
|
|
276
|
+
container.remove();
|
|
277
|
+
else {
|
|
278
|
+
a.remove();
|
|
279
|
+
b.remove();
|
|
280
|
+
}
|
|
281
|
+
const remaining = $getCouplets(block);
|
|
282
|
+
const previousCouplet = remaining[coupletIndex - 1] ?? remaining[0];
|
|
283
|
+
previousCouplet?.[1].selectEnd();
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
const previousLine = misraIndex === 1 ? a : couplets[coupletIndex - 1]?.[1];
|
|
287
|
+
previousLine?.selectEnd();
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Forward Delete at the end of a misra never merges the next line into it, for
|
|
292
|
+
* the same reason as $deletePoetryOnBackspace: the caret moves to the start of
|
|
293
|
+
* the next line instead. Other caret positions fall through to normal handling.
|
|
294
|
+
*/
|
|
295
|
+
export function $deletePoetryForward() {
|
|
296
|
+
const selection = $getSelection();
|
|
297
|
+
if (!$isRangeSelection(selection) || !selection.isCollapsed())
|
|
298
|
+
return false;
|
|
299
|
+
const block = $getPoetryBlockFromSelection();
|
|
300
|
+
if (!block)
|
|
301
|
+
return false;
|
|
302
|
+
const couplets = $getCouplets(block);
|
|
303
|
+
const position = $findCoupletPosition(couplets, selection.anchor.getNode());
|
|
304
|
+
if (!position)
|
|
305
|
+
return false;
|
|
306
|
+
const { coupletIndex, misraIndex } = position;
|
|
307
|
+
const misra = couplets[coupletIndex][misraIndex];
|
|
308
|
+
if (!$isPointAt(misra, selection.anchor, true))
|
|
309
|
+
return false;
|
|
310
|
+
const nextLine = misraIndex === 0 ? couplets[coupletIndex][1] : couplets[coupletIndex + 1]?.[0];
|
|
311
|
+
nextLine?.selectStart();
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Enter inside a poetry block never grows a couplet past its fixed two
|
|
316
|
+
* misras. The block's *last* couplet being completely empty is always the
|
|
317
|
+
* exit trigger, regardless of which of its two (both-empty) misras the
|
|
318
|
+
* caret happens to be on — pressing Enter once on a non-empty last couplet
|
|
319
|
+
* appends a fresh empty one to continue into ("new couplet after last"),
|
|
320
|
+
* and pressing Enter again on that still-untouched couplet exits ("double
|
|
321
|
+
* enter to exit the block"): the same empty-item convention most editors
|
|
322
|
+
* use for lists. Outside that case: pressed in a couplet's first misra,
|
|
323
|
+
* Enter moves the caret to the second (already there; nothing inserted —
|
|
324
|
+
* this is what stops ParagraphNode's default insertNewAfter from growing
|
|
325
|
+
* the couplet, which would otherwise insert the new paragraph as a sibling
|
|
326
|
+
* of wherever the caret was, i.e. still inside the couplet); pressed in
|
|
327
|
+
* the last misra of a couplet that isn't the block's last, it moves to the
|
|
328
|
+
* next couplet's first misra.
|
|
329
|
+
*/
|
|
330
|
+
export function $exitPoetryOnEnter() {
|
|
331
|
+
const selection = $getSelection();
|
|
332
|
+
if (!$isRangeSelection(selection) || !selection.isCollapsed())
|
|
333
|
+
return false;
|
|
334
|
+
const block = $getPoetryBlockFromSelection();
|
|
335
|
+
if (!block)
|
|
336
|
+
return false;
|
|
337
|
+
const couplets = $getCouplets(block);
|
|
338
|
+
const position = $findCoupletPosition(couplets, selection.anchor.getNode());
|
|
339
|
+
if (!position)
|
|
340
|
+
return false;
|
|
341
|
+
const { coupletIndex, misraIndex } = position;
|
|
342
|
+
const [a, b] = couplets[coupletIndex];
|
|
343
|
+
const isLastCouplet = coupletIndex === couplets.length - 1;
|
|
344
|
+
const coupletEmpty = a.isEmpty() && b.isEmpty();
|
|
345
|
+
if (isLastCouplet && coupletEmpty) {
|
|
346
|
+
// Drop the couplet we ourselves auto-created on the previous Enter —
|
|
347
|
+
// unless it's the block's only couplet, in which case leave the
|
|
348
|
+
// section as the user's (empty) content rather than deleting it.
|
|
349
|
+
if (couplets.length > 1) {
|
|
350
|
+
const container = $findMatchingParent(a, $isLayoutContainerNode);
|
|
351
|
+
if (container)
|
|
352
|
+
container.remove();
|
|
353
|
+
else {
|
|
354
|
+
a.remove();
|
|
355
|
+
b.remove();
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const existingNext = block.getNextSibling();
|
|
359
|
+
const paragraph = $isParagraphNode(existingNext) ? existingNext : $createParagraphNode();
|
|
360
|
+
if (paragraph !== existingNext)
|
|
361
|
+
block.insertAfter(paragraph);
|
|
362
|
+
paragraph.select();
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
if (misraIndex === 0) {
|
|
366
|
+
b.selectEnd();
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
369
|
+
if (!isLastCouplet) {
|
|
370
|
+
couplets[coupletIndex + 1][0].selectEnd();
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
const [newA] = $appendCoupletTo(block);
|
|
374
|
+
newA.selectStart();
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
/** Steps the caret's poetry block one notch looser (+1) or tighter (-1) on
|
|
378
|
+
* the couplet-spacing scale; clamps at either end. */
|
|
379
|
+
export function $adjustPoetrySpacing(step) {
|
|
380
|
+
const block = $getPoetryBlockFromSelection();
|
|
381
|
+
if (!block)
|
|
382
|
+
return false;
|
|
383
|
+
const index = POETRY_SPACINGS.indexOf(block.getSpacing());
|
|
384
|
+
const next = POETRY_SPACINGS[Math.min(POETRY_SPACINGS.length - 1, Math.max(0, index + step))];
|
|
385
|
+
block.setSpacing(next);
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
/** Steps the caret's two-column gutter one notch wider (+1) or narrower (-1);
|
|
389
|
+
* clamps at either end. */
|
|
390
|
+
export function $adjustPoetryGutter(step) {
|
|
391
|
+
const block = $getPoetryBlockFromSelection();
|
|
392
|
+
if (!block)
|
|
393
|
+
return false;
|
|
394
|
+
const index = POETRY_GUTTERS.indexOf(block.getGutter());
|
|
395
|
+
const next = POETRY_GUTTERS[Math.min(POETRY_GUTTERS.length - 1, Math.max(0, index + step))];
|
|
396
|
+
block.setGutter(next);
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
/** Steps the caret's staggered couplet width one notch narrower (-1) or wider
|
|
400
|
+
* (+1); clamps at either end. */
|
|
401
|
+
export function $adjustPoetryStagger(step) {
|
|
402
|
+
const block = $getPoetryBlockFromSelection();
|
|
403
|
+
if (!block)
|
|
404
|
+
return false;
|
|
405
|
+
const index = POETRY_STAGGERS.indexOf(block.getStagger());
|
|
406
|
+
const next = POETRY_STAGGERS[Math.min(POETRY_STAGGERS.length - 1, Math.max(0, index + step))];
|
|
407
|
+
block.setStagger(next);
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
/** A centered couplet is a two-column row holding both misras in one item
|
|
411
|
+
* (a single-item row), rather than one item per misra. */
|
|
412
|
+
function $isCoupletRowCentered(row) {
|
|
413
|
+
return row.getChildren().filter($isLayoutItemNode).length === 1;
|
|
414
|
+
}
|
|
415
|
+
function $findCoupletRow(couplet) {
|
|
416
|
+
return $findMatchingParent(couplet[0], $isLayoutContainerNode);
|
|
417
|
+
}
|
|
418
|
+
/** Whether the couplet the caret is in is centered, or null outside a
|
|
419
|
+
* two-column poetry block (centering only applies there). */
|
|
420
|
+
export function $getCoupletCenteredFromSelection() {
|
|
421
|
+
const block = $getPoetryBlockFromSelection();
|
|
422
|
+
if (!block || block.getLayout() !== 'two-column')
|
|
423
|
+
return null;
|
|
424
|
+
const selection = $getSelection();
|
|
425
|
+
if (!$isRangeSelection(selection))
|
|
426
|
+
return null;
|
|
427
|
+
const couplets = $getCouplets(block);
|
|
428
|
+
const position = $findCoupletPosition(couplets, selection.anchor.getNode());
|
|
429
|
+
if (!position)
|
|
430
|
+
return null;
|
|
431
|
+
const row = $findCoupletRow(couplets[position.coupletIndex]);
|
|
432
|
+
return row ? $isCoupletRowCentered(row) : false;
|
|
433
|
+
}
|
|
434
|
+
/** Centers (or un-centers) the caret's couplet within a two-column block by
|
|
435
|
+
* rebuilding its row around the same two misra paragraphs. */
|
|
436
|
+
export function $setCoupletCentered(centered) {
|
|
437
|
+
const block = $getPoetryBlockFromSelection();
|
|
438
|
+
if (!block || block.getLayout() !== 'two-column')
|
|
439
|
+
return false;
|
|
440
|
+
const selection = $getSelection();
|
|
441
|
+
if (!$isRangeSelection(selection))
|
|
442
|
+
return false;
|
|
443
|
+
const couplets = $getCouplets(block);
|
|
444
|
+
const position = $findCoupletPosition(couplets, selection.anchor.getNode());
|
|
445
|
+
if (!position)
|
|
446
|
+
return false;
|
|
447
|
+
const couplet = couplets[position.coupletIndex];
|
|
448
|
+
const row = $findCoupletRow(couplet);
|
|
449
|
+
if (!row || $isCoupletRowCentered(row) === centered)
|
|
450
|
+
return true;
|
|
451
|
+
const [a, b] = couplet;
|
|
452
|
+
const newRow = $createLayoutContainerNode(centered ? '1fr' : 'repeat(2, 1fr)');
|
|
453
|
+
if (centered) {
|
|
454
|
+
newRow.append($createLayoutItemNode().append(a, b));
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
newRow.append($createLayoutItemNode().append(a), $createLayoutItemNode().append(b));
|
|
458
|
+
}
|
|
459
|
+
row.insertAfter(newRow);
|
|
460
|
+
row.remove();
|
|
461
|
+
a.selectStart();
|
|
462
|
+
return true;
|
|
463
|
+
}
|
|
464
|
+
/** Removes the whole poetry block the caret is in, including every couplet. */
|
|
465
|
+
export function $deletePoetryBlock() {
|
|
466
|
+
const block = $getPoetryBlockFromSelection();
|
|
467
|
+
if (!block)
|
|
468
|
+
return false;
|
|
469
|
+
block.remove();
|
|
470
|
+
return true;
|
|
471
|
+
}
|
|
472
|
+
/** Single-column blocks pair their paragraphs into couplets, so a line left
|
|
473
|
+
* without a partner (from pasted or imported content) would be invisible to
|
|
474
|
+
* the couplet logic. Give it an empty partner instead, so every line belongs
|
|
475
|
+
* to a couplet and can be deleted like any other. */
|
|
476
|
+
export function $completeSingleCoupletBlock(node) {
|
|
477
|
+
if (node.getLayout() === 'two-column')
|
|
478
|
+
return;
|
|
479
|
+
if (node.getChildrenSize() % 2 === 1)
|
|
480
|
+
node.append($createParagraphNode());
|
|
481
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Toolbar.d.ts","sourceRoot":"","sources":["../../src/components/Toolbar.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Toolbar.d.ts","sourceRoot":"","sources":["../../src/components/Toolbar.tsx"],"names":[],"mappings":"AAqFA,OAAO,EAAgB,KAAK,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAEzE,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,2BAA2B,CAAC;AAC7E,OAAO,EAA6D,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AAItG,OAAO,KAAK,EAAE,MAAM,EAAW,MAAM,iBAAiB,CAAC;AA8gBvD,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,2BAA2B,CAAC;IACpC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sEAAsE;IACtE,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;IAC3B,wFAAwF;IACxF,SAAS,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;IAC1B,oFAAoF;IACpF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kGAAkG;IAClG,MAAM,CAAC,EAAE,oBAAoB,CAAC;CAC/B;AAED,wBAAgB,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,WAAkC,EAAE,SAAiB,EAAE,MAAa,EAAE,MAAM,EAAE,EAAE,YAAY,+BAmkCxJ"}
|