@texuli/core 0.5.0 → 0.6.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/README.md +29 -5
- package/dist/editor/document.js +8 -1
- package/dist/editor/extensions.d.ts +64 -0
- package/dist/editor/extensions.js +116 -0
- package/dist/editor/inline.d.ts +5 -3
- package/dist/editor/inline.js +24 -12
- package/dist/editor/load.d.ts +10 -0
- package/dist/editor/load.js +16 -0
- package/dist/editor/markdown.d.ts +10 -1
- package/dist/editor/markdown.js +193 -83
- package/dist/editor/model.d.ts +3 -1
- package/dist/editor/model.js +3 -1
- package/dist/editor/transforms.d.ts +1 -1
- package/dist/editor/tree.d.ts +11 -1
- package/dist/editor/tree.js +11 -2
- package/dist/editor/types.d.ts +10 -0
- package/dist/editor/types.js +1 -0
- package/dist/inert/inert.d.ts +11 -0
- package/dist/inert/inert.js +5 -3
- package/dist/markup/index.d.ts +5 -0
- package/dist/markup/index.js +5 -0
- package/dist/markup/markup.d.ts +72 -0
- package/dist/markup/markup.js +233 -0
- package/dist/motion/animate.d.ts +109 -0
- package/dist/motion/animate.js +225 -0
- package/dist/motion/flip.d.ts +58 -0
- package/dist/motion/flip.js +167 -0
- package/dist/motion/index.d.ts +15 -0
- package/dist/motion/index.js +15 -0
- package/dist/motion/preferences.d.ts +68 -0
- package/dist/motion/preferences.js +111 -0
- package/dist/motion/presence.d.ts +61 -0
- package/dist/motion/presence.js +231 -0
- package/dist/motion/sequence.d.ts +70 -0
- package/dist/motion/sequence.js +159 -0
- package/dist/motion/shared.d.ts +45 -0
- package/dist/motion/shared.js +144 -0
- package/dist/motion/spring.d.ts +109 -0
- package/dist/motion/spring.js +166 -0
- package/dist/overlay/core.js +14 -3
- package/dist/zip/index.d.ts +5 -0
- package/dist/zip/index.js +5 -0
- package/dist/zip/zip.d.ts +35 -0
- package/dist/zip/zip.js +190 -0
- package/package.json +6 -2
package/dist/editor/markdown.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalizeDocument, paragraph } from "./document.js";
|
|
2
|
+
import { acceptsExtensionId, EDITOR_EXTENSION_LIMITS, isExtensionIdChar, toInlineExtensions, } from "./extensions.js";
|
|
2
3
|
import { markKey, normalizeInline, sortMarks } from "./inline.js";
|
|
3
4
|
import { LoadBudget } from "./load.js";
|
|
4
5
|
/**
|
|
@@ -299,9 +300,51 @@ class Scan {
|
|
|
299
300
|
source;
|
|
300
301
|
closers = new Map();
|
|
301
302
|
brackets = null;
|
|
303
|
+
tokens = null;
|
|
302
304
|
constructor(source) {
|
|
303
305
|
this.source = source;
|
|
304
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* The extension token `<sigil>{id|label}` whose sigil is at `at`, or null.
|
|
309
|
+
* Two linear passes the first time (the id-character run from each index,
|
|
310
|
+
* and the next unescaped `}` before a newline), then constant time.
|
|
311
|
+
*/
|
|
312
|
+
extension(at) {
|
|
313
|
+
const { source } = this;
|
|
314
|
+
if (source[at + 1] !== '{')
|
|
315
|
+
return null;
|
|
316
|
+
if (!this.tokens) {
|
|
317
|
+
const n = source.length;
|
|
318
|
+
const idRun = new Int32Array(n + 1);
|
|
319
|
+
for (let j = n - 1; j >= 0; j -= 1)
|
|
320
|
+
idRun[j] = isExtensionIdChar(source.charCodeAt(j)) ? idRun[j + 1] + 1 : 0;
|
|
321
|
+
const escaped = new Uint8Array(n);
|
|
322
|
+
for (let j = 0; j < n; j += 1) {
|
|
323
|
+
if (source[j] === '\\') {
|
|
324
|
+
if (j + 1 < n)
|
|
325
|
+
escaped[j + 1] = 1;
|
|
326
|
+
j += 1;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const close = new Int32Array(n + 1).fill(-1);
|
|
330
|
+
for (let j = n - 1; j >= 0; j -= 1) {
|
|
331
|
+
const c = source[j];
|
|
332
|
+
close[j] = c === '\n' ? -1 : c === '}' && !escaped[j] ? j : close[j + 1];
|
|
333
|
+
}
|
|
334
|
+
this.tokens = { idRun, close };
|
|
335
|
+
}
|
|
336
|
+
const idStart = at + 2;
|
|
337
|
+
const idEnd = idStart + (this.tokens.idRun[idStart] ?? 0);
|
|
338
|
+
if (idEnd === idStart || idEnd - idStart > EDITOR_EXTENSION_LIMITS.id)
|
|
339
|
+
return null;
|
|
340
|
+
if (source[idEnd] !== '|')
|
|
341
|
+
return null;
|
|
342
|
+
const labelStart = idEnd + 1;
|
|
343
|
+
const labelEnd = this.tokens.close[labelStart] ?? -1;
|
|
344
|
+
if (labelEnd <= labelStart || labelEnd - labelStart > EDITOR_EXTENSION_LIMITS.label)
|
|
345
|
+
return null;
|
|
346
|
+
return { idStart, idEnd, labelStart, labelEnd, end: labelEnd + 1 };
|
|
347
|
+
}
|
|
305
348
|
/** The first valid closer of `marker` at or after `from`, or -1. */
|
|
306
349
|
closer(marker, from) {
|
|
307
350
|
let table = this.closers.get(marker);
|
|
@@ -373,16 +416,31 @@ class Scan {
|
|
|
373
416
|
/** Inline Markdown into runs; `marks` are the marks open around `source`. */
|
|
374
417
|
function parseInline(source, depth, marks, inLink, out, budget) {
|
|
375
418
|
let buffer = '';
|
|
419
|
+
// Spaces at the end of `buffer`, kept as it grows: reading the buffer's
|
|
420
|
+
// last characters would flatten the string being built on every soft line
|
|
421
|
+
// break (quadratic in a long paragraph).
|
|
422
|
+
let spaces = 0;
|
|
423
|
+
const append = (text) => {
|
|
424
|
+
buffer += text;
|
|
425
|
+
let end = text.length;
|
|
426
|
+
while (end > 0 && text.charCodeAt(end - 1) === 32)
|
|
427
|
+
end -= 1;
|
|
428
|
+
spaces = end === 0 ? spaces + text.length : text.length - end;
|
|
429
|
+
};
|
|
376
430
|
const flush = () => {
|
|
377
431
|
if (buffer)
|
|
378
432
|
out.push(...budget.inline(buffer.replace(/\n/g, ' '), marks));
|
|
379
433
|
buffer = '';
|
|
434
|
+
spaces = 0;
|
|
380
435
|
};
|
|
381
436
|
const push = (text, extra) => {
|
|
382
437
|
flush();
|
|
383
438
|
out.push(...budget.inline(text, sortMarks([...marks, ...extra])));
|
|
384
439
|
};
|
|
385
440
|
const scan = new Scan(source);
|
|
441
|
+
const extensions = budget.extensions;
|
|
442
|
+
// The characters that can start an inline construct, registered sigils included.
|
|
443
|
+
const special = extensions.special;
|
|
386
444
|
let i = 0;
|
|
387
445
|
while (i < source.length) {
|
|
388
446
|
const ch = source[i] ?? '';
|
|
@@ -396,17 +454,13 @@ function parseInline(source, depth, marks, inLink, out, budget) {
|
|
|
396
454
|
continue;
|
|
397
455
|
}
|
|
398
456
|
if (ESCAPABLE.test(next)) {
|
|
399
|
-
|
|
457
|
+
append(next);
|
|
400
458
|
i += 2;
|
|
401
459
|
continue;
|
|
402
460
|
}
|
|
403
461
|
}
|
|
404
462
|
if (ch === '\n') {
|
|
405
|
-
//
|
|
406
|
-
// whole buffer on every soft line break (quadratic in a long paragraph).
|
|
407
|
-
let spaces = 0;
|
|
408
|
-
while (buffer.charCodeAt(buffer.length - 1 - spaces) === 32)
|
|
409
|
-
spaces += 1;
|
|
463
|
+
// Two or more trailing spaces make a hard break.
|
|
410
464
|
if (spaces >= 2) {
|
|
411
465
|
buffer = buffer.slice(0, buffer.length - spaces);
|
|
412
466
|
flush();
|
|
@@ -414,10 +468,22 @@ function parseInline(source, depth, marks, inLink, out, budget) {
|
|
|
414
468
|
budget.chars += 1;
|
|
415
469
|
}
|
|
416
470
|
else
|
|
417
|
-
|
|
471
|
+
append('\n');
|
|
418
472
|
i += 1;
|
|
419
473
|
continue;
|
|
420
474
|
}
|
|
475
|
+
const extension = inLink ? undefined : extensions.bySigil.get(ch);
|
|
476
|
+
if (extension) {
|
|
477
|
+
const token = scan.extension(i);
|
|
478
|
+
const id = token ? source.slice(token.idStart, token.idEnd) : '';
|
|
479
|
+
if (token && acceptsExtensionId(extensions, extension.name, id)) {
|
|
480
|
+
push(unescapeMarkdown(source.slice(token.labelStart, token.labelEnd)), [
|
|
481
|
+
{ type: 'extension', name: extension.name, id },
|
|
482
|
+
]);
|
|
483
|
+
i = token.end;
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
421
487
|
if (ch === '`') {
|
|
422
488
|
const run = /^`+/.exec(source.slice(i, i + 64))?.[0] ?? '`';
|
|
423
489
|
const close = source.indexOf(run, i + run.length);
|
|
@@ -427,7 +493,7 @@ function parseInline(source, depth, marks, inLink, out, budget) {
|
|
|
427
493
|
i = close + run.length;
|
|
428
494
|
continue;
|
|
429
495
|
}
|
|
430
|
-
|
|
496
|
+
append(run);
|
|
431
497
|
i += run.length;
|
|
432
498
|
continue;
|
|
433
499
|
}
|
|
@@ -443,7 +509,7 @@ function parseInline(source, depth, marks, inLink, out, budget) {
|
|
|
443
509
|
if (ch === '!' && source[i + 1] === '[') {
|
|
444
510
|
const image = matchLink(scan, i + 1);
|
|
445
511
|
if (image) {
|
|
446
|
-
|
|
512
|
+
append(unescapeMarkdown(source.slice(image.labelStart, image.labelEnd)));
|
|
447
513
|
i = image.end;
|
|
448
514
|
continue;
|
|
449
515
|
}
|
|
@@ -469,8 +535,12 @@ function parseInline(source, depth, marks, inLink, out, budget) {
|
|
|
469
535
|
}
|
|
470
536
|
}
|
|
471
537
|
}
|
|
472
|
-
|
|
473
|
-
|
|
538
|
+
// Plain text up to the next character that can open a construct is
|
|
539
|
+
// copied in one slice, not one character at a time.
|
|
540
|
+
special.lastIndex = i + 1;
|
|
541
|
+
const stop = special.test(source) ? special.lastIndex - 1 : source.length;
|
|
542
|
+
append(source.slice(i, stop));
|
|
543
|
+
i = stop;
|
|
474
544
|
}
|
|
475
545
|
flush();
|
|
476
546
|
}
|
|
@@ -581,6 +651,15 @@ function readLinkTarget(source, open) {
|
|
|
581
651
|
// ---------------------------------------------------------------------------
|
|
582
652
|
// Writing
|
|
583
653
|
const escapeText = (text) => text.replace(/[\\`*_[\]~<!]/g, '\\$&');
|
|
654
|
+
/** Plain text: a registered sigil before `{` would read as a token, so `{` is escaped. */
|
|
655
|
+
const escapePlain = (text, extensions) => extensions.literal ? escapeText(text).replace(extensions.literal, '$1\\{') : escapeText(text);
|
|
656
|
+
/** An extension token: the label escaped so no closer or delimiter inside it counts. */
|
|
657
|
+
function extensionToken(mark, label, extensions) {
|
|
658
|
+
const extension = extensions.byName.get(mark.name);
|
|
659
|
+
if (!extension)
|
|
660
|
+
return null;
|
|
661
|
+
return `${extension.sigil}{${mark.id}|${escapeText(label).replace(/[{}]/g, '\\$&')}}`;
|
|
662
|
+
}
|
|
584
663
|
/** A line that would start another block gets its first marker escaped. */
|
|
585
664
|
function escapeLineStart(line) {
|
|
586
665
|
const ordered = /^( {0,3}\d{1,9})([.)])([ \t]|$)/.exec(line);
|
|
@@ -598,71 +677,86 @@ function codeSpan(text) {
|
|
|
598
677
|
const pad = text.startsWith('`') || text.endsWith('`') || /^ .* $/.test(text) ? ' ' : '';
|
|
599
678
|
return `${fence}${pad}${text}${pad}${fence}`;
|
|
600
679
|
}
|
|
601
|
-
/**
|
|
680
|
+
/**
|
|
681
|
+
* Move whitespace and trailing backslashes out of strong, em and strike spans
|
|
682
|
+
* (Markdown cannot open or close there). Works on runs, splitting a run only
|
|
683
|
+
* where a span edge moves: one pass per mark type, no object per character.
|
|
684
|
+
*/
|
|
602
685
|
function prepareMarks(content) {
|
|
603
|
-
|
|
604
|
-
for (const node of content) {
|
|
605
|
-
if (node.type === 'break')
|
|
606
|
-
chars.push(null);
|
|
607
|
-
else
|
|
608
|
-
for (const ch of node.text)
|
|
609
|
-
chars.push({ ch, marks: node.marks });
|
|
610
|
-
}
|
|
611
|
-
// The nearest character on each side of every position, so a run of
|
|
612
|
-
// breaks is crossed in constant time (linear, not quadratic, in breaks).
|
|
613
|
-
const previous = new Array(chars.length);
|
|
614
|
-
const next = new Array(chars.length);
|
|
615
|
-
for (let k = 0, last = -1; k < chars.length; k += 1) {
|
|
616
|
-
previous[k] = last;
|
|
617
|
-
if (chars[k] !== null)
|
|
618
|
-
last = k;
|
|
619
|
-
}
|
|
620
|
-
for (let k = chars.length - 1, last = chars.length; k >= 0; k -= 1) {
|
|
621
|
-
next[k] = last;
|
|
622
|
-
if (chars[k] !== null)
|
|
623
|
-
last = k;
|
|
624
|
-
}
|
|
686
|
+
let runs = content.flatMap((node) => node.type === 'break' ? [null] : node.text ? [{ text: node.text, marks: node.marks }] : []);
|
|
625
687
|
for (const type of ['strong', 'em', 'strike']) {
|
|
688
|
+
const marked = (run) => run?.marks.some((m) => m.type === type) === true;
|
|
689
|
+
const strip = (run, text) => ({
|
|
690
|
+
text,
|
|
691
|
+
marks: run.marks.filter((m) => m.type !== type),
|
|
692
|
+
});
|
|
693
|
+
const out = [];
|
|
626
694
|
let i = 0;
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
if (chars[k] !== null)
|
|
631
|
-
return marked(chars[k]);
|
|
632
|
-
const left = previous[k];
|
|
633
|
-
return left >= 0 && marked(chars[left]) && marked(chars[next[k]]);
|
|
634
|
-
};
|
|
635
|
-
while (i < chars.length) {
|
|
636
|
-
if (!has(i)) {
|
|
695
|
+
while (i < runs.length) {
|
|
696
|
+
if (!marked(runs[i])) {
|
|
697
|
+
out.push(runs[i]);
|
|
637
698
|
i += 1;
|
|
638
699
|
continue;
|
|
639
700
|
}
|
|
701
|
+
// A span: marked text, and breaks with marked text on both sides.
|
|
640
702
|
let end = i;
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
703
|
+
for (let k = i + 1; k < runs.length; k += 1) {
|
|
704
|
+
if (runs[k] === null)
|
|
705
|
+
continue;
|
|
706
|
+
if (!marked(runs[k]))
|
|
707
|
+
break;
|
|
708
|
+
end = k;
|
|
709
|
+
}
|
|
710
|
+
const span = runs.slice(i, end + 1);
|
|
711
|
+
// Leading whitespace leaves the span, up to the first other character
|
|
712
|
+
// or break; then trailing whitespace and backslashes, from the end.
|
|
713
|
+
const head = [];
|
|
714
|
+
const tail = [];
|
|
715
|
+
let a = 0;
|
|
716
|
+
let b = span.length - 1;
|
|
717
|
+
for (; a <= b; a += 1) {
|
|
718
|
+
const run = span[a];
|
|
719
|
+
if (!run)
|
|
720
|
+
break;
|
|
721
|
+
let cut = 0;
|
|
722
|
+
while (/\s/.test(run.text[cut] ?? ''))
|
|
723
|
+
cut += 1;
|
|
724
|
+
if (cut)
|
|
725
|
+
head.push(strip(run, run.text.slice(0, cut)));
|
|
726
|
+
if (cut < run.text.length) {
|
|
727
|
+
span[a] = { text: run.text.slice(cut), marks: run.marks };
|
|
728
|
+
break;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
for (; b >= a; b -= 1) {
|
|
732
|
+
const run = span[b];
|
|
733
|
+
if (!run)
|
|
734
|
+
break;
|
|
735
|
+
let cut = run.text.length;
|
|
736
|
+
while (/[\s\\]/.test(run.text[cut - 1] ?? ''))
|
|
737
|
+
cut -= 1;
|
|
738
|
+
if (cut < run.text.length)
|
|
739
|
+
tail.push(strip(run, run.text.slice(cut)));
|
|
740
|
+
if (cut) {
|
|
741
|
+
span[b] = { text: run.text.slice(0, cut), marks: run.marks };
|
|
742
|
+
break;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
// One push per run: a spread of a long span could overflow the call stack.
|
|
746
|
+
for (const run of [...head, ...span.slice(a, b + 1), ...tail.reverse()])
|
|
747
|
+
out.push(run);
|
|
653
748
|
i = end + 1;
|
|
654
749
|
}
|
|
750
|
+
runs = out;
|
|
655
751
|
}
|
|
656
|
-
|
|
657
|
-
for (const c of chars)
|
|
658
|
-
out.push(c ? { type: 'text', text: c.ch, marks: c.marks } : { type: 'break' });
|
|
659
|
-
return normalizeInline(out);
|
|
752
|
+
return normalizeInline(runs.map((run) => (run ? { type: 'text', ...run } : { type: 'break' })));
|
|
660
753
|
}
|
|
661
|
-
function writeInline(content, em) {
|
|
754
|
+
function writeInline(content, em, extensions) {
|
|
662
755
|
const open = { strong: '**', em, strike: '~~' };
|
|
663
756
|
const texts = content.flatMap((node) => (node.type === 'text' ? [{ node }] : []));
|
|
664
757
|
const stack = [];
|
|
665
|
-
|
|
758
|
+
// Code spans and extension tokens are leaves: other marks nest around them.
|
|
759
|
+
const outer = (marks) => marks.filter((m) => m.type !== 'code' && m.type !== 'extension');
|
|
666
760
|
// Where the span of each mark starting at text `t` ends (marks that end
|
|
667
761
|
// later nest outside), computed once from the end so long spans stay linear.
|
|
668
762
|
const ends = new Array(texts.length);
|
|
@@ -691,7 +785,7 @@ function writeInline(content, em) {
|
|
|
691
785
|
const close = (to) => {
|
|
692
786
|
while (stack.length > to) {
|
|
693
787
|
const mark = stack.pop();
|
|
694
|
-
out += mark.type === 'link' ? `](${encodeRef(mark.href)})` : open[mark.type];
|
|
788
|
+
out += mark.type === 'link' ? `](${encodeRef(mark.href)})` : (open[mark.type] ?? '');
|
|
695
789
|
}
|
|
696
790
|
};
|
|
697
791
|
const keepCommon = (wanted) => {
|
|
@@ -716,10 +810,16 @@ function writeInline(content, em) {
|
|
|
716
810
|
const wanted = ordered(t);
|
|
717
811
|
const common = keepCommon(wanted);
|
|
718
812
|
for (const mark of wanted.slice(common)) {
|
|
719
|
-
out += mark.type === 'link' ? '[' : open[mark.type];
|
|
813
|
+
out += mark.type === 'link' ? '[' : (open[mark.type] ?? '');
|
|
720
814
|
stack.push(mark);
|
|
721
815
|
}
|
|
722
|
-
|
|
816
|
+
const entity = node.marks.find((m) => m.type === 'extension');
|
|
817
|
+
const token = entity ? extensionToken(entity, node.text, extensions) : null;
|
|
818
|
+
out +=
|
|
819
|
+
token ??
|
|
820
|
+
(node.marks.some((m) => m.type === 'code')
|
|
821
|
+
? codeSpan(node.text)
|
|
822
|
+
: escapePlain(node.text, extensions));
|
|
723
823
|
t += 1;
|
|
724
824
|
});
|
|
725
825
|
close(0);
|
|
@@ -733,8 +833,8 @@ const sameInline = (a, b) => a.length === b.length &&
|
|
|
733
833
|
return (node.text === other.text && node.marks.map(markKey).join() === other.marks.map(markKey).join());
|
|
734
834
|
});
|
|
735
835
|
/** Read inline Markdown back without caps or policy changes (for the write check). */
|
|
736
|
-
function reread(markdown) {
|
|
737
|
-
const budget = new LoadBudget({ trust: 'developer' });
|
|
836
|
+
function reread(markdown, extensions) {
|
|
837
|
+
const budget = new LoadBudget({ trust: 'developer', extensions });
|
|
738
838
|
const out = [];
|
|
739
839
|
parseInline(markdown, 0, [], false, out, budget);
|
|
740
840
|
return normalizeInline(out);
|
|
@@ -744,11 +844,11 @@ function reread(markdown) {
|
|
|
744
844
|
* combination has no faithful spelling in the subset, the result drops the
|
|
745
845
|
* least significant marks (em, then strike, then strong) until it does.
|
|
746
846
|
*/
|
|
747
|
-
function inlineMarkdown(content, lineStart) {
|
|
748
|
-
let prepared = prepareMarks(content);
|
|
847
|
+
function inlineMarkdown(content, lineStart, extensions) {
|
|
848
|
+
let prepared = prepareMarks(withRegistered(content, extensions));
|
|
749
849
|
const attempt = (em) => {
|
|
750
|
-
const text = writeInline(prepared, em).split('\n').map(lineStart).join('\n');
|
|
751
|
-
return sameInline(reread(text), prepared) ? text : null;
|
|
850
|
+
const text = writeInline(prepared, em, extensions).split('\n').map(lineStart).join('\n');
|
|
851
|
+
return sameInline(reread(text, extensions), prepared) ? text : null;
|
|
752
852
|
};
|
|
753
853
|
for (const drop of [null, 'em', 'strike', 'strong']) {
|
|
754
854
|
if (drop)
|
|
@@ -757,25 +857,34 @@ function inlineMarkdown(content, lineStart) {
|
|
|
757
857
|
if (text !== null)
|
|
758
858
|
return text;
|
|
759
859
|
}
|
|
760
|
-
return writeInline(prepared, '*').split('\n').map(lineStart).join('\n');
|
|
860
|
+
return writeInline(prepared, '*', extensions).split('\n').map(lineStart).join('\n');
|
|
861
|
+
}
|
|
862
|
+
/** Extension marks of unregistered names leave (their label stays plain text). */
|
|
863
|
+
function withRegistered(content, extensions) {
|
|
864
|
+
const foreign = (mark) => mark.type === 'extension' && !extensions.byName.has(mark.name);
|
|
865
|
+
if (!content.some((n) => n.type === 'text' && n.marks.some(foreign)))
|
|
866
|
+
return content;
|
|
867
|
+
return normalizeInline(content.map((n) => n.type === 'text' && n.marks.some(foreign)
|
|
868
|
+
? { ...n, marks: n.marks.filter((m) => !foreign(m)) }
|
|
869
|
+
: n));
|
|
761
870
|
}
|
|
762
871
|
const indent = (text, by) => text
|
|
763
872
|
.split('\n')
|
|
764
873
|
.map((line) => (line ? by + line : line))
|
|
765
874
|
.join('\n');
|
|
766
|
-
function writeBlocks(blocks) {
|
|
875
|
+
function writeBlocks(blocks, extensions) {
|
|
767
876
|
return blocks
|
|
768
|
-
.map(writeBlock)
|
|
877
|
+
.map((block) => writeBlock(block, extensions))
|
|
769
878
|
.filter((text) => text !== '')
|
|
770
879
|
.join('\n\n');
|
|
771
880
|
}
|
|
772
|
-
function writeBlock(block) {
|
|
881
|
+
function writeBlock(block, extensions) {
|
|
773
882
|
switch (block.type) {
|
|
774
883
|
case 'paragraph':
|
|
775
|
-
return inlineMarkdown(trimEdges(block.content), escapeLineStart);
|
|
884
|
+
return inlineMarkdown(trimEdges(block.content), escapeLineStart, extensions);
|
|
776
885
|
case 'heading': {
|
|
777
886
|
const flat = block.content.map((n) => (n.type === 'break' ? { type: 'text', text: ' ', marks: [] } : n));
|
|
778
|
-
const text = inlineMarkdown(trimEdges(normalizeInline(flat)), (line) => line);
|
|
887
|
+
const text = inlineMarkdown(trimEdges(normalizeInline(flat)), (line) => line, extensions);
|
|
779
888
|
return `${'#'.repeat(block.level)}${text ? ` ${text}` : ''}${text.endsWith('#') ? ' #' : ''}`;
|
|
780
889
|
}
|
|
781
890
|
case 'code': {
|
|
@@ -792,13 +901,13 @@ function writeBlock(block) {
|
|
|
792
901
|
? `- [${item.checked ? 'x' : ' '}] `
|
|
793
902
|
: '- ';
|
|
794
903
|
const [first, ...rest] = item.blocks;
|
|
795
|
-
const head = first ? indent(writeBlock(first), ' ').trimStart() : '';
|
|
796
|
-
const tail = writeBlocks(rest);
|
|
904
|
+
const head = first ? indent(writeBlock(first, extensions), ' ').trimStart() : '';
|
|
905
|
+
const tail = writeBlocks(rest, extensions);
|
|
797
906
|
return `${marker}${head}${tail ? `\n\n${indent(tail, ' ')}` : ''}`;
|
|
798
907
|
})
|
|
799
908
|
.join('\n');
|
|
800
909
|
case 'quote':
|
|
801
|
-
return writeBlocks(block.blocks)
|
|
910
|
+
return writeBlocks(block.blocks, extensions)
|
|
802
911
|
.split('\n')
|
|
803
912
|
.map((line) => (line ? `> ${line}` : '>'))
|
|
804
913
|
.join('\n');
|
|
@@ -815,7 +924,8 @@ function trimEdges(content) {
|
|
|
815
924
|
out.shift();
|
|
816
925
|
while (out.at(-1)?.type === 'break')
|
|
817
926
|
out.pop();
|
|
818
|
-
const plain = (node) => node?.type === 'text' &&
|
|
927
|
+
const plain = (node) => node?.type === 'text' &&
|
|
928
|
+
!node.marks.some((mark) => mark.type === 'code' || mark.type === 'extension');
|
|
819
929
|
const first = out[0];
|
|
820
930
|
if (plain(first))
|
|
821
931
|
out[0] = { ...first, text: first.text.trimStart() };
|
|
@@ -830,6 +940,6 @@ function trimEdges(content) {
|
|
|
830
940
|
* paragraphs, breaks in headings (written as spaces), breaks at the edges of
|
|
831
941
|
* a block, and whitespace at the edges of a paragraph or a mark.
|
|
832
942
|
*/
|
|
833
|
-
export function toMarkdown(doc) {
|
|
834
|
-
return writeBlocks(doc.blocks);
|
|
943
|
+
export function toMarkdown(doc, options = {}) {
|
|
944
|
+
return writeBlocks(doc.blocks, toInlineExtensions(options.extensions));
|
|
835
945
|
}
|
package/dist/editor/model.d.ts
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
* `@texuli/core/editor-model`: the rich text editor document model (ADR-003).
|
|
3
3
|
* Types and caps, the closed vocabularies, constructors, normalization,
|
|
4
4
|
* validation, the untrusted loader, selection helpers, grapheme and word
|
|
5
|
-
* boundaries,
|
|
5
|
+
* boundaries, the nested inline interchange shape, and host inline
|
|
6
|
+
* extensions (mentions, references). Framework-free.
|
|
6
7
|
*/
|
|
7
8
|
export { blockLength, blockText, createEditorIds, type EditorAncestor, type EditorIdSource, type EditorLocation, editorBlockCount, editorCharCount, editorLeaves, editorPlainText, emptyEditorDocument, isTextBlock, isVoidBlock, locateBlock, nextEditorId, normalizeDocument, paragraph, removeBlocks, replaceBlock, replaceItem, validateEditorDocument, } from './document.js';
|
|
9
|
+
export { acceptsExtensionId, createInlineExtensions, EDITOR_EXTENSION_LIMITS, EDITOR_EXTENSION_SIGILS, type EditorExtensionSigil, type EditorInlineExtension, type EditorInlineExtensions, extensionHref, toInlineExtensions, } from './extensions.js';
|
|
8
10
|
export { graphemeBoundaries, nextGrapheme, nextWord, previousGrapheme, previousWord, snapToGrapheme, } from './graphemes.js';
|
|
9
11
|
export { cleanText, hasMark, inheritedMarks, inlineLength, inlineText, markKey, normalizeInline, sliceInline, sortMarks, textToInline, } from './inline.js';
|
|
10
12
|
export { type EditorLoadOptions, type EditorLoadResult, loadEditorDocument } from './load.js';
|
package/dist/editor/model.js
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
* `@texuli/core/editor-model`: the rich text editor document model (ADR-003).
|
|
3
3
|
* Types and caps, the closed vocabularies, constructors, normalization,
|
|
4
4
|
* validation, the untrusted loader, selection helpers, grapheme and word
|
|
5
|
-
* boundaries,
|
|
5
|
+
* boundaries, the nested inline interchange shape, and host inline
|
|
6
|
+
* extensions (mentions, references). Framework-free.
|
|
6
7
|
*/
|
|
7
8
|
export { blockLength, blockText, createEditorIds, editorBlockCount, editorCharCount, editorLeaves, editorPlainText, emptyEditorDocument, isTextBlock, isVoidBlock, locateBlock, nextEditorId, normalizeDocument, paragraph, removeBlocks, replaceBlock, replaceItem, validateEditorDocument, } from "./document.js";
|
|
9
|
+
export { acceptsExtensionId, createInlineExtensions, EDITOR_EXTENSION_LIMITS, EDITOR_EXTENSION_SIGILS, extensionHref, toInlineExtensions, } from "./extensions.js";
|
|
8
10
|
export { graphemeBoundaries, nextGrapheme, nextWord, previousGrapheme, previousWord, snapToGrapheme, } from "./graphemes.js";
|
|
9
11
|
export { cleanText, hasMark, inheritedMarks, inlineLength, inlineText, markKey, normalizeInline, sliceInline, sortMarks, textToInline, } from "./inline.js";
|
|
10
12
|
export { loadEditorDocument } from "./load.js";
|
|
@@ -50,7 +50,7 @@ export declare function toggleQuote(state: EditorState, options?: EditorTransfor
|
|
|
50
50
|
* it). At a caret it toggles the stored marks for the next typed text. Code
|
|
51
51
|
* text takes only `link` besides `code`.
|
|
52
52
|
*/
|
|
53
|
-
export declare function toggleMark(state: EditorState, type: Exclude<EditorMarkType, 'link'>, options?: EditorTransformOptions): EditorTransformResult;
|
|
53
|
+
export declare function toggleMark(state: EditorState, type: Exclude<EditorMarkType, 'link' | 'extension'>, options?: EditorTransformOptions): EditorTransformResult;
|
|
54
54
|
export interface SetLinkOptions extends EditorTransformOptions {
|
|
55
55
|
/** URL policy tier. Default `untrusted`; pasted and typed URLs stay untrusted. */
|
|
56
56
|
trust?: HrefTrust;
|
package/dist/editor/tree.d.ts
CHANGED
|
@@ -20,8 +20,18 @@ export type EditorInlineNode = {
|
|
|
20
20
|
type: 'link';
|
|
21
21
|
href: string;
|
|
22
22
|
children: EditorInlineNode[];
|
|
23
|
+
}
|
|
24
|
+
/** A host inline extension entity (`name`, `id`) around its label. */
|
|
25
|
+
| {
|
|
26
|
+
type: 'extension';
|
|
27
|
+
name: string;
|
|
28
|
+
id: string;
|
|
29
|
+
children: EditorInlineNode[];
|
|
23
30
|
};
|
|
24
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* Fold runs into nested nodes, outermost mark first (`link`, `strong`, `em`,
|
|
33
|
+
* `strike`, then `extension` innermost around its label).
|
|
34
|
+
*/
|
|
25
35
|
export declare function inlineToTree(content: readonly EditorInline[]): EditorInlineNode[];
|
|
26
36
|
/**
|
|
27
37
|
* Flatten a nested inline tree (this shape, the catalog's or the blocks
|
package/dist/editor/tree.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { markKey, normalizeInline, sortMarks } from "./inline.js";
|
|
2
2
|
import { LoadBudget } from "./load.js";
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Fold runs into nested nodes, outermost mark first (`link`, `strong`, `em`,
|
|
5
|
+
* `strike`, then `extension` innermost around its label).
|
|
6
|
+
*/
|
|
4
7
|
export function inlineToTree(content) {
|
|
5
8
|
const root = [];
|
|
6
9
|
const stack = [];
|
|
@@ -22,7 +25,9 @@ export function inlineToTree(content) {
|
|
|
22
25
|
for (const mark of wanted.slice(common)) {
|
|
23
26
|
const node = mark.type === 'link'
|
|
24
27
|
? { type: 'link', href: mark.href, children: [] }
|
|
25
|
-
:
|
|
28
|
+
: mark.type === 'extension'
|
|
29
|
+
? { type: 'extension', name: mark.name, id: mark.id, children: [] }
|
|
30
|
+
: { type: mark.type, children: [] };
|
|
26
31
|
children().push(node);
|
|
27
32
|
stack.push({ mark, node });
|
|
28
33
|
}
|
|
@@ -90,6 +95,10 @@ export function inlineFromTree(nodes, options = {}) {
|
|
|
90
95
|
const href = budget.href(node.href);
|
|
91
96
|
visit(node.children, href ? sortMarks([...marks, { type: 'link', href }]) : marks, depth + 1);
|
|
92
97
|
}
|
|
98
|
+
else if (type === 'extension') {
|
|
99
|
+
const extension = budget.extension(node.name, node.id);
|
|
100
|
+
visit(node.children, extension ? sortMarks([...marks, extension]) : marks, depth + 1);
|
|
101
|
+
}
|
|
93
102
|
else {
|
|
94
103
|
budget.report('unknown-node', 'Unknown inline nodes kept only their text.');
|
|
95
104
|
if (text !== undefined)
|
package/dist/editor/types.d.ts
CHANGED
|
@@ -18,6 +18,16 @@ export type EditorMark = {
|
|
|
18
18
|
} | {
|
|
19
19
|
type: 'link';
|
|
20
20
|
href: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A host inline extension entity (a mention, a reference) over its label
|
|
24
|
+
* text: `name` is a registered extension, `id` the entity (G48). Excludes
|
|
25
|
+
* `link` and `code` on the same run.
|
|
26
|
+
*/
|
|
27
|
+
| {
|
|
28
|
+
type: 'extension';
|
|
29
|
+
name: string;
|
|
30
|
+
id: string;
|
|
21
31
|
};
|
|
22
32
|
export type EditorMarkType = EditorMark['type'];
|
|
23
33
|
export type EditorInline = {
|
package/dist/editor/types.js
CHANGED
package/dist/inert/inert.d.ts
CHANGED
|
@@ -34,10 +34,21 @@ export interface InertExemptionInput {
|
|
|
34
34
|
element: Element;
|
|
35
35
|
/** Optional debug label (`toast-region`, `announcer`, …). */
|
|
36
36
|
label?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Whether the region stays above overlays opened after it (default `true`):
|
|
39
|
+
* when it is open as a popover, the overlay engine re-shows it in the top
|
|
40
|
+
* layer each time another overlay opens, so the toast region keeps painting
|
|
41
|
+
* on top. Pass `false` for exemptions that belong under later overlays: a
|
|
42
|
+
* dialog backdrop, or a menu or listbox opened inside a modal, which must
|
|
43
|
+
* never cover the overlay opened from it.
|
|
44
|
+
*/
|
|
45
|
+
raise?: boolean;
|
|
37
46
|
}
|
|
38
47
|
export interface InertExemption {
|
|
39
48
|
readonly element: Element;
|
|
40
49
|
readonly label?: string;
|
|
50
|
+
/** `false` when the region must not be re-raised above later overlays (omitted means `true`). */
|
|
51
|
+
readonly raise?: false;
|
|
41
52
|
}
|
|
42
53
|
export interface InertExemptionHandle {
|
|
43
54
|
/** Stop exempting this element (idempotent). Open scopes recompute. */
|
package/dist/inert/inert.js
CHANGED
|
@@ -142,9 +142,11 @@ export function createInertRegistry() {
|
|
|
142
142
|
if (!isElement(input.element)) {
|
|
143
143
|
throw new TypeError('InertRegistry.exempt: element must be a DOM Element');
|
|
144
144
|
}
|
|
145
|
-
const entry =
|
|
146
|
-
|
|
147
|
-
|
|
145
|
+
const entry = {
|
|
146
|
+
element: input.element,
|
|
147
|
+
...(input.label === undefined ? {} : { label: input.label }),
|
|
148
|
+
...(input.raise === false ? { raise: false } : {}),
|
|
149
|
+
};
|
|
148
150
|
exemptions.push(entry);
|
|
149
151
|
apply();
|
|
150
152
|
let released = false;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @texuli/core/markup: escaped XML and HTML builders (ADR-016). Typed nodes
|
|
3
|
+
* only, no raw-markup API; URL attributes go through the URL policy.
|
|
4
|
+
*/
|
|
5
|
+
export { el, escapeMarkupAttribute, escapeMarkupText, HTML_ELEMENTS, HTML_URL_ATTRIBUTES, type HtmlSerializeOptions, imageUrl, isMarkupUrl, linkUrl, type MarkupAttribute, type MarkupAttributeValue, type MarkupChild, type MarkupElement, type MarkupNode, type MarkupText, type MarkupUrl, sanitizeMarkupChars, serializeHtml, serializeXml, text, type XmlSerializeOptions, } from './markup.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @texuli/core/markup: escaped XML and HTML builders (ADR-016). Typed nodes
|
|
3
|
+
* only, no raw-markup API; URL attributes go through the URL policy.
|
|
4
|
+
*/
|
|
5
|
+
export { el, escapeMarkupAttribute, escapeMarkupText, HTML_ELEMENTS, HTML_URL_ATTRIBUTES, imageUrl, isMarkupUrl, linkUrl, sanitizeMarkupChars, serializeHtml, serializeXml, text, } from "./markup.js";
|