@heyramzi/ui 2.11.0 → 2.11.2

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 (267) hide show
  1. package/dist/agents/helpers.js +38 -38
  2. package/dist/agents/page-state.js +49 -50
  3. package/dist/agents/vendor/color.js +146 -127
  4. package/dist/agents/vendor/draw.js +617 -508
  5. package/dist/agents/vendor/engine.js +698 -599
  6. package/dist/agents/vendor/mesh.js +653 -683
  7. package/dist/agents/vendor/plastic.js +3913 -3112
  8. package/dist/agents/vendor/presets.js +163 -164
  9. package/dist/agents/vendor/shapes.js +21 -20
  10. package/dist/agents/vendor/ticker.js +41 -42
  11. package/dist/agents/vendor/types.js +1 -0
  12. package/dist/agents/vendor/wear.js +1421 -1448
  13. package/dist/ai/confirmation/confirmation-context.svelte.js +6 -6
  14. package/dist/ai/confirmation/index.js +3 -1
  15. package/dist/ai/index.js +33 -0
  16. package/dist/ai/prompt-input/index.js +3 -1
  17. package/dist/ai/reasoning/index.js +3 -1
  18. package/dist/ai/reasoning/reasoning-context.svelte.js +6 -6
  19. package/dist/ai/suggestion/index.js +3 -1
  20. package/dist/ai/typing-dots/index.js +3 -1
  21. package/dist/animations/text-swap.js +24 -24
  22. package/dist/brand/index.js +54 -48
  23. package/dist/canvas/card/resize.js +5 -3
  24. package/dist/canvas/edges/edge-path.js +255 -242
  25. package/dist/canvas/editor/context-menu.js +39 -36
  26. package/dist/canvas/editor/dispatch.svelte.js +154 -151
  27. package/dist/canvas/editor/editor-context.js +7 -2
  28. package/dist/canvas/editor/editor-mode.js +1 -0
  29. package/dist/canvas/editor/frame.js +73 -76
  30. package/dist/canvas/editor/hand-context.js +5 -2
  31. package/dist/canvas/editor/history-core.js +163 -191
  32. package/dist/canvas/editor/keymap.js +95 -169
  33. package/dist/canvas/editor/layout-actions.js +143 -144
  34. package/dist/canvas/editor/lock.js +52 -51
  35. package/dist/canvas/editor/rebase.js +217 -240
  36. package/dist/canvas/editor/style.js +132 -137
  37. package/dist/canvas/editor/text-field-context.js +7 -2
  38. package/dist/canvas/editor/z-order.js +65 -70
  39. package/dist/canvas/headless/alignment.js +22 -16
  40. package/dist/canvas/headless/break-apart-find.js +286 -279
  41. package/dist/canvas/headless/break-apart-pieces.js +62 -58
  42. package/dist/canvas/headless/break-apart-plan.js +299 -325
  43. package/dist/canvas/headless/break-apart-style.js +75 -70
  44. package/dist/canvas/headless/break-apart.js +188 -181
  45. package/dist/canvas/headless/canvas-cleanup.js +78 -58
  46. package/dist/canvas/headless/edge-anchors.js +115 -116
  47. package/dist/canvas/headless/edge-memo.js +24 -22
  48. package/dist/canvas/headless/edge-serialization.js +17 -27
  49. package/dist/canvas/headless/figure-geometry.js +232 -264
  50. package/dist/canvas/headless/fit-viewport.js +84 -67
  51. package/dist/canvas/headless/grid.js +88 -86
  52. package/dist/canvas/headless/icon-node.js +44 -36
  53. package/dist/canvas/headless/index.js +1 -0
  54. package/dist/canvas/headless/linkify.js +17 -19
  55. package/dist/canvas/headless/pen-marks.js +101 -95
  56. package/dist/canvas/headless/sketch.js +231 -209
  57. package/dist/canvas/headless/snap-guides.js +99 -83
  58. package/dist/canvas/headless/svg-figure.js +133 -117
  59. package/dist/canvas/headless/table-layout.js +146 -107
  60. package/dist/canvas/index.js +2 -0
  61. package/dist/canvas/lod/lod-mode.svelte.js +20 -18
  62. package/dist/canvas/lod/render-all.svelte.js +36 -39
  63. package/dist/canvas/ops/arrange.js +78 -71
  64. package/dist/canvas/ops/ops.js +260 -255
  65. package/dist/canvas/pen/colors.js +37 -35
  66. package/dist/canvas/pen/eraser.js +28 -27
  67. package/dist/canvas/pen/geometry.js +120 -130
  68. package/dist/canvas/pen/index.js +1 -0
  69. package/dist/canvas/pen/ops.js +11 -11
  70. package/dist/canvas/pen/pen-session.js +150 -159
  71. package/dist/canvas/pen/plugin.js +107 -104
  72. package/dist/canvas/pen/shapes.js +73 -68
  73. package/dist/canvas/pen/stroke-path.js +32 -30
  74. package/dist/canvas/pen/types.js +21 -21
  75. package/dist/canvas/pen/ui.js +10 -8
  76. package/dist/canvas/plugin.js +46 -46
  77. package/dist/canvas/screen/glyphs.js +12 -11
  78. package/dist/canvas/screen/labels.js +22 -22
  79. package/dist/canvas/screen/parts.js +98 -111
  80. package/dist/canvas/screen/plugin.js +47 -56
  81. package/dist/canvas/screen/types.js +30 -35
  82. package/dist/canvas/screen/ui.js +7 -7
  83. package/dist/canvas/test-utils.js +14 -14
  84. package/dist/canvas/types.js +1 -0
  85. package/dist/canvas/ui/forward-wheel.js +24 -25
  86. package/dist/canvas/ui/icons.js +64 -131
  87. package/dist/canvas/ui/place-panel.js +58 -49
  88. package/dist/canvas/ui/shortcut-tip.js +14 -12
  89. package/dist/canvas/ui/tools.js +300 -385
  90. package/dist/charts/dashboard/chart.js +4 -0
  91. package/dist/charts/dashboard/data-table.js +11 -0
  92. package/dist/charts/dashboard/windows.js +8 -3
  93. package/dist/charts/edge-fade.js +10 -5
  94. package/dist/charts/hover-band.js +16 -11
  95. package/dist/charts/index.js +0 -1
  96. package/dist/charts/motion.js +11 -7
  97. package/dist/charts/primitives/chart-utils.js +12 -12
  98. package/dist/components/copy/clipboard.js +32 -32
  99. package/dist/components/copy/copied.svelte.js +31 -23
  100. package/dist/components/data-table/data-table.js +39 -39
  101. package/dist/components/docs/index.js +4 -0
  102. package/dist/components/docs/scroll-spy.svelte.js +73 -68
  103. package/dist/components/list-page/filter-section.js +4 -1
  104. package/dist/components/list-page/index.js +2 -0
  105. package/dist/components/list-page/list-table.js +7 -2
  106. package/dist/components/list-page/queue-rail.js +14 -0
  107. package/dist/components/list-page/section-host.js +4 -2
  108. package/dist/components/list-page/step.js +4 -3
  109. package/dist/components/list-page/types.js +1 -0
  110. package/dist/components/picker/trigger.js +48 -30
  111. package/dist/components/picker/types.js +1 -0
  112. package/dist/components/segmented/collapse.js +38 -27
  113. package/dist/components/segmented/index.js +8 -0
  114. package/dist/components/segmented/segmented.js +67 -42
  115. package/dist/components/star-rating-input/index.js +3 -1
  116. package/dist/components/table-view/types.js +1 -0
  117. package/dist/components/tags-input/tags.js +81 -69
  118. package/dist/components/theme/index.js +4 -0
  119. package/dist/components/toolbar/index.js +4 -0
  120. package/dist/components/toolbar/toolbar-trigger.js +14 -1
  121. package/dist/editor/anchors.js +17 -13
  122. package/dist/editor/block-id.js +52 -51
  123. package/dist/editor/block-ops.js +108 -80
  124. package/dist/editor/doc-blocks.js +155 -166
  125. package/dist/editor/extensions.js +139 -180
  126. package/dist/editor/index.js +4 -0
  127. package/dist/editor/markdown-test-helpers.js +4 -2
  128. package/dist/editor/markdown.js +420 -408
  129. package/dist/editor/serializer.js +204 -200
  130. package/dist/editor/slash-items.js +89 -101
  131. package/dist/editor/youtube-url.js +25 -20
  132. package/dist/effects/animated-beam/path-calculator.svelte.js +43 -38
  133. package/dist/effects/animated-beam/types.js +1 -0
  134. package/dist/effects/border-beam/css.js +92 -78
  135. package/dist/effects/border-beam/presets.js +1705 -1720
  136. package/dist/effects/border-beam/register-properties.js +43 -33
  137. package/dist/effects/border-beam/types.js +1 -0
  138. package/dist/effects/image-comparison/compare.js +3 -2
  139. package/dist/effects/image-reveal/types.js +1 -0
  140. package/dist/effects/magnetic/magnetic.js +58 -52
  141. package/dist/effects/reveal/observe.js +50 -40
  142. package/dist/effects/reveal/types.js +1 -0
  143. package/dist/effects/shader-gradient/color.js +36 -29
  144. package/dist/effects/shader-gradient/renderer.js +384 -295
  145. package/dist/effects/shader-gradient/shaders.js +31 -0
  146. package/dist/effects/shader-gradient/types.js +1 -0
  147. package/dist/icons/apple-symbol-exports.js +99 -124
  148. package/dist/icons/sf-symbols.js +357 -461
  149. package/dist/icons/trades.js +43 -28
  150. package/dist/inbox/index.js +1 -0
  151. package/dist/inbox/logic.js +126 -122
  152. package/dist/inbox/types.js +43 -43
  153. package/dist/lib/arrive.js +19 -12
  154. package/dist/lib/icon-types.js +1 -0
  155. package/dist/lib/initials.js +16 -10
  156. package/dist/lib/layout.js +5 -0
  157. package/dist/lib/panel.js +13 -0
  158. package/dist/lib/result.js +22 -22
  159. package/dist/lib/sliding-pill.js +59 -44
  160. package/dist/lib/theme-transition.js +43 -43
  161. package/dist/lib/tone.js +31 -24
  162. package/dist/lib/utils.js +3 -1
  163. package/dist/onboarding/define-tours.js +24 -23
  164. package/dist/onboarding/onboarding.svelte.js +16 -12
  165. package/dist/onboarding/pick.js +10 -6
  166. package/dist/onboarding/stores.js +40 -41
  167. package/dist/onboarding/wait.js +24 -23
  168. package/dist/panel/side-panel-types.js +1 -0
  169. package/dist/record/demo-data.js +459 -486
  170. package/dist/record/fields.js +129 -131
  171. package/dist/record/format.js +70 -82
  172. package/dist/record/gutter.js +1 -0
  173. package/dist/record/kinds.js +95 -105
  174. package/dist/record/logic.js +238 -219
  175. package/dist/record/types.js +49 -48
  176. package/dist/shadcn/accordion/index.js +3 -1
  177. package/dist/shadcn/alert/index.js +3 -1
  178. package/dist/shadcn/alert-dialog/index.js +3 -1
  179. package/dist/shadcn/attachment/index.js +3 -1
  180. package/dist/shadcn/avatar/index.js +3 -1
  181. package/dist/shadcn/breadcrumb/index.js +3 -1
  182. package/dist/shadcn/bubble/index.js +3 -1
  183. package/dist/shadcn/button/index.js +3 -1
  184. package/dist/shadcn/calendar/index.js +3 -1
  185. package/dist/shadcn/card/index.js +3 -1
  186. package/dist/shadcn/checkbox/index.js +3 -1
  187. package/dist/shadcn/code-block/index.js +3 -1
  188. package/dist/shadcn/collapsible/index.js +3 -1
  189. package/dist/shadcn/command/index.js +4 -1
  190. package/dist/shadcn/context-menu/index.js +3 -1
  191. package/dist/shadcn/dialog/index.js +3 -1
  192. package/dist/shadcn/dropdown-menu/menu-item.js +40 -20
  193. package/dist/shadcn/empty/index.js +3 -1
  194. package/dist/shadcn/field/index.js +3 -1
  195. package/dist/shadcn/hooks/highlighter.js +77 -75
  196. package/dist/shadcn/hooks/is-mobile.svelte.js +3 -3
  197. package/dist/shadcn/hooks/use-highlighted-code.svelte.js +62 -62
  198. package/dist/shadcn/input/index.js +3 -1
  199. package/dist/shadcn/kbd/index.js +3 -1
  200. package/dist/shadcn/label/index.js +3 -1
  201. package/dist/shadcn/marker/index.js +3 -1
  202. package/dist/shadcn/message/index.js +3 -1
  203. package/dist/shadcn/message-scroller/index.js +3 -1
  204. package/dist/shadcn/popover/index.js +3 -1
  205. package/dist/shadcn/progress/index.js +3 -1
  206. package/dist/shadcn/progress/tone.js +8 -7
  207. package/dist/shadcn/radio-group/index.js +3 -1
  208. package/dist/shadcn/scroll-area/index.js +3 -1
  209. package/dist/shadcn/select/index.js +3 -1
  210. package/dist/shadcn/separator/index.js +3 -1
  211. package/dist/shadcn/sheet/index.js +3 -1
  212. package/dist/shadcn/sidebar/context.svelte.js +42 -28
  213. package/dist/shadcn/sidebar/index.js +3 -1
  214. package/dist/shadcn/skeleton/index.js +3 -1
  215. package/dist/shadcn/slider/index.js +3 -1
  216. package/dist/shadcn/switch/index.js +3 -1
  217. package/dist/shadcn/table/index.js +3 -1
  218. package/dist/shadcn/tabs/index.js +3 -1
  219. package/dist/shadcn/textarea/index.js +3 -1
  220. package/dist/shadcn/toggle/index.js +3 -1
  221. package/dist/shadcn/toggle-group/index.js +3 -1
  222. package/dist/shadcn/tooltip/context.js +7 -2
  223. package/dist/shadcn/tooltip/index.js +3 -1
  224. package/dist/shadcn/trace/index.js +3 -1
  225. package/dist/slides/bridges.js +65 -59
  226. package/dist/slides/deck-controller.svelte.js +210 -207
  227. package/dist/slides/export/capture.js +66 -71
  228. package/dist/slides/export/ffmpeg.js +76 -72
  229. package/dist/slides/export/index.js +3 -0
  230. package/dist/slides/export/launch.js +9 -6
  231. package/dist/slides/export/pdf.js +60 -54
  232. package/dist/slides/export/types.js +1 -0
  233. package/dist/slides/labels.js +24 -24
  234. package/dist/slides/layout.js +39 -41
  235. package/dist/slides/model.js +9 -7
  236. package/dist/slides/navigation.js +78 -73
  237. package/dist/slides/types.js +2 -2
  238. package/dist/status/define-statuses.js +18 -14
  239. package/dist/status/status-types.js +1 -0
  240. package/dist/test-setup.js +24 -20
  241. package/dist/thread/logic.js +73 -77
  242. package/dist/thread/types.js +21 -21
  243. package/dist/work/board-order.js +94 -90
  244. package/dist/work/dates.js +16 -9
  245. package/dist/work/detail/detail-types.js +18 -18
  246. package/dist/work/docs/edited.js +32 -29
  247. package/dist/work/docs/index.js +4 -0
  248. package/dist/work/docs/labels.js +34 -34
  249. package/dist/work/docs/menu.js +113 -106
  250. package/dist/work/docs/outline.js +39 -29
  251. package/dist/work/gantt/logic.js +211 -235
  252. package/dist/work/inbox/inbox-logic.js +71 -75
  253. package/dist/work/inbox/inbox-statuses.js +75 -72
  254. package/dist/work/inbox/inbox-types.js +1 -0
  255. package/dist/work/kanban-drag.js +27 -22
  256. package/dist/work/kanban-types.js +1 -0
  257. package/dist/work/presets/sales.js +36 -32
  258. package/dist/work/reorder.js +9 -8
  259. package/dist/work/task-types.js +1 -0
  260. package/dist/work/time/duration.js +51 -46
  261. package/dist/work/time/time-types.js +23 -23
  262. package/dist/work/use-kanban-dnd.svelte.js +126 -125
  263. package/package.json +3 -1
  264. package/src/components/list-page/master-detail.svelte +3 -1
  265. package/src/components/list-page/rail-row.svelte +4 -4
  266. package/src/components/tags-input/tags-input.svelte +2 -0
  267. package/src/editor/markdown.ts +30 -35
