@quillmark/svelte 0.26.2 → 0.28.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.
Files changed (97) hide show
  1. package/NOTICE +4 -4
  2. package/README.md +26 -8
  3. package/THEMING.md +40 -4
  4. package/dist/core/address.d.ts +35 -64
  5. package/dist/core/address.js +35 -55
  6. package/dist/core/bloom.d.ts +4 -10
  7. package/dist/core/bloom.js +14 -27
  8. package/dist/core/codec/blocks.d.ts +41 -43
  9. package/dist/core/codec/blocks.js +97 -130
  10. package/dist/core/codec/code.d.ts +7 -10
  11. package/dist/core/codec/code.js +37 -46
  12. package/dist/core/codec/decode.d.ts +19 -23
  13. package/dist/core/codec/decode.js +76 -111
  14. package/dist/core/codec/encode.d.ts +11 -15
  15. package/dist/core/codec/encode.js +60 -67
  16. package/dist/core/codec/field.d.ts +62 -95
  17. package/dist/core/codec/field.js +107 -179
  18. package/dist/core/codec/islands.d.ts +33 -41
  19. package/dist/core/codec/islands.js +76 -86
  20. package/dist/core/codec/lists.d.ts +17 -26
  21. package/dist/core/codec/lists.js +117 -157
  22. package/dist/core/codec/marks.d.ts +4 -3
  23. package/dist/core/codec/marks.js +8 -3
  24. package/dist/core/codec/prose.css +84 -46
  25. package/dist/core/codec/schema.d.ts +18 -23
  26. package/dist/core/codec/schema.js +84 -76
  27. package/dist/core/codec/slash.d.ts +42 -48
  28. package/dist/core/codec/slash.js +0 -0
  29. package/dist/core/codec/table-copy.d.ts +4 -3
  30. package/dist/core/codec/table-copy.js +25 -7
  31. package/dist/core/codec/table-view.d.ts +12 -12
  32. package/dist/core/codec/table-view.js +460 -383
  33. package/dist/core/codec/table.d.ts +94 -61
  34. package/dist/core/codec/table.js +331 -83
  35. package/dist/core/codec/tasks.d.ts +6 -0
  36. package/dist/core/codec/tasks.js +30 -3
  37. package/dist/core/errors.d.ts +15 -30
  38. package/dist/core/errors.js +2 -6
  39. package/dist/core/index.js +7 -12
  40. package/dist/core/lifecycle.d.ts +7 -16
  41. package/dist/core/lifecycle.js +10 -21
  42. package/dist/core/rebind.svelte.d.ts +10 -20
  43. package/dist/core/rebind.svelte.js +15 -27
  44. package/dist/core/rungs.d.ts +14 -11
  45. package/dist/core/rungs.js +17 -19
  46. package/dist/core/teardown.d.ts +4 -6
  47. package/dist/core/teardown.js +7 -10
  48. package/dist/core/theme.css +74 -54
  49. package/dist/index.js +3 -8
  50. package/dist/preset/app.css +71 -0
  51. package/dist/preset/index.css +8 -7
  52. package/dist/preset/recipes.css +19 -164
  53. package/dist/preset/scale.css +28 -18
  54. package/dist/preset/shell.css +157 -0
  55. package/dist/preview/Preview.svelte +8 -0
  56. package/dist/preview/bridge.d.ts +18 -22
  57. package/dist/preview/bridge.js +29 -58
  58. package/dist/preview/controller.d.ts +21 -29
  59. package/dist/preview/controller.js +14 -39
  60. package/dist/preview/geometry.d.ts +13 -34
  61. package/dist/preview/geometry.js +13 -34
  62. package/dist/preview/index.js +4 -16
  63. package/dist/preview/paint.d.ts +3 -3
  64. package/dist/preview/paint.js +28 -68
  65. package/dist/preview/strings.d.ts +1 -1
  66. package/dist/preview/strings.js +2 -14
  67. package/dist/preview/viewport.js +20 -15
  68. package/dist/preview/zoom.d.ts +21 -22
  69. package/dist/preview/zoom.js +53 -30
  70. package/dist/visual/ArrayField.svelte +77 -16
  71. package/dist/visual/BooleanField.svelte +28 -15
  72. package/dist/visual/Card.svelte +66 -51
  73. package/dist/visual/CardControls.svelte +5 -11
  74. package/dist/visual/DateField.svelte +20 -4
  75. package/dist/visual/EnumField.svelte +3 -0
  76. package/dist/visual/FieldHint.svelte +2 -1
  77. package/dist/visual/FormatPopover.svelte +82 -15
  78. package/dist/visual/FormatPopover.svelte.d.ts +4 -0
  79. package/dist/visual/MatrixAdd.svelte +30 -12
  80. package/dist/visual/MatrixField.svelte +24 -16
  81. package/dist/visual/NumberField.svelte +7 -0
  82. package/dist/visual/ObjectField.svelte +4 -4
  83. package/dist/visual/ProseField.svelte +0 -5
  84. package/dist/visual/TipsCard.svelte +11 -10
  85. package/dist/visual/VariantField.svelte +3 -1
  86. package/dist/visual/VisualEditorInner.svelte +19 -3
  87. package/dist/visual/controls.css +7 -2
  88. package/dist/visual/fonts.d.ts +18 -0
  89. package/dist/visual/fonts.js +59 -0
  90. package/dist/visual/icons/nodes.d.ts +0 -5
  91. package/dist/visual/icons/nodes.js +0 -4
  92. package/dist/visual/props.d.ts +14 -1
  93. package/dist/visual/strings.d.ts +7 -0
  94. package/dist/visual/strings.js +5 -2
  95. package/dist/visual/structure.d.ts +43 -87
  96. package/dist/visual/structure.js +42 -83
  97. package/package.json +2 -2
