@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.
- package/dist/agents/helpers.js +38 -38
- package/dist/agents/page-state.js +49 -50
- package/dist/agents/vendor/color.js +146 -127
- package/dist/agents/vendor/draw.js +617 -508
- package/dist/agents/vendor/engine.js +698 -599
- package/dist/agents/vendor/mesh.js +653 -683
- package/dist/agents/vendor/plastic.js +3913 -3112
- package/dist/agents/vendor/presets.js +163 -164
- package/dist/agents/vendor/shapes.js +21 -20
- package/dist/agents/vendor/ticker.js +41 -42
- package/dist/agents/vendor/types.js +1 -0
- package/dist/agents/vendor/wear.js +1421 -1448
- package/dist/ai/confirmation/confirmation-context.svelte.js +6 -6
- package/dist/ai/confirmation/index.js +3 -1
- package/dist/ai/index.js +33 -0
- package/dist/ai/prompt-input/index.js +3 -1
- package/dist/ai/reasoning/index.js +3 -1
- package/dist/ai/reasoning/reasoning-context.svelte.js +6 -6
- package/dist/ai/suggestion/index.js +3 -1
- package/dist/ai/typing-dots/index.js +3 -1
- package/dist/animations/text-swap.js +24 -24
- package/dist/brand/index.js +54 -48
- package/dist/canvas/card/resize.js +5 -3
- package/dist/canvas/edges/edge-path.js +255 -242
- package/dist/canvas/editor/context-menu.js +39 -36
- package/dist/canvas/editor/dispatch.svelte.js +154 -151
- package/dist/canvas/editor/editor-context.js +7 -2
- package/dist/canvas/editor/editor-mode.js +1 -0
- package/dist/canvas/editor/frame.js +73 -76
- package/dist/canvas/editor/hand-context.js +5 -2
- package/dist/canvas/editor/history-core.js +163 -191
- package/dist/canvas/editor/keymap.js +95 -169
- package/dist/canvas/editor/layout-actions.js +143 -144
- package/dist/canvas/editor/lock.js +52 -51
- package/dist/canvas/editor/rebase.js +217 -240
- package/dist/canvas/editor/style.js +132 -137
- package/dist/canvas/editor/text-field-context.js +7 -2
- package/dist/canvas/editor/z-order.js +65 -70
- package/dist/canvas/headless/alignment.js +22 -16
- package/dist/canvas/headless/break-apart-find.js +286 -279
- package/dist/canvas/headless/break-apart-pieces.js +62 -58
- package/dist/canvas/headless/break-apart-plan.js +299 -325
- package/dist/canvas/headless/break-apart-style.js +75 -70
- package/dist/canvas/headless/break-apart.js +188 -181
- package/dist/canvas/headless/canvas-cleanup.js +78 -58
- package/dist/canvas/headless/edge-anchors.js +115 -116
- package/dist/canvas/headless/edge-memo.js +24 -22
- package/dist/canvas/headless/edge-serialization.js +17 -27
- package/dist/canvas/headless/figure-geometry.js +232 -264
- package/dist/canvas/headless/fit-viewport.js +84 -67
- package/dist/canvas/headless/grid.js +88 -86
- package/dist/canvas/headless/icon-node.js +44 -36
- package/dist/canvas/headless/index.js +1 -0
- package/dist/canvas/headless/linkify.js +17 -19
- package/dist/canvas/headless/pen-marks.js +101 -95
- package/dist/canvas/headless/sketch.js +231 -209
- package/dist/canvas/headless/snap-guides.js +99 -83
- package/dist/canvas/headless/svg-figure.js +133 -117
- package/dist/canvas/headless/table-layout.js +146 -107
- package/dist/canvas/index.js +2 -0
- package/dist/canvas/lod/lod-mode.svelte.js +20 -18
- package/dist/canvas/lod/render-all.svelte.js +36 -39
- package/dist/canvas/ops/arrange.js +78 -71
- package/dist/canvas/ops/ops.js +260 -255
- package/dist/canvas/pen/colors.js +37 -35
- package/dist/canvas/pen/eraser.js +28 -27
- package/dist/canvas/pen/geometry.js +120 -130
- package/dist/canvas/pen/index.js +1 -0
- package/dist/canvas/pen/ops.js +11 -11
- package/dist/canvas/pen/pen-session.js +150 -159
- package/dist/canvas/pen/plugin.js +107 -104
- package/dist/canvas/pen/shapes.js +73 -68
- package/dist/canvas/pen/stroke-path.js +32 -30
- package/dist/canvas/pen/types.js +21 -21
- package/dist/canvas/pen/ui.js +10 -8
- package/dist/canvas/plugin.js +46 -46
- package/dist/canvas/screen/glyphs.js +12 -11
- package/dist/canvas/screen/labels.js +22 -22
- package/dist/canvas/screen/parts.js +98 -111
- package/dist/canvas/screen/plugin.js +47 -56
- package/dist/canvas/screen/types.js +30 -35
- package/dist/canvas/screen/ui.js +7 -7
- package/dist/canvas/test-utils.js +14 -14
- package/dist/canvas/types.js +1 -0
- package/dist/canvas/ui/forward-wheel.js +24 -25
- package/dist/canvas/ui/icons.js +64 -131
- package/dist/canvas/ui/place-panel.js +58 -49
- package/dist/canvas/ui/shortcut-tip.js +14 -12
- package/dist/canvas/ui/tools.js +300 -385
- package/dist/charts/dashboard/chart.js +4 -0
- package/dist/charts/dashboard/data-table.js +11 -0
- package/dist/charts/dashboard/windows.js +8 -3
- package/dist/charts/edge-fade.js +10 -5
- package/dist/charts/hover-band.js +16 -11
- package/dist/charts/index.js +0 -1
- package/dist/charts/motion.js +11 -7
- package/dist/charts/primitives/chart-utils.js +12 -12
- package/dist/components/copy/clipboard.js +32 -32
- package/dist/components/copy/copied.svelte.js +31 -23
- package/dist/components/data-table/data-table.js +39 -39
- package/dist/components/docs/index.js +4 -0
- package/dist/components/docs/scroll-spy.svelte.js +73 -68
- package/dist/components/list-page/filter-section.js +4 -1
- package/dist/components/list-page/index.js +2 -0
- package/dist/components/list-page/list-table.js +7 -2
- package/dist/components/list-page/queue-rail.js +14 -0
- package/dist/components/list-page/section-host.js +4 -2
- package/dist/components/list-page/step.js +4 -3
- package/dist/components/list-page/types.js +1 -0
- package/dist/components/picker/trigger.js +48 -30
- package/dist/components/picker/types.js +1 -0
- package/dist/components/segmented/collapse.js +38 -27
- package/dist/components/segmented/index.js +8 -0
- package/dist/components/segmented/segmented.js +67 -42
- package/dist/components/star-rating-input/index.js +3 -1
- package/dist/components/table-view/types.js +1 -0
- package/dist/components/tags-input/tags.js +81 -69
- package/dist/components/theme/index.js +4 -0
- package/dist/components/toolbar/index.js +4 -0
- package/dist/components/toolbar/toolbar-trigger.js +14 -1
- package/dist/editor/anchors.js +17 -13
- package/dist/editor/block-id.js +52 -51
- package/dist/editor/block-ops.js +108 -80
- package/dist/editor/doc-blocks.js +155 -166
- package/dist/editor/extensions.js +139 -180
- package/dist/editor/index.js +4 -0
- package/dist/editor/markdown-test-helpers.js +4 -2
- package/dist/editor/markdown.js +420 -408
- package/dist/editor/serializer.js +204 -200
- package/dist/editor/slash-items.js +89 -101
- package/dist/editor/youtube-url.js +25 -20
- package/dist/effects/animated-beam/path-calculator.svelte.js +43 -38
- package/dist/effects/animated-beam/types.js +1 -0
- package/dist/effects/border-beam/css.js +92 -78
- package/dist/effects/border-beam/presets.js +1705 -1720
- package/dist/effects/border-beam/register-properties.js +43 -33
- package/dist/effects/border-beam/types.js +1 -0
- package/dist/effects/image-comparison/compare.js +3 -2
- package/dist/effects/image-reveal/types.js +1 -0
- package/dist/effects/magnetic/magnetic.js +58 -52
- package/dist/effects/reveal/observe.js +50 -40
- package/dist/effects/reveal/types.js +1 -0
- package/dist/effects/shader-gradient/color.js +36 -29
- package/dist/effects/shader-gradient/renderer.js +384 -295
- package/dist/effects/shader-gradient/shaders.js +31 -0
- package/dist/effects/shader-gradient/types.js +1 -0
- package/dist/icons/apple-symbol-exports.js +99 -124
- package/dist/icons/sf-symbols.js +357 -461
- package/dist/icons/trades.js +43 -28
- package/dist/inbox/index.js +1 -0
- package/dist/inbox/logic.js +126 -122
- package/dist/inbox/types.js +43 -43
- package/dist/lib/arrive.js +19 -12
- package/dist/lib/icon-types.js +1 -0
- package/dist/lib/initials.js +16 -10
- package/dist/lib/layout.js +5 -0
- package/dist/lib/panel.js +13 -0
- package/dist/lib/result.js +22 -22
- package/dist/lib/sliding-pill.js +59 -44
- package/dist/lib/theme-transition.js +43 -43
- package/dist/lib/tone.js +31 -24
- package/dist/lib/utils.js +3 -1
- package/dist/onboarding/define-tours.js +24 -23
- package/dist/onboarding/onboarding.svelte.js +16 -12
- package/dist/onboarding/pick.js +10 -6
- package/dist/onboarding/stores.js +40 -41
- package/dist/onboarding/wait.js +24 -23
- package/dist/panel/side-panel-types.js +1 -0
- package/dist/record/demo-data.js +459 -486
- package/dist/record/fields.js +129 -131
- package/dist/record/format.js +70 -82
- package/dist/record/gutter.js +1 -0
- package/dist/record/kinds.js +95 -105
- package/dist/record/logic.js +238 -219
- package/dist/record/types.js +49 -48
- package/dist/shadcn/accordion/index.js +3 -1
- package/dist/shadcn/alert/index.js +3 -1
- package/dist/shadcn/alert-dialog/index.js +3 -1
- package/dist/shadcn/attachment/index.js +3 -1
- package/dist/shadcn/avatar/index.js +3 -1
- package/dist/shadcn/breadcrumb/index.js +3 -1
- package/dist/shadcn/bubble/index.js +3 -1
- package/dist/shadcn/button/index.js +3 -1
- package/dist/shadcn/calendar/index.js +3 -1
- package/dist/shadcn/card/index.js +3 -1
- package/dist/shadcn/checkbox/index.js +3 -1
- package/dist/shadcn/code-block/index.js +3 -1
- package/dist/shadcn/collapsible/index.js +3 -1
- package/dist/shadcn/command/index.js +4 -1
- package/dist/shadcn/context-menu/index.js +3 -1
- package/dist/shadcn/dialog/index.js +3 -1
- package/dist/shadcn/dropdown-menu/menu-item.js +40 -20
- package/dist/shadcn/empty/index.js +3 -1
- package/dist/shadcn/field/index.js +3 -1
- package/dist/shadcn/hooks/highlighter.js +77 -75
- package/dist/shadcn/hooks/is-mobile.svelte.js +3 -3
- package/dist/shadcn/hooks/use-highlighted-code.svelte.js +62 -62
- package/dist/shadcn/input/index.js +3 -1
- package/dist/shadcn/kbd/index.js +3 -1
- package/dist/shadcn/label/index.js +3 -1
- package/dist/shadcn/marker/index.js +3 -1
- package/dist/shadcn/message/index.js +3 -1
- package/dist/shadcn/message-scroller/index.js +3 -1
- package/dist/shadcn/popover/index.js +3 -1
- package/dist/shadcn/progress/index.js +3 -1
- package/dist/shadcn/progress/tone.js +8 -7
- package/dist/shadcn/radio-group/index.js +3 -1
- package/dist/shadcn/scroll-area/index.js +3 -1
- package/dist/shadcn/select/index.js +3 -1
- package/dist/shadcn/separator/index.js +3 -1
- package/dist/shadcn/sheet/index.js +3 -1
- package/dist/shadcn/sidebar/context.svelte.js +42 -28
- package/dist/shadcn/sidebar/index.js +3 -1
- package/dist/shadcn/skeleton/index.js +3 -1
- package/dist/shadcn/slider/index.js +3 -1
- package/dist/shadcn/switch/index.js +3 -1
- package/dist/shadcn/table/index.js +3 -1
- package/dist/shadcn/tabs/index.js +3 -1
- package/dist/shadcn/textarea/index.js +3 -1
- package/dist/shadcn/toggle/index.js +3 -1
- package/dist/shadcn/toggle-group/index.js +3 -1
- package/dist/shadcn/tooltip/context.js +7 -2
- package/dist/shadcn/tooltip/index.js +3 -1
- package/dist/shadcn/trace/index.js +3 -1
- package/dist/slides/bridges.js +65 -59
- package/dist/slides/deck-controller.svelte.js +210 -207
- package/dist/slides/export/capture.js +66 -71
- package/dist/slides/export/ffmpeg.js +76 -72
- package/dist/slides/export/index.js +3 -0
- package/dist/slides/export/launch.js +9 -6
- package/dist/slides/export/pdf.js +60 -54
- package/dist/slides/export/types.js +1 -0
- package/dist/slides/labels.js +24 -24
- package/dist/slides/layout.js +39 -41
- package/dist/slides/model.js +9 -7
- package/dist/slides/navigation.js +78 -73
- package/dist/slides/types.js +2 -2
- package/dist/status/define-statuses.js +18 -14
- package/dist/status/status-types.js +1 -0
- package/dist/test-setup.js +24 -20
- package/dist/thread/logic.js +73 -77
- package/dist/thread/types.js +21 -21
- package/dist/work/board-order.js +94 -90
- package/dist/work/dates.js +16 -9
- package/dist/work/detail/detail-types.js +18 -18
- package/dist/work/docs/edited.js +32 -29
- package/dist/work/docs/index.js +4 -0
- package/dist/work/docs/labels.js +34 -34
- package/dist/work/docs/menu.js +113 -106
- package/dist/work/docs/outline.js +39 -29
- package/dist/work/gantt/logic.js +211 -235
- package/dist/work/inbox/inbox-logic.js +71 -75
- package/dist/work/inbox/inbox-statuses.js +75 -72
- package/dist/work/inbox/inbox-types.js +1 -0
- package/dist/work/kanban-drag.js +27 -22
- package/dist/work/kanban-types.js +1 -0
- package/dist/work/presets/sales.js +36 -32
- package/dist/work/reorder.js +9 -8
- package/dist/work/task-types.js +1 -0
- package/dist/work/time/duration.js +51 -46
- package/dist/work/time/time-types.js +23 -23
- package/dist/work/use-kanban-dnd.svelte.js +126 -125
- package/package.json +3 -1
- package/src/components/list-page/master-detail.svelte +3 -1
- package/src/components/list-page/rail-row.svelte +4 -4
- package/src/components/tags-input/tags-input.svelte +2 -0
- package/src/editor/markdown.ts +30 -35
package/dist/editor/markdown.js
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
48
|
-
|
|
61
|
+
const inlineTok = tokens[i];
|
|
62
|
+
return inlineTok?.type === "inline" ? inlineTok.children ?? [] : [];
|
|
49
63
|
}
|
|
50
64
|
function orderedStart(tok) {
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
function parseHeading(tokens, i) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
function parseFence(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
317
|
-
|
|
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
|
-
|
|
325
|
-
|
|
319
|
+
const n = markup.length;
|
|
320
|
+
return Math.min(Math.max(n, 1), 6);
|
|
326
321
|
}
|
|
327
322
|
function stampId(node) {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
353
|
-
|
|
352
|
+
const value = node.attrs?.[key];
|
|
353
|
+
return typeof value === "number" ? value : undefined;
|
|
354
354
|
}
|
|
355
355
|
function strAttr(node, key) {
|
|
356
|
-
|
|
357
|
-
|
|
356
|
+
const value = node.attrs?.[key];
|
|
357
|
+
return typeof value === "string" ? value : undefined;
|
|
358
358
|
}
|
|
359
359
|
function childrenToMd(node, listDepth) {
|
|
360
|
-
|
|
360
|
+
return (node.content ?? []).map((child) => nodeToMd(child, listDepth)).filter(Boolean).join("\n\n");
|
|
361
361
|
}
|
|
362
362
|
function headingToMd(node) {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
369
|
-
|
|
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
|
-
|
|
372
|
+
return childrenToMd(node, 0).split("\n").map((line) => `> ${line}`).join("\n");
|
|
373
373
|
}
|
|
374
374
|
function codeBlockToMd(node) {
|
|
375
|
-
|
|
376
|
-
|
|
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 ``.
|
|
378
380
|
function youtubeToMd(node) {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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 ``;
|
|
388
|
+
}
|
|
389
|
+
const dropped = () => "";
|
|
388
390
|
function taskBullet(item) {
|
|
389
|
-
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
-
|
|
430
|
-
|
|
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
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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
|
-
|
|
464
|
-
|
|
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
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
-
|
|
490
|
+
return text.replace(/\\/g, "\\\\").replace(/\*/g, "\\*").replace(/_/g, "\\_").replace(/`/g, "\\`").replace(/\[/g, "\\[");
|
|
479
491
|
}
|
|
480
492
|
function extractPlainText(nodes) {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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
|
}
|