@@ -1,486 +1,498 @@
1
+ // markdown-it, not a hand-rolled tokenizer: CommonMark opens underscore
2
+ // emphasis only at word boundaries, so `clic_tempo_v2` is never italic. Pure
3
+ // string transformer, so it is Workers-safe. Italic serialises as `*`.
1
4
  import MarkdownIt from "markdown-it";
2
5
  import { generateBlockId } from "./block-id.js";
3
6
  import { YOUTUBE_ID_RE, parseYoutubeUrl } from "./youtube-url.js";
7
+ // =========================
8
+ // PARSER (markdown -> TipTap)
9
+ // =========================
10
+ // WHY the default preset: commonmark has no strikethrough (s_open/s_close).
11
+ // linkify: false keeps bare URLs as text. breaks: false keeps single newlines soft.
4
12
  const mdi = new MarkdownIt({
5
- breaks: false,
6
- linkify: false
13
+ breaks: false,
14
+ linkify: false
7
15
  });
16
+ // WHY: a paragraph holding one image token with a YouTube src is the markdown
17
+ // form of a `youtube` block. It reads markdown-it's own image token, so no
18
+ // custom parser rule.
8
19
  function imageTokenToYoutubeNode(tok) {
9
- if (tok.type !== "image") return null;
10
- const src = String(tok.attrGet("src") ?? "");
11
- const parsed = parseYoutubeUrl(src);
12
- if (!parsed) return null;
13
- return {
14
- type: "youtube",
15
- attrs: {
16
- videoId: parsed.videoId,
17
- title: tok.content ?? "",
18
- start: parsed.start,
19
- id: null
20
- }
21
- };
20
+ if (tok.type !== "image") return null;
21
+ const src = String(tok.attrGet("src") ?? "");
22
+ const parsed = parseYoutubeUrl(src);
23
+ if (!parsed) return null;
24
+ return {
25
+ type: "youtube",
26
+ attrs: {
27
+ videoId: parsed.videoId,
28
+ title: tok.content ?? "",
29
+ start: parsed.start,
30
+ id: null
31
+ }
32
+ };
22
33
  }
