@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.
- package/LICENSE +21 -0
- package/README.md +105 -2
- package/dist/browser/file-actions.js +80 -0
- package/dist/browser/file-actions.js.map +1 -0
- package/dist/core/block-facts.js +72 -0
- package/dist/core/block-facts.js.map +1 -0
- package/dist/core/blocks.d.ts +25 -0
- package/dist/core/blocks.js +464 -0
- package/dist/core/blocks.js.map +1 -0
- package/dist/core/bounded-cache.js +42 -0
- package/dist/core/bounded-cache.js.map +1 -0
- package/dist/core/continues.js +15 -0
- package/dist/core/continues.js.map +1 -0
- package/dist/core/dollars.d.ts +12 -0
- package/dist/core/dollars.js +88 -0
- package/dist/core/dollars.js.map +1 -0
- package/dist/core/fence-scan.js +62 -0
- package/dist/core/fence-scan.js.map +1 -0
- package/dist/core/file-tree-parse.d.ts +26 -0
- package/dist/core/file-tree-parse.js +51 -0
- package/dist/core/file-tree-parse.js.map +1 -0
- package/dist/core/forms-html.js +63 -0
- package/dist/core/forms-html.js.map +1 -0
- package/dist/core/forms.d.ts +28 -0
- package/dist/core/forms.js +23 -0
- package/dist/core/forms.js.map +1 -0
- package/dist/core/headings.d.ts +11 -0
- package/dist/core/headings.js +74 -0
- package/dist/core/headings.js.map +1 -0
- package/dist/core/heal.d.ts +11 -0
- package/dist/core/heal.js +220 -0
- package/dist/core/heal.js.map +1 -0
- package/dist/core/load-once.js +23 -0
- package/dist/core/load-once.js.map +1 -0
- package/dist/core/math-delimiters.js +33 -0
- package/dist/core/math-delimiters.js.map +1 -0
- package/dist/core/math-syntax.js +56 -0
- package/dist/core/math-syntax.js.map +1 -0
- package/dist/core/prose.d.ts +10 -0
- package/dist/core/prose.js +77 -0
- package/dist/core/prose.js.map +1 -0
- package/dist/core/remark-answer-blocks.d.ts +5 -0
- package/dist/core/remark-answer-blocks.js +172 -0
- package/dist/core/remark-answer-blocks.js.map +1 -0
- package/dist/core/remark-answer-containers.js +299 -0
- package/dist/core/remark-answer-containers.js.map +1 -0
- package/dist/core/sanitize-schema.d.ts +18 -0
- package/dist/core/sanitize-schema.js +62 -0
- package/dist/core/sanitize-schema.js.map +1 -0
- package/dist/core/syntax.d.ts +8 -0
- package/dist/core/syntax.js +32 -0
- package/dist/core/syntax.js.map +1 -0
- package/dist/dialect.d.ts +92 -0
- package/dist/dialect.js +176 -0
- package/dist/dialect.js.map +1 -0
- package/dist/document/fence.d.ts +25 -0
- package/dist/document/fence.js +102 -0
- package/dist/document/fence.js.map +1 -0
- package/dist/document/index.d.ts +46 -0
- package/dist/document/index.js +66 -0
- package/dist/document/index.js.map +1 -0
- package/dist/document/table.d.ts +11 -0
- package/dist/document/table.js +35 -0
- package/dist/document/table.js.map +1 -0
- package/dist/dom/index.d.ts +21 -0
- package/dist/dom/index.js +141 -0
- package/dist/dom/index.js.map +1 -0
- package/dist/dom/patch.js +104 -0
- package/dist/dom/patch.js.map +1 -0
- package/dist/element/index.d.ts +35 -0
- package/dist/element/index.js +64 -0
- package/dist/element/index.js.map +1 -0
- package/dist/engine/blocks.js +785 -0
- package/dist/engine/blocks.js.map +1 -0
- package/dist/engine/entities.js +83 -0
- package/dist/engine/entities.js.map +1 -0
- package/dist/engine/entity-core.js +9 -0
- package/dist/engine/entity-core.js.map +1 -0
- package/dist/engine/entity-table.js +9 -0
- package/dist/engine/entity-table.js.map +1 -0
- package/dist/engine/html.d.ts +7 -0
- package/dist/engine/html.js +207 -0
- package/dist/engine/html.js.map +1 -0
- package/dist/engine/index.d.ts +7 -0
- package/dist/engine/index.js +74 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/engine/inlines.js +887 -0
- package/dist/engine/inlines.js.map +1 -0
- package/dist/engine/types.d.ts +109 -0
- package/dist/engine/url.d.ts +11 -0
- package/dist/engine/url.js +16 -0
- package/dist/engine/url.js.map +1 -0
- package/dist/engine/visit.d.ts +10 -0
- package/dist/engine/visit.js +29 -0
- package/dist/engine/visit.js.map +1 -0
- package/dist/highlight/highlight.d.ts +4 -0
- package/dist/highlight/highlight.js +144 -0
- package/dist/highlight/highlight.js.map +1 -0
- package/dist/highlight/index.d.ts +4 -0
- package/dist/highlight/index.js +18 -0
- package/dist/highlight/index.js.map +1 -0
- package/dist/html/index.d.ts +4 -0
- package/dist/html/index.js +544 -0
- package/dist/html/index.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +13 -0
- package/dist/math/index.d.ts +17 -0
- package/dist/math/index.js +20 -0
- package/dist/math/index.js.map +1 -0
- package/dist/math/typeset.d.ts +12 -0
- package/dist/math/typeset.js +74 -0
- package/dist/math/typeset.js.map +1 -0
- package/dist/mermaid/diagram.d.ts +8 -0
- package/dist/mermaid/diagram.js +114 -0
- package/dist/mermaid/diagram.js.map +1 -0
- package/dist/mermaid/drawing.js +139 -0
- package/dist/mermaid/drawing.js.map +1 -0
- package/dist/mermaid/icons.js +16 -0
- package/dist/mermaid/icons.js.map +1 -0
- package/dist/mermaid/index.d.ts +13 -0
- package/dist/mermaid/index.js +3 -0
- package/dist/mermaid/loader.d.ts +5 -0
- package/dist/mermaid/loader.js +18 -0
- package/dist/mermaid/loader.js.map +1 -0
- package/dist/mermaid/look.d.ts +16 -0
- package/dist/mermaid/look.js +167 -0
- package/dist/mermaid/look.js.map +1 -0
- package/dist/mermaid/normalize-svg.js +44 -0
- package/dist/mermaid/normalize-svg.js.map +1 -0
- package/dist/mermaid/renderer.d.ts +22 -0
- package/dist/mermaid/renderer.js +132 -0
- package/dist/mermaid/renderer.js.map +1 -0
- package/dist/mermaid/viewer.js +309 -0
- package/dist/mermaid/viewer.js.map +1 -0
- package/dist/prompt/index.d.ts +19 -0
- package/dist/prompt/index.js +54 -0
- package/dist/prompt/index.js.map +1 -0
- package/dist/react/callout.d.ts +9 -0
- package/dist/react/callout.js +12 -0
- package/dist/react/callout.js.map +1 -0
- package/dist/react/code-block.d.ts +18 -0
- package/dist/react/code-block.js +94 -0
- package/dist/react/code-block.js.map +1 -0
- package/dist/react/context.d.ts +18 -0
- package/dist/react/context.js +17 -0
- package/dist/react/context.js.map +1 -0
- package/dist/react/draw.d.ts +2 -0
- package/dist/react/draw.js +17 -0
- package/dist/react/draw.js.map +1 -0
- package/dist/react/file-tree.d.ts +16 -0
- package/dist/react/file-tree.js +69 -0
- package/dist/react/file-tree.js.map +1 -0
- package/dist/react/growing-code.js +36 -0
- package/dist/react/growing-code.js.map +1 -0
- package/dist/react/helpers.d.ts +6 -0
- package/dist/react/helpers.js +17 -0
- package/dist/react/helpers.js.map +1 -0
- package/dist/react/icons.js +23 -0
- package/dist/react/icons.js.map +1 -0
- package/dist/react/labels.js +9 -0
- package/dist/react/labels.js.map +1 -0
- package/dist/react/markdown.d.ts +78 -0
- package/dist/react/markdown.js +206 -0
- package/dist/react/markdown.js.map +1 -0
- package/dist/react/render.d.ts +8 -0
- package/dist/react/render.js +41 -0
- package/dist/react/render.js.map +1 -0
- package/dist/react/scrolls.js +62 -0
- package/dist/react/scrolls.js.map +1 -0
- package/dist/react/table-rows.js +66 -0
- package/dist/react/table-rows.js.map +1 -0
- package/dist/react/table.js +22 -0
- package/dist/react/table.js.map +1 -0
- package/dist/react/tabs.d.ts +25 -0
- package/dist/react/tabs.js +58 -0
- package/dist/react/tabs.js.map +1 -0
- package/dist/react/use-smooth-text.d.ts +6 -0
- package/dist/react/use-smooth-text.js +157 -0
- package/dist/react/use-smooth-text.js.map +1 -0
- package/dist/react/use-stage.d.ts +9 -0
- package/dist/react/use-stage.js +57 -0
- package/dist/react/use-stage.js.map +1 -0
- package/dist/remark.d.ts +20 -0
- package/dist/remark.js +10 -0
- package/dist/render/factory.d.ts +15 -0
- package/dist/render/labels.d.ts +46 -0
- package/dist/render/labels.js +46 -0
- package/dist/render/labels.js.map +1 -0
- package/dist/render/tree.js +314 -0
- package/dist/render/tree.js.map +1 -0
- package/dist/render/view.d.ts +58 -0
- package/dist/render/view.js +306 -0
- package/dist/render/view.js.map +1 -0
- package/dist/render/views.d.ts +16 -0
- package/dist/render/views.js +261 -0
- package/dist/render/views.js.map +1 -0
- package/dist/render/vnode.d.ts +21 -0
- package/dist/render/vnode.js +194 -0
- package/dist/render/vnode.js.map +1 -0
- package/dist/static/index.d.ts +28 -0
- package/dist/static/index.js +27 -0
- package/dist/static/index.js.map +1 -0
- package/dist/stream.d.ts +12 -0
- package/dist/stream.js +3 -0
- package/dist/styles.css +1362 -0
- package/dist/tui/code.d.ts +5 -0
- package/dist/tui/code.js +240 -0
- package/dist/tui/code.js.map +1 -0
- package/dist/tui/index.d.ts +54 -0
- package/dist/tui/index.js +99 -0
- package/dist/tui/index.js.map +1 -0
- package/dist/tui/math.js +242 -0
- package/dist/tui/math.js.map +1 -0
- package/dist/tui/printable.d.ts +12 -0
- package/dist/tui/printable.js +31 -0
- package/dist/tui/printable.js.map +1 -0
- package/dist/tui/render.d.ts +12 -0
- package/dist/tui/render.js +524 -0
- package/dist/tui/render.js.map +1 -0
- package/dist/tui/style.d.ts +35 -0
- package/dist/tui/style.js +77 -0
- package/dist/tui/style.js.map +1 -0
- package/dist/tui/table.js +73 -0
- package/dist/tui/table.js.map +1 -0
- package/dist/tui/width.d.ts +2 -0
- package/dist/tui/width.js +133 -0
- package/dist/tui/width.js.map +1 -0
- package/dist/tui/wrap.d.ts +8 -0
- package/dist/tui/wrap.js +191 -0
- package/dist/tui/wrap.js.map +1 -0
- package/package.json +128 -4
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// src/document/table.ts
|
|
2
|
+
/**
|
|
3
|
+
* A block that is one table, read without parsing it: what a renderer needs to draw a long table
|
|
4
|
+
* in pieces, a token costing only its last rows.
|
|
5
|
+
*/
|
|
6
|
+
const ROW = /^ {0,3}\|/;
|
|
7
|
+
const DELIMITER_CELL = /^[ \t]*:?-+:?[ \t]*$/;
|
|
8
|
+
/** The cells of a row written with its outer pipes. Null when a pipe could be an escaped one. */
|
|
9
|
+
function cells(line) {
|
|
10
|
+
if (line.includes("\\")) return null;
|
|
11
|
+
return line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|");
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The header (its two lines) and the body rows of a block that is one table and nothing else:
|
|
15
|
+
* every line opens with a pipe, so none of them can be anything but a row. Null for any other
|
|
16
|
+
* block, and for a table this cannot read with certainty; those are drawn by one parse, as ever.
|
|
17
|
+
*/
|
|
18
|
+
function tableRows(source) {
|
|
19
|
+
const text = source.replace(/^\n+/, "").trimEnd();
|
|
20
|
+
if (text.charCodeAt(0) !== 124 && !ROW.test(text)) return null;
|
|
21
|
+
const lines = text.split("\n");
|
|
22
|
+
if (lines.length < 3 || !lines.every((line) => ROW.test(line))) return null;
|
|
23
|
+
const header = cells(lines[0]);
|
|
24
|
+
const delimiter = cells(lines[1]);
|
|
25
|
+
if (!header || !delimiter || header.length !== delimiter.length) return null;
|
|
26
|
+
if (!delimiter.every((cell) => DELIMITER_CELL.test(cell))) return null;
|
|
27
|
+
if (!header.some((cell) => cell.trim())) return null;
|
|
28
|
+
return {
|
|
29
|
+
head: `${lines[0]}\n${lines[1]}\n`,
|
|
30
|
+
rows: lines.slice(2)
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export { tableRows };
|
|
34
|
+
|
|
35
|
+
//# sourceMappingURL=table.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"table.js","names":[],"sources":["../../src/document/table.ts"],"sourcesContent":["/**\n * A block that is one table, read without parsing it: what a renderer needs to draw a long table\n * in pieces, a token costing only its last rows.\n */\nconst ROW = /^ {0,3}\\|/\nconst DELIMITER_CELL = /^[ \\t]*:?-+:?[ \\t]*$/\n\n/** The cells of a row written with its outer pipes. Null when a pipe could be an escaped one. */\nfunction cells(line: string): string[] | null {\n if (line.includes('\\\\')) return null\n const inner = line.trim().replace(/^\\|/, '').replace(/\\|$/, '')\n return inner.split('|')\n}\n\n/** A table's header (its two lines, each with its line break) and its body rows. */\nexport interface TableRows {\n head: string\n rows: string[]\n}\n\n/**\n * The header (its two lines) and the body rows of a block that is one table and nothing else:\n * every line opens with a pipe, so none of them can be anything but a row. Null for any other\n * block, and for a table this cannot read with certainty; those are drawn by one parse, as ever.\n */\nexport function tableRows(source: string): TableRows | null {\n const text = source.replace(/^\\n+/, '').trimEnd()\n if (text.charCodeAt(0) !== 124 && !ROW.test(text)) return null\n const lines = text.split('\\n')\n if (lines.length < 3 || !lines.every((line) => ROW.test(line))) return null\n const header = cells(lines[0]!)\n const delimiter = cells(lines[1]!)\n if (!header || !delimiter || header.length !== delimiter.length) return null\n if (!delimiter.every((cell) => DELIMITER_CELL.test(cell))) return null\n if (!header.some((cell) => cell.trim())) return null\n return { head: `${lines[0]}\\n${lines[1]}\\n`, rows: lines.slice(2) }\n}\n"],"mappings":";;;;;AAIA,MAAM,MAAM;AACZ,MAAM,iBAAiB;;AAGvB,SAAS,MAAM,MAA+B;CAC5C,IAAI,KAAK,SAAS,IAAI,GAAG,OAAO;CAEhC,OADc,KAAK,KAAK,CAAC,CAAC,QAAQ,OAAO,EAAE,CAAC,CAAC,QAAQ,OAAO,EACjD,CAAC,CAAC,MAAM,GAAG;AACxB;;;;;;AAaA,SAAgB,UAAU,QAAkC;CAC1D,MAAM,OAAO,OAAO,QAAQ,QAAQ,EAAE,CAAC,CAAC,QAAQ;CAChD,IAAI,KAAK,WAAW,CAAC,MAAM,OAAO,CAAC,IAAI,KAAK,IAAI,GAAG,OAAO;CAC1D,MAAM,QAAQ,KAAK,MAAM,IAAI;CAC7B,IAAI,MAAM,SAAS,KAAK,CAAC,MAAM,OAAO,SAAS,IAAI,KAAK,IAAI,CAAC,GAAG,OAAO;CACvE,MAAM,SAAS,MAAM,MAAM,EAAG;CAC9B,MAAM,YAAY,MAAM,MAAM,EAAG;CACjC,IAAI,CAAC,UAAU,CAAC,aAAa,OAAO,WAAW,UAAU,QAAQ,OAAO;CACxE,IAAI,CAAC,UAAU,OAAO,SAAS,eAAe,KAAK,IAAI,CAAC,GAAG,OAAO;CAClE,IAAI,CAAC,OAAO,MAAM,SAAS,KAAK,KAAK,CAAC,GAAG,OAAO;CAChD,OAAO;EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,MAAM,GAAG;EAAK,MAAM,MAAM,MAAM,CAAC;CAAE;AACpE"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ViewComponents, ViewOptions } from "../render/view.js";
|
|
2
|
+
import { VComponent, VElement, VNode } from "../render/vnode.js";
|
|
3
|
+
import { DEFAULT_LABELS, LabelOverrides, MarkdownLabels } from "../render/labels.js";
|
|
4
|
+
import { Highlighter } from "../render/views.js";
|
|
5
|
+
import { UrlTransform, defaultUrlTransform } from "../engine/url.js";
|
|
6
|
+
import { Extension, Node } from "../engine/types.js";
|
|
7
|
+
export type MountOptions = Omit<ViewOptions, 'host' | 'idPrefix'>;
|
|
8
|
+
export interface UpdateOptions {
|
|
9
|
+
/** The text is a reply still arriving: its unfinished end is completed, and a caret follows it. */
|
|
10
|
+
streaming?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface Mounted {
|
|
13
|
+
/** The document's root: `<div class="flowdown">`, the last child of what it was mounted in. */
|
|
14
|
+
readonly element: HTMLElement;
|
|
15
|
+
/** Shows `markdown`. Call it on every token: what did not change is not drawn again. */
|
|
16
|
+
update(markdown: string, options?: UpdateOptions): void;
|
|
17
|
+
/** Takes the document off the page and stops everything it started. */
|
|
18
|
+
destroy(): void;
|
|
19
|
+
}
|
|
20
|
+
export declare function mount(target: Element, options?: MountOptions): Mounted;
|
|
21
|
+
export { DEFAULT_LABELS, type Extension, type Highlighter, type LabelOverrides, type MarkdownLabels, type Node, type UrlTransform, type VComponent, type VElement, type VNode, type ViewComponents, defaultUrlTransform };
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { codeFileName, copyText, downloadFile } from "../browser/file-actions.js";
|
|
2
|
+
import { DEFAULT_LABELS, mergeLabels } from "../render/labels.js";
|
|
3
|
+
import { createView } from "../render/view.js";
|
|
4
|
+
import { create, patch } from "./patch.js";
|
|
5
|
+
import { defaultUrlTransform } from "../engine/url.js";
|
|
6
|
+
// src/dom/index.ts
|
|
7
|
+
/**
|
|
8
|
+
* `@localix-ai/flowdown/dom`: Markdown on a page with no framework.
|
|
9
|
+
*
|
|
10
|
+
* import { mount } from '@localix-ai/flowdown/dom'
|
|
11
|
+
* import '@localix-ai/flowdown/styles.css'
|
|
12
|
+
*
|
|
13
|
+
* const reply = mount(document.querySelector('#reply'))
|
|
14
|
+
* for await (const text of stream) reply.update(text, { streaming: true })
|
|
15
|
+
* reply.update(text)
|
|
16
|
+
*
|
|
17
|
+
* The elements are the ones `<Markdown>` draws, and the page is kept up to date the same way: a
|
|
18
|
+
* block that did not change is not touched, so a token costs the last block. Tabs switch, a file
|
|
19
|
+
* tree folds, a code block copies and saves, a wide table scrolls: all with the keyboard too.
|
|
20
|
+
*
|
|
21
|
+
* It is also what a binding for a framework is made of: mount into the element the framework
|
|
22
|
+
* gives you, call `update` when the text changes, `destroy` when the element goes.
|
|
23
|
+
*/
|
|
24
|
+
/** Documents mounted so far: each gets its own ids for its tabs. */
|
|
25
|
+
let mounts = 0;
|
|
26
|
+
/**
|
|
27
|
+
* A table's box, or a code block's `<pre>`, scrolls sideways when what it holds is wider: then the
|
|
28
|
+
* keyboard must reach it.
|
|
29
|
+
*/
|
|
30
|
+
function measure(box, labels) {
|
|
31
|
+
const scrolls = box.scrollWidth > box.clientWidth + 1;
|
|
32
|
+
if (scrolls === box.hasAttribute("tabindex")) return;
|
|
33
|
+
const label = box.localName === "pre" ? labels.scrollableCode : labels.scrollableTable;
|
|
34
|
+
if (scrolls) {
|
|
35
|
+
box.setAttribute("role", "group");
|
|
36
|
+
box.setAttribute("aria-label", label);
|
|
37
|
+
box.setAttribute("tabindex", "0");
|
|
38
|
+
} else {
|
|
39
|
+
box.removeAttribute("role");
|
|
40
|
+
box.removeAttribute("aria-label");
|
|
41
|
+
box.removeAttribute("tabindex");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function mount(target, options = {}) {
|
|
45
|
+
const document = target.ownerDocument;
|
|
46
|
+
const labels = mergeLabels(options.labels);
|
|
47
|
+
let text = "";
|
|
48
|
+
let streaming = false;
|
|
49
|
+
let shown = null;
|
|
50
|
+
let root = null;
|
|
51
|
+
let drawing = false;
|
|
52
|
+
let again = false;
|
|
53
|
+
let gone = false;
|
|
54
|
+
/** The table boxes on the page, and the ones made since the last drawing. */
|
|
55
|
+
const boxes = /* @__PURE__ */ new WeakSet();
|
|
56
|
+
let fresh = [];
|
|
57
|
+
/** Whether a measuring of the last code block is already waiting for the next frame. */
|
|
58
|
+
let asked = false;
|
|
59
|
+
const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver((entries) => {
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
const box = entry.target.closest(".fd-table, pre");
|
|
62
|
+
if (box) measure(box, labels);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
const view = createView({
|
|
66
|
+
...options,
|
|
67
|
+
idPrefix: `fd-${mounts += 1}-`,
|
|
68
|
+
host: {
|
|
69
|
+
redraw: () => draw(),
|
|
70
|
+
copy: copyText,
|
|
71
|
+
download: (code, title, language) => downloadFile(code, codeFileName(title, language)),
|
|
72
|
+
table: (box) => {
|
|
73
|
+
const element = box;
|
|
74
|
+
if (boxes.has(element)) return;
|
|
75
|
+
boxes.add(element);
|
|
76
|
+
fresh.push(element);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
function draw() {
|
|
81
|
+
if (gone) return;
|
|
82
|
+
if (drawing) {
|
|
83
|
+
again = true;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
drawing = true;
|
|
87
|
+
try {
|
|
88
|
+
do {
|
|
89
|
+
again = false;
|
|
90
|
+
const next = view.render(text, streaming);
|
|
91
|
+
if (root === null || shown === null) {
|
|
92
|
+
root = create(next, document);
|
|
93
|
+
target.appendChild(root);
|
|
94
|
+
} else root = patch(root, shown, next, document);
|
|
95
|
+
shown = next;
|
|
96
|
+
} while (again);
|
|
97
|
+
} finally {
|
|
98
|
+
drawing = false;
|
|
99
|
+
}
|
|
100
|
+
for (const box of fresh) {
|
|
101
|
+
measure(box, labels);
|
|
102
|
+
if (observer) {
|
|
103
|
+
observer.observe(box);
|
|
104
|
+
const table = box.querySelector("table");
|
|
105
|
+
if (table) observer.observe(table);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
fresh = [];
|
|
109
|
+
if (!asked && root !== null && typeof requestAnimationFrame === "function") {
|
|
110
|
+
asked = true;
|
|
111
|
+
requestAnimationFrame(() => {
|
|
112
|
+
asked = false;
|
|
113
|
+
if (gone || root === null) return;
|
|
114
|
+
const blocks = root.getElementsByTagName("pre");
|
|
115
|
+
const last = blocks[blocks.length - 1];
|
|
116
|
+
if (last && boxes.has(last)) measure(last, labels);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
draw();
|
|
121
|
+
return {
|
|
122
|
+
get element() {
|
|
123
|
+
return root;
|
|
124
|
+
},
|
|
125
|
+
update(markdown, { streaming: live = false } = {}) {
|
|
126
|
+
text = markdown;
|
|
127
|
+
streaming = live;
|
|
128
|
+
draw();
|
|
129
|
+
},
|
|
130
|
+
destroy() {
|
|
131
|
+
if (gone) return;
|
|
132
|
+
gone = true;
|
|
133
|
+
observer?.disconnect();
|
|
134
|
+
view.dispose();
|
|
135
|
+
root?.remove();
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
export { DEFAULT_LABELS, defaultUrlTransform, mount };
|
|
140
|
+
|
|
141
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/dom/index.ts"],"sourcesContent":["/**\n * `@localix-ai/flowdown/dom`: Markdown on a page with no framework.\n *\n * import { mount } from '@localix-ai/flowdown/dom'\n * import '@localix-ai/flowdown/styles.css'\n *\n * const reply = mount(document.querySelector('#reply'))\n * for await (const text of stream) reply.update(text, { streaming: true })\n * reply.update(text)\n *\n * The elements are the ones `<Markdown>` draws, and the page is kept up to date the same way: a\n * block that did not change is not touched, so a token costs the last block. Tabs switch, a file\n * tree folds, a code block copies and saves, a wide table scrolls: all with the keyboard too.\n *\n * It is also what a binding for a framework is made of: mount into the element the framework\n * gives you, call `update` when the text changes, `destroy` when the element goes.\n */\nimport { codeFileName, copyText, downloadFile } from '../browser/file-actions'\nimport { mergeLabels, type MarkdownLabels } from '../render/labels'\nimport { createView, type ViewOptions } from '../render/view'\nimport type { DrawnElement } from '../render/vnode'\nimport { create, patch } from './patch'\n\nexport type { ViewComponents } from '../render/view'\nexport type { VComponent, VElement, VNode } from '../render/vnode'\nexport { DEFAULT_LABELS, type LabelOverrides, type MarkdownLabels } from '../render/labels'\nexport type { Highlighter } from '../render/views'\nexport { defaultUrlTransform, type UrlTransform } from '../engine/url'\nexport type { Extension, Node } from '../engine/types'\n\nexport type MountOptions = Omit<ViewOptions, 'host' | 'idPrefix'>\n\nexport interface UpdateOptions {\n /** The text is a reply still arriving: its unfinished end is completed, and a caret follows it. */\n streaming?: boolean\n}\n\nexport interface Mounted {\n /** The document's root: `<div class=\"flowdown\">`, the last child of what it was mounted in. */\n readonly element: HTMLElement\n /** Shows `markdown`. Call it on every token: what did not change is not drawn again. */\n update(markdown: string, options?: UpdateOptions): void\n /** Takes the document off the page and stops everything it started. */\n destroy(): void\n}\n\n/** Documents mounted so far: each gets its own ids for its tabs. */\nlet mounts = 0\n\n/**\n * A table's box, or a code block's `<pre>`, scrolls sideways when what it holds is wider: then the\n * keyboard must reach it.\n */\nfunction measure(box: Element, labels: MarkdownLabels): void {\n // One pixel of slack: a subpixel content width rounds up past the box it fits in.\n const scrolls = box.scrollWidth > box.clientWidth + 1\n if (scrolls === box.hasAttribute('tabindex')) return\n const label = box.localName === 'pre' ? labels.scrollableCode : labels.scrollableTable\n if (scrolls) {\n box.setAttribute('role', 'group')\n box.setAttribute('aria-label', label)\n box.setAttribute('tabindex', '0')\n } else {\n box.removeAttribute('role')\n box.removeAttribute('aria-label')\n box.removeAttribute('tabindex')\n }\n}\n\nexport function mount(target: Element, options: MountOptions = {}): Mounted {\n const document = target.ownerDocument\n const labels = mergeLabels(options.labels)\n let text = ''\n let streaming = false\n let shown: DrawnElement | null = null\n let root: HTMLElement | null = null\n let drawing = false\n let again = false\n let gone = false\n\n /** The table boxes on the page, and the ones made since the last drawing. */\n const boxes = new WeakSet<Element>()\n let fresh: Element[] = []\n /** Whether a measuring of the last code block is already waiting for the next frame. */\n let asked = false\n const observer =\n typeof ResizeObserver === 'undefined'\n ? null\n : new ResizeObserver((entries) => {\n for (const entry of entries) {\n const box = entry.target.closest('.fd-table, pre')\n if (box) measure(box, labels)\n }\n })\n\n const view = createView({\n ...options,\n idPrefix: `fd-${(mounts += 1)}-`,\n host: {\n redraw: () => draw(),\n copy: copyText,\n download: (code, title, language) => downloadFile(code, codeFileName(title, language)),\n table: (box) => {\n const element = box as Element\n if (boxes.has(element)) return\n boxes.add(element)\n fresh.push(element)\n },\n },\n })\n\n function draw(): void {\n if (gone) return\n // A listener that fires while the page is being changed asks for one more drawing after it.\n if (drawing) {\n again = true\n return\n }\n drawing = true\n try {\n do {\n again = false\n const next = view.render(text, streaming)\n if (root === null || shown === null) {\n root = create(next, document) as HTMLElement\n target.appendChild(root)\n } else {\n root = patch(root, shown, next, document) as HTMLElement\n }\n shown = next\n } while (again)\n } finally {\n drawing = false\n }\n // Measured once it is on the page, and again whenever it or its table changes size: a reply\n // that streams a row in, a window made narrower.\n for (const box of fresh) {\n measure(box, labels)\n if (observer) {\n observer.observe(box)\n const table = box.querySelector('table')\n if (table) observer.observe(table)\n }\n }\n fresh = []\n // A line of code getting longer resizes nothing an observer watches. The block being written\n // is the last one: it is measured once a frame, when the page is laid out anyway.\n if (!asked && root !== null && typeof requestAnimationFrame === 'function') {\n asked = true\n requestAnimationFrame(() => {\n asked = false\n if (gone || root === null) return\n const blocks = root.getElementsByTagName('pre')\n const last = blocks[blocks.length - 1]\n if (last && boxes.has(last)) measure(last, labels)\n })\n }\n }\n\n draw()\n\n return {\n get element() {\n return root!\n },\n update(markdown, { streaming: live = false } = {}) {\n text = markdown\n streaming = live\n draw()\n },\n destroy() {\n if (gone) return\n gone = true\n observer?.disconnect()\n view.dispose()\n root?.remove()\n },\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { attributeName, attributeOf } from "../render/vnode.js";
|
|
2
|
+
// src/dom/patch.ts
|
|
3
|
+
/**
|
|
4
|
+
* Plain elements (`render/vnode.ts`) put on a page and kept up to date.
|
|
5
|
+
*
|
|
6
|
+
* What was drawn last is compared with what is drawn now, and only the difference touches the
|
|
7
|
+
* page. Two elements that are the very same object are not looked into: a view hands back a block
|
|
8
|
+
* that did not change as the same elements, so a token of a streaming reply costs its last block.
|
|
9
|
+
*
|
|
10
|
+
* The page is compared with nothing: what a reader changed on it by hand (a selection, a scroll
|
|
11
|
+
* position, an attribute a script set) stays, as long as the drawing does not change in that place.
|
|
12
|
+
*/
|
|
13
|
+
const SVG = "http://www.w3.org/2000/svg";
|
|
14
|
+
const inner = (node) => node.props?.innerHTML ?? null;
|
|
15
|
+
/** `onClick` and its like: a function under a name that starts with `on`. */
|
|
16
|
+
const isListener = (name) => name.length > 2 && name.charCodeAt(0) === 111 && name.charCodeAt(1) === 110;
|
|
17
|
+
function setProp(element, name, value, was) {
|
|
18
|
+
if (name === "ref" || name === "key" || name === "node" || name === "innerHTML") return;
|
|
19
|
+
if (typeof value === "function" || typeof was === "function") {
|
|
20
|
+
if (isListener(name)) element[name.toLowerCase()] = typeof value === "function" ? value : null;
|
|
21
|
+
if (typeof value === "function") return;
|
|
22
|
+
}
|
|
23
|
+
const attribute = attributeOf(name, value);
|
|
24
|
+
if (attribute === null) element.removeAttribute(attributeName(name));
|
|
25
|
+
else element.setAttribute(attribute.name, attribute.value);
|
|
26
|
+
if (name === "checked") element.checked = Boolean(value);
|
|
27
|
+
}
|
|
28
|
+
/** The node for `drawn`, with everything inside it. */
|
|
29
|
+
function create(drawn, document, svg = false) {
|
|
30
|
+
if (typeof drawn === "string") return document.createTextNode(drawn);
|
|
31
|
+
const inSvg = svg || drawn.tag === "svg";
|
|
32
|
+
const element = inSvg ? document.createElementNS(SVG, drawn.tag) : document.createElement(drawn.tag);
|
|
33
|
+
const props = drawn.props;
|
|
34
|
+
if (props !== null) for (const name in props) setProp(element, name, props[name], void 0);
|
|
35
|
+
const html = inner(drawn);
|
|
36
|
+
if (html !== null) element.innerHTML = String(html);
|
|
37
|
+
else {
|
|
38
|
+
const below = inSvg && drawn.tag !== "foreignObject";
|
|
39
|
+
for (const child of drawn.children) element.appendChild(create(child, document, below));
|
|
40
|
+
}
|
|
41
|
+
if (typeof props?.ref === "function") props.ref(element);
|
|
42
|
+
return element;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Makes `node`, which shows `old`, show `next`. Returns the node that shows it: `node` itself, or
|
|
46
|
+
* the one put in its place when the two are not the same kind of thing.
|
|
47
|
+
*/
|
|
48
|
+
function patch(node, old, next, document, svg = false) {
|
|
49
|
+
if (old === next) return node;
|
|
50
|
+
if (typeof next === "string") {
|
|
51
|
+
if (typeof old === "string") {
|
|
52
|
+
node.data = next;
|
|
53
|
+
return node;
|
|
54
|
+
}
|
|
55
|
+
return replace(node, next, document, svg);
|
|
56
|
+
}
|
|
57
|
+
if (typeof old === "string" || old.tag !== next.tag || inner(old) === null !== (inner(next) === null)) return replace(node, next, document, svg);
|
|
58
|
+
const element = node;
|
|
59
|
+
const before = old.props;
|
|
60
|
+
const after = next.props;
|
|
61
|
+
if (before !== after) {
|
|
62
|
+
if (after !== null) for (const name in after) {
|
|
63
|
+
const was = before === null ? void 0 : before[name];
|
|
64
|
+
if (was !== after[name]) setProp(element, name, after[name], was);
|
|
65
|
+
}
|
|
66
|
+
if (before !== null) {
|
|
67
|
+
for (const name in before) if (after === null || !(name in after)) setProp(element, name, void 0, before[name]);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const html = inner(next);
|
|
71
|
+
if (html !== null) {
|
|
72
|
+
if (String(html) !== String(inner(old))) element.innerHTML = String(html);
|
|
73
|
+
} else {
|
|
74
|
+
const inSvg = svg || next.tag === "svg";
|
|
75
|
+
patchChildren(element, old.children, next.children, document, inSvg && next.tag !== "foreignObject");
|
|
76
|
+
}
|
|
77
|
+
if (typeof after?.ref === "function") after.ref(element);
|
|
78
|
+
return node;
|
|
79
|
+
}
|
|
80
|
+
function replace(node, next, document, svg) {
|
|
81
|
+
const made = create(next, document, svg);
|
|
82
|
+
node.parentNode?.replaceChild(made, node);
|
|
83
|
+
return made;
|
|
84
|
+
}
|
|
85
|
+
/** Children are matched by place: a reply grows at its end, and what is before the end stays put. */
|
|
86
|
+
function patchChildren(parent, old, next, document, svg) {
|
|
87
|
+
const shared = Math.min(old.length, next.length);
|
|
88
|
+
let child = parent.firstChild;
|
|
89
|
+
let index = 0;
|
|
90
|
+
for (; index < shared && child !== null; index += 1) {
|
|
91
|
+
const following = child.nextSibling;
|
|
92
|
+
patch(child, old[index], next[index], document, svg);
|
|
93
|
+
child = following;
|
|
94
|
+
}
|
|
95
|
+
while (child !== null) {
|
|
96
|
+
const following = child.nextSibling;
|
|
97
|
+
parent.removeChild(child);
|
|
98
|
+
child = following;
|
|
99
|
+
}
|
|
100
|
+
for (; index < next.length; index += 1) parent.appendChild(create(next[index], document, svg));
|
|
101
|
+
}
|
|
102
|
+
export { create, patch };
|
|
103
|
+
|
|
104
|
+
//# sourceMappingURL=patch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"patch.js","names":[],"sources":["../../src/dom/patch.ts"],"sourcesContent":["/**\n * Plain elements (`render/vnode.ts`) put on a page and kept up to date.\n *\n * What was drawn last is compared with what is drawn now, and only the difference touches the\n * page. Two elements that are the very same object are not looked into: a view hands back a block\n * that did not change as the same elements, so a token of a streaming reply costs its last block.\n *\n * The page is compared with nothing: what a reader changed on it by hand (a selection, a scroll\n * position, an attribute a script set) stays, as long as the drawing does not change in that place.\n */\nimport { attributeName, attributeOf, type Drawn, type DrawnElement } from '../render/vnode'\n\nconst SVG = 'http://www.w3.org/2000/svg'\n\nconst inner = (node: DrawnElement): unknown => node.props?.innerHTML ?? null\n\n/** `onClick` and its like: a function under a name that starts with `on`. */\nconst isListener = (name: string): boolean =>\n name.length > 2 && name.charCodeAt(0) === 111 && name.charCodeAt(1) === 110\n\nfunction setProp(element: Element, name: string, value: unknown, was: unknown): void {\n if (name === 'ref' || name === 'key' || name === 'node' || name === 'innerHTML') return\n if (typeof value === 'function' || typeof was === 'function') {\n // One listener a kind, kept as the element's own (`onclick`): drawing again replaces it.\n if (isListener(name))\n (element as unknown as Record<string, unknown>)[name.toLowerCase()] =\n typeof value === 'function' ? value : null\n if (typeof value === 'function') return\n }\n const attribute = attributeOf(name, value)\n if (attribute === null) element.removeAttribute(attributeName(name))\n else element.setAttribute(attribute.name, attribute.value)\n // A box a reader has ticked no longer follows its attribute.\n if (name === 'checked') (element as HTMLInputElement).checked = Boolean(value)\n}\n\n/** The node for `drawn`, with everything inside it. */\nexport function create(drawn: Drawn, document: Document, svg = false): Node {\n if (typeof drawn === 'string') return document.createTextNode(drawn)\n const inSvg = svg || drawn.tag === 'svg'\n const element = inSvg\n ? document.createElementNS(SVG, drawn.tag)\n : document.createElement(drawn.tag)\n const props = drawn.props\n if (props !== null) for (const name in props) setProp(element, name, props[name], undefined)\n const html = inner(drawn)\n if (html !== null) element.innerHTML = String(html)\n else {\n const below = inSvg && drawn.tag !== 'foreignObject'\n for (const child of drawn.children) element.appendChild(create(child, document, below))\n }\n if (typeof props?.ref === 'function') (props.ref as (element: Element) => void)(element)\n return element\n}\n\n/**\n * Makes `node`, which shows `old`, show `next`. Returns the node that shows it: `node` itself, or\n * the one put in its place when the two are not the same kind of thing.\n */\nexport function patch(node: Node, old: Drawn, next: Drawn, document: Document, svg = false): Node {\n if (old === next) return node\n if (typeof next === 'string') {\n if (typeof old === 'string') {\n ;(node as Text).data = next\n return node\n }\n return replace(node, next, document, svg)\n }\n if (\n typeof old === 'string' ||\n old.tag !== next.tag ||\n (inner(old) === null) !== (inner(next) === null)\n )\n return replace(node, next, document, svg)\n const element = node as Element\n const before = old.props\n const after = next.props\n if (before !== after) {\n if (after !== null)\n for (const name in after) {\n const was = before === null ? undefined : before[name]\n if (was !== after[name]) setProp(element, name, after[name], was)\n }\n if (before !== null)\n for (const name in before)\n if (after === null || !(name in after)) setProp(element, name, undefined, before[name])\n }\n const html = inner(next)\n if (html !== null) {\n if (String(html) !== String(inner(old))) element.innerHTML = String(html)\n } else {\n const inSvg = svg || next.tag === 'svg'\n patchChildren(\n element,\n old.children,\n next.children,\n document,\n inSvg && next.tag !== 'foreignObject',\n )\n }\n if (typeof after?.ref === 'function') (after.ref as (element: Element) => void)(element)\n return node\n}\n\nfunction replace(node: Node, next: Drawn, document: Document, svg: boolean): Node {\n const made = create(next, document, svg)\n node.parentNode?.replaceChild(made, node)\n return made\n}\n\n/** Children are matched by place: a reply grows at its end, and what is before the end stays put. */\nfunction patchChildren(\n parent: Element,\n old: readonly Drawn[],\n next: readonly Drawn[],\n document: Document,\n svg: boolean,\n): void {\n const shared = Math.min(old.length, next.length)\n let child = parent.firstChild\n let index = 0\n for (; index < shared && child !== null; index += 1) {\n const following = child.nextSibling\n patch(child, old[index]!, next[index]!, document, svg)\n child = following\n }\n // What the page holds past the drawing's end goes; what the drawing holds past the page's is made.\n while (child !== null) {\n const following = child.nextSibling\n parent.removeChild(child)\n child = following\n }\n for (; index < next.length; index += 1) parent.appendChild(create(next[index]!, document, svg))\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@localix-ai/flowdown/element`: Markdown as an element of the page's own, for anything that writes HTML
|
|
3
|
+
* (Vue, Svelte, Solid, Angular, a template, a plain page).
|
|
4
|
+
*
|
|
5
|
+
* import { defineMarkdownElement } from '@localix-ai/flowdown/element'
|
|
6
|
+
* import '@localix-ai/flowdown/styles.css'
|
|
7
|
+
*
|
|
8
|
+
* defineMarkdownElement()
|
|
9
|
+
*
|
|
10
|
+
* <flow-down streaming></flow-down>
|
|
11
|
+
* element.markdown = text
|
|
12
|
+
*
|
|
13
|
+
* The text is the `markdown` property; the `streaming` attribute says the reply is still arriving.
|
|
14
|
+
* Text written inside the element is shown until the property is first set. The document is drawn
|
|
15
|
+
* in the page, not in a shadow tree, so the page's stylesheet and theme reach it.
|
|
16
|
+
*/
|
|
17
|
+
import { MountOptions } from "../dom/index.js";
|
|
18
|
+
export interface MarkdownElement extends HTMLElement {
|
|
19
|
+
/** The Markdown shown. */
|
|
20
|
+
markdown: string;
|
|
21
|
+
/** The reply is still arriving; the same as the `streaming` attribute. */
|
|
22
|
+
streaming: boolean;
|
|
23
|
+
}
|
|
24
|
+
declare global {
|
|
25
|
+
interface HTMLElementTagNameMap {
|
|
26
|
+
/** The element under its default name, so `querySelector('flow-down')` has `markdown`. */
|
|
27
|
+
'flow-down': MarkdownElement;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Registers the element under `name`. `options` are what every such element is drawn with: a
|
|
32
|
+
* highlighter, math, raw HTML, labels. Registering a name twice keeps the first.
|
|
33
|
+
*/
|
|
34
|
+
export declare function defineMarkdownElement(name?: string, options?: MountOptions): void;
|
|
35
|
+
export type { MountOptions };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { mount } from "../dom/index.js";
|
|
2
|
+
// src/element/index.ts
|
|
3
|
+
/**
|
|
4
|
+
* `@localix-ai/flowdown/element`: Markdown as an element of the page's own, for anything that writes HTML
|
|
5
|
+
* (Vue, Svelte, Solid, Angular, a template, a plain page).
|
|
6
|
+
*
|
|
7
|
+
* import { defineMarkdownElement } from '@localix-ai/flowdown/element'
|
|
8
|
+
* import '@localix-ai/flowdown/styles.css'
|
|
9
|
+
*
|
|
10
|
+
* defineMarkdownElement()
|
|
11
|
+
*
|
|
12
|
+
* <flow-down streaming></flow-down>
|
|
13
|
+
* element.markdown = text
|
|
14
|
+
*
|
|
15
|
+
* The text is the `markdown` property; the `streaming` attribute says the reply is still arriving.
|
|
16
|
+
* Text written inside the element is shown until the property is first set. The document is drawn
|
|
17
|
+
* in the page, not in a shadow tree, so the page's stylesheet and theme reach it.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Registers the element under `name`. `options` are what every such element is drawn with: a
|
|
21
|
+
* highlighter, math, raw HTML, labels. Registering a name twice keeps the first.
|
|
22
|
+
*/
|
|
23
|
+
function defineMarkdownElement(name = "flow-down", options = {}) {
|
|
24
|
+
if (typeof customElements === "undefined" || customElements.get(name)) return;
|
|
25
|
+
class Element extends HTMLElement {
|
|
26
|
+
static observedAttributes = ["streaming"];
|
|
27
|
+
#text = null;
|
|
28
|
+
#mounted = null;
|
|
29
|
+
get markdown() {
|
|
30
|
+
return this.#text ?? "";
|
|
31
|
+
}
|
|
32
|
+
set markdown(value) {
|
|
33
|
+
this.#text = value === null || value === void 0 ? "" : String(value);
|
|
34
|
+
this.#show();
|
|
35
|
+
}
|
|
36
|
+
get streaming() {
|
|
37
|
+
return this.hasAttribute("streaming");
|
|
38
|
+
}
|
|
39
|
+
set streaming(value) {
|
|
40
|
+
this.toggleAttribute("streaming", Boolean(value));
|
|
41
|
+
}
|
|
42
|
+
connectedCallback() {
|
|
43
|
+
if (this.#mounted) return;
|
|
44
|
+
if (this.#text === null) this.#text = this.textContent ?? "";
|
|
45
|
+
this.textContent = "";
|
|
46
|
+
this.#mounted = mount(this, options);
|
|
47
|
+
this.#show();
|
|
48
|
+
}
|
|
49
|
+
disconnectedCallback() {
|
|
50
|
+
this.#mounted?.destroy();
|
|
51
|
+
this.#mounted = null;
|
|
52
|
+
}
|
|
53
|
+
attributeChangedCallback() {
|
|
54
|
+
this.#show();
|
|
55
|
+
}
|
|
56
|
+
#show() {
|
|
57
|
+
this.#mounted?.update(this.#text ?? "", { streaming: this.streaming });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
customElements.define(name, Element);
|
|
61
|
+
}
|
|
62
|
+
export { defineMarkdownElement };
|
|
63
|
+
|
|
64
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/element/index.ts"],"sourcesContent":["/**\n * `@localix-ai/flowdown/element`: Markdown as an element of the page's own, for anything that writes HTML\n * (Vue, Svelte, Solid, Angular, a template, a plain page).\n *\n * import { defineMarkdownElement } from '@localix-ai/flowdown/element'\n * import '@localix-ai/flowdown/styles.css'\n *\n * defineMarkdownElement()\n *\n * <flow-down streaming></flow-down>\n * element.markdown = text\n *\n * The text is the `markdown` property; the `streaming` attribute says the reply is still arriving.\n * Text written inside the element is shown until the property is first set. The document is drawn\n * in the page, not in a shadow tree, so the page's stylesheet and theme reach it.\n */\nimport { mount, type Mounted, type MountOptions } from '../dom'\n\nexport type { MountOptions } from '../dom'\n\nexport interface MarkdownElement extends HTMLElement {\n /** The Markdown shown. */\n markdown: string\n /** The reply is still arriving; the same as the `streaming` attribute. */\n streaming: boolean\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n /** The element under its default name, so `querySelector('flow-down')` has `markdown`. */\n 'flow-down': MarkdownElement\n }\n}\n\n/**\n * Registers the element under `name`. `options` are what every such element is drawn with: a\n * highlighter, math, raw HTML, labels. Registering a name twice keeps the first.\n */\nexport function defineMarkdownElement(name = 'flow-down', options: MountOptions = {}): void {\n // Read here, not at import: a server that renders the page has no elements to extend.\n if (typeof customElements === 'undefined' || customElements.get(name)) return\n\n class Element extends HTMLElement implements MarkdownElement {\n static observedAttributes = ['streaming']\n #text: string | null = null\n #mounted: Mounted | null = null\n\n get markdown(): string {\n return this.#text ?? ''\n }\n set markdown(value: string) {\n this.#text = value === null || value === undefined ? '' : String(value)\n this.#show()\n }\n\n get streaming(): boolean {\n return this.hasAttribute('streaming')\n }\n set streaming(value: boolean) {\n this.toggleAttribute('streaming', Boolean(value))\n }\n\n connectedCallback(): void {\n if (this.#mounted) return\n // What the page wrote inside the element is its first text.\n if (this.#text === null) this.#text = this.textContent ?? ''\n this.textContent = ''\n this.#mounted = mount(this, options)\n this.#show()\n }\n\n disconnectedCallback(): void {\n this.#mounted?.destroy()\n this.#mounted = null\n }\n\n attributeChangedCallback(): void {\n this.#show()\n }\n\n #show(): void {\n this.#mounted?.update(this.#text ?? '', { streaming: this.streaming })\n }\n }\n\n customElements.define(name, Element)\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAsCA,SAAgB,sBAAsB,OAAO,aAAa,UAAwB,CAAC,GAAS;CAE1F,IAAI,OAAO,mBAAmB,eAAe,eAAe,IAAI,IAAI,GAAG;CAEvE,MAAM,gBAAgB,YAAuC;EAC3D,OAAO,qBAAqB,CAAC,WAAW;EACxC,QAAuB;EACvB,WAA2B;EAE3B,IAAI,WAAmB;GACrB,OAAO,KAAK,SAAS;EACvB;EACA,IAAI,SAAS,OAAe;GAC1B,KAAK,QAAQ,UAAU,QAAQ,UAAU,KAAA,IAAY,KAAK,OAAO,KAAK;GACtE,KAAK,MAAM;EACb;EAEA,IAAI,YAAqB;GACvB,OAAO,KAAK,aAAa,WAAW;EACtC;EACA,IAAI,UAAU,OAAgB;GAC5B,KAAK,gBAAgB,aAAa,QAAQ,KAAK,CAAC;EAClD;EAEA,oBAA0B;GACxB,IAAI,KAAK,UAAU;GAEnB,IAAI,KAAK,UAAU,MAAM,KAAK,QAAQ,KAAK,eAAe;GAC1D,KAAK,cAAc;GACnB,KAAK,WAAW,MAAM,MAAM,OAAO;GACnC,KAAK,MAAM;EACb;EAEA,uBAA6B;GAC3B,KAAK,UAAU,QAAQ;GACvB,KAAK,WAAW;EAClB;EAEA,2BAAiC;GAC/B,KAAK,MAAM;EACb;EAEA,QAAc;GACZ,KAAK,UAAU,OAAO,KAAK,SAAS,IAAI,EAAE,WAAW,KAAK,UAAU,CAAC;EACvE;CACF;CAEA,eAAe,OAAO,MAAM,OAAO;AACrC"}
|