@localix-ai/flowdown 0.0.0-stage → 0.5.1

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 (231) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +105 -2
  3. package/dist/browser/file-actions.js +80 -0
  4. package/dist/browser/file-actions.js.map +1 -0
  5. package/dist/core/block-facts.js +72 -0
  6. package/dist/core/block-facts.js.map +1 -0
  7. package/dist/core/blocks.d.ts +25 -0
  8. package/dist/core/blocks.js +464 -0
  9. package/dist/core/blocks.js.map +1 -0
  10. package/dist/core/bounded-cache.js +42 -0
  11. package/dist/core/bounded-cache.js.map +1 -0
  12. package/dist/core/continues.js +15 -0
  13. package/dist/core/continues.js.map +1 -0
  14. package/dist/core/dollars.d.ts +12 -0
  15. package/dist/core/dollars.js +88 -0
  16. package/dist/core/dollars.js.map +1 -0
  17. package/dist/core/fence-scan.js +62 -0
  18. package/dist/core/fence-scan.js.map +1 -0
  19. package/dist/core/file-tree-parse.d.ts +26 -0
  20. package/dist/core/file-tree-parse.js +51 -0
  21. package/dist/core/file-tree-parse.js.map +1 -0
  22. package/dist/core/forms-html.js +63 -0
  23. package/dist/core/forms-html.js.map +1 -0
  24. package/dist/core/forms.d.ts +28 -0
  25. package/dist/core/forms.js +23 -0
  26. package/dist/core/forms.js.map +1 -0
  27. package/dist/core/headings.d.ts +11 -0
  28. package/dist/core/headings.js +74 -0
  29. package/dist/core/headings.js.map +1 -0
  30. package/dist/core/heal.d.ts +11 -0
  31. package/dist/core/heal.js +220 -0
  32. package/dist/core/heal.js.map +1 -0
  33. package/dist/core/load-once.js +23 -0
  34. package/dist/core/load-once.js.map +1 -0
  35. package/dist/core/math-delimiters.js +33 -0
  36. package/dist/core/math-delimiters.js.map +1 -0
  37. package/dist/core/math-syntax.js +56 -0
  38. package/dist/core/math-syntax.js.map +1 -0
  39. package/dist/core/prose.d.ts +10 -0
  40. package/dist/core/prose.js +77 -0
  41. package/dist/core/prose.js.map +1 -0
  42. package/dist/core/remark-answer-blocks.d.ts +5 -0
  43. package/dist/core/remark-answer-blocks.js +172 -0
  44. package/dist/core/remark-answer-blocks.js.map +1 -0
  45. package/dist/core/remark-answer-containers.js +299 -0
  46. package/dist/core/remark-answer-containers.js.map +1 -0
  47. package/dist/core/sanitize-schema.d.ts +18 -0
  48. package/dist/core/sanitize-schema.js +62 -0
  49. package/dist/core/sanitize-schema.js.map +1 -0
  50. package/dist/core/syntax.d.ts +8 -0
  51. package/dist/core/syntax.js +32 -0
  52. package/dist/core/syntax.js.map +1 -0
  53. package/dist/dialect.d.ts +92 -0
  54. package/dist/dialect.js +176 -0
  55. package/dist/dialect.js.map +1 -0
  56. package/dist/document/fence.d.ts +25 -0
  57. package/dist/document/fence.js +102 -0
  58. package/dist/document/fence.js.map +1 -0
  59. package/dist/document/index.d.ts +46 -0
  60. package/dist/document/index.js +66 -0
  61. package/dist/document/index.js.map +1 -0
  62. package/dist/document/table.d.ts +11 -0
  63. package/dist/document/table.js +35 -0
  64. package/dist/document/table.js.map +1 -0
  65. package/dist/dom/index.d.ts +21 -0
  66. package/dist/dom/index.js +141 -0
  67. package/dist/dom/index.js.map +1 -0
  68. package/dist/dom/patch.js +104 -0
  69. package/dist/dom/patch.js.map +1 -0
  70. package/dist/element/index.d.ts +35 -0
  71. package/dist/element/index.js +64 -0
  72. package/dist/element/index.js.map +1 -0
  73. package/dist/engine/blocks.js +785 -0
  74. package/dist/engine/blocks.js.map +1 -0
  75. package/dist/engine/entities.js +83 -0
  76. package/dist/engine/entities.js.map +1 -0
  77. package/dist/engine/entity-core.js +9 -0
  78. package/dist/engine/entity-core.js.map +1 -0
  79. package/dist/engine/entity-table.js +9 -0
  80. package/dist/engine/entity-table.js.map +1 -0
  81. package/dist/engine/html.d.ts +7 -0
  82. package/dist/engine/html.js +207 -0
  83. package/dist/engine/html.js.map +1 -0
  84. package/dist/engine/index.d.ts +7 -0
  85. package/dist/engine/index.js +74 -0
  86. package/dist/engine/index.js.map +1 -0
  87. package/dist/engine/inlines.js +887 -0
  88. package/dist/engine/inlines.js.map +1 -0
  89. package/dist/engine/types.d.ts +109 -0
  90. package/dist/engine/url.d.ts +11 -0
  91. package/dist/engine/url.js +16 -0
  92. package/dist/engine/url.js.map +1 -0
  93. package/dist/engine/visit.d.ts +10 -0
  94. package/dist/engine/visit.js +29 -0
  95. package/dist/engine/visit.js.map +1 -0
  96. package/dist/highlight/highlight.d.ts +4 -0
  97. package/dist/highlight/highlight.js +144 -0
  98. package/dist/highlight/highlight.js.map +1 -0
  99. package/dist/highlight/index.d.ts +4 -0
  100. package/dist/highlight/index.js +18 -0
  101. package/dist/highlight/index.js.map +1 -0
  102. package/dist/html/index.d.ts +4 -0
  103. package/dist/html/index.js +544 -0
  104. package/dist/html/index.js.map +1 -0
  105. package/dist/index.d.ts +27 -0
  106. package/dist/index.js +13 -0
  107. package/dist/math/index.d.ts +17 -0
  108. package/dist/math/index.js +20 -0
  109. package/dist/math/index.js.map +1 -0
  110. package/dist/math/typeset.d.ts +12 -0
  111. package/dist/math/typeset.js +74 -0
  112. package/dist/math/typeset.js.map +1 -0
  113. package/dist/mermaid/diagram.d.ts +8 -0
  114. package/dist/mermaid/diagram.js +114 -0
  115. package/dist/mermaid/diagram.js.map +1 -0
  116. package/dist/mermaid/drawing.js +139 -0
  117. package/dist/mermaid/drawing.js.map +1 -0
  118. package/dist/mermaid/icons.js +16 -0
  119. package/dist/mermaid/icons.js.map +1 -0
  120. package/dist/mermaid/index.d.ts +13 -0
  121. package/dist/mermaid/index.js +3 -0
  122. package/dist/mermaid/loader.d.ts +5 -0
  123. package/dist/mermaid/loader.js +18 -0
  124. package/dist/mermaid/loader.js.map +1 -0
  125. package/dist/mermaid/look.d.ts +16 -0
  126. package/dist/mermaid/look.js +167 -0
  127. package/dist/mermaid/look.js.map +1 -0
  128. package/dist/mermaid/normalize-svg.js +44 -0
  129. package/dist/mermaid/normalize-svg.js.map +1 -0
  130. package/dist/mermaid/renderer.d.ts +22 -0
  131. package/dist/mermaid/renderer.js +132 -0
  132. package/dist/mermaid/renderer.js.map +1 -0
  133. package/dist/mermaid/viewer.js +309 -0
  134. package/dist/mermaid/viewer.js.map +1 -0
  135. package/dist/prompt/index.d.ts +19 -0
  136. package/dist/prompt/index.js +54 -0
  137. package/dist/prompt/index.js.map +1 -0
  138. package/dist/react/callout.d.ts +9 -0
  139. package/dist/react/callout.js +12 -0
  140. package/dist/react/callout.js.map +1 -0
  141. package/dist/react/code-block.d.ts +18 -0
  142. package/dist/react/code-block.js +94 -0
  143. package/dist/react/code-block.js.map +1 -0
  144. package/dist/react/context.d.ts +18 -0
  145. package/dist/react/context.js +17 -0
  146. package/dist/react/context.js.map +1 -0
  147. package/dist/react/draw.d.ts +2 -0
  148. package/dist/react/draw.js +17 -0
  149. package/dist/react/draw.js.map +1 -0
  150. package/dist/react/file-tree.d.ts +16 -0
  151. package/dist/react/file-tree.js +69 -0
  152. package/dist/react/file-tree.js.map +1 -0
  153. package/dist/react/growing-code.js +36 -0
  154. package/dist/react/growing-code.js.map +1 -0
  155. package/dist/react/helpers.d.ts +6 -0
  156. package/dist/react/helpers.js +17 -0
  157. package/dist/react/helpers.js.map +1 -0
  158. package/dist/react/icons.js +23 -0
  159. package/dist/react/icons.js.map +1 -0
  160. package/dist/react/labels.js +9 -0
  161. package/dist/react/labels.js.map +1 -0
  162. package/dist/react/markdown.d.ts +78 -0
  163. package/dist/react/markdown.js +206 -0
  164. package/dist/react/markdown.js.map +1 -0
  165. package/dist/react/render.d.ts +8 -0
  166. package/dist/react/render.js +41 -0
  167. package/dist/react/render.js.map +1 -0
  168. package/dist/react/scrolls.js +62 -0
  169. package/dist/react/scrolls.js.map +1 -0
  170. package/dist/react/table-rows.js +66 -0
  171. package/dist/react/table-rows.js.map +1 -0
  172. package/dist/react/table.js +22 -0
  173. package/dist/react/table.js.map +1 -0
  174. package/dist/react/tabs.d.ts +25 -0
  175. package/dist/react/tabs.js +58 -0
  176. package/dist/react/tabs.js.map +1 -0
  177. package/dist/react/use-smooth-text.d.ts +6 -0
  178. package/dist/react/use-smooth-text.js +157 -0
  179. package/dist/react/use-smooth-text.js.map +1 -0
  180. package/dist/react/use-stage.d.ts +9 -0
  181. package/dist/react/use-stage.js +57 -0
  182. package/dist/react/use-stage.js.map +1 -0
  183. package/dist/remark.d.ts +20 -0
  184. package/dist/remark.js +10 -0
  185. package/dist/render/factory.d.ts +15 -0
  186. package/dist/render/labels.d.ts +46 -0
  187. package/dist/render/labels.js +46 -0
  188. package/dist/render/labels.js.map +1 -0
  189. package/dist/render/tree.js +314 -0
  190. package/dist/render/tree.js.map +1 -0
  191. package/dist/render/view.d.ts +58 -0
  192. package/dist/render/view.js +306 -0
  193. package/dist/render/view.js.map +1 -0
  194. package/dist/render/views.d.ts +16 -0
  195. package/dist/render/views.js +261 -0
  196. package/dist/render/views.js.map +1 -0
  197. package/dist/render/vnode.d.ts +21 -0
  198. package/dist/render/vnode.js +194 -0
  199. package/dist/render/vnode.js.map +1 -0
  200. package/dist/static/index.d.ts +28 -0
  201. package/dist/static/index.js +27 -0
  202. package/dist/static/index.js.map +1 -0
  203. package/dist/stream.d.ts +12 -0
  204. package/dist/stream.js +3 -0
  205. package/dist/styles.css +1362 -0
  206. package/dist/tui/code.d.ts +5 -0
  207. package/dist/tui/code.js +240 -0
  208. package/dist/tui/code.js.map +1 -0
  209. package/dist/tui/index.d.ts +54 -0
  210. package/dist/tui/index.js +99 -0
  211. package/dist/tui/index.js.map +1 -0
  212. package/dist/tui/math.js +242 -0
  213. package/dist/tui/math.js.map +1 -0
  214. package/dist/tui/printable.d.ts +12 -0
  215. package/dist/tui/printable.js +31 -0
  216. package/dist/tui/printable.js.map +1 -0
  217. package/dist/tui/render.d.ts +12 -0
  218. package/dist/tui/render.js +524 -0
  219. package/dist/tui/render.js.map +1 -0
  220. package/dist/tui/style.d.ts +35 -0
  221. package/dist/tui/style.js +77 -0
  222. package/dist/tui/style.js.map +1 -0
  223. package/dist/tui/table.js +73 -0
  224. package/dist/tui/table.js.map +1 -0
  225. package/dist/tui/width.d.ts +2 -0
  226. package/dist/tui/width.js +133 -0
  227. package/dist/tui/width.js.map +1 -0
  228. package/dist/tui/wrap.d.ts +8 -0
  229. package/dist/tui/wrap.js +191 -0
  230. package/dist/tui/wrap.js.map +1 -0
  231. package/package.json +128 -4