23
34
  export function markdownToTiptap(md) {
24
- const tokens = mdi.parse(md.trim(), {});
25
- const blocks = parseTokensToBlocks(tokens);
26
- return {
27
- type: "doc",
28
- content: blocks
29
- };
30
- }
35
+ const tokens = mdi.parse(md.trim(), {});
36
+ const blocks = parseTokensToBlocks(tokens);
37
+ return {
38
+ type: "doc",
39
+ content: blocks
40
+ };
41
+ }
42
+ // Walk the flat token array. markdown-it emits block-level open/close pairs
43
+ // at nesting level 0 in the top-level array; inline content is in token.children.
31
44
  function parseTokensToBlocks(tokens) {
32
- const blocks = [];
33
- let i = 0;
34
- while(i < tokens.length){
35
- const parse = Object.hasOwn(BLOCK_PARSERS, tokens[i].type) ? BLOCK_PARSERS[tokens[i].type] : null;
36
- if (!parse) {
37
- i++;
38
- continue;
39
- }
40
- const { node, next } = parse(tokens, i);
41
- blocks.push(stampId(node));
42
- i = next;
43
- }
44
- return blocks;
45
+ const blocks = [];
46
+ let i = 0;
47
+ for (let tok = tokens[i]; tok; tok = tokens[i]) {
48
+ const parse = Object.hasOwn(BLOCK_PARSERS, tok.type) ? BLOCK_PARSERS[tok.type] : null;
49
+ // Unknown or unhandled tokens (html_block, etc.) are skipped.
50
+ if (!parse) {
51
+ i++;
52
+ continue;
53
+ }
54
+ const { node, next } = parse(tokens, i, tok);
55
+ blocks.push(stampId(node));
56
+ i = next;
57
+ }
58
+ return blocks;
45
59
  }