@@ -23,11 +23,12 @@
23
23
  outline: var(--_qm-ring);
24
24
  }
25
25
  /* Upstream's `-32px` is measured against the UA's 40px list indent, which the list
26
- rule below replaces with a rung. The offset moves with it, or a selected item's
27
- outline hangs into the margin by the difference. */
26
+ rule below replaces. The offset is that indent, so a selected item's outline takes in
27
+ its marker and stands on the list's own edge rather than crossing a `10.` or hanging
28
+ into the margin. */
28
29
  li.ProseMirror-selectednode::after {
29
30
  border: var(--_qm-ring);
30
- left: calc(var(--_qm-space-4) * -1);
31
+ left: calc(-1 * (var(--_qm-marker-digits) + var(--_qm-nest)));
31
32
  }
32
33
  /* A node selection is drawn while its leaf holds the focus, as a text selection is: a
33
34
  ring left behind on a field the focus has moved off names a subject no key reaches. */
@@ -57,29 +58,17 @@ li.ProseMirror-selectednode::after {
57
58
  /* One rhythm between sibling blocks, in place of the UA's collapsed pair. Scoped by
58
59
  parent rather than by tag, which is what keeps it true as the schema grows: the
59
60
  containers below are every node whose children are blocks (`schema.ts`), whatever
60
- kinds exist; a task item holds its blocks in a body past its checkbox (`tasks.ts`),
61
- so the body is the container rather than the item. A list and an item are
62
- containers like any other: an item stands off its neighbours as a paragraph does, a
63
- list nested under an item stands off the item's text, and an item's unnumbered line
64
- has the gap below it that the next item draws above, where a tight run would read it
65
- as the head of the next item.
61
+ kinds exist. A list is the exception: the whole list is one block among its
62
+ siblings, so it stands off them by the gap and its insides are tight: items, an
63
+ item's unnumbered lines, a task item's body and a sub-list all sit flush, told
64
+ apart by marker and indent rather than by space.
66
65
 
67
66
  A line box is ~22px at the paper size and the body leading, so the rung is a
68
67
  little over half a line: enough that a break reads as a break, short of the full
69
68
  blank line that doubles the leading and makes a short document read as a list of
70
69
  fragments. */
71
70
  .ProseMirror > * + *,
72
- .ProseMirror
73
- :is(
74
- blockquote,
75
- ul,
76
- ol,
77
- li:not([data-qm-checked]),
78
- [data-qm-task-body],
79
- [data-qm-unknown-container]
80
- )
81
- > *
82
- + * {
71
+ .ProseMirror :is(blockquote, [data-qm-unknown-container]) > * + * {
83
72
  margin-top: var(--_qm-space-3);
84
73
  }
85
74
 
@@ -203,7 +192,9 @@ li.ProseMirror-selectednode::after {
203
192
  }
204
193
 
205
194
  /* The UA's 40px list indent is the one box the reset above does not take (it is
206
- padding, not margin), and it is a length nothing chose. A rung replaces it.
195
+ padding, not margin), and it is a length nothing chose. The indent is room for a
196
+ marker's two digits and the nesting step past them, so a marker, `10.` included,
197
+ hangs into its own list's indent and never past the leaf's edge.
207
198
 
208
199
  The markers are the UA's, reclaimed rather than inherited. `disc` and `decimal`
209
200
  are right and this restates neither: a host reset (Tailwind's preflight sets
@@ -217,7 +208,7 @@ li.ProseMirror-selectednode::after {
217
208
  that takes those ever turns up: the positive half states what it leaves to the
218
209
  UA, and what a reset takes is what has to be asked back. */
219
210
  .ProseMirror :is(ul, ol) {
220
- padding-left: var(--_qm-space-4);
211
+ padding-left: calc(var(--_qm-marker-digits) + var(--_qm-nest));
221
212
  list-style: revert;
222
213
  }
223
214
 
@@ -240,7 +231,7 @@ li.ProseMirror-selectednode::after {
240
231
  width: var(--_qm-glyph-control);
241
232
  height: var(--_qm-glyph-control);
242
233
  border: var(--_qm-border-width) solid var(--_qm-border);
243
- border-radius: calc(var(--_qm-radius-inner) / 2);
234
+ border-radius: var(--_qm-radius-inner);
244
235
  background: var(--_qm-surface-well);
245
236
  cursor: pointer;
246
237
  transition:
@@ -277,10 +268,8 @@ li.ProseMirror-selectednode::after {
277
268
  /* Six levels, one weight, no size step. The quill reads no level at all, so a size
278
269
  ramp here would be the editor predicting a hierarchy the paper does not set; the
279
270
  level stays in the model, which is where the codec round-trips it. It is also all
280
- the ramp could carry: this leaf renders at `--_qm-text-paper` and the next rung up
281
- is `--_qm-text-title`, a chrome rung a heading inside a document has no business
282
- reading. Six levels do not fit in the gap, and
283
- minting five more is the drift the scale exists to prevent. */
271
+ the ramp could carry: this leaf renders at `--_qm-text-paper`, the top of the ramp,
272
+ and minting six more is the drift the scale exists to prevent. */
284
273
  .ProseMirror :is(h1, h2, h3, h4, h5, h6) {
285
274
  font-size: inherit;
286
275
  font-weight: var(--_qm-weight-strong);
@@ -413,7 +402,7 @@ li.ProseMirror-selectednode::after {
413
402
  .qm-table {
414
403
  border-collapse: separate;
415
404
  border-spacing: 0;
416
- border: var(--_qm-border-width) solid var(--_qm-rule);
405
+ border: var(--_qm-border-width) solid var(--_qm-border);
417
406
  border-radius: var(--_qm-radius);
418
407
  }
419
408
  /* One rule at one strength, the frame's and every line between cells, so the grid
@@ -421,19 +410,32 @@ li.ProseMirror-selectednode::after {
421
410
  `collapse`: a collapsed border cannot be rounded. */
422
411
  .qm-table-cell {
423
412
  position: relative;
424
- border-right: var(--_qm-border-width) solid var(--_qm-rule);
425
- border-bottom: var(--_qm-border-width) solid var(--_qm-rule);
413
+ border-right: var(--_qm-border-width) solid var(--_qm-border);
414
+ border-bottom: var(--_qm-border-width) solid var(--_qm-border);
426
415
  padding: var(--_qm-space) var(--_qm-space-2);
427
416
  vertical-align: top;
428
417
  font-weight: inherit;
429
418
  text-align: inherit;
430
419
  }
431
- .qm-table tr > .qm-table-cell:last-child {
420
+ .qm-table-cell[data-edge-end] {
432
421
  border-right: none;
433
422
  }
434
- .qm-table tbody tr:last-child > .qm-table-cell {
423
+ .qm-table-cell[data-edge-bottom] {
435
424
  border-bottom: none;
436
425
  }
426
+ /* The rails: a row of no height above the first and a column of no width before the
427
+ first, which the grips and border handles hang off rather than the first row's and
428
+ column's cells, a merged cell there standing over several lines. */
429
+ .qm-table-rail-cell {
430
+ position: relative;
431
+ width: 0;
432
+ height: 0;
433
+ padding: 0;
434
+ border: none;
435
+ }
436
+ .qm-table-rail-col {
437
+ width: 0;
438
+ }
437
439
  /* The header row is the one weight distinction the island draws: `header` is a
438
440
  separate field in the model rather than a flag on a row, so the surface says so.
439
441
  It takes a tone with it, the band rung, a step toward the ink at both poles. The
@@ -444,20 +446,17 @@ li.ProseMirror-selectednode::after {
444
446
  }
445
447
  /* The corner cells take the frame's corner, inside its stroke, so a header's band or a
446
448
  selection's wash keeps to the rounded edge rather than painting a square one over
447
- it. The first row is the colgroup's next sibling's, whichever section holds it. */
448
- .qm-table > colgroup + * > tr:first-child > .qm-table-cell:first-child {
449
+ it. A merged cell takes each corner its span reaches. */
450
+ .qm-table-cell[data-corners~='start-start'] {
449
451
  border-start-start-radius: calc(var(--_qm-radius) - var(--_qm-border-width));
450
452
  }
451
- .qm-table > colgroup + * > tr:first-child > .qm-table-cell:last-child {
453
+ .qm-table-cell[data-corners~='start-end'] {
452
454
  border-start-end-radius: calc(var(--_qm-radius) - var(--_qm-border-width));
453
455
  }
454
- .qm-table
455
- > :is(:last-child, thead:has(+ tbody:empty))
456
- > tr:last-child
457
- > .qm-table-cell:first-child {
456
+ .qm-table-cell[data-corners~='end-start'] {
458
457
  border-end-start-radius: calc(var(--_qm-radius) - var(--_qm-border-width));
459
458
  }
460
- .qm-table > :is(:last-child, thead:has(+ tbody:empty)) > tr:last-child > .qm-table-cell:last-child {
459
+ .qm-table-cell[data-corners~='end-end'] {
461
460
  border-end-end-radius: calc(var(--_qm-radius) - var(--_qm-border-width));
462
461
  }
463
462
  /* A cell holds a whole nested view. Its measure floor keeps an empty table from
@@ -659,10 +658,14 @@ li.ProseMirror-selectednode::after {
659
658
  /* The held column's alignment cluster, hung off its header cell and floating over the
660
659
  first row, the band's top edge being the grips'. Up only while its column is held and
661
660
  absent otherwise (`table-view.ts` §`alignCluster`), so the reveal is `hidden` rather
662
- than the band's opacity. It floats over content it did not choose, so the edge and
663
- `--_qm-lift` separate it and the raised rung joins them in dark (ARCHITECTURE
664
- §Styling). The pressed toggle takes the selected grip's tint and accent: it names the
665
- held column's alignment the way the grip names the held line. */
661
+ than the band's opacity. It is a row of glyphs floating over text, the format popover's
662
+ family (ARCHITECTURE §Styling), and takes that recipe: the popover's translucent
663
+ surface over a blur, the edge and `--_qm-lift` for its separation, and a scale-in on
664
+ arrival, stated from its starting style since the codec shows it by clearing
665
+ `hidden`. Its geometry is the popover's too, a row of toggles held one rung in from
666
+ a surface at the card's radius, so the two floating rows of glyphs are one height,
667
+ one shape and one material. The pressed toggle takes the selected grip's tint and
668
+ accent: it names the held column's alignment the way the grip names the held line. */
666
669
  .qm-table-align {
667
670
  position: absolute;
668
671
  z-index: 2;
@@ -671,10 +674,21 @@ li.ProseMirror-selectednode::after {
671
674
  width: max-content;
672
675
  margin-inline: auto;
673
676
  display: flex;
677
+ gap: var(--_qm-space-half);
678
+ padding: var(--_qm-space);
674
679
  border: var(--_qm-border-width) solid var(--_qm-border);
675
- border-radius: var(--_qm-radius-inner);
676
- background: var(--_qm-surface-raised);
680
+ border-radius: var(--_qm-radius);
681
+ background: var(--_qm-surface-popover);
682
+ backdrop-filter: blur(var(--_qm-blur));
683
+ -webkit-backdrop-filter: blur(var(--_qm-blur));
677
684
  box-shadow: var(--_qm-lift);
685
+ transition:
686
+ opacity var(--_qm-duration-fast) var(--_qm-ease-reverse),
687
+ transform var(--_qm-duration-fast) var(--_qm-ease-reverse);
688
+ @starting-style {
689
+ opacity: 0;
690
+ transform: scale(0.94);
691
+ }
678
692
  }
679
693
  .qm-table-align[hidden] {
680
694
  display: none;
@@ -684,6 +698,30 @@ li.ProseMirror-selectednode::after {
684
698
  .qm-table-align[data-header] {
685
699
  margin-inline: auto 0;
686
700
  }
701
+ /* The merge and split cluster hangs under the held block's first cell, flush with its
702
+ leading edge, its options worded. */
703
+ .qm-table-align[data-cells] {
704
+ margin-inline: 0 auto;
705
+ }
706
+ .qm-table-cells-option {
707
+ display: flex;
708
+ align-items: center;
709
+ min-height: var(--_qm-tap-min);
710
+ padding: 0 var(--_qm-space-2);
711
+ border: none;
712
+ border-radius: var(--_qm-radius-inner);
713
+ background: none;
714
+ color: var(--_qm-ink);
715
+ font: inherit;
716
+ white-space: nowrap;
717
+ cursor: pointer;
718
+ }
719
+ .qm-table-cells-option:hover {
720
+ background: var(--_qm-surface-hover);
721
+ }
722
+ .qm-table-cells-option[hidden] {
723
+ display: none;
724
+ }
687
725
  .qm-table-align-option {
688
726
  display: flex;
689
727
  align-items: center;
@@ -2,34 +2,28 @@ import { Schema } from 'prosemirror-model';
2
2
  import type { NodeType } from 'prosemirror-model';
3
3
  /** The full field schema: every block kind, container, mark, and island. */
4
4
  export declare const blockSchema: Schema<string, string>;
5
- /** The constrained inline schema: one paragraph, no block splitting, no
6
- * containers, no islands, and the full mark set (a `richtext(inline)` field). */
5
+ /**
6
+ * The constrained inline schema: one paragraph, no containers or islands, the full mark set (a
7
+ * `richtext(inline)` field).
8
+ */
7
9
  export declare const inlineSchema: Schema<string, string>;
8
10
  /**
9
- * The inline schema with no mark types at all: a `plaintext(inline)` field, whose
10
- * value is literal text the boundary refuses to coerce back once it carries a mark.
11
- *
12
- * Declaring none is what makes that structural rather than a rule each path
13
- * restates: `toggleMark` has no type to apply, the mark input rules build nothing,
14
- * a paste parses its marks away, and `decode` drops any a stray content arrives
15
- * carrying (CODEC §Inline mode).
11
+ * The inline schema with no mark types: a `plaintext(inline)` field, whose value is literal text
12
+ * the boundary refuses to coerce back once it carries a mark. Declaring none makes that structural:
13
+ * `toggleMark` has no type, the mark input rules build nothing, a paste parses marks away, and
14
+ * `decode` drops any a stray content carries (CODEC §Inline mode).
16
15
  */
17
16
  export declare const plaintextSchema: Schema<string, never>;
18
17
  /**
19
- * A table cell: the inline schema plus `hard_break`, which a `TableCell` stores as a
20
- * `\n` in its `text`. Still one paragraph, so Enter has no block to split; a break is
21
- * Shift-Enter's (`breaks.ts`). Declaring the break routes `decode` through the plain
22
- * path, which folds a cell's `continues` lines into that one paragraph.
18
+ * A table cell: the inline schema plus `hard_break`, which a `TableCell` stores as a `\n` in its
19
+ * `text`. Still one paragraph; a break is Shift-Enter's (`breaks.ts`). Declaring it routes `decode`
20
+ * through the plain path, folding a cell's `continues` lines into that paragraph.
23
21
  */
24
22
  export declare const cellSchema: Schema<"hard_break", string>;
25
23
  /**
26
- * A `text` field declaring `multiline: true`: paragraphs and hard breaks, no marks, no
27
- * islands, no containers, which is the shape `Content::is_plain` admits. Mark-free
28
- * for the reason {@link plaintextSchema} is.
29
- *
30
- * A paragraph and a break both store one `\n`, so a paragraph here is a line and
31
- * says so (`data-qm-line`): `prose.css` draws a run of them at the leading, and a
32
- * boundary reads as the one line down that a break does.
24
+ * A `text` field declaring `multiline: true`: paragraphs and hard breaks, no marks, islands or
25
+ * containers, the shape `Content::is_plain` admits. A paragraph and a break both store one `\n`, so
26
+ * a paragraph here is a line (`data-qm-line`): `prose.css` draws a run of them at the leading.
33
27
  */
34
28
  export declare const plainSchema: Schema<"text" | "doc" | "paragraph" | "hard_break", never>;
35
29
  /** The schema a leaf of the declared type mounts: `inline` narrows either type to one
@@ -46,7 +40,8 @@ export declare function isInlineSchema(schema: Schema): boolean;
46
40
  /** True for {@link blockSchema}, the one holding blocks past the paragraph: what a
47
41
  * leaf's island, gap-cursor and slash machinery mounts on. */
48
42
  export declare function isBlockSchema(schema: Schema): boolean;
49
- /** Whether `type` can hold a within-block line break, which a heading cannot (§`heading`).
50
- * What a `\n` landing in a textblock is rewritten to turns on it (`breaks.ts`), and so
51
- * does what decode puts between a block's continued segments. */
43
+ /**
44
+ * Whether `type` can hold a within-block line break, which a heading cannot (§`heading`); a `\n`
45
+ * landing in a textblock and decode's continued segments turn on it (`breaks.ts`).
46
+ */
52
47
  export declare function takesLineBreak(type: NodeType): boolean;
@@ -1,43 +1,41 @@
1
- // The PM schema: the codec owns it; decode/encode target it. Nodes mirror the
2
- // content block kinds (para/heading/code/rule/island) and its container nesting
3
- // (list_item/quote → lists/blockquote); marks mirror the content formatting set.
4
- // Each set is closed upstream, so the schema names its whole vocabulary and carries
5
- // nothing inert. `blockSchema` is the full field; `inlineSchema` is the constrained
6
- // single-textblock form for a one-line `richtext` (one paragraph, no block split, no
7
- // containers, no islands) and `plaintextSchema` is that one without marks, for a
8
- // one-line `text`; `plainSchema` is a `text` field declaring `multiline: true`,
9
- // paragraphs and hard breaks and nothing else; `cellSchema` is the inline form with
10
- // hard breaks, for a table cell. Same decode/lower/position machinery,
11
- // narrower shapes. Anchors are not marks here (decorations).
1
+ // The PM schema: the codec owns it; decode/encode target it. Nodes mirror the content block kinds
2
+ // (para/heading/code/rule/island) and container nesting (list_item/quote); marks mirror the content
3
+ // formatting set. Each set is closed upstream, so the schema names its whole vocabulary.
4
+ // `blockSchema` is the full field; `inlineSchema` the single-textblock form for a one-line
5
+ // `richtext`; `plaintextSchema` that without marks, for a one-line `text`; `plainSchema` a `text`
6
+ // field declaring `multiline: true` (paragraphs and hard breaks only); `cellSchema` the inline form
7
+ // with hard breaks, for a table cell. Anchors are not marks here (decorations).
12
8
  //
13
- // `toDOM` and `parseDOM` are one tier, not two halves of a rendering: a copy and a
14
- // paste inside one body run the whole document through them (CODEC §"Markdown at the
15
- // edges"), so every attribute written here is read back here. Foreign HTML spells none
16
- // of this package's `data-qm-*` names, so what a paste takes off the web is unchanged;
17
- // the rules here that widen that door on purpose are the fence's and a span's styled
18
- // marks, a `<table>` being the block leaf's clipboard parser's (`table-paste.ts`).
9
+ // `toDOM` and `parseDOM` are one tier: a copy and a paste inside one body run the whole document
10
+ // through them (CODEC §"Markdown at the edges"), so every attribute written here is read back here.
11
+ // Foreign HTML spells none of this package's `data-qm-*` names; the rules that widen that door on
12
+ // purpose are the fence's and a span's styled marks, a `<table>` being the block leaf's clipboard
13
+ // parser's (`table-paste.ts`).
19
14
  import { Schema } from 'prosemirror-model';
20
15
  import { islandBlockSpec, islandInlineSpec } from './islands.js';
21
16
  import { taskBody, taskChecked, taskDOM } from './tasks.js';
22
17
  import { rendersHref, storableUrl } from './urls.js';
23
18
  // ── Marks spelled as style ──────────────────────────────────────────────────
24
- // A word processor writes formatting as a span's inline style rather than as a tag:
25
- // Google Docs spells all four marks below that way, and wraps a whole copy in a `<b>`
26
- // stating a regular weight. Read off a `<span>` alone, since the same style on a cell or
27
- // a heading is that block's face rather than a run's. An underline inside a link is the
28
- // link's own: Docs underlines every one, and Chrome inlines the computed decoration on a
29
- // copy off any page.
19
+ // A word processor writes formatting as a span's inline style rather than a tag: Google Docs spells
20
+ // all four marks below that way, and wraps a whole copy in a `<b>` stating a regular weight. Read
21
+ // off a `<span>` alone (the same style on a cell or heading is that block's face). An underline
22
+ // inside a link is the link's own: Docs underlines every one, and Chrome inlines the computed
23
+ // decoration on a copy.
30
24
  /** Whether `el`'s inline style states a bold weight. */
31
25
  const bold = (el) => /^(?:bold(?:er)?|[6-9]\d\d)$/.test(el.style.fontWeight);
32
26
  /** The text decoration `el` states, as a shorthand or a longhand. */
33
27
  const decoration = (el) => `${el.style.textDecoration} ${el.style.textDecorationLine}`;
34
- /** A mark a `<span>` carries where `styled` holds of it. Not consuming, so every mark a
35
- * span states matches, and so do the span's own rules after them. */
28
+ /**
29
+ * A mark a `<span>` carries where `styled` holds of it. Not consuming, so every mark a span states
30
+ * matches, and so do the span's own rules.
31
+ */
36
32
  const styledSpan = (styled) => ({
37
33
  tag: 'span',
38
34
  consuming: false,
39
35
  getAttrs: (el) => styled(el) && null
40
36
  });
37
+ /** `s` as a CSS string literal. */
38
+ const cssString = (s) => `"${s.replace(/[\\"]/g, '\\$&').replace(/\n/g, '\\a ')}"`;
41
39
  // ── Marks (the block and inline schemas share them; plaintext declares none) ─
42
40
  const marks = {
43
41
  // Order matters: it fixes mark-set sort order and parse precedence. `link`
@@ -74,6 +72,29 @@ const marks = {
74
72
  toDOM: () => ['s', 0]
75
73
  },
76
74
  code: { parseDOM: [{ tag: 'code' }], toDOM: () => ['code', 0] },
75
+ // One family a character, PM's default exclusion of a mark by its own type: a second family
76
+ // replaces the first, as the store's keyed `add` does. A family the page cannot draw falls to
77
+ // the root's face. A span styling `font-family` is the source's face, never a mark.
78
+ font_family: {
79
+ attrs: { name: { default: '' } },
80
+ parseDOM: [
81
+ {
82
+ tag: 'span[data-qm-family]',
83
+ getAttrs: (el) => {
84
+ const name = el.getAttribute('data-qm-family');
85
+ return name ? { name } : false;
86
+ }
87
+ }
88
+ ],
89
+ toDOM: (mark) => {
90
+ const name = mark.attrs.name;
91
+ return [
92
+ 'span',
93
+ { 'data-qm-family': name, style: `font-family: ${cssString(name)}, var(--_qm-font)` },
94
+ 0
95
+ ];
96
+ }
97
+ },
77
98
  link: {
78
99
  attrs: { href: { default: '' } },
79
100
  inclusive: false,
@@ -84,11 +105,8 @@ const marks = {
84
105
  getAttrs: (el) => ({ href: storableUrl(el.getAttribute('data-qm-href') ?? '') })
85
106
  }
86
107
  ],
87
- // A refused href draws as a bare span: the text stands, unstyled and
88
- // unclickable, and the mark keeps its value for encode. The value rides on the
89
- // span rather than going with the tag — an `href` no renderer will follow is one
90
- // the document still holds, and a copy is not the explicit conversion allowed to
91
- // lose it.
108
+ // A refused href draws as a bare span, unstyled and unclickable, and the mark keeps its
109
+ // value for encode: a copy is not the explicit conversion allowed to lose it.
92
110
  toDOM: (mark) => {
93
111
  const href = mark.attrs.href;
94
112
  return rendersHref(href) ? ['a', { href }, 0] : ['span', { 'data-qm-href': href }, 0];
@@ -96,15 +114,14 @@ const marks = {
96
114
  }
97
115
  };
98
116
  // ── The fence's language ────────────────────────────────────────────────────
99
- // Minted by the fence shorthand, which fires on Enter with the language typed after the
100
- // backticks (`inputrules.ts`), and by a paste, which this reads: a fence copied off a
101
- // highlighted page arrives carrying the language that page stated, one copied inside
102
- // the body keeps its own.
117
+ // Minted by the fence shorthand (`inputrules.ts`) and by a paste: a fence copied off a highlighted
118
+ // page carries the page's language, one copied inside the body keeps its own.
103
119
  /** A `language-x` / `lang-x` token in a class list. */
104
120
  const LANG_CLASS = /(?:^|\s)lang(?:uage)?-(\S+)/;
105
- /** The language a `<pre>` states: `data-lang`, which is what `toDOM` writes, else the
106
- * class convention on the `pre` or on the `code` it wraps. `null` where it states
107
- * none, which is also what an empty value is. */
121
+ /**
122
+ * The language a `<pre>` states: `data-lang` (what `toDOM` writes), else the class convention on
123
+ * the `pre` or its `code`. `null` for none or an empty value.
124
+ */
108
125
  function fenceLang(pre) {
109
126
  const stated = pre.getAttribute('data-lang');
110
127
  if (stated)
@@ -125,10 +142,9 @@ const blockNodes = {
125
142
  parseDOM: [{ tag: 'p' }],
126
143
  toDOM: () => ['p', 0]
127
144
  },
128
- // A heading holds no `hard_break`: it is a block of one line, so the store clears a
129
- // `continues` after one and markdown has no syntax for a break inside it. Declaring
130
- // it out is what makes that structural — a paste parses the `<br>` away and a
131
- // conversion onto a heading drops it, rather than each door holding the rule.
145
+ // A heading holds no `hard_break`: the store clears a `continues` after one and markdown has no
146
+ // syntax for a break inside it. Declaring it out makes that structural rather than a rule each
147
+ // door holds.
132
148
  heading: {
133
149
  content: '(text | island_inline)*',
134
150
  group: 'block',
@@ -174,12 +190,11 @@ const blockNodes = {
174
190
  {
175
191
  tag: 'ol',
176
192
  getAttrs: (el) => {
177
- // A list stating no `start` starts at one, the absence `toDOM` writes for
178
- // it; `Number(null)` would read that absence as zero. A value the store
179
- // would normalize away reads as that absence too: the leaf re-hydrates
180
- // only on an external change (CODEC §Reconciliation), so a PM doc keeping
181
- // one disagrees with the store for the rest of the session. Zero is not
182
- // one — `0.` is an ordinal `importMarkdown` produces and the store keeps.
193
+ // A list stating no `start` starts at one, the absence `toDOM` writes;
194
+ // `Number(null)` would read it as zero. A value the store would normalize away
195
+ // reads as that absence too, since a PM doc keeping one would disagree with the
196
+ // store for the session (CODEC §Reconciliation). Zero is not one: `0.` is an
197
+ // ordinal `importMarkdown` produces.
183
198
  const stated = el.getAttribute('start');
184
199
  const start = stated ? Number(stated) : 1;
185
200
  return { start: Number.isSafeInteger(start) && start >= 0 ? start : 1 };
@@ -213,9 +228,8 @@ const blockNodes = {
213
228
  // ── Inline nodes ────────────────────────────────────────────────────────────
214
229
  const inlineLeafNodes = {
215
230
  text: { group: 'inline' },
216
- // `linebreakReplacement` is what makes a join across a `code_block`'s edge
217
- // lossless: `Transform.join` and `setBlockType` convert its `\n`s to breaks
218
- // rather than flattening them to spaces, and a break is a `continues` line.
231
+ // `linebreakReplacement` makes a join across a `code_block`'s edge lossless: `Transform.join`
232
+ // and `setBlockType` convert its `\n`s to breaks, which are `continues` lines.
219
233
  hard_break: {
220
234
  inline: true,
221
235
  group: 'inline',
@@ -231,45 +245,38 @@ export const blockSchema = new Schema({
231
245
  nodes: { ...blockNodes, ...inlineLeafNodes },
232
246
  marks
233
247
  });
234
- // ── The constrained inline nodes (the inline schemas and the cell's) ────
235
- // `doc: "paragraph"` (exactly one child) is what makes an Enter a no-op at the
236
- // model level: there is no second block for a split to land.
248
+ // `doc: "paragraph"` (exactly one child) makes Enter a no-op at the model level: no second block
249
+ // for a split to land.
237
250
  const inlineNodes = {
238
251
  doc: { content: 'paragraph' },
239
252
  paragraph: { content: 'inline*', toDOM: () => ['p', 0] },
240
253
  text: { group: 'inline' }
241
254
  };
242
- /** The constrained inline schema: one paragraph, no block splitting, no
243
- * containers, no islands, and the full mark set (a `richtext(inline)` field). */
255
+ /**
256
+ * The constrained inline schema: one paragraph, no containers or islands, the full mark set (a
257
+ * `richtext(inline)` field).
258
+ */
244
259
  export const inlineSchema = new Schema({ nodes: inlineNodes, marks });
245
260
  /**
246
- * The inline schema with no mark types at all: a `plaintext(inline)` field, whose
247
- * value is literal text the boundary refuses to coerce back once it carries a mark.
248
- *
249
- * Declaring none is what makes that structural rather than a rule each path
250
- * restates: `toggleMark` has no type to apply, the mark input rules build nothing,
251
- * a paste parses its marks away, and `decode` drops any a stray content arrives
252
- * carrying (CODEC §Inline mode).
261
+ * The inline schema with no mark types: a `plaintext(inline)` field, whose value is literal text
262
+ * the boundary refuses to coerce back once it carries a mark. Declaring none makes that structural:
263
+ * `toggleMark` has no type, the mark input rules build nothing, a paste parses marks away, and
264
+ * `decode` drops any a stray content carries (CODEC §Inline mode).
253
265
  */
254
266
  export const plaintextSchema = new Schema({ nodes: inlineNodes, marks: {} });
255
267
  /**
256
- * A table cell: the inline schema plus `hard_break`, which a `TableCell` stores as a
257
- * `\n` in its `text`. Still one paragraph, so Enter has no block to split; a break is
258
- * Shift-Enter's (`breaks.ts`). Declaring the break routes `decode` through the plain
259
- * path, which folds a cell's `continues` lines into that one paragraph.
268
+ * A table cell: the inline schema plus `hard_break`, which a `TableCell` stores as a `\n` in its
269
+ * `text`. Still one paragraph; a break is Shift-Enter's (`breaks.ts`). Declaring it routes `decode`
270
+ * through the plain path, folding a cell's `continues` lines into that paragraph.
260
271
  */
261
272
  export const cellSchema = new Schema({
262
273
  nodes: { ...inlineNodes, hard_break: inlineLeafNodes.hard_break },
263
274
  marks
264
275
  });
265
276
  /**
266
- * A `text` field declaring `multiline: true`: paragraphs and hard breaks, no marks, no
267
- * islands, no containers, which is the shape `Content::is_plain` admits. Mark-free
268
- * for the reason {@link plaintextSchema} is.
269
- *
270
- * A paragraph and a break both store one `\n`, so a paragraph here is a line and
271
- * says so (`data-qm-line`): `prose.css` draws a run of them at the leading, and a
272
- * boundary reads as the one line down that a break does.
277
+ * A `text` field declaring `multiline: true`: paragraphs and hard breaks, no marks, islands or
278
+ * containers, the shape `Content::is_plain` admits. A paragraph and a break both store one `\n`, so
279
+ * a paragraph here is a line (`data-qm-line`): `prose.css` draws a run of them at the leading.
273
280
  */
274
281
  export const plainSchema = new Schema({
275
282
  nodes: {
@@ -301,9 +308,10 @@ export function isInlineSchema(schema) {
301
308
  export function isBlockSchema(schema) {
302
309
  return !!schema.nodes.blockquote;
303
310
  }
304
- /** Whether `type` can hold a within-block line break, which a heading cannot (§`heading`).
305
- * What a `\n` landing in a textblock is rewritten to turns on it (`breaks.ts`), and so
306
- * does what decode puts between a block's continued segments. */
311
+ /**
312
+ * Whether `type` can hold a within-block line break, which a heading cannot (§`heading`); a `\n`
313
+ * landing in a textblock and decode's continued segments turn on it (`breaks.ts`).
314
+ */
307
315
  export function takesLineBreak(type) {
308
316
  const br = type.schema.nodes.hard_break;
309
317
  return !!br && type.contentMatch.matchType(br) !== null;