@lexical/markdown 0.49.1-nightly.20260902.0 → 0.50.0
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/LexicalMarkdown.dev.js +231 -41
- package/dist/LexicalMarkdown.dev.mjs +232 -42
- package/dist/LexicalMarkdown.prod.js +1 -1
- package/dist/LexicalMarkdown.prod.mjs +1 -1
- package/dist/MarkdownTransformers.d.ts +13 -0
- package/package.json +10 -10
- package/src/MarkdownImport.ts +66 -37
- package/src/MarkdownTransformers.ts +213 -19
|
@@ -280,6 +280,112 @@ function getIndent(whitespaces) {
|
|
|
280
280
|
}
|
|
281
281
|
return indent;
|
|
282
282
|
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* The column the text after `whitespaces` starts at, expanding a tab to the
|
|
286
|
+
* next multiple of `LIST_INDENT_SIZE` the way CommonMark does.
|
|
287
|
+
*/
|
|
288
|
+
function getColumn(whitespaces) {
|
|
289
|
+
let column = 0;
|
|
290
|
+
for (const char of whitespaces) {
|
|
291
|
+
column += char === '\t' ? LIST_INDENT_SIZE - column % LIST_INDENT_SIZE : 1;
|
|
292
|
+
}
|
|
293
|
+
return column;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* The content columns of the list levels the lines placed so far have left
|
|
298
|
+
* open, outermost first, or null outside a markdown import.
|
|
299
|
+
*
|
|
300
|
+
* A sublist is measured against the column where its parent item's content
|
|
301
|
+
* begins rather than against a fixed number of spaces: that is what lets
|
|
302
|
+
* `1. a` take a three-space sublist while `- a` takes a two-space one, and
|
|
303
|
+
* what keeps two items written at the same column siblings even when that
|
|
304
|
+
* column is deep enough to have opened a level.
|
|
305
|
+
*
|
|
306
|
+
* Only the line that opened a level knows the column it was written at, and
|
|
307
|
+
* nothing in the tree records it afterwards, so the columns are carried from
|
|
308
|
+
* line to line for the length of one import. A shortcut typed into an editor
|
|
309
|
+
* is a line on its own with no such run behind it and keeps reading its indent
|
|
310
|
+
* as a fixed `LIST_INDENT_SIZE` per level, which is the step `$listExport`
|
|
311
|
+
* writes and the one the toolbar and Tab indent by.
|
|
312
|
+
*/
|
|
313
|
+
let importListColumns = null;
|
|
314
|
+
let importJoinsLooseLists = false;
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Run `fn` with the columns of a single markdown import tracked across the
|
|
318
|
+
* lines it places. Restores whatever was being tracked around it, so an import
|
|
319
|
+
* that runs inside another one does not disturb it.
|
|
320
|
+
*
|
|
321
|
+
* `joinLooseLists` reads a blank line between list lines as making the list
|
|
322
|
+
* loose rather than ending it. An import that preserves new lines keeps blank
|
|
323
|
+
* paragraphs as content, so there a blank line closes the list like any other
|
|
324
|
+
* block.
|
|
325
|
+
*
|
|
326
|
+
* @internal
|
|
327
|
+
*/
|
|
328
|
+
function withListIndentColumns(joinLooseLists, fn) {
|
|
329
|
+
const previousColumns = importListColumns;
|
|
330
|
+
const previousJoins = importJoinsLooseLists;
|
|
331
|
+
importListColumns = [];
|
|
332
|
+
importJoinsLooseLists = joinLooseLists;
|
|
333
|
+
try {
|
|
334
|
+
return fn();
|
|
335
|
+
} finally {
|
|
336
|
+
importListColumns = previousColumns;
|
|
337
|
+
importJoinsLooseLists = previousJoins;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* The indent `whitespaces` names given the levels open above it: the innermost
|
|
343
|
+
* level whose content column it reaches, or 0 when it reaches none of them.
|
|
344
|
+
*/
|
|
345
|
+
function getColumnIndent(columns, whitespaces) {
|
|
346
|
+
const column = getColumn(whitespaces);
|
|
347
|
+
for (let i = columns.length - 1; i >= 0; i--) {
|
|
348
|
+
if (column >= columns[i]) {
|
|
349
|
+
return i + 1;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return 0;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* The column this line opens its content at: where the matched marker ends,
|
|
357
|
+
* with tabs expanded, so `-\ta` opens at the tab stop and not two columns in.
|
|
358
|
+
* A check list item is marked by its bullet — the `[ ]` is content — so
|
|
359
|
+
* `- [ ] a` opens where `- a` does. Capped one `LIST_INDENT_SIZE` past the
|
|
360
|
+
* marker so that the indent `$listExport` writes always nests: a sublist of
|
|
361
|
+
* `100. ` is exported with four spaces, short of that marker's real content
|
|
362
|
+
* column.
|
|
363
|
+
*/
|
|
364
|
+
function getContentColumn(match, listType) {
|
|
365
|
+
let prefix = match[0];
|
|
366
|
+
if (listType === 'check') {
|
|
367
|
+
const bullet = prefix.slice(match[1].length).match(/^[-*+]\s/);
|
|
368
|
+
if (bullet) {
|
|
369
|
+
prefix = match[1] + bullet[0];
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return Math.min(getColumn(prefix), getColumn(match[1]) + LIST_INDENT_SIZE);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Record the content column that this line leaves open for the lines below it,
|
|
377
|
+
* closing the levels it stepped back out of.
|
|
378
|
+
*/
|
|
379
|
+
function setOpenColumn(columns, indent, match, listType) {
|
|
380
|
+
// An indent read from `getIndent` rather than from the columns can name a
|
|
381
|
+
// level no line of this import opened, so any gap below it is filled with
|
|
382
|
+
// the fixed step that reading assumed.
|
|
383
|
+
for (let i = columns.length; i < indent; i++) {
|
|
384
|
+
columns[i] = (i + 1) * LIST_INDENT_SIZE;
|
|
385
|
+
}
|
|
386
|
+
columns.length = indent;
|
|
387
|
+
columns[indent] = getContentColumn(match, listType);
|
|
388
|
+
}
|
|
283
389
|
const listReplace = listType => {
|
|
284
390
|
return (parentNode, children, match, isImport) => {
|
|
285
391
|
if (richText.$isHeadingNode(parentNode) || !isImport && $isUnreplaceableBlock(parentNode)) {
|
|
@@ -287,13 +393,34 @@ const listReplace = listType => {
|
|
|
287
393
|
}
|
|
288
394
|
const previousNode = parentNode.getPreviousSibling();
|
|
289
395
|
const nextNode = parentNode.getNextSibling();
|
|
290
|
-
const listItem = list.$createListItemNode(
|
|
396
|
+
const listItem = list.$createListItemNode(
|
|
397
|
+
// CHECK_LIST_REGEX matches case-insensitively, so `[X]` is checked too.
|
|
398
|
+
listType === 'check' ? /^x$/i.test(match[3] || '') : undefined);
|
|
291
399
|
const firstMatchChar = match[0].trim()[0];
|
|
292
400
|
const listMarker = (listType === 'bullet' || listType === 'check') && firstMatchChar === listMarkerState.parse(firstMatchChar) ? firstMatchChar : undefined;
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
401
|
+
// A block of another kind closes every level above this line. Blank lines
|
|
402
|
+
// do not: they only make the list loose, and a blank line must not decide
|
|
403
|
+
// how the line after it is read, or the same sublist would be read one way
|
|
404
|
+
// written tightly and another written with a blank line before it. So the
|
|
405
|
+
// line is measured against, and placed into, the list it follows across
|
|
406
|
+
// any blank lines, and the levels close only when the nearest block that
|
|
407
|
+
// is not one is no longer the list itself. Outside such an import — a
|
|
408
|
+
// typed shortcut, or an import that keeps its blank lines as content — a
|
|
409
|
+
// blank line above the line is a block like any other.
|
|
410
|
+
const columns = importListColumns;
|
|
411
|
+
let precedingBlock = previousNode;
|
|
412
|
+
if (columns !== null) {
|
|
413
|
+
if (importJoinsLooseLists) {
|
|
414
|
+
while (precedingBlock !== null && isEmptyParagraph(precedingBlock)) {
|
|
415
|
+
precedingBlock = precedingBlock.getPreviousSibling();
|
|
416
|
+
}
|
|
296
417
|
}
|
|
418
|
+
if (!list.$isListNode(precedingBlock)) {
|
|
419
|
+
columns.length = 0;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
const indent = columns === null || columns.length === 0 ? getIndent(match[1]) : getColumnIndent(columns, match[1]);
|
|
423
|
+
if (list.$isListNode(nextNode) && nextNode.getListType() === listType) {
|
|
297
424
|
const firstChild = nextNode.getFirstChild();
|
|
298
425
|
if (firstChild !== null) {
|
|
299
426
|
firstChild.insertBefore(listItem);
|
|
@@ -302,24 +429,24 @@ const listReplace = listType => {
|
|
|
302
429
|
nextNode.append(listItem);
|
|
303
430
|
}
|
|
304
431
|
// The new list item lands at index 0, so the typed number becomes the
|
|
305
|
-
// list's starting value. #8677.
|
|
306
|
-
|
|
432
|
+
// list's starting value. #8677. An indented item only passes through —
|
|
433
|
+
// `setIndent` below moves it into a sublist — so its number belongs to
|
|
434
|
+
// that sublist, not to the list it leaves.
|
|
435
|
+
if (listType === 'number' && indent === 0) {
|
|
307
436
|
nextNode.setStart(Number(match[2]));
|
|
308
437
|
}
|
|
309
438
|
parentNode.remove();
|
|
310
|
-
} else if (list.$isListNode(
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
|
|
439
|
+
} else if (list.$isListNode(precedingBlock) && (precedingBlock.getListType() === listType || indent > 0)) {
|
|
440
|
+
// An item of the same type continues the list, and inherits the
|
|
441
|
+
// existing sequence — the typed number is intentionally ignored. An
|
|
442
|
+
// indented item of another type belongs inside it too: it is appended
|
|
443
|
+
// at the top level here, `setIndent` below moves it to its level, and a
|
|
444
|
+
// list of the right type is spliced in once the level it lands in is
|
|
445
|
+
// known.
|
|
446
|
+
precedingBlock.append(listItem);
|
|
317
447
|
parentNode.remove();
|
|
318
448
|
} else {
|
|
319
449
|
const list$1 = list.$createListNode(listType, listType === 'number' ? Number(match[2]) : undefined);
|
|
320
|
-
if (listMarker) {
|
|
321
|
-
lexical.$setState(list$1, listMarkerState, listMarker);
|
|
322
|
-
}
|
|
323
450
|
list$1.append(listItem);
|
|
324
451
|
parentNode.replace(list$1);
|
|
325
452
|
}
|
|
@@ -327,12 +454,58 @@ const listReplace = listType => {
|
|
|
327
454
|
if (!isImport) {
|
|
328
455
|
listItem.select(0, 0);
|
|
329
456
|
}
|
|
330
|
-
const indent = getIndent(match[1]);
|
|
331
457
|
if (indent) {
|
|
332
458
|
listItem.setIndent(indent);
|
|
459
|
+
$retypeNestedList(listItem, listType, match);
|
|
460
|
+
}
|
|
461
|
+
// The marker belongs to the list the item actually ends up in, which is
|
|
462
|
+
// only known once it has been indented and retyped.
|
|
463
|
+
const listNode = listItem.getParent();
|
|
464
|
+
if (listMarker && list.$isListNode(listNode)) {
|
|
465
|
+
lexical.$setState(listNode, listMarkerState, listMarker);
|
|
466
|
+
}
|
|
467
|
+
if (columns !== null) {
|
|
468
|
+
setOpenColumn(columns, indent, match, listType);
|
|
333
469
|
}
|
|
334
470
|
};
|
|
335
471
|
};
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* `setIndent` nests an item by copying the list it is in, so an item whose
|
|
475
|
+
* type differs from the list above it lands in a nested list of the wrong
|
|
476
|
+
* type. Split it out into a list of its own type, in place, so that a sublist
|
|
477
|
+
* can change type the way CommonMark lets it — `1. a` may be followed by an
|
|
478
|
+
* indented `- b`, and that by an indented `- [ ] c`.
|
|
479
|
+
*/
|
|
480
|
+
function $retypeNestedList(listItem, listType, match) {
|
|
481
|
+
const nestedList = listItem.getParent();
|
|
482
|
+
if (!list.$isListNode(nestedList) || nestedList.getListType() === listType) {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const wrapper = nestedList.getParent();
|
|
486
|
+
if (!list.$isListItemNode(wrapper)) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
const retypedList = list.$createListNode(listType, listType === 'number' ? Number(match[2]) : undefined);
|
|
490
|
+
// `setIndent` either appends the item to the nested list or puts it in
|
|
491
|
+
// front of it, so the list of its own type belongs on whichever side of the
|
|
492
|
+
// one it was placed in it already sits on.
|
|
493
|
+
const isFirst = listItem.getPreviousSibling() === null;
|
|
494
|
+
// `append` moves the item without disturbing the selection, which
|
|
495
|
+
// `remove` would relocate to a sibling and leave there — the caret has to
|
|
496
|
+
// stay in the item the shortcut just created.
|
|
497
|
+
retypedList.append(listItem);
|
|
498
|
+
const retypedWrapper = list.$createListItemNode();
|
|
499
|
+
retypedWrapper.append(retypedList);
|
|
500
|
+
if (isFirst) {
|
|
501
|
+
wrapper.insertBefore(retypedWrapper);
|
|
502
|
+
} else {
|
|
503
|
+
wrapper.insertAfter(retypedWrapper);
|
|
504
|
+
}
|
|
505
|
+
if (nestedList.getChildrenSize() === 0) {
|
|
506
|
+
wrapper.remove();
|
|
507
|
+
}
|
|
508
|
+
}
|
|
336
509
|
const $listExport = (listNode, exportChildren, depth, selection) => {
|
|
337
510
|
const output = [];
|
|
338
511
|
const children = listNode.getChildren();
|
|
@@ -1702,15 +1875,22 @@ function $importMarkdownNodes(markdownString, container, transformers, shouldPre
|
|
|
1702
1875
|
const textFormatTransformersIndex = createTextFormatTransformersIndex(byType.textFormat);
|
|
1703
1876
|
const lines = markdownString.split('\n');
|
|
1704
1877
|
const linesLength = lines.length;
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1878
|
+
|
|
1879
|
+
// A list line is measured against the column its parent item's content
|
|
1880
|
+
// starts at, which only the line that opened that level knows. Blank lines
|
|
1881
|
+
// between list lines make the list loose rather than ending it — except
|
|
1882
|
+
// when they are being preserved, where they are content like any block.
|
|
1883
|
+
withListIndentColumns(!shouldPreserveNewLines, () => {
|
|
1884
|
+
for (let i = 0; i < linesLength; i++) {
|
|
1885
|
+
const lineText = lines[i];
|
|
1886
|
+
const [imported, shiftedIndex] = $importMultiline(lines, i, byType.multilineElement, container);
|
|
1887
|
+
if (imported) {
|
|
1888
|
+
i = shiftedIndex;
|
|
1889
|
+
continue;
|
|
1890
|
+
}
|
|
1891
|
+
$importBlocks(lineText, container, byType.element, textFormatTransformersIndex, byType.textMatch, shouldPreserveNewLines);
|
|
1711
1892
|
}
|
|
1712
|
-
|
|
1713
|
-
}
|
|
1893
|
+
});
|
|
1714
1894
|
const children = container.getChildren();
|
|
1715
1895
|
for (const child of children) {
|
|
1716
1896
|
if (!shouldPreserveNewLines && isEmptyParagraph(child) && container.getChildrenSize() > 1) {
|
|
@@ -1854,26 +2034,36 @@ function $importBlocks(lineText, rootNode, elementTransformers, textFormatTransf
|
|
|
1854
2034
|
}
|
|
1855
2035
|
}
|
|
1856
2036
|
|
|
1857
|
-
// Look in node for '\t' and create a TabNode for each occurrence.
|
|
2037
|
+
// Look in node for '\t' and create a TabNode for each occurrence. The
|
|
2038
|
+
// replacement nodes are built directly rather than through
|
|
2039
|
+
// `splitText(...offsets)`: spreading one argument per tab boundary overflows
|
|
2040
|
+
// the call stack on a long run of tabs, and the text can hold arbitrarily
|
|
2041
|
+
// many.
|
|
1858
2042
|
function $normalizeMarkdownTextNode(textNode) {
|
|
1859
|
-
|
|
2043
|
+
// A TabNode is a TextNode whose content is a tab, so without this guard the
|
|
2044
|
+
// rebuild below would destroy it and create an equivalent one in its place.
|
|
2045
|
+
if (lexical.$isTabNode(textNode)) {
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
1860
2048
|
const text = textNode.getTextContent();
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
// Find all tab occurrences
|
|
1864
|
-
while (index !== -1) {
|
|
1865
|
-
tabOffsets.add(index);
|
|
1866
|
-
tabOffsets.add(index + 1);
|
|
1867
|
-
index = text.indexOf('\t', index + 1);
|
|
2049
|
+
if (!text.includes('\t')) {
|
|
2050
|
+
return;
|
|
1868
2051
|
}
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
const
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
2052
|
+
const format = textNode.getFormat();
|
|
2053
|
+
const style = textNode.getStyle();
|
|
2054
|
+
const nodes = [];
|
|
2055
|
+
let start = 0;
|
|
2056
|
+
for (let index = text.indexOf('\t'); index !== -1; index = text.indexOf('\t', index + 1)) {
|
|
2057
|
+
if (index > start) {
|
|
2058
|
+
nodes.push(lexical.$createTextNode(text.slice(start, index)).setFormat(format).setStyle(style));
|
|
1875
2059
|
}
|
|
1876
|
-
|
|
2060
|
+
nodes.push(lexical.$createTabNode());
|
|
2061
|
+
start = index + 1;
|
|
2062
|
+
}
|
|
2063
|
+
if (start < text.length) {
|
|
2064
|
+
nodes.push(lexical.$createTextNode(text.slice(start)).setFormat(format).setStyle(style));
|
|
2065
|
+
}
|
|
2066
|
+
textNode.getParentOrThrow().splice(textNode.getIndexWithinParent(), 1, nodes);
|
|
1877
2067
|
}
|
|
1878
2068
|
function createTextFormatTransformersIndex(textTransformers) {
|
|
1879
2069
|
const transformersByTag = {};
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { $isParagraphNode, $isTextNode, $setState, $createTextNode, $getState, $findMatchingParent, $createLineBreakNode, createState, $isLineBreakNode, $getRoot, $isElementNode, $isDecoratorNode, $
|
|
9
|
+
import { $isParagraphNode, $isTextNode, $setState, $createTextNode, $getState, $findMatchingParent, $createLineBreakNode, createState, $isLineBreakNode, $getRoot, $isElementNode, $isDecoratorNode, $isTabNode, $createTabNode, $createParagraphNode, mergeRegister, COLLABORATION_TAG, HISTORIC_TAG, COMPOSITION_END_TAG, $getSelection, $isRangeSelection, $addUpdateTag, HISTORY_PUSH_TAG, KEY_ENTER_COMMAND, COMMAND_PRIORITY_LOW, $isRootOrShadowRoot, $createRangeSelection, $setSelection, TEXT_TYPE_TO_FORMAT, ArtificialNode__DO_NOT_USE } from 'lexical';
|
|
10
10
|
import { $sliceSelectedTextNodeContent } from '@lexical/selection';
|
|
11
11
|
import { CodeNode, $createCodeNode, $isCodeNode } from '@lexical/code-core';
|
|
12
12
|
import { LinkNode, $isLinkNode, $createLinkNode, $isAutoLinkNode } from '@lexical/link';
|
|
@@ -278,6 +278,112 @@ function getIndent(whitespaces) {
|
|
|
278
278
|
}
|
|
279
279
|
return indent;
|
|
280
280
|
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* The column the text after `whitespaces` starts at, expanding a tab to the
|
|
284
|
+
* next multiple of `LIST_INDENT_SIZE` the way CommonMark does.
|
|
285
|
+
*/
|
|
286
|
+
function getColumn(whitespaces) {
|
|
287
|
+
let column = 0;
|
|
288
|
+
for (const char of whitespaces) {
|
|
289
|
+
column += char === '\t' ? LIST_INDENT_SIZE - column % LIST_INDENT_SIZE : 1;
|
|
290
|
+
}
|
|
291
|
+
return column;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* The content columns of the list levels the lines placed so far have left
|
|
296
|
+
* open, outermost first, or null outside a markdown import.
|
|
297
|
+
*
|
|
298
|
+
* A sublist is measured against the column where its parent item's content
|
|
299
|
+
* begins rather than against a fixed number of spaces: that is what lets
|
|
300
|
+
* `1. a` take a three-space sublist while `- a` takes a two-space one, and
|
|
301
|
+
* what keeps two items written at the same column siblings even when that
|
|
302
|
+
* column is deep enough to have opened a level.
|
|
303
|
+
*
|
|
304
|
+
* Only the line that opened a level knows the column it was written at, and
|
|
305
|
+
* nothing in the tree records it afterwards, so the columns are carried from
|
|
306
|
+
* line to line for the length of one import. A shortcut typed into an editor
|
|
307
|
+
* is a line on its own with no such run behind it and keeps reading its indent
|
|
308
|
+
* as a fixed `LIST_INDENT_SIZE` per level, which is the step `$listExport`
|
|
309
|
+
* writes and the one the toolbar and Tab indent by.
|
|
310
|
+
*/
|
|
311
|
+
let importListColumns = null;
|
|
312
|
+
let importJoinsLooseLists = false;
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Run `fn` with the columns of a single markdown import tracked across the
|
|
316
|
+
* lines it places. Restores whatever was being tracked around it, so an import
|
|
317
|
+
* that runs inside another one does not disturb it.
|
|
318
|
+
*
|
|
319
|
+
* `joinLooseLists` reads a blank line between list lines as making the list
|
|
320
|
+
* loose rather than ending it. An import that preserves new lines keeps blank
|
|
321
|
+
* paragraphs as content, so there a blank line closes the list like any other
|
|
322
|
+
* block.
|
|
323
|
+
*
|
|
324
|
+
* @internal
|
|
325
|
+
*/
|
|
326
|
+
function withListIndentColumns(joinLooseLists, fn) {
|
|
327
|
+
const previousColumns = importListColumns;
|
|
328
|
+
const previousJoins = importJoinsLooseLists;
|
|
329
|
+
importListColumns = [];
|
|
330
|
+
importJoinsLooseLists = joinLooseLists;
|
|
331
|
+
try {
|
|
332
|
+
return fn();
|
|
333
|
+
} finally {
|
|
334
|
+
importListColumns = previousColumns;
|
|
335
|
+
importJoinsLooseLists = previousJoins;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* The indent `whitespaces` names given the levels open above it: the innermost
|
|
341
|
+
* level whose content column it reaches, or 0 when it reaches none of them.
|
|
342
|
+
*/
|
|
343
|
+
function getColumnIndent(columns, whitespaces) {
|
|
344
|
+
const column = getColumn(whitespaces);
|
|
345
|
+
for (let i = columns.length - 1; i >= 0; i--) {
|
|
346
|
+
if (column >= columns[i]) {
|
|
347
|
+
return i + 1;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return 0;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* The column this line opens its content at: where the matched marker ends,
|
|
355
|
+
* with tabs expanded, so `-\ta` opens at the tab stop and not two columns in.
|
|
356
|
+
* A check list item is marked by its bullet — the `[ ]` is content — so
|
|
357
|
+
* `- [ ] a` opens where `- a` does. Capped one `LIST_INDENT_SIZE` past the
|
|
358
|
+
* marker so that the indent `$listExport` writes always nests: a sublist of
|
|
359
|
+
* `100. ` is exported with four spaces, short of that marker's real content
|
|
360
|
+
* column.
|
|
361
|
+
*/
|
|
362
|
+
function getContentColumn(match, listType) {
|
|
363
|
+
let prefix = match[0];
|
|
364
|
+
if (listType === 'check') {
|
|
365
|
+
const bullet = prefix.slice(match[1].length).match(/^[-*+]\s/);
|
|
366
|
+
if (bullet) {
|
|
367
|
+
prefix = match[1] + bullet[0];
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return Math.min(getColumn(prefix), getColumn(match[1]) + LIST_INDENT_SIZE);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Record the content column that this line leaves open for the lines below it,
|
|
375
|
+
* closing the levels it stepped back out of.
|
|
376
|
+
*/
|
|
377
|
+
function setOpenColumn(columns, indent, match, listType) {
|
|
378
|
+
// An indent read from `getIndent` rather than from the columns can name a
|
|
379
|
+
// level no line of this import opened, so any gap below it is filled with
|
|
380
|
+
// the fixed step that reading assumed.
|
|
381
|
+
for (let i = columns.length; i < indent; i++) {
|
|
382
|
+
columns[i] = (i + 1) * LIST_INDENT_SIZE;
|
|
383
|
+
}
|
|
384
|
+
columns.length = indent;
|
|
385
|
+
columns[indent] = getContentColumn(match, listType);
|
|
386
|
+
}
|
|
281
387
|
const listReplace = listType => {
|
|
282
388
|
return (parentNode, children, match, isImport) => {
|
|
283
389
|
if ($isHeadingNode(parentNode) || !isImport && $isUnreplaceableBlock(parentNode)) {
|
|
@@ -285,13 +391,34 @@ const listReplace = listType => {
|
|
|
285
391
|
}
|
|
286
392
|
const previousNode = parentNode.getPreviousSibling();
|
|
287
393
|
const nextNode = parentNode.getNextSibling();
|
|
288
|
-
const listItem = $createListItemNode(
|
|
394
|
+
const listItem = $createListItemNode(
|
|
395
|
+
// CHECK_LIST_REGEX matches case-insensitively, so `[X]` is checked too.
|
|
396
|
+
listType === 'check' ? /^x$/i.test(match[3] || '') : undefined);
|
|
289
397
|
const firstMatchChar = match[0].trim()[0];
|
|
290
398
|
const listMarker = (listType === 'bullet' || listType === 'check') && firstMatchChar === listMarkerState.parse(firstMatchChar) ? firstMatchChar : undefined;
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
399
|
+
// A block of another kind closes every level above this line. Blank lines
|
|
400
|
+
// do not: they only make the list loose, and a blank line must not decide
|
|
401
|
+
// how the line after it is read, or the same sublist would be read one way
|
|
402
|
+
// written tightly and another written with a blank line before it. So the
|
|
403
|
+
// line is measured against, and placed into, the list it follows across
|
|
404
|
+
// any blank lines, and the levels close only when the nearest block that
|
|
405
|
+
// is not one is no longer the list itself. Outside such an import — a
|
|
406
|
+
// typed shortcut, or an import that keeps its blank lines as content — a
|
|
407
|
+
// blank line above the line is a block like any other.
|
|
408
|
+
const columns = importListColumns;
|
|
409
|
+
let precedingBlock = previousNode;
|
|
410
|
+
if (columns !== null) {
|
|
411
|
+
if (importJoinsLooseLists) {
|
|
412
|
+
while (precedingBlock !== null && isEmptyParagraph(precedingBlock)) {
|
|
413
|
+
precedingBlock = precedingBlock.getPreviousSibling();
|
|
414
|
+
}
|
|
294
415
|
}
|
|
416
|
+
if (!$isListNode(precedingBlock)) {
|
|
417
|
+
columns.length = 0;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
const indent = columns === null || columns.length === 0 ? getIndent(match[1]) : getColumnIndent(columns, match[1]);
|
|
421
|
+
if ($isListNode(nextNode) && nextNode.getListType() === listType) {
|
|
295
422
|
const firstChild = nextNode.getFirstChild();
|
|
296
423
|
if (firstChild !== null) {
|
|
297
424
|
firstChild.insertBefore(listItem);
|
|
@@ -300,24 +427,24 @@ const listReplace = listType => {
|
|
|
300
427
|
nextNode.append(listItem);
|
|
301
428
|
}
|
|
302
429
|
// The new list item lands at index 0, so the typed number becomes the
|
|
303
|
-
// list's starting value. #8677.
|
|
304
|
-
|
|
430
|
+
// list's starting value. #8677. An indented item only passes through —
|
|
431
|
+
// `setIndent` below moves it into a sublist — so its number belongs to
|
|
432
|
+
// that sublist, not to the list it leaves.
|
|
433
|
+
if (listType === 'number' && indent === 0) {
|
|
305
434
|
nextNode.setStart(Number(match[2]));
|
|
306
435
|
}
|
|
307
436
|
parentNode.remove();
|
|
308
|
-
} else if ($isListNode(
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
//
|
|
313
|
-
//
|
|
314
|
-
|
|
437
|
+
} else if ($isListNode(precedingBlock) && (precedingBlock.getListType() === listType || indent > 0)) {
|
|
438
|
+
// An item of the same type continues the list, and inherits the
|
|
439
|
+
// existing sequence — the typed number is intentionally ignored. An
|
|
440
|
+
// indented item of another type belongs inside it too: it is appended
|
|
441
|
+
// at the top level here, `setIndent` below moves it to its level, and a
|
|
442
|
+
// list of the right type is spliced in once the level it lands in is
|
|
443
|
+
// known.
|
|
444
|
+
precedingBlock.append(listItem);
|
|
315
445
|
parentNode.remove();
|
|
316
446
|
} else {
|
|
317
447
|
const list = $createListNode(listType, listType === 'number' ? Number(match[2]) : undefined);
|
|
318
|
-
if (listMarker) {
|
|
319
|
-
$setState(list, listMarkerState, listMarker);
|
|
320
|
-
}
|
|
321
448
|
list.append(listItem);
|
|
322
449
|
parentNode.replace(list);
|
|
323
450
|
}
|
|
@@ -325,12 +452,58 @@ const listReplace = listType => {
|
|
|
325
452
|
if (!isImport) {
|
|
326
453
|
listItem.select(0, 0);
|
|
327
454
|
}
|
|
328
|
-
const indent = getIndent(match[1]);
|
|
329
455
|
if (indent) {
|
|
330
456
|
listItem.setIndent(indent);
|
|
457
|
+
$retypeNestedList(listItem, listType, match);
|
|
458
|
+
}
|
|
459
|
+
// The marker belongs to the list the item actually ends up in, which is
|
|
460
|
+
// only known once it has been indented and retyped.
|
|
461
|
+
const listNode = listItem.getParent();
|
|
462
|
+
if (listMarker && $isListNode(listNode)) {
|
|
463
|
+
$setState(listNode, listMarkerState, listMarker);
|
|
464
|
+
}
|
|
465
|
+
if (columns !== null) {
|
|
466
|
+
setOpenColumn(columns, indent, match, listType);
|
|
331
467
|
}
|
|
332
468
|
};
|
|
333
469
|
};
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* `setIndent` nests an item by copying the list it is in, so an item whose
|
|
473
|
+
* type differs from the list above it lands in a nested list of the wrong
|
|
474
|
+
* type. Split it out into a list of its own type, in place, so that a sublist
|
|
475
|
+
* can change type the way CommonMark lets it — `1. a` may be followed by an
|
|
476
|
+
* indented `- b`, and that by an indented `- [ ] c`.
|
|
477
|
+
*/
|
|
478
|
+
function $retypeNestedList(listItem, listType, match) {
|
|
479
|
+
const nestedList = listItem.getParent();
|
|
480
|
+
if (!$isListNode(nestedList) || nestedList.getListType() === listType) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
const wrapper = nestedList.getParent();
|
|
484
|
+
if (!$isListItemNode(wrapper)) {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
const retypedList = $createListNode(listType, listType === 'number' ? Number(match[2]) : undefined);
|
|
488
|
+
// `setIndent` either appends the item to the nested list or puts it in
|
|
489
|
+
// front of it, so the list of its own type belongs on whichever side of the
|
|
490
|
+
// one it was placed in it already sits on.
|
|
491
|
+
const isFirst = listItem.getPreviousSibling() === null;
|
|
492
|
+
// `append` moves the item without disturbing the selection, which
|
|
493
|
+
// `remove` would relocate to a sibling and leave there — the caret has to
|
|
494
|
+
// stay in the item the shortcut just created.
|
|
495
|
+
retypedList.append(listItem);
|
|
496
|
+
const retypedWrapper = $createListItemNode();
|
|
497
|
+
retypedWrapper.append(retypedList);
|
|
498
|
+
if (isFirst) {
|
|
499
|
+
wrapper.insertBefore(retypedWrapper);
|
|
500
|
+
} else {
|
|
501
|
+
wrapper.insertAfter(retypedWrapper);
|
|
502
|
+
}
|
|
503
|
+
if (nestedList.getChildrenSize() === 0) {
|
|
504
|
+
wrapper.remove();
|
|
505
|
+
}
|
|
506
|
+
}
|
|
334
507
|
const $listExport = (listNode, exportChildren, depth, selection) => {
|
|
335
508
|
const output = [];
|
|
336
509
|
const children = listNode.getChildren();
|
|
@@ -1700,15 +1873,22 @@ function $importMarkdownNodes(markdownString, container, transformers, shouldPre
|
|
|
1700
1873
|
const textFormatTransformersIndex = createTextFormatTransformersIndex(byType.textFormat);
|
|
1701
1874
|
const lines = markdownString.split('\n');
|
|
1702
1875
|
const linesLength = lines.length;
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1876
|
+
|
|
1877
|
+
// A list line is measured against the column its parent item's content
|
|
1878
|
+
// starts at, which only the line that opened that level knows. Blank lines
|
|
1879
|
+
// between list lines make the list loose rather than ending it — except
|
|
1880
|
+
// when they are being preserved, where they are content like any block.
|
|
1881
|
+
withListIndentColumns(!shouldPreserveNewLines, () => {
|
|
1882
|
+
for (let i = 0; i < linesLength; i++) {
|
|
1883
|
+
const lineText = lines[i];
|
|
1884
|
+
const [imported, shiftedIndex] = $importMultiline(lines, i, byType.multilineElement, container);
|
|
1885
|
+
if (imported) {
|
|
1886
|
+
i = shiftedIndex;
|
|
1887
|
+
continue;
|
|
1888
|
+
}
|
|
1889
|
+
$importBlocks(lineText, container, byType.element, textFormatTransformersIndex, byType.textMatch, shouldPreserveNewLines);
|
|
1709
1890
|
}
|
|
1710
|
-
|
|
1711
|
-
}
|
|
1891
|
+
});
|
|
1712
1892
|
const children = container.getChildren();
|
|
1713
1893
|
for (const child of children) {
|
|
1714
1894
|
if (!shouldPreserveNewLines && isEmptyParagraph(child) && container.getChildrenSize() > 1) {
|
|
@@ -1852,26 +2032,36 @@ function $importBlocks(lineText, rootNode, elementTransformers, textFormatTransf
|
|
|
1852
2032
|
}
|
|
1853
2033
|
}
|
|
1854
2034
|
|
|
1855
|
-
// Look in node for '\t' and create a TabNode for each occurrence.
|
|
2035
|
+
// Look in node for '\t' and create a TabNode for each occurrence. The
|
|
2036
|
+
// replacement nodes are built directly rather than through
|
|
2037
|
+
// `splitText(...offsets)`: spreading one argument per tab boundary overflows
|
|
2038
|
+
// the call stack on a long run of tabs, and the text can hold arbitrarily
|
|
2039
|
+
// many.
|
|
1856
2040
|
function $normalizeMarkdownTextNode(textNode) {
|
|
1857
|
-
|
|
2041
|
+
// A TabNode is a TextNode whose content is a tab, so without this guard the
|
|
2042
|
+
// rebuild below would destroy it and create an equivalent one in its place.
|
|
2043
|
+
if ($isTabNode(textNode)) {
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
1858
2046
|
const text = textNode.getTextContent();
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
// Find all tab occurrences
|
|
1862
|
-
while (index !== -1) {
|
|
1863
|
-
tabOffsets.add(index);
|
|
1864
|
-
tabOffsets.add(index + 1);
|
|
1865
|
-
index = text.indexOf('\t', index + 1);
|
|
2047
|
+
if (!text.includes('\t')) {
|
|
2048
|
+
return;
|
|
1866
2049
|
}
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
const
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
2050
|
+
const format = textNode.getFormat();
|
|
2051
|
+
const style = textNode.getStyle();
|
|
2052
|
+
const nodes = [];
|
|
2053
|
+
let start = 0;
|
|
2054
|
+
for (let index = text.indexOf('\t'); index !== -1; index = text.indexOf('\t', index + 1)) {
|
|
2055
|
+
if (index > start) {
|
|
2056
|
+
nodes.push($createTextNode(text.slice(start, index)).setFormat(format).setStyle(style));
|
|
1873
2057
|
}
|
|
1874
|
-
|
|
2058
|
+
nodes.push($createTabNode());
|
|
2059
|
+
start = index + 1;
|
|
2060
|
+
}
|
|
2061
|
+
if (start < text.length) {
|
|
2062
|
+
nodes.push($createTextNode(text.slice(start)).setFormat(format).setStyle(style));
|
|
2063
|
+
}
|
|
2064
|
+
textNode.getParentOrThrow().splice(textNode.getIndexWithinParent(), 1, nodes);
|
|
1875
2065
|
}
|
|
1876
2066
|
function createTextFormatTransformersIndex(textTransformers) {
|
|
1877
2067
|
const transformersByTag = {};
|