46
60
  function inlineChildrenAt(tokens, i) {
47
- const inlineTok = tokens[i];
48
- return inlineTok?.type === "inline" ? inlineTok.children ?? [] : [];
61
+ const inlineTok = tokens[i];
62
+ return inlineTok?.type === "inline" ? inlineTok.children ?? [] : [];
49
63
  }
50
64
  function orderedStart(tok) {
51
- const startAttr = tok.attrGet("start");
52
- return startAttr !== null ? parseInt(String(startAttr), 10) : 1;
65
+ const startAttr = tok.attrGet("start");
66
+ return startAttr !== null ? parseInt(String(startAttr), 10) : 1;
53
67
  }
68
+ // open + inline + close, so the next block starts three tokens on.
54
69
  function parseParagraph(tokens, i) {
55
- const children = inlineChildrenAt(tokens, i + 1);
56
- const youtube = children.length === 1 ? imageTokenToYoutubeNode(children[0]) : null;
57
- const node = youtube ?? {
58
- type: "paragraph",
59
- attrs: {
60
- id: null
61
- },
62
- content: parseInlineChildren(children)
63
- };
64
- return {
65
- node,
66
- next: i + 3
67
- };
68
- }
69
- function parseHeading(tokens, i) {
70
- const level = headingLevel(tokens[i].markup);
71
- const content = parseInlineChildren(inlineChildrenAt(tokens, i + 1));
72
- return {
73
- node: {
74
- type: "heading",
75
- attrs: {
76
- level,
77
- id: null
78
- },
79
- content
80
- },
81
- next: i + 3
82
- };
83
- }
84
- function parseFence(tokens, i) {
85
- const tok = tokens[i];
86
- const lang = tok.info.trim() || null;
87
- const code = tok.content.endsWith("\n") ? tok.content.slice(0, -1) : tok.content;
88
- const node = {
89
- type: "codeBlock",
90
- attrs: {
91
- language: lang,
92
- id: null
93
- },
94
- content: code ? [
95
- {
96
- type: "text",
97
- text: code
98
- }
99
- ] : []
100
- };
101
- return {
102
- node,
103
- next: i + 1
104
- };
70
+ const children = inlineChildrenAt(tokens, i + 1);
71
+ // A lone image with a YouTube src becomes a `youtube` block.
72
+ const [only, ...rest] = children;
73
+ const youtube = only && !rest.length ? imageTokenToYoutubeNode(only) : null;
74
+ const node = youtube ?? {
75
+ type: "paragraph",
76
+ attrs: { id: null },
77
+ content: parseInlineChildren(children)
78
+ };
79
+ return {
80
+ node,
81
+ next: i + 3
82
+ };
83
+ }
84
+ function parseHeading(tokens, i, tok) {
85
+ const level = headingLevel(tok.markup);
86
+ const content = parseInlineChildren(inlineChildrenAt(tokens, i + 1));
87
+ return {
88
+ node: {
89
+ type: "heading",
90
+ attrs: {
91
+ level,
92
+ id: null
93
+ },
94
+ content
95
+ },
96
+ next: i + 3
97
+ };
98
+ }
99
+ function parseFence(_tokens, i, tok) {
100
+ const lang = tok.info.trim() || null;
101
+ const code = tok.content.endsWith("\n") ? tok.content.slice(0, -1) : tok.content;
102
+ const node = {
103
+ type: "codeBlock",
104
+ attrs: {
105
+ language: lang,
106
+ id: null
107
+ },
108
+ content: code ? [{
109
+ type: "text",
110
+ text: code
111
+ }] : []
112
+ };
113
+ return {
114
+ node,
115
+ next: i + 1
116
+ };
105
117
  }
106
118
  function parseBlockquote(tokens, i) {
107
- const { inner, next } = collectUntilClose(tokens, i + 1, "blockquote");
108
- const innerBlocks = parseTokensToBlocks(inner).map(stripId);
109
- const content = innerBlocks.length ? innerBlocks : [
110
- {
111
- type: "paragraph",
112
- content: []
113
- }
114
- ];
115
- return {
116
- node: {
117
- type: "blockquote",
118
- attrs: {
119
- id: null
120
- },
121
- content
122
- },
123
- next: next + 1
124
- };
119
+ const { inner, next } = collectUntilClose(tokens, i + 1, "blockquote");
120
+ const innerBlocks = parseTokensToBlocks(inner).map(stripId);
121
+ const content = innerBlocks.length ? innerBlocks : [{
122
+ type: "paragraph",
123
+ content: []
124
+ }];
125
+ // next is the blockquote_close, which is skipped.
126
+ return {
127
+ node: {
128
+ type: "blockquote",
129
+ attrs: { id: null },
130
+ content
131
+ },
132
+ next: next + 1
133
+ };
125
134
  }
126
135
  function parseBulletList(tokens, i) {
127
- const { inner, next } = collectUntilClose(tokens, i + 1, "bullet_list");
128
- const node = {
129
- type: "bulletList",
130
- attrs: {
131
- id: null
132
- },
133
- content: parseListItems(inner)
134
- };
135
- return {
136
- node,
137
- next: next + 1
138
- };
139
- }
140
- function parseOrderedList(tokens, i) {
141
- const start = orderedStart(tokens[i]);
142
- const { inner, next } = collectUntilClose(tokens, i + 1, "ordered_list");
143
- const node = {
144
- type: "orderedList",
145
- attrs: {
146
- id: null,
147
- start
148
- },
149
- content: parseListItems(inner)
150
- };
151
- return {
152
- node,
153
- next: next + 1
154
- };
136
+ const { inner, next } = collectUntilClose(tokens, i + 1, "bullet_list");
137
+ const node = {
138
+ type: "bulletList",
139
+ attrs: { id: null },
140
+ content: parseListItems(inner)
141
+ };
142
+ return {
143
+ node,
144
+ next: next + 1
145
+ };
146
+ }
147
+ function parseOrderedList(tokens, i, tok) {
148
+ const start = orderedStart(tok);
149
+ const { inner, next } = collectUntilClose(tokens, i + 1, "ordered_list");
150
+ const node = {
151
+ type: "orderedList",
152
+ attrs: {
153
+ id: null,
154
+ start
155
+ },
156
+ content: parseListItems(inner)
157
+ };
158
+ return {
159
+ node,
160
+ next: next + 1
161
+ };
155
162
  }
156
163
  const BLOCK_PARSERS = {
157
- paragraph_open: parseParagraph,
158
- heading_open: parseHeading,
159
- hr: (_tokens, i)=>({
160
- node: {
161
- type: "horizontalRule",
162
- attrs: {
163
- id: null
164
- }
165
- },
166
- next: i + 1
167
- }),
168
- fence: parseFence,
169
- blockquote_open: parseBlockquote,
170
- bullet_list_open: parseBulletList,
171
- ordered_list_open: parseOrderedList
164
+ paragraph_open: parseParagraph,
165
+ heading_open: parseHeading,
166
+ hr: (_tokens, i) => ({
167
+ node: {
168
+ type: "horizontalRule",
169
+ attrs: { id: null }
170
+ },
171
+ next: i + 1
172
+ }),
173
+ fence: parseFence,
174
+ blockquote_open: parseBlockquote,
175
+ bullet_list_open: parseBulletList,
176
+ ordered_list_open: parseOrderedList
172
177
  };
178
+ // Collect tokens between the current position and the matching _close token.
179
+ // Returns the inner tokens (excluding open/close) and the index of the close token.
173
180
  function collectUntilClose(tokens, start, type) {
174
- const openType = `${type}_open`;
175
- const closeType = `${type}_close`;
176
- let depth = 1;
177
- let i = start;
178
- while(i < tokens.length){
179
- if (tokens[i].type === openType) depth++;
180
- else if (tokens[i].type === closeType) {
181
- depth--;
182
- if (depth === 0) {
183
- return {
184
- inner: tokens.slice(start, i),
185
- next: i
186
- };
187
- }
188
- }
189
- i++;
190
- }
191
- return {
192
- inner: tokens.slice(start),
193
- next: i
194
- };
195
- }
181
+ const openType = `${type}_open`;
182
+ const closeType = `${type}_close`;
183
+ let depth = 1;
184
+ for (let i = start; i < tokens.length; i++) {
185
+ const type = tokens[i]?.type;
186
+ if (type === openType) depth++;
187
+ else if (type === closeType) {
188
+ depth--;
189
+ if (depth === 0) {
190
+ return {
191
+ inner: tokens.slice(start, i),
192
+ next: i
193
+ };
194
+ }
195
+ }
196
+ }
197
+ // Unclosed block: return everything remaining.
198
+ return {
199
+ inner: tokens.slice(start),
200
+ next: tokens.length
201
+ };
202
+ }
203
+ // Parse list_item_open ... list_item_close pairs into listItem nodes.
196
204
  function parseListItems(tokens) {
197
- const items = [];
198
- let i = 0;
199
- while(i < tokens.length){
200
- if (tokens[i].type === "list_item_open") {
201
- const { inner, next } = collectUntilClose(tokens, i + 1, "list_item");
202
- items.push(parseListItemContent(inner));
203
- i = next + 1;
204
- } else {
205
- i++;
206
- }
207
- }
208
- return items;
209
- }
205
+ const items = [];
206
+ let i = 0;
207
+ for (let tok = tokens[i]; tok; tok = tokens[i]) {
208
+ if (tok.type === "list_item_open") {
209
+ const { inner, next } = collectUntilClose(tokens, i + 1, "list_item");
210
+ items.push(parseListItemContent(inner));
211
+ i = next + 1;
212
+ } else {
213
+ i++;
214
+ }
215
+ }
216
+ return items;
217
+ }
218
+ // Turn a list_item's inner tokens (paragraphs and nested lists) into a listItem.
219
+ // WHY: nested nodes get no ids; only top-level blocks do.
210
220
  function parseListItemContent(tokens) {
211
- const content = [];
212
- let i = 0;
213
- while(i < tokens.length){
214
- const tok = tokens[i];
215
- if (tok.type === "paragraph_open") {
216
- content.push({
217
- type: "paragraph",
218
- content: parseInlineChildren(inlineChildrenAt(tokens, i + 1))
219
- });
220
- i += 3;
221
- } else if (tok.type === "bullet_list_open" || tok.type === "ordered_list_open") {
222
- const { node, next } = BLOCK_PARSERS[tok.type](tokens, i);
223
- content.push(stripId(node));
224
- i = next;
225
- } else {
226
- i++;
227
- }
228
- }
229
- if (!content.length) {
230
- content.push({
231
- type: "paragraph",
232
- content: []
233
- });
234
- }
235
- return {
236
- type: "listItem",
237
- content
238
- };
239
- }
221
+ const content = [];
222
+ let i = 0;
223
+ for (let tok = tokens[i]; tok; tok = tokens[i]) {
224
+ if (tok.type === "paragraph_open") {
225
+ content.push({
226
+ type: "paragraph",
227
+ content: parseInlineChildren(inlineChildrenAt(tokens, i + 1))
228
+ });
229
+ i += 3;
230
+ } else if (tok.type === "bullet_list_open" || tok.type === "ordered_list_open") {
231
+ const parse = tok.type === "bullet_list_open" ? parseBulletList : parseOrderedList;
232
+ const { node, next } = parse(tokens, i, tok);
233
+ content.push(stripId(node));
234
+ i = next;
235
+ } else {
236
+ i++;
237
+ }
238
+ }
239
+ if (!content.length) {
240
+ content.push({
241
+ type: "paragraph",
242
+ content: []
243
+ });
244
+ }
245
+ return {
246
+ type: "listItem",
247
+ content
248
+ };
249
+ }
250
+ // Parse inline token children (flat array from mdi.parse → inline token → children).
251
+ // Maintains a mark stack to accumulate active marks for each text token.
240
252
  function parseInlineChildren(tokens) {
241
- const nodes = [];
242
- const markStack = [];
243
- for (const tok of tokens){
244
- const handle = Object.hasOwn(INLINE_HANDLERS, tok.type) ? INLINE_HANDLERS[tok.type] : null;
245
- const node = handle?.(tok, markStack);
246
- if (node) nodes.push(node);
247
- }
248
- return nodes;
253
+ const nodes = [];
254
+ const markStack = [];
255
+ for (const tok of tokens) {
256
+ const handle = Object.hasOwn(INLINE_HANDLERS, tok.type) ? INLINE_HANDLERS[tok.type] : null;
257
+ // html_inline, entity, image and the rest are dropped. Images are out of scope.
258
+ const node = handle?.(tok, markStack);
259
+ if (node) nodes.push(node);
260
+ }
261
+ return nodes;
249
262
  }
250
263
  function textNode(tok, markStack) {
251
- if (!tok.content) return;
252
- const marks = markStack.length ? [
253
- ...markStack
254
- ] : undefined;
255
- return {
256
- type: "text",
257
- text: tok.content,
258
- ...marks ? {
259
- marks
260
- } : {}
261
- };
262
- }
263
- const openMark = (type)=>(_tok, markStack)=>{
264
- markStack.push({
265
- type
266
- });
267
- };
268
- const closeMark = (type)=>(_tok, markStack)=>{
269
- removeLastMark(markStack, type);
270
- };
264
+ if (!tok.content) return;
265
+ const marks = markStack.length ? [...markStack] : undefined;
266
+ return {
267
+ type: "text",
268
+ text: tok.content,
269
+ ...marks ? { marks } : {}
270
+ };
271
+ }
272
+ const openMark = (type) => (_tok, markStack) => {
273
+ markStack.push({ type });
274
+ };
275
+ const closeMark = (type) => (_tok, markStack) => {
276
+ removeLastMark(markStack, type);
277
+ };
271
278
  function openLink(tok, markStack) {
272
- const href = tok.attrGet("href") ?? "";
273
- const title = tok.attrGet("title");
274
- markStack.push({
275
- type: "link",
276
- attrs: {
277
- href,
278
- target: null,
279
- rel: null,
280
- ...title ? {
281
- title
282
- } : {}
283
- }
284
- });
279
+ const href = tok.attrGet("href") ?? "";
280
+ const title = tok.attrGet("title");
281
+ markStack.push({
282
+ type: "link",
283
+ attrs: {
284
+ href,
285
+ target: null,
286
+ rel: null,
287
+ ...title ? { title } : {}
288
+ }
289
+ });
285
290
  }
286
291
  const INLINE_HANDLERS = {
287
- text: textNode,
288
- text_special: textNode,
289
- softbreak: ()=>({
290
- type: "text",
291
- text: " "
292
- }),
293
- hardbreak: ()=>({
294
- type: "hardBreak"
295
- }),
296
- code_inline: (tok, markStack)=>({
297
- type: "text",
298
- text: tok.content,
299
- marks: [
300
- ...markStack,
301
- {
302
- type: "code"
303
- }
304
- ]
305
- }),
306
- strong_open: openMark("bold"),
307
- strong_close: closeMark("bold"),
308
- em_open: openMark("italic"),
309
- em_close: closeMark("italic"),
310
- s_open: openMark("strike"),
311
- s_close: closeMark("strike"),
312
- link_open: openLink,
313
- link_close: closeMark("link")
292
+ text: textNode,
293
+ text_special: textNode,
294
+ // WHY: a softbreak is a newline inside a paragraph, so it reads as a space.
295
+ softbreak: () => ({
296
+ type: "text",
297
+ text: " "
298
+ }),
299
+ hardbreak: () => ({ type: "hardBreak" }),
300
+ code_inline: (tok, markStack) => ({
301
+ type: "text",
302
+ text: tok.content,
303
+ marks: [...markStack, { type: "code" }]
304
+ }),
305
+ strong_open: openMark("bold"),
306
+ strong_close: closeMark("bold"),
307
+ em_open: openMark("italic"),
308
+ em_close: closeMark("italic"),
309
+ s_open: openMark("strike"),
310
+ s_close: closeMark("strike"),
311
+ link_open: openLink,
312
+ link_close: closeMark("link")
314
313
  };
315
314
  function removeLastMark(stack, type) {
316
- for(let i = stack.length - 1; i >= 0; i--){
317
- if (stack[i].type === type) {
318
- stack.splice(i, 1);
319
- return;
320
- }
321
- }
315
+ const i = stack.findLastIndex((mark) => mark.type === type);
316
+ if (i >= 0) stack.splice(i, 1);
322
317
  }
323
318
  function headingLevel(markup) {
324
- const n = markup.length;
325
- return Math.min(Math.max(n, 1), 6);
319
+ const n = markup.length;
320
+ return Math.min(Math.max(n, 1), 6);
326
321
  }
327
322
  function stampId(node) {
328
- return {
329
- ...node,
330
- attrs: {
331
- ...node.attrs,
332
- id: generateBlockId()
333
- }
334
- };
335
- }
323
+ return {
324
+ ...node,
325
+ attrs: {
326
+ ...node.attrs,
327
+ id: generateBlockId()
328
+ }
329
+ };
330
+ }
331
+ // WHY: Nested blocks (inside blockquote, list items) must not carry top-level IDs.
332
+ // The ID stamping contract: only direct children of "doc" get an id attribute.
336
333
  function stripId(node) {
337
- if (!node.attrs || !("id" in node.attrs)) return node;
338
- const { id: _id, ...rest } = node.attrs;
339
- if (Object.keys(rest).length > 0) return {
340
- ...node,
341
- attrs: rest
342
- };
343
- const { attrs: _attrs, ...bare } = node;
344
- return bare;
345
- }
334
+ if (!node.attrs || !("id" in node.attrs)) return node;
335
+ const { id: _id, ...rest } = node.attrs;
336
+ if (Object.keys(rest).length > 0) return {
337
+ ...node,
338
+ attrs: rest
339
+ };
340
+ const { attrs: _attrs, ...bare } = node;
341
+ return bare;
342
+ }
343
+ // =========================
344
+ // SERIALIZER (TipTap -> markdown)
345
+ // =========================
346
346
  export function tiptapToMarkdown(doc) {
347
- const d = doc;
348
- if (!d || d.type !== "doc" || !Array.isArray(d.content)) return "";
349
- return d.content.map((node)=>nodeToMd(node, 0)).filter(Boolean).join("\n\n");
347
+ const d = doc;
348
+ if (!d || d.type !== "doc" || !Array.isArray(d.content)) return "";
349
+ return d.content.map((node) => nodeToMd(node, 0)).filter(Boolean).join("\n\n");
350
350
  }
351
351
  function numAttr(node, key) {
352
- const value = node.attrs?.[key];
353
- return typeof value === "number" ? value : undefined;
352
+ const value = node.attrs?.[key];
353
+ return typeof value === "number" ? value : undefined;
354
354
  }
355
355
  function strAttr(node, key) {
356
- const value = node.attrs?.[key];
357
- return typeof value === "string" ? value : undefined;
356
+ const value = node.attrs?.[key];
357
+ return typeof value === "string" ? value : undefined;
358
358
  }
359
359
  function childrenToMd(node, listDepth) {
360
- return (node.content ?? []).map((child)=>nodeToMd(child, listDepth)).filter(Boolean).join("\n\n");
360
+ return (node.content ?? []).map((child) => nodeToMd(child, listDepth)).filter(Boolean).join("\n\n");
361
361
  }