@@ -0,0 +1,306 @@
1
+ import { parseFileTree } from "../core/file-tree-parse.js";
2
+ import { flowdownDialect } from "../core/remark-answer-blocks.js";
3
+ import "../dialect.js";
4
+ import { createDocument, usesHtml, usesMath } from "../document/index.js";
5
+ import { createCutter, createFenceMemory, growingFence } from "../document/fence.js";
6
+ import { tableRows } from "../document/table.js";
7
+ import { parse } from "../engine/index.js";
8
+ import { defaultUrlTransform } from "../engine/url.js";
9
+ import { mergeLabels } from "./labels.js";
10
+ import { createRenderer } from "./tree.js";
11
+ import { callout, code, codeBar, codeFrame, copyButton, diagramSource, downloadButton, fileTree, readCodeBlock, readTabs, table, tabs } from "./views.js";
12
+ import { expand, vnode } from "./vnode.js";
13
+ // src/render/view.ts
14
+ /**
15
+ * A whole document as plain elements (`vnode.ts`): what `<Markdown>` draws with React, for the
16
+ * targets that have no framework. The HTML text of one (`@localix-ai/flowdown/static`) and the page kept
17
+ * up to date from one (`@localix-ai/flowdown/dom`) are both made here, so they hold the same elements.
18
+ */
19
+ /** How long "Copied" stays before the button reads "Copy" again. */
20
+ const COPIED_MS = 1600;
21
+ const focusOn = (element) => element?.focus?.();
22
+ const draw = createRenderer(vnode);
23
+ const NOTHING = () => [];
24
+ const CHILDREN = (_props, children) => children;
25
+ /** How many body rows one piece of a table holds: the same pieces `<Markdown>` draws. */
26
+ const ROWS = 16;
27
+ const headingKeyOf = (own) => own ? [...own].map(([at, id]) => `${at}:${id}`).join(";") : "";
28
+ function createView(options = {}) {
29
+ const labels = mergeLabels(options.labels);
30
+ const highlight = options.highlight ?? null;
31
+ const own = options.components;
32
+ let extensions = [flowdownDialect];
33
+ let math = null;
34
+ const urlTransform = options.urlTransform ?? defaultUrlTransform;
35
+ const ownCode = Boolean(own?.pre || own?.code);
36
+ const ownTable = Boolean(own?.table || own?.thead || own?.tbody || (options.extensions?.length ?? 0) > 0);
37
+ const prefix = options.idPrefix ?? "fd-";
38
+ const { diagram: pageDiagram, tree: pageTree } = options;
39
+ /** Where the block being drawn starts in the document, and whether it is the fence still growing. */
40
+ let offset = 0;
41
+ let growing = false;
42
+ const host = options.host ?? null;
43
+ /** Blocks, by where they start, that a reader's action changed: drawn again at the next render. */
44
+ const stale = /* @__PURE__ */ new Set();
45
+ /** How many live parts the block being drawn has made: with the block's offset, a part's name. */
46
+ let serial = 0;
47
+ const tabStates = /* @__PURE__ */ new Map();
48
+ const treeStates = /* @__PURE__ */ new Map();
49
+ /** The code blocks that read "Copied", each with the timer that ends it. */
50
+ const copied = /* @__PURE__ */ new Map();
51
+ const changed = (at) => {
52
+ stale.add(at);
53
+ host.redraw();
54
+ };
55
+ const buttons = (text, title, language) => {
56
+ if (!host) return [downloadButton(vnode, labels), copyButton(vnode, labels, false)];
57
+ const at = offset;
58
+ const id = `${at}.${serial++}`;
59
+ const copy = () => {
60
+ host.copy(text).then((done) => {
61
+ if (!done) return;
62
+ clearTimeout(copied.get(id));
63
+ copied.set(id, setTimeout(() => {
64
+ copied.delete(id);
65
+ changed(at);
66
+ }, COPIED_MS));
67
+ changed(at);
68
+ });
69
+ };
70
+ return [downloadButton(vnode, labels, () => host.download(text, title, language)), copyButton(vnode, labels, copied.has(id), copy)];
71
+ };
72
+ /** `[!TABS]` on a page: the WAI-ARIA tabs pattern, as `<Markdown>` has it. */
73
+ const liveTabs = (list, id) => {
74
+ const at = offset;
75
+ let state = tabStates.get(id);
76
+ if (!state) tabStates.set(id, state = {
77
+ active: 0,
78
+ buttons: []
79
+ });
80
+ const tab = state;
81
+ tab.buttons = [];
82
+ const current = tab.active < list.length ? tab.active : 0;
83
+ const select = (index, focus) => {
84
+ const next = (index + list.length) % list.length;
85
+ tab.active = next;
86
+ changed(at);
87
+ if (focus) focusOn(tab.buttons[next]);
88
+ };
89
+ return tabs(vnode, list, current, id, labels, (index) => ({
90
+ ref: (element) => {
91
+ tab.buttons[index] = element;
92
+ },
93
+ onClick: () => select(index, false),
94
+ onKeyDown: (event) => {
95
+ const target = {
96
+ ArrowRight: index + 1,
97
+ ArrowLeft: index - 1,
98
+ Home: 0,
99
+ End: list.length - 1
100
+ }[event.key];
101
+ if (target === void 0) return;
102
+ event.preventDefault();
103
+ select(target, true);
104
+ }
105
+ }));
106
+ };
107
+ /** A ```tree fence on a page: folders fold, and the arrow keys walk the rows (the tree pattern). */
108
+ const liveTree = (rows) => {
109
+ const at = offset;
110
+ const id = `${at}.${serial++}`;
111
+ let state = treeStates.get(id);
112
+ if (!state) treeStates.set(id, state = {
113
+ collapsed: /* @__PURE__ */ new Set(),
114
+ focused: null,
115
+ elements: /* @__PURE__ */ new Map()
116
+ });
117
+ const tree = state;
118
+ tree.elements = /* @__PURE__ */ new Map();
119
+ const toggle = (path) => {
120
+ if (!tree.collapsed.delete(path)) tree.collapsed.add(path);
121
+ changed(at);
122
+ };
123
+ const focus = (row) => {
124
+ if (row) focusOn(tree.elements.get(row.path));
125
+ };
126
+ return fileTree(vnode, {
127
+ rows,
128
+ collapsed: tree.collapsed,
129
+ focused: tree.focused,
130
+ on: (row, visible) => ({
131
+ ref: (element) => {
132
+ tree.elements.set(row.path, element);
133
+ },
134
+ onFocus: () => {
135
+ if (tree.focused === row.path) return;
136
+ tree.focused = row.path;
137
+ changed(at);
138
+ },
139
+ onClick: (event) => {
140
+ focusOn(event.currentTarget);
141
+ if (row.folder) toggle(row.path);
142
+ },
143
+ onKeyDown: (event) => {
144
+ const index = visible.indexOf(row);
145
+ const open = row.folder && !tree.collapsed.has(row.path);
146
+ const parent = row.parent === null ? void 0 : rows[row.parent];
147
+ const action = {
148
+ ArrowDown: () => focus(visible[index + 1]),
149
+ ArrowUp: () => focus(visible[index - 1]),
150
+ Home: () => focus(visible[0]),
151
+ End: () => focus(visible.at(-1)),
152
+ ArrowRight: () => open ? focus(visible[index + 1]) : row.folder && toggle(row.path),
153
+ ArrowLeft: () => open ? toggle(row.path) : focus(parent),
154
+ Enter: () => row.folder && toggle(row.path),
155
+ " ": () => row.folder && toggle(row.path)
156
+ }[event.key];
157
+ if (!action) return;
158
+ event.preventDefault();
159
+ event.stopPropagation();
160
+ action();
161
+ }
162
+ })
163
+ }, labels);
164
+ };
165
+ const components = {
166
+ blockquote: (props, children) => callout(vnode, props, children, labels),
167
+ table: (props, children) => table(vnode, props, children, false, labels, host?.table ? { ref: host.table } : void 0),
168
+ "answer-tabs": (props, children) => {
169
+ const list = readTabs(vnode, children);
170
+ if (list.length === 0) return [];
171
+ const at = props.node?.position?.start.offset ?? 0;
172
+ const id = `${prefix}${offset + at}`;
173
+ return host ? liveTabs(list, id) : tabs(vnode, list, 0, id, labels);
174
+ },
175
+ pre: ({ node: _node, ...props }, children) => {
176
+ const { language, title, text } = readCodeBlock(vnode, children);
177
+ return codeFrame(vnode, language, codeBar(vnode, language, title, buttons(text, title, language)), host?.table ? {
178
+ ...props,
179
+ ref: host.table
180
+ } : props, children);
181
+ },
182
+ code: (props, children) => code(vnode, props, children, highlight),
183
+ ...own
184
+ };
185
+ const special = (language, source) => language === "mermaid" ? pageDiagram ? pageDiagram(source, growing) : diagramSource(vnode, source, null, false) : pageTree ? pageTree(source) : treeOf(source);
186
+ const treeOf = (source) => {
187
+ const rows = parseFileTree(source);
188
+ if (rows.length === 0) return [];
189
+ if (host) return liveTree(rows);
190
+ return fileTree(vnode, {
191
+ rows,
192
+ collapsed: /* @__PURE__ */ new Set(),
193
+ focused: null
194
+ }, labels);
195
+ };
196
+ const one = (source, at, ids, using) => draw(parse(source, { extensions }), {
197
+ components: using,
198
+ urlTransform,
199
+ special,
200
+ labels,
201
+ offset: at,
202
+ headingIds: ids
203
+ });
204
+ const headParts = {
205
+ ...components,
206
+ table: CHILDREN,
207
+ tbody: NOTHING
208
+ };
209
+ const bodyParts = {
210
+ ...components,
211
+ table: CHILDREN,
212
+ thead: NOTHING,
213
+ tbody: CHILDREN
214
+ };
215
+ const memory = createFenceMemory();
216
+ const cut = createCutter();
217
+ /** A code fence still being written, in the pieces it is highlighted by. */
218
+ const growingCode = (language, title, body) => {
219
+ const known = Boolean(language && highlight && highlight("", language) !== null);
220
+ const className = [language ? `language-${language}` : null, known ? "fd-highlighted" : null].filter(Boolean).join(" ");
221
+ const pieces = cut(body).parts.map((text) => {
222
+ const html = known ? highlight(text, language) : null;
223
+ return html === null ? text : vnode.element("span", { innerHTML: html }, []);
224
+ });
225
+ if (!known && body) pieces.push("\n");
226
+ return codeFrame(vnode, language, codeBar(vnode, language, title, buttons(body, title, language)), host?.table ? { ref: host.table } : null, [vnode.element("code", { className: className || void 0 }, pieces)]);
227
+ };
228
+ const block = (open, closer, ids) => {
229
+ const fence = growing && !ownCode ? growingFence(open, closer, memory) : null;
230
+ if (fence && fence.language !== "mermaid" && fence.language !== "tree" && !(fence.language === "math" && math)) return growingCode(fence.language, fence.title, fence.body);
231
+ const source = closer ? open + closer : open;
232
+ const rows = ownTable ? null : tableRows(source);
233
+ if (rows) {
234
+ const pieces = [];
235
+ for (let at = 0; at < rows.rows.length; at += ROWS) pieces.push(one(rows.head + rows.rows.slice(at, at + ROWS).join("\n"), 0, null, bodyParts));
236
+ return vnode.element(components.table, null, [one(rows.head, 0, null, headParts), vnode.element("tbody", null, pieces)]);
237
+ }
238
+ return one(source, offset, ids, components);
239
+ };
240
+ const doc = createDocument();
241
+ /** What each block drew last: a block that did not change is not drawn again. */
242
+ const drawn = /* @__PURE__ */ new WeakMap();
243
+ return {
244
+ render(text, streaming = false) {
245
+ const state = doc.update(text, {
246
+ streaming,
247
+ math: Boolean(options.math),
248
+ headingIds: options.headingIds ?? false
249
+ });
250
+ math = options.math && usesMath(state.blocks) ? options.math : null;
251
+ const html = options.html && usesHtml(state.blocks) ? options.html : null;
252
+ extensions = [
253
+ flowdownDialect,
254
+ ...math ? [math] : [],
255
+ ...options.extensions ?? [],
256
+ ...html ? [html] : []
257
+ ];
258
+ const stages = `${math ? "m" : ""}${html ? "h" : ""}`;
259
+ const children = [];
260
+ const last = state.blocks.length - 1;
261
+ state.blocks.forEach((item, index) => {
262
+ const closer = index === last ? state.closer : "";
263
+ const grows = index === last && state.growing;
264
+ const ids = state.headingIds?.[index] ?? null;
265
+ const headingKey = headingKeyOf(ids);
266
+ let made = drawn.get(item);
267
+ if (made === void 0 || stale.has(item.offset) || made.stages !== stages || made.closer !== closer || made.growing !== grows || made.offset !== item.offset || made.headingKey !== headingKey) {
268
+ offset = item.offset;
269
+ growing = grows;
270
+ serial = 0;
271
+ const nodes = expand(block(item.source, closer, ids && ids.size > 0 ? ids : null));
272
+ made = {
273
+ stages,
274
+ closer,
275
+ growing: grows,
276
+ offset: item.offset,
277
+ headingKey,
278
+ nodes
279
+ };
280
+ drawn.set(item, made);
281
+ }
282
+ if (index > 0) children.push("\n");
283
+ for (const node of made.nodes) children.push(node);
284
+ });
285
+ stale.clear();
286
+ const caret = options.caret ?? streaming;
287
+ const { className, unstyled } = options;
288
+ return {
289
+ tag: "div",
290
+ props: {
291
+ className: unstyled ? className : className ? `flowdown ${className}` : "flowdown",
292
+ "data-streaming": streaming || void 0,
293
+ "data-caret": streaming && caret || void 0
294
+ },
295
+ children
296
+ };
297
+ },
298
+ dispose() {
299
+ for (const timer of copied.values()) clearTimeout(timer);
300
+ copied.clear();
301
+ }
302
+ };
303
+ }
304
+ export { createView };
305
+
306
+ //# sourceMappingURL=view.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"view.js","names":["views.downloadButton","views.copyButton","views.tabs","views.fileTree","views.callout","views.table","views.readTabs","views.readCodeBlock","views.codeFrame","views.codeBar","views.code","views.diagramSource"],"sources":["../../src/render/view.ts"],"sourcesContent":["/**\n * A whole document as plain elements (`vnode.ts`): what `<Markdown>` draws with React, for the\n * targets that have no framework. The HTML text of one (`@localix-ai/flowdown/static`) and the page kept\n * up to date from one (`@localix-ai/flowdown/dom`) are both made here, so they hold the same elements.\n */\nimport { parseFileTree, type TreeRow } from '../core/file-tree-parse'\nimport { flowdownDialect } from '../core/remark-answer-blocks'\nimport { DIAGRAM_FENCE_LANGUAGE } from '../dialect'\nimport { createDocument, usesHtml, usesMath, type MarkdownBlock } from '../document'\nimport { createCutter, createFenceMemory, growingFence } from '../document/fence'\nimport { tableRows } from '../document/table'\nimport { parse } from '../engine'\nimport type { Extension, Node } from '../engine/types'\nimport { defaultUrlTransform, type UrlTransform } from '../engine/url'\nimport type { Props } from './factory'\nimport { mergeLabels, type LabelOverrides, type MarkdownLabels } from './labels'\nimport { createRenderer } from './tree'\nimport * as views from './views'\nimport { expand, vnode, type Drawn, type DrawnElement, type VComponent, type VNode } from './vnode'\n\n/** Your own elements for any tag (`a`, `img`, `pre`, `table`, …) or the dialect's. */\nexport type ViewComponents = Record<string, VComponent | undefined>\n\nexport interface ViewOptions {\n components?: ViewComponents\n /** Your own drawing of a ```mermaid fence; `growing` while the fence is still being written. */\n diagram?: (source: string, growing: boolean) => VNode\n /** Your own drawing of a ```tree fence. */\n tree?: (source: string) => VNode\n /** Your own syntax, after the dialect's (`Extension`). */\n extensions?: Extension[]\n /** Give headings ids (`setup`, `setup-1`) to link to. */\n headingIds?: boolean\n /** Raw HTML in the text, parsed and sanitized: `@localix-ai/flowdown/html`. Without it the tags are left out. */\n html?: Extension | null\n /** Typeset `$…$`: `@localix-ai/flowdown/math`. */\n math?: Extension | null\n /** Highlight fenced code: `@localix-ai/flowdown/highlight`, or your own. */\n highlight?: views.Highlighter | null\n /** The words the renderer adds (callout names, button labels), for a page in another language. */\n labels?: LabelOverrides\n /** Rewrite or refuse a link or image URL; by default only safe protocols are kept. */\n urlTransform?: UrlTransform | null\n className?: string\n /** Leaves the `flowdown` class off the root element, so the package's stylesheet does not apply. */\n unstyled?: boolean\n /** The caret after streaming text; on by default while streaming. */\n caret?: boolean\n /**\n * What the ids of a `[!TABS]` form's tabs and panels start with. Two documents on one page need\n * two prefixes; `@localix-ai/flowdown/dom` gives each of its own.\n */\n idPrefix?: string\n /** The page the view is drawn on: with one, its tabs, trees and buttons answer the reader. */\n host?: ViewHost\n}\n\n/**\n * What makes a view alive. A view drawn as HTML text has none, and its tabs, file trees and code\n * buttons are drawn and nothing more; given one, they carry listeners (`onClick`, `onKeyDown`)\n * that change what the view shows and ask the host to draw it again.\n */\nexport interface ViewHost {\n /** Something the reader did changed what the document shows: draw it again, now. */\n redraw(): void\n /** Put `text` on the clipboard; resolves whether it got there. */\n copy(text: string): Promise<boolean>\n /** Save `text` as a file named after its fence's title, or its language. */\n download(text: string, title: string | undefined, language: string | undefined): void\n /**\n * The box of a table or the `<pre>` of a code block, each time it is drawn: a host that can\n * measure makes a wide one a Tab stop.\n */\n table?(box: unknown): void\n}\n\n/** What a listener is given, as far as the view reads it. */\ninterface KeyPress {\n key: string\n preventDefault(): void\n stopPropagation(): void\n}\n\n/** How long \"Copied\" stays before the button reads \"Copy\" again. */\nconst COPIED_MS = 1_600\n\nconst focusOn = (element: unknown): void =>\n (element as { focus?: () => void } | undefined)?.focus?.()\n\nexport interface View {\n /**\n * The document's root element for `text`; `streaming` while the reply is still arriving. What\n * a block drew is kept: a block that did not change gives the very same elements again, so a\n * target that compares by identity leaves them alone.\n */\n render(text: string, streaming?: boolean): DrawnElement\n /** Stops what a reader's action left running (a \"Copied\" that has yet to go back to \"Copy\"). */\n dispose(): void\n}\n\nconst draw = createRenderer(vnode)\n\nconst NOTHING: VComponent = () => []\nconst CHILDREN: VComponent = (_props, children) => children\n\n/** How many body rows one piece of a table holds: the same pieces `<Markdown>` draws. */\nconst ROWS = 16\n\nconst headingKeyOf = (own: ReadonlyMap<number, string> | null): string =>\n own ? [...own].map(([at, id]) => `${at}:${id}`).join(';') : ''\n\ninterface Kept {\n stages: string\n closer: string\n growing: boolean\n offset: number\n headingKey: string\n nodes: Drawn[]\n}\n\nexport function createView(options: ViewOptions = {}): View {\n const labels: MarkdownLabels = mergeLabels(options.labels)\n const highlight = options.highlight ?? null\n const own = options.components\n // What the document is read with. Math and raw HTML are passes a document without either does\n // not pay for, exactly as `<Markdown>` leaves them out: set for each `render`.\n let extensions: Extension[] = [flowdownDialect]\n let math: Extension | null = null\n const urlTransform = options.urlTransform ?? defaultUrlTransform\n const ownCode = Boolean(own?.pre || own?.code)\n const ownTable = Boolean(\n own?.table || own?.thead || own?.tbody || (options.extensions?.length ?? 0) > 0,\n )\n const prefix = options.idPrefix ?? 'fd-'\n const { diagram: pageDiagram, tree: pageTree } = options\n\n /** Where the block being drawn starts in the document, and whether it is the fence still growing. */\n let offset = 0\n let growing = false\n\n const host = options.host ?? null\n /** Blocks, by where they start, that a reader's action changed: drawn again at the next render. */\n const stale = new Set<number>()\n /** How many live parts the block being drawn has made: with the block's offset, a part's name. */\n let serial = 0\n const tabStates = new Map<string, { active: number; buttons: unknown[] }>()\n const treeStates = new Map<\n string,\n { collapsed: Set<string>; focused: string | null; elements: Map<string, unknown> }\n >()\n /** The code blocks that read \"Copied\", each with the timer that ends it. */\n const copied = new Map<string, ReturnType<typeof setTimeout>>()\n\n const changed = (at: number) => {\n stale.add(at)\n host!.redraw()\n }\n\n const buttons = (\n text: string,\n title: string | undefined,\n language: string | undefined,\n ): VNode[] => {\n if (!host) return [views.downloadButton(vnode, labels), views.copyButton(vnode, labels, false)]\n const at = offset\n const id = `${at}.${serial++}`\n const copy = () => {\n // When the clipboard refuses there is nothing to say: the code is still selectable.\n void host.copy(text).then((done) => {\n if (!done) return\n clearTimeout(copied.get(id))\n copied.set(\n id,\n setTimeout(() => {\n copied.delete(id)\n changed(at)\n }, COPIED_MS),\n )\n changed(at)\n })\n }\n return [\n views.downloadButton(vnode, labels, () => host.download(text, title, language)),\n views.copyButton(vnode, labels, copied.has(id), copy),\n ]\n }\n\n /** `[!TABS]` on a page: the WAI-ARIA tabs pattern, as `<Markdown>` has it. */\n const liveTabs = (list: Array<views.TabView<VNode>>, id: string): VNode => {\n const at = offset\n let state = tabStates.get(id)\n if (!state) tabStates.set(id, (state = { active: 0, buttons: [] }))\n const tab = state\n tab.buttons = []\n // A streamed form can lose a tab between renders only by being re-parsed; never point past it.\n const current = tab.active < list.length ? tab.active : 0\n const select = (index: number, focus: boolean) => {\n const next = (index + list.length) % list.length\n tab.active = next\n changed(at)\n if (focus) focusOn(tab.buttons[next])\n }\n return views.tabs(vnode, list, current, id, labels, (index) => ({\n ref: (element: unknown) => {\n tab.buttons[index] = element\n },\n onClick: () => select(index, false),\n onKeyDown: (event: KeyPress) => {\n const moves: Record<string, number> = {\n ArrowRight: index + 1,\n ArrowLeft: index - 1,\n Home: 0,\n End: list.length - 1,\n }\n const target = moves[event.key]\n if (target === undefined) return\n event.preventDefault()\n select(target, true)\n },\n }))\n }\n\n /** A ```tree fence on a page: folders fold, and the arrow keys walk the rows (the tree pattern). */\n const liveTree = (rows: TreeRow[]): VNode => {\n const at = offset\n const id = `${at}.${serial++}`\n let state = treeStates.get(id)\n if (!state)\n treeStates.set(id, (state = { collapsed: new Set(), focused: null, elements: new Map() }))\n const tree = state\n tree.elements = new Map()\n const toggle = (path: string) => {\n if (!tree.collapsed.delete(path)) tree.collapsed.add(path)\n changed(at)\n }\n const focus = (row: TreeRow | undefined) => {\n if (row) focusOn(tree.elements.get(row.path))\n }\n return views.fileTree(\n vnode,\n {\n rows,\n collapsed: tree.collapsed,\n focused: tree.focused,\n on: (row, visible) => ({\n ref: (element: unknown) => {\n tree.elements.set(row.path, element)\n },\n onFocus: () => {\n if (tree.focused === row.path) return\n tree.focused = row.path\n changed(at)\n },\n onClick: (event: { currentTarget: unknown }) => {\n // Safari leaves a clicked row unfocused; the keys must carry on from it.\n focusOn(event.currentTarget)\n if (row.folder) toggle(row.path)\n },\n onKeyDown: (event: KeyPress) => {\n const index = visible.indexOf(row)\n const open = row.folder && !tree.collapsed.has(row.path)\n const parent = row.parent === null ? undefined : rows[row.parent]\n const actions: Record<string, () => void> = {\n ArrowDown: () => focus(visible[index + 1]),\n ArrowUp: () => focus(visible[index - 1]),\n Home: () => focus(visible[0]),\n End: () => focus(visible.at(-1)),\n ArrowRight: () => (open ? focus(visible[index + 1]) : row.folder && toggle(row.path)),\n ArrowLeft: () => (open ? toggle(row.path) : focus(parent)),\n Enter: () => row.folder && toggle(row.path),\n ' ': () => row.folder && toggle(row.path),\n }\n const action = actions[event.key]\n if (!action) return\n // Handled here, so not also by the page around the reply; the keys the tree leaves\n // (Escape, Tab) still reach it.\n event.preventDefault()\n event.stopPropagation()\n action()\n },\n }),\n },\n labels,\n )\n }\n\n const components: Record<string, VComponent | undefined> = {\n blockquote: (props, children) => views.callout(vnode, props, children, labels),\n table: (props, children) =>\n views.table(\n vnode,\n props,\n children,\n false,\n labels,\n host?.table ? { ref: host.table } : undefined,\n ),\n 'answer-tabs': (props, children) => {\n const list = views.readTabs(vnode, children)\n if (list.length === 0) return []\n const at = (props.node as Node | undefined)?.position?.start.offset ?? 0\n const id = `${prefix}${offset + at}`\n return host ? liveTabs(list, id) : views.tabs(vnode, list, 0, id, labels)\n },\n pre: ({ node: _node, ...props }, children) => {\n const { language, title, text } = views.readCodeBlock(vnode, children)\n return views.codeFrame(\n vnode,\n language,\n views.codeBar(vnode, language, title, buttons(text, title, language)),\n host?.table ? { ...props, ref: host.table } : props,\n children,\n )\n },\n code: (props, children) => views.code(vnode, props, children, highlight),\n ...own,\n }\n\n const special = (language: string, source: string): VNode =>\n language === DIAGRAM_FENCE_LANGUAGE\n ? pageDiagram\n ? pageDiagram(source, growing)\n : views.diagramSource(vnode, source, null, false)\n : pageTree\n ? pageTree(source)\n : treeOf(source)\n\n const treeOf = (source: string): VNode => {\n const rows = parseFileTree(source)\n if (rows.length === 0) return []\n if (host) return liveTree(rows)\n return views.fileTree(vnode, { rows, collapsed: new Set(), focused: null }, labels)\n }\n\n const one = (\n source: string,\n at: number,\n ids: ReadonlyMap<number, string> | null,\n using: Record<string, VComponent | undefined>,\n ): VNode =>\n draw(parse(source, { extensions }), {\n components: using,\n urlTransform,\n special,\n labels,\n offset: at,\n headingIds: ids,\n })\n\n // A table's own shell taken off: a piece yields its rows alone.\n const headParts = { ...components, table: CHILDREN, tbody: NOTHING }\n const bodyParts = { ...components, table: CHILDREN, thead: NOTHING, tbody: CHILDREN }\n\n const memory = createFenceMemory()\n const cut = createCutter()\n\n /** A code fence still being written, in the pieces it is highlighted by. */\n const growingCode = (language: string | undefined, title: string, body: string): VNode => {\n // The class a finished block gets when its language is one the highlighter knows.\n const known = Boolean(language && highlight && highlight('', language) !== null)\n const className = [language ? `language-${language}` : null, known ? 'fd-highlighted' : null]\n .filter(Boolean)\n .join(' ')\n const pieces: VNode[] = cut(body).parts.map((text) => {\n const html = known ? highlight!(text, language!) : null\n return html === null ? text : vnode.element('span', { innerHTML: html }, [])\n })\n // A finished block's text ends in a newline unless it is highlighted; so does this.\n if (!known && body) pieces.push('\\n')\n return views.codeFrame(\n vnode,\n language,\n views.codeBar(vnode, language, title, buttons(body, title, language)),\n host?.table ? { ref: host.table } : null,\n [vnode.element('code', { className: className || undefined }, pieces)],\n )\n }\n\n const block = (open: string, closer: string, ids: ReadonlyMap<number, string> | null): VNode => {\n const fence = growing && !ownCode ? growingFence(open, closer, memory) : null\n if (\n fence &&\n fence.language !== DIAGRAM_FENCE_LANGUAGE &&\n fence.language !== 'tree' &&\n // A `math` fence is a formula where the page typesets math, and code where it does not.\n !(fence.language === 'math' && math)\n )\n return growingCode(fence.language, fence.title, fence.body)\n const source = closer ? open + closer : open\n // A table that is the whole block is drawn as its header and its rows, as `<Markdown>` does.\n const rows = ownTable ? null : tableRows(source)\n if (rows) {\n const pieces: VNode[] = []\n for (let at = 0; at < rows.rows.length; at += ROWS)\n pieces.push(one(rows.head + rows.rows.slice(at, at + ROWS).join('\\n'), 0, null, bodyParts))\n return vnode.element(components.table!, null, [\n one(rows.head, 0, null, headParts),\n vnode.element('tbody', null, pieces),\n ])\n }\n return one(source, offset, ids, components)\n }\n\n const doc = createDocument()\n /** What each block drew last: a block that did not change is not drawn again. */\n const drawn = new WeakMap<MarkdownBlock, Kept>()\n\n return {\n render(text, streaming = false) {\n const state = doc.update(text, {\n streaming,\n math: Boolean(options.math),\n headingIds: options.headingIds ?? false,\n })\n math = options.math && usesMath(state.blocks) ? options.math : null\n // Whether the document has HTML is decided for the whole of it, not per block: a tag still\n // being typed (`<div`) is its own block, and only the whole document says it is HTML.\n const html = options.html && usesHtml(state.blocks) ? options.html : null\n extensions = [\n flowdownDialect,\n ...(math ? [math] : []),\n ...(options.extensions ?? []),\n // Raw HTML is made into elements last, so every pass before it sees the text as written.\n ...(html ? [html] : []),\n ]\n const stages = `${math ? 'm' : ''}${html ? 'h' : ''}`\n const children: Drawn[] = []\n const last = state.blocks.length - 1\n state.blocks.forEach((item, index) => {\n const closer = index === last ? state.closer : ''\n const grows = index === last && state.growing\n const ids = state.headingIds?.[index] ?? null\n const headingKey = headingKeyOf(ids)\n let made = drawn.get(item)\n if (\n made === undefined ||\n stale.has(item.offset) ||\n made.stages !== stages ||\n made.closer !== closer ||\n made.growing !== grows ||\n made.offset !== item.offset ||\n made.headingKey !== headingKey\n ) {\n offset = item.offset\n growing = grows\n serial = 0\n const nodes = expand(block(item.source, closer, ids && ids.size > 0 ? ids : null))\n made = { stages, closer, growing: grows, offset: item.offset, headingKey, nodes }\n drawn.set(item, made)\n }\n // One parse puts a line break between top-level blocks; so does this.\n if (index > 0) children.push('\\n')\n for (const node of made.nodes) children.push(node)\n })\n stale.clear()\n const caret = options.caret ?? streaming\n const { className, unstyled } = options\n const props: Props = {\n className: unstyled ? className : className ? `flowdown ${className}` : 'flowdown',\n 'data-streaming': streaming || undefined,\n 'data-caret': (streaming && caret) || undefined,\n }\n return { tag: 'div', props, children }\n },\n dispose() {\n for (const timer of copied.values()) clearTimeout(timer)\n copied.clear()\n },\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * What the dialect's own elements are made of, written once for every target that has elements.
3
+ *
4
+ * A view is a function of a {@link Factory} and of what the element shows right now: which tab is
5
+ * open, whether the code was just copied. It keeps no state and listens to nothing: a target that
6
+ * can (React, the DOM) holds the state and hands in the listeners, and one that cannot (a server)
7
+ * draws the element as it first appears.
8
+ */
9
+ /** The language a `<code>`'s class names: `language-ts` is `ts`. */
10
+ export declare function getLanguage(className?: string): string | undefined;
11
+ /**
12
+ * Turns a fence's code into highlighted HTML, or returns null to leave it plain. The HTML goes into
13
+ * the page as is, so a highlighter must escape the code it was given (highlight.js and Shiki do);
14
+ * `@localix-ai/flowdown/highlight` is one.
15
+ */
16
+ export type Highlighter = (code: string, language: string) => string | null;
@@ -0,0 +1,261 @@
1
+ import { CALLOUT_KINDS } from "../dialect.js";
2
+ // src/render/views.ts
3
+ /**
4
+ * The few glyphs the renderer draws, inline. A dependency on an icon library for eleven outlines
5
+ * would cost every consumer an install, and a theme cannot restyle an icon font as simply as an
6
+ * SVG that strokes with `currentColor`.
7
+ *
8
+ * Each glyph is one `<path>`: its strokes are subpaths of the same outline, and its circle or
9
+ * rounded square is written out as arcs. A reply with a dozen code blocks and callouts draws
10
+ * dozens of icons, and every stroke that is its own element is one more node on the page.
11
+ *
12
+ * Outlines from Lucide (https://lucide.dev), ISC License, Copyright (c) Lucide Contributors.
13
+ */
14
+ const OUTLINES = {
15
+ info: "M2 12a10 10 0 1 0 20 0 10 10 0 1 0-20 0zM12 16v-4M12 8h.01",
16
+ tip: "M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5M9 18h6M10 22h4",
17
+ important: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2zM12 7v2M12 13h.01",
18
+ warning: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3M12 9v4M12 17h.01",
19
+ caution: "M2.586 16.726A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2h6.624a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586zM12 8v4M12 16h.01",
20
+ chevron: "m9 18 6-6-6-6",
21
+ file: "M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7ZM14 2v4a2 2 0 0 0 2 2h4",
22
+ folder: "M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z",
23
+ folderOpen: "m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2",
24
+ copy: "M10 8h10a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H10a2 2 0 0 1-2-2V10a2 2 0 0 1 2-2zM4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2",
25
+ check: "M20 6 9 17l-5-5",
26
+ download: "M12 15V3M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5"
27
+ };
28
+ /** One glyph. `props` go on its `<svg>` after its own. */
29
+ const glyph = (f, outline, props) => f.element("svg", {
30
+ xmlns: "http://www.w3.org/2000/svg",
31
+ viewBox: "0 0 24 24",
32
+ fill: "none",
33
+ stroke: "currentColor",
34
+ strokeWidth: 1.75,
35
+ strokeLinecap: "round",
36
+ strokeLinejoin: "round",
37
+ "aria-hidden": true,
38
+ focusable: "false",
39
+ ...props
40
+ }, [f.element("path", { d: outline }, [])]);
41
+ /** The language a `<code>`'s class names: `language-ts` is `ts`. */
42
+ function getLanguage(className) {
43
+ return /language-([^\s]+)/.exec(className ?? "")?.[1];
44
+ }
45
+ /** The text of some children, whatever elements it is in. */
46
+ function textOf(f, children) {
47
+ let out = "";
48
+ for (const child of f.read(children)) out += typeof child === "string" ? child : textOf(f, child.children);
49
+ return out;
50
+ }
51
+ const CALLOUT_OUTLINES = {
52
+ note: OUTLINES.info,
53
+ tip: OUTLINES.tip,
54
+ important: OUTLINES.important,
55
+ warning: OUTLINES.warning,
56
+ caution: OUTLINES.caution
57
+ };
58
+ function calloutKind(value) {
59
+ return typeof value === "string" && CALLOUT_KINDS.includes(value) ? value : null;
60
+ }
61
+ /**
62
+ * A quote, or, when the dialect marked it, a typed callout (`> [!WARNING]`). An `aside` with
63
+ * `role="note"` rather than a styled quote: a warning is not a quotation, and assistive tech should
64
+ * announce it as the aside it is, its kind first.
65
+ *
66
+ * The kind is carried by the glyph alone, in its colour. Its name is spoken, not printed, and a
67
+ * title is printed only when the author wrote one (`> [!TIP] Faster builds`).
68
+ */
69
+ function callout(f, { node: _node, "data-callout": marked, "data-callout-title": title, className, cite, ...quoteProps }, children, labels) {
70
+ const kind = calloutKind(marked);
71
+ if (!kind) return f.element("blockquote", {
72
+ className,
73
+ cite,
74
+ ...quoteProps
75
+ }, children);
76
+ return f.element("aside", {
77
+ ...quoteProps,
78
+ className: className ? `fd-callout ${className}` : "fd-callout",
79
+ "data-callout": kind,
80
+ role: "note",
81
+ "aria-label": title || labels.callouts[kind]
82
+ }, [glyph(f, CALLOUT_OUTLINES[kind], { className: "fd-callout-icon" }), f.element("div", { className: "fd-callout-body" }, [...title ? [f.element("div", { className: "fd-callout-title" }, [f.text(title)])] : [], ...children])]);
83
+ }
84
+ /**
85
+ * Code, inline or in a fence. A fenced block is highlighted when the page gave a highlighter and
86
+ * the fence names a language it knows; otherwise its text is printed as is.
87
+ */
88
+ function code(f, { node: _node, className, ...props }, children, highlight) {
89
+ const language = getLanguage(className);
90
+ const html = language && highlight ? highlight(textOf(f, children).replace(/\n$/, ""), language) : null;
91
+ if (html === null) return f.element("code", {
92
+ className,
93
+ ...props
94
+ }, children);
95
+ return f.element("code", {
96
+ className: [className, "fd-highlighted"].filter(Boolean).join(" "),
97
+ ...props,
98
+ innerHTML: html
99
+ }, []);
100
+ }
101
+ /** What a code block's bar and buttons need of a `<pre>`'s children: its one `<code>`. */
102
+ function readCodeBlock(f, children) {
103
+ const items = f.read(children);
104
+ const only = items.length === 1 && typeof items[0] !== "string" ? items[0] : null;
105
+ return {
106
+ language: getLanguage(only?.props.className),
107
+ title: only?.props["data-title"],
108
+ text: only ? textOf(f, only.children).replace(/\n$/, "") : ""
109
+ };
110
+ }
111
+ /** The copy button of a code block, before and after a copy. */
112
+ const copyButton = (f, labels, copied, onClick) => f.element("button", {
113
+ type: "button",
114
+ className: "fd-code-copy",
115
+ onClick,
116
+ "aria-label": copied ? labels.copied : labels.copy,
117
+ title: copied ? labels.copied : labels.copy,
118
+ "data-copied": copied || void 0
119
+ }, [glyph(f, copied ? OUTLINES.check : OUTLINES.copy)]);
120
+ /** Saves the code as a file: named as its fence's title, or `code.<ext>` from its language. */
121
+ const downloadButton = (f, labels, onClick) => f.element("button", {
122
+ type: "button",
123
+ className: "fd-code-download",
124
+ onClick,
125
+ "aria-label": labels.download,
126
+ title: labels.download
127
+ }, [glyph(f, OUTLINES.download)]);
128
+ /** The bar of a code block: the file its fence names, its language, and the two buttons. */
129
+ const codeBar = (f, language, title, buttons) => f.element("figcaption", { className: "fd-code-bar" }, [
130
+ ...title ? [f.element("span", { className: "fd-code-title" }, [f.text(title)])] : [],
131
+ ...language ? [f.element("span", { className: "fd-code-language" }, [f.text(language)])] : [],
132
+ ...buttons
133
+ ]);
134
+ /**
135
+ * The frame of a code block: the bar, then the `<pre>`. A fence that names its file
136
+ * (` ```ts title="src/app.ts" `) shows the file first, the language after it; a fence with no
137
+ * language shows none.
138
+ */
139
+ const codeFrame = (f, language, bar, preProps, children) => f.element("figure", {
140
+ className: "fd-code",
141
+ "data-language": language
142
+ }, [bar, f.element("pre", { ...preProps }, children)]);
143
+ /** The variants among some children: every element, whatever is between them. */
144
+ function readTabs(f, children) {
145
+ const list = [];
146
+ for (const child of f.read(children)) if (typeof child !== "string") list.push({
147
+ label: child.props["data-label"],
148
+ children: child.children
149
+ });
150
+ return list;
151
+ }
152
+ /**
153
+ * `[!TABS]`: the variants of one answer (an install per OS, an example per language), of which
154
+ * the reader needs one. Every panel stays in the document, `hidden`, so a search on the page, a
155
+ * copy of the whole reply and a screen reader's panel navigation still reach the others.
156
+ *
157
+ * The WAI-ARIA tabs pattern: one Tab stop for the row, each tab names the panel it controls.
158
+ * `on` gives a tab its listeners and its handle (`onClick`, `onKeyDown`, `ref`).
159
+ */
160
+ function tabs(f, list, current, base, labels, on) {
161
+ const tabId = (index) => `${base}-tab-${index}`;
162
+ const panelId = (index) => `${base}-panel-${index}`;
163
+ return f.element("div", { className: "fd-tabs" }, [f.element("div", {
164
+ className: "fd-tab-list",
165
+ role: "tablist",
166
+ "aria-label": labels.variants
167
+ }, list.map((tab, index) => f.element("button", {
168
+ key: index,
169
+ type: "button",
170
+ role: "tab",
171
+ id: tabId(index),
172
+ className: "fd-tab",
173
+ "aria-selected": index === current,
174
+ "aria-controls": panelId(index),
175
+ tabIndex: index === current ? 0 : -1,
176
+ ...on?.(index)
177
+ }, [f.text(tab.label || String(index + 1))]))), ...list.map((tab, index) => f.element("div", {
178
+ key: index,
179
+ role: "tabpanel",
180
+ id: panelId(index),
181
+ "aria-labelledby": tabId(index),
182
+ className: "fd-tab-panel",
183
+ hidden: index !== current
184
+ }, tab.children))]);
185
+ }
186
+ /**
187
+ * A table in a box of its own that scrolls sideways: a wide comparison then scrolls inside the
188
+ * reply instead of widening the page. A box that scrolls is a named group the Tab key stops on, so the
189
+ * arrow keys can scroll it (WCAG 2.1.1); a table that fits stays out of the Tab order.
190
+ */
191
+ const table = (f, { node: _node, ...props }, children, scrolls, labels, box) => f.element("div", {
192
+ ...box,
193
+ className: "fd-table",
194
+ role: scrolls ? "group" : void 0,
195
+ "aria-label": scrolls ? labels.scrollableTable : void 0,
196
+ tabIndex: scrolls ? 0 : void 0
197
+ }, [f.element("table", props, children)]);
198
+ /** The rows of a tree that no shut folder hides. */
199
+ function visibleRows(rows, collapsed) {
200
+ const hidden = (row) => {
201
+ for (let parent = row.parent; parent !== null; parent = rows[parent].parent) if (collapsed.has(rows[parent].path)) return true;
202
+ return false;
203
+ };
204
+ return rows.filter((row) => !hidden(row));
205
+ }
206
+ /**
207
+ * A ```tree fence: a directory layout drawn as a tree instead of box characters in monospace. The
208
+ * tree is one Tab stop walked with the arrow keys (the WAI-ARIA tree pattern).
209
+ */
210
+ function fileTree(f, { rows, collapsed, focused, icon, on }, labels) {
211
+ const visible = visibleRows(rows, collapsed);
212
+ const rover = visible.some((row) => row.path === focused) ? focused : visible[0]?.path;
213
+ return f.element("div", {
214
+ role: "tree",
215
+ "aria-label": labels.fileTree,
216
+ className: "fd-tree"
217
+ }, visible.map((row) => {
218
+ const open = !collapsed.has(row.path);
219
+ return f.element("div", {
220
+ key: row.path,
221
+ role: "treeitem",
222
+ "aria-level": row.depth + 1,
223
+ "aria-selected": false,
224
+ ...row.folder ? { "aria-expanded": open } : {},
225
+ tabIndex: row.path === rover ? 0 : -1,
226
+ className: "fd-tree-row",
227
+ "data-folder": row.folder || void 0,
228
+ style: { "--fd-tree-depth": row.depth },
229
+ ...on?.(row, visible)
230
+ }, [
231
+ ...row.folder ? [glyph(f, OUTLINES.chevron, {
232
+ className: "fd-tree-chevron",
233
+ "data-open": open || void 0
234
+ })] : [],
235
+ icon ? icon(row, open) : glyph(f, row.folder ? open ? OUTLINES.folderOpen : OUTLINES.folder : OUTLINES.file, { className: "fd-tree-icon" }),
236
+ f.element("span", {
237
+ className: "fd-tree-name",
238
+ title: row.name
239
+ }, [f.text(row.name.replace(/\/$/, ""))]),
240
+ ...row.comment ? [f.element("span", {
241
+ className: "fd-tree-comment",
242
+ title: row.comment
243
+ }, [f.text(row.comment)])] : []
244
+ ]);
245
+ }));
246
+ }
247
+ /**
248
+ * A ```mermaid fence shown as the source it is: before it is drawn, where nothing draws it, or
249
+ * when it could not be drawn, with a line saying why.
250
+ */
251
+ const diagramSource = (f, source, note, failed, figure) => f.element("figure", {
252
+ ...figure,
253
+ className: "fd-diagram",
254
+ "data-state": failed ? "error" : "source"
255
+ }, [f.element("pre", null, [f.element("code", { className: "language-mermaid" }, [f.text(source)])]), ...note ? [f.element("figcaption", {
256
+ className: "fd-diagram-note",
257
+ role: "status"
258
+ }, [f.text(note)])] : []]);
259
+ export { OUTLINES, callout, code, codeBar, codeFrame, copyButton, diagramSource, downloadButton, fileTree, getLanguage, glyph, readCodeBlock, readTabs, table, tabs, textOf, visibleRows };
260
+
261
+ //# sourceMappingURL=views.js.map