362
362
  function headingToMd(node) {
363
- const level = numAttr(node, "level") ?? 1;
364
- const prefix = "#".repeat(Math.min(Math.max(level, 1), 6));
365
- return `${prefix} ${inlineToMd(node.content ?? [])}`;
363
+ const level = numAttr(node, "level") ?? 1;
364
+ const prefix = "#".repeat(Math.min(Math.max(level, 1), 6));
365
+ return `${prefix} ${inlineToMd(node.content ?? [])}`;
366
366
  }
367
367
  function orderedListToMd(node, listDepth) {
368
- const start = numAttr(node, "start") ?? 1;
369
- return (node.content ?? []).map((item, i)=>listItemToMd(item, `${start + i}.`, listDepth)).join("\n");
368
+ const start = numAttr(node, "start") ?? 1;
369
+ return (node.content ?? []).map((item, i) => listItemToMd(item, `${start + i}.`, listDepth)).join("\n");
370
370
  }
371
371
  function blockquoteToMd(node) {
372
- return childrenToMd(node, 0).split("\n").map((line)=>`> ${line}`).join("\n");
372
+ return childrenToMd(node, 0).split("\n").map((line) => `> ${line}`).join("\n");
373
373
  }
374
374
  function codeBlockToMd(node) {
375
- const lang = strAttr(node, "language") ?? "";
376
- return `\`\`\`${lang}\n${extractPlainText(node.content ?? [])}\n\`\`\``;
375
+ const lang = strAttr(node, "language") ?? "";
376
+ return `\`\`\`${lang}\n${extractPlainText(node.content ?? [])}\n\`\`\``;
377
377
  }
378
+ // WHY: the contract's markdown form is an image alone in its paragraph, so this
379
+ // writes back `![title](watch-url)`.
378
380
  function youtubeToMd(node) {
379
- const videoId = strAttr(node, "videoId") ?? "";
380
- if (!YOUTUBE_ID_RE.test(videoId)) return "";
381
- const title = strAttr(node, "title") ?? "";
382
- const rawStart = numAttr(node, "start") ?? 0;
383
- const start = rawStart > 0 ? rawStart : 0;
384
- const url = `https://www.youtube.com/watch?v=${videoId}${start ? `&t=${start}` : ""}`;
385
- return `![${escapeMdText(title)}](${url})`;
386
- }
387
- const dropped = ()=>"";
381
+ const videoId = strAttr(node, "videoId") ?? "";
382
+ if (!YOUTUBE_ID_RE.test(videoId)) return "";
383
+ const title = strAttr(node, "title") ?? "";
384
+ const rawStart = numAttr(node, "start") ?? 0;
385
+ const start = rawStart > 0 ? rawStart : 0;
386
+ const url = `https://www.youtube.com/watch?v=${videoId}${start ? `&t=${start}` : ""}`;
387
+ return `![${escapeMdText(title)}](${url})`;
388
+ }
389
+ const dropped = () => "";
388
390
  function taskBullet(item) {
389
- return item.attrs?.checked === true ? "- [x]" : "- [ ]";
391
+ return item.attrs?.checked === true ? "- [x]" : "- [ ]";
390
392
  }
393
+ // GFM pipe table. The first row is the header; a cell holds its paragraphs on one line.
391
394
  function tableToMd(node) {
392
- const rows = (node.content ?? []).map((row)=>(row.content ?? []).map((cell)=>(cell.content ?? []).map((block)=>inlineToMd(block.content ?? [])).join(" ").replace(/\|/g, "\\|")));
393
- if (rows.length === 0) return "";
394
- const width = Math.max(...rows.map((row)=>row.length));
395
- const line = (cells)=>`| ${Array.from({
396
- length: width
397
- }, (_, i)=>cells[i] ?? "").join(" | ")} |`;
398
- const rule = `| ${Array.from({
399
- length: width
400
- }, ()=>"---").join(" | ")} |`;
401
- return [
402
- line(rows[0]),
403
- rule,
404
- ...rows.slice(1).map(line)
405
- ].join("\n");
395
+ const rows = (node.content ?? []).map((row) => (row.content ?? []).map((cell) => (cell.content ?? []).map((block) => inlineToMd(block.content ?? [])).join(" ").replace(/\|/g, "\\|")));
396
+ const [header, ...body] = rows;
397
+ if (!header) return "";
398
+ const width = Math.max(...rows.map((row) => row.length));
399
+ const line = (cells) => `| ${Array.from({ length: width }, (_, i) => cells[i] ?? "").join(" | ")} |`;
400
+ const rule = `| ${Array.from({ length: width }, () => "---").join(" | ")} |`;
401
+ return [
402
+ line(header),
403
+ rule,
404
+ ...body.map(line)
405
+ ].join("\n");
406
406
  }
407
407
  const MD_WRITERS = {
408
- paragraph: (node)=>inlineToMd(node.content ?? []),
409
- heading: headingToMd,
410
- bulletList: (node, listDepth)=>(node.content ?? []).map((item)=>listItemToMd(item, "-", listDepth)).join("\n"),
411
- orderedList: orderedListToMd,
412
- listItem: (node, listDepth)=>listItemContentsToMd(node, listDepth),
413
- blockquote: blockquoteToMd,
414
- codeBlock: codeBlockToMd,
415
- horizontalRule: ()=>"---",
416
- hardBreak: ()=>" \n",
417
- youtube: youtubeToMd,
418
- taskList: (node, listDepth)=>(node.content ?? []).map((item)=>listItemToMd(item, taskBullet(item), listDepth)).join("\n"),
419
- taskItem: (node, listDepth)=>listItemContentsToMd(node, listDepth),
420
- table: tableToMd,
421
- image: dropped,
422
- cmsBlock: dropped,
423
- tableRow: dropped,
424
- tableCell: dropped,
425
- tableHeader: dropped,
426
- text: (node)=>inlineTextToMd(node)
408
+ paragraph: (node) => inlineToMd(node.content ?? []),
409
+ heading: headingToMd,
410
+ bulletList: (node, listDepth) => (node.content ?? []).map((item) => listItemToMd(item, "-", listDepth)).join("\n"),
411
+ orderedList: orderedListToMd,
412
+ // WHY: a listItem outside a list shouldn't happen, but its content still renders.
413
+ listItem: (node, listDepth) => listItemContentsToMd(node, listDepth),
414
+ blockquote: blockquoteToMd,
415
+ codeBlock: codeBlockToMd,
416
+ horizontalRule: () => "---",
417
+ hardBreak: () => " \n",
418
+ youtube: youtubeToMd,
419
+ taskList: (node, listDepth) => (node.content ?? []).map((item) => listItemToMd(item, taskBullet(item), listDepth)).join("\n"),
420
+ taskItem: (node, listDepth) => listItemContentsToMd(node, listDepth),
421
+ table: tableToMd,
422
+ // WHY: images and cmsBlock are out of scope for the notes bridge.
423
+ image: dropped,
424
+ cmsBlock: dropped,
425
+ tableRow: dropped,
426
+ tableCell: dropped,
427
+ tableHeader: dropped,
428
+ text: (node) => inlineTextToMd(node)
427
429
  };
428
430
  function nodeToMd(node, listDepth) {
429
- if (Object.hasOwn(MD_WRITERS, node.type)) return MD_WRITERS[node.type](node, listDepth);
430
- return childrenToMd(node, listDepth);
431
+ const write = Object.hasOwn(MD_WRITERS, node.type) ? MD_WRITERS[node.type] : null;
432
+ return write ? write(node, listDepth) : childrenToMd(node, listDepth);
431
433
  }
432
434
  function listItemToMd(item, bullet, depth) {
433
- const indent = " ".repeat(depth);
434
- const content = listItemContentsToMd(item, depth + 1);
435
- const lines = content.split("\n");
436
- if (!lines.length) return `${indent}${bullet} `;
437
- const first = `${indent}${bullet} ${lines[0]}`;
438
- const rest = lines.slice(1).map((l)=>l.trim() ? `${indent} ${l}` : l).join("\n");
439
- return rest ? `${first}\n${rest}` : first;
435
+ const indent = " ".repeat(depth);
436
+ const content = listItemContentsToMd(item, depth + 1);
437
+ const lines = content.split("\n");
438
+ if (!lines.length) return `${indent}${bullet} `;
439
+ const first = `${indent}${bullet} ${lines[0]}`;
440
+ const rest = lines.slice(1).map((l) => l.trim() ? `${indent} ${l}` : l).join("\n");
441
+ return rest ? `${first}\n${rest}` : first;
440
442
  }
441
443
  function listItemContentsToMd(item, depth) {
442
- const parts = [];
443
- for (const child of item.content ?? []){
444
- if (child.type === "paragraph") {
445
- parts.push(inlineToMd(child.content ?? []));
446
- } else if (child.type === "bulletList" || child.type === "orderedList") {
447
- parts.push(nodeToMd(child, depth));
448
- } else {
449
- parts.push(nodeToMd(child, depth));
450
- }
451
- }
452
- return parts.join("\n");
444
+ const parts = [];
445
+ for (const child of item.content ?? []) {
446
+ if (child.type === "paragraph") {
447
+ parts.push(inlineToMd(child.content ?? []));
448
+ } else if (child.type === "bulletList" || child.type === "orderedList") {
449
+ parts.push(nodeToMd(child, depth));
450
+ } else {
451
+ parts.push(nodeToMd(child, depth));
452
+ }
453
+ }
454
+ return parts.join("\n");
453
455
  }
454
456
  function inlineToMd(nodes) {
455
- return nodes.map((n)=>inlineTextToMd(n)).join("");
457
+ return nodes.map((n) => inlineTextToMd(n)).join("");
456
458
  }
459
+ // WHY `*` for italic, never `_`: underscore emphasis has CommonMark word-boundary
460
+ // rules, so `_` could break round-trips next to identifiers.
457
461
  const MARK_WRAPS = {
458
- bold: (text)=>`**${text}**`,
459
- italic: (text)=>`*${text}*`,
460
- strike: (text)=>`~~${text}~~`
462
+ bold: (text) => `**${text}**`,
463
+ italic: (text) => `*${text}*`,
464
+ strike: (text) => `~~${text}~~`
461
465
  };
466
+ // A code span keeps its raw text verbatim and replaces whatever was wrapped before it.
462
467
  function applyMark(text, mark, raw) {
463
- if (mark.type === "code") return `\`${raw}\``;
464
- return Object.hasOwn(MARK_WRAPS, mark.type) ? MARK_WRAPS[mark.type](text) : text;
468
+ if (mark.type === "code") return `\`${raw}\``;
469
+ const wrap = Object.hasOwn(MARK_WRAPS, mark.type) ? MARK_WRAPS[mark.type] : null;
470
+ return wrap ? wrap(text) : text;
465
471
  }
466
472
  function inlineTextToMd(node) {
467
- if (node.type === "hardBreak") return " \n";
468
- if (node.type !== "text") return "";
469
- const escaped = escapeMdText(node.text ?? "");
470
- if (!node.marks?.length) return escaped;
471
- const linkMark = node.marks.find((m)=>m.type === "link");
472
- const text = node.marks.filter((m)=>m.type !== "link").reduce((acc, mark)=>applyMark(acc, mark, node.text ?? ""), escaped);
473
- if (!linkMark) return text;
474
- const href = typeof linkMark.attrs?.href === "string" ? linkMark.attrs.href : "";
475
- return `[${text}](${href})`;
476
- }
473
+ if (node.type === "hardBreak") return " \n";
474
+ if (node.type !== "text") return "";
475
+ // WHY: plain text is escaped so literal *, _, `, [ and \ never read as markup.
476
+ const escaped = escapeMdText(node.text ?? "");
477
+ if (!node.marks?.length) return escaped;
478
+ // WHY: the link wraps outermost to make [text](url), so it goes last.
479
+ const linkMark = node.marks.find((m) => m.type === "link");
480
+ const text = node.marks.filter((m) => m.type !== "link").reduce((acc, mark) => applyMark(acc, mark, node.text ?? ""), escaped);
481
+ if (!linkMark) return text;
482
+ const href = typeof linkMark.attrs?.href === "string" ? linkMark.attrs.href : "";
483
+ // WHY: link text is already escaped and the href never is. A code mark already
484
+ // swapped in its raw text, so text is used as-is.
485
+ return `[${text}](${href})`;
486
+ }
487
+ // WHY: Escape characters that markdown parsers treat as markup delimiters.
488
+ // Only escapes in plain text; code spans and link hrefs handle their own content.
477
489
  function escapeMdText(text) {
478
- return text.replace(/\\/g, "\\\\").replace(/\*/g, "\\*").replace(/_/g, "\\_").replace(/`/g, "\\`").replace(/\[/g, "\\[");
490
+ return text.replace(/\\/g, "\\\\").replace(/\*/g, "\\*").replace(/_/g, "\\_").replace(/`/g, "\\`").replace(/\[/g, "\\[");
479
491
  }
480
492
  function extractPlainText(nodes) {
481
- return nodes.map((n)=>{
482
- if (n.type === "text") return n.text ?? "";
483
- if (n.type === "hardBreak") return "\n";
484
- return extractPlainText(n.content ?? []);
485
- }).join("");
493
+ return nodes.map((n) => {
494
+ if (n.type === "text") return n.text ?? "";
495
+ if (n.type === "hardBreak") return "\n";
496
+ return extractPlainText(n.content ?? []);
497
+ }).join("");
486
498
  }