@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 LocalixAI and flowdown contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,106 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/rsaryev/flowdown/main/docs/assets/logo.svg" width="72" height="72" alt="">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<h1 align="center">flowdown</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>Ultrafast, lightweight streaming Markdown for React and AI replies</strong><br>
|
|
9
|
+
Its own parser · 0 dependencies · 32 KB gzipped · 17× faster on a long stream
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://www.npmjs.com/package/@localix-ai/flowdown"><img alt="npm" src="https://img.shields.io/npm/v/@localix-ai/flowdown?color=cb3837"></a>
|
|
14
|
+
<a href="https://github.com/rsaryev/flowdown/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/rsaryev/flowdown/actions/workflows/ci.yml/badge.svg"></a>
|
|
15
|
+
<a href="https://github.com/rsaryev/flowdown/blob/main/packages/flowdown/LICENSE"><img alt="MIT licence" src="https://img.shields.io/badge/licence-MIT-blue"></a>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<p align="center">
|
|
19
|
+
<a href="https://flowdown.dev"><b>flowdown.dev</b></a> ·
|
|
20
|
+
<a href="https://flowdown.dev/docs/getting-started">Get started</a> ·
|
|
21
|
+
<a href="https://flowdown.dev/docs/syntax">Syntax</a> ·
|
|
22
|
+
<a href="https://flowdown.dev/docs/api">API</a> ·
|
|
23
|
+
<a href="https://flowdown.dev/docs/comparison">Comparison</a>
|
|
24
|
+
</p>
|
|
25
|
+
|
|
26
|
+
<p align="center">
|
|
27
|
+
<img alt="One reply streaming twice, side by side on black: on a page as callouts, steps, tabs, highlighted code, headline figures, a table, a diagram and a formula, and in a terminal as the same forms in styled text" src="https://raw.githubusercontent.com/rsaryev/flowdown/main/docs/assets/readme/pair.gif" width="878">
|
|
28
|
+
</p>
|
|
29
|
+
|
|
30
|
+
<p align="center">
|
|
31
|
+
<sub>One reply, streamed twice: by <code><Markdown></code> on a page and by <code>@localix-ai/flowdown/tui</code> in a terminal.</sub>
|
|
32
|
+
</p>
|
|
33
|
+
|
|
34
|
+
GitHub Markdown, plus the forms a model can write and a reader can scan: steps, tabs, headline
|
|
35
|
+
figures, pros and cons, callouts, folded sections, sources, file trees, titled code, diagrams and
|
|
36
|
+
math. It is drawn as the text arrives, without flicker or raw syntax showing.
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
npm install @localix-ai/flowdown
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
import { Markdown } from '@localix-ai/flowdown'
|
|
44
|
+
import '@localix-ai/flowdown/styles.css'
|
|
45
|
+
|
|
46
|
+
export function Reply({ text, done }: { text: string; done: boolean }) {
|
|
47
|
+
return <Markdown streaming={!done}>{text}</Markdown>
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`<Markdown>` needs React 18 or 19. A model writes the forms once its system prompt has the dialect:
|
|
52
|
+
`answerFormatPrompt()` from `@localix-ai/flowdown/prompt` is that text ([prompting](https://flowdown.dev/docs/prompting)).
|
|
53
|
+
|
|
54
|
+
## Why
|
|
55
|
+
|
|
56
|
+
- **Fast.** A 100k-character reply streams in 0.14 s. The next fastest renderer takes 2.4 s and
|
|
57
|
+
Streamdown 6.5 s.
|
|
58
|
+
- **Linear.** A reply four times longer costs about three times more, not sixteen, and no update
|
|
59
|
+
takes more than 5 ms.
|
|
60
|
+
- **Repairs a reply mid-stream.** Unfinished `**bold`, half-typed links and open code fences never
|
|
61
|
+
show their raw syntax.
|
|
62
|
+
- **Complete.** Its parser passes every example of CommonMark and of GitHub's extensions.
|
|
63
|
+
- **Light.** One package with no dependencies, 32 KB gzipped, styled without Tailwind.
|
|
64
|
+
- **Safe.** Raw HTML from a reply is sanitized, and links are held to safe protocols.
|
|
65
|
+
|
|
66
|
+
<p align="center">
|
|
67
|
+
<a href="https://flowdown.dev/docs/comparison"><img alt="flowdown beside five other React Markdown renderers: time to stream six kinds of reply, download size, packages installed, and the cost of a four times longer reply" src="https://raw.githubusercontent.com/rsaryev/flowdown/main/bench/results/charts/compare.png" width="878"></a>
|
|
68
|
+
</p>
|
|
69
|
+
|
|
70
|
+
Every figure, its method and where another renderer suits better are in
|
|
71
|
+
[the comparison](https://flowdown.dev/docs/comparison).
|
|
72
|
+
|
|
73
|
+
## Beyond React
|
|
74
|
+
|
|
75
|
+
The same reply, from the same package, by import path. None but the first imports React.
|
|
76
|
+
|
|
77
|
+
| Import | Draws |
|
|
78
|
+
| ------------------ | -------------------------------------------------- |
|
|
79
|
+
| `@localix-ai/flowdown` | React elements |
|
|
80
|
+
| `@localix-ai/flowdown/static` | HTML text, for a server or an email |
|
|
81
|
+
| `@localix-ai/flowdown/dom` | a page with no framework, kept up to date in place |
|
|
82
|
+
| `@localix-ai/flowdown/element` | `<flow-down>`, for Vue, Svelte, Solid or Angular |
|
|
83
|
+
| `@localix-ai/flowdown/tui` | the rows of a terminal |
|
|
84
|
+
|
|
85
|
+
[Other targets](https://flowdown.dev/docs/targets) has each one's options.
|
|
86
|
+
|
|
87
|
+
## Documentation
|
|
88
|
+
|
|
89
|
+
Everything is at **[flowdown.dev](https://flowdown.dev)**:
|
|
90
|
+
[getting started](https://flowdown.dev/docs/getting-started) ·
|
|
91
|
+
[syntax](https://flowdown.dev/docs/syntax) ·
|
|
92
|
+
[prompting](https://flowdown.dev/docs/prompting) ·
|
|
93
|
+
[streaming](https://flowdown.dev/docs/streaming) ·
|
|
94
|
+
[styling](https://flowdown.dev/docs/styling) ·
|
|
95
|
+
[components](https://flowdown.dev/docs/components) ·
|
|
96
|
+
[math](https://flowdown.dev/docs/math) ·
|
|
97
|
+
[diagrams](https://flowdown.dev/docs/diagrams) ·
|
|
98
|
+
[highlighting](https://flowdown.dev/docs/highlighting) ·
|
|
99
|
+
[security](https://flowdown.dev/docs/security) ·
|
|
100
|
+
[extensions](https://flowdown.dev/docs/remark) ·
|
|
101
|
+
[API](https://flowdown.dev/docs/api) ·
|
|
102
|
+
[releases](https://github.com/rsaryev/flowdown/blob/main/packages/flowdown/CHANGELOG.md)
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
MIT
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// src/browser/file-actions.ts
|
|
2
|
+
/** What a code block's buttons do with its text: put it on the clipboard, or save it as a file. */
|
|
3
|
+
/**
|
|
4
|
+
* Puts `text` on the clipboard. The Clipboard API where the page has it; on an insecure origin, or
|
|
5
|
+
* where it is refused, a selected hidden textarea and the old copy command, which still works on a
|
|
6
|
+
* click. Resolves whether the text was copied.
|
|
7
|
+
*/
|
|
8
|
+
async function copyText(text) {
|
|
9
|
+
try {
|
|
10
|
+
if (typeof navigator !== "undefined" && navigator.clipboard) {
|
|
11
|
+
await navigator.clipboard.writeText(text);
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
} catch {}
|
|
15
|
+
if (typeof document === "undefined" || typeof document.execCommand !== "function") return false;
|
|
16
|
+
const area = document.createElement("textarea");
|
|
17
|
+
area.value = text;
|
|
18
|
+
area.setAttribute("readonly", "");
|
|
19
|
+
area.style.cssText = "position:fixed;top:0;left:0;opacity:0;pointer-events:none";
|
|
20
|
+
const active = document.activeElement;
|
|
21
|
+
document.body.append(area);
|
|
22
|
+
area.focus();
|
|
23
|
+
area.select();
|
|
24
|
+
let copied = false;
|
|
25
|
+
try {
|
|
26
|
+
copied = document.execCommand("copy");
|
|
27
|
+
} catch {
|
|
28
|
+
copied = false;
|
|
29
|
+
}
|
|
30
|
+
area.remove();
|
|
31
|
+
active?.focus();
|
|
32
|
+
return copied;
|
|
33
|
+
}
|
|
34
|
+
/** Saves `text` as a file named `name`, through a temporary `a[download]`. */
|
|
35
|
+
function downloadFile(text, name, type = "text/plain") {
|
|
36
|
+
const url = URL.createObjectURL(new Blob([text], { type }));
|
|
37
|
+
const link = document.createElement("a");
|
|
38
|
+
link.href = url;
|
|
39
|
+
link.download = name;
|
|
40
|
+
link.style.display = "none";
|
|
41
|
+
document.body.append(link);
|
|
42
|
+
link.click();
|
|
43
|
+
link.remove();
|
|
44
|
+
setTimeout(() => URL.revokeObjectURL(url), 1e3);
|
|
45
|
+
}
|
|
46
|
+
/** The file extension of a fence's language, for a code block saved without a title. */
|
|
47
|
+
const EXTENSIONS = {
|
|
48
|
+
ts: "ts",
|
|
49
|
+
typescript: "ts",
|
|
50
|
+
tsx: "tsx",
|
|
51
|
+
js: "js",
|
|
52
|
+
javascript: "js",
|
|
53
|
+
jsx: "jsx",
|
|
54
|
+
json: "json",
|
|
55
|
+
sh: "sh",
|
|
56
|
+
bash: "sh",
|
|
57
|
+
shell: "sh",
|
|
58
|
+
zsh: "sh",
|
|
59
|
+
py: "py",
|
|
60
|
+
python: "py",
|
|
61
|
+
css: "css",
|
|
62
|
+
html: "html",
|
|
63
|
+
yaml: "yaml",
|
|
64
|
+
yml: "yaml",
|
|
65
|
+
sql: "sql",
|
|
66
|
+
md: "md",
|
|
67
|
+
markdown: "md"
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* The name a code block is saved under: the file its fence names (`title="src/app.ts"`, its last
|
|
71
|
+
* path segment), or `code.<ext>` from its language, `code.txt` when the language is unknown.
|
|
72
|
+
*/
|
|
73
|
+
function codeFileName(title, language) {
|
|
74
|
+
const named = title?.split(/[\\/]/).pop()?.trim();
|
|
75
|
+
if (named) return named;
|
|
76
|
+
return `code.${language && EXTENSIONS[language.toLowerCase()] || "txt"}`;
|
|
77
|
+
}
|
|
78
|
+
export { codeFileName, copyText, downloadFile };
|
|
79
|
+
|
|
80
|
+
//# sourceMappingURL=file-actions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-actions.js","names":[],"sources":["../../src/browser/file-actions.ts"],"sourcesContent":["/** What a code block's buttons do with its text: put it on the clipboard, or save it as a file. */\n\n/**\n * Puts `text` on the clipboard. The Clipboard API where the page has it; on an insecure origin, or\n * where it is refused, a selected hidden textarea and the old copy command, which still works on a\n * click. Resolves whether the text was copied.\n */\nexport async function copyText(text: string): Promise<boolean> {\n try {\n if (typeof navigator !== 'undefined' && navigator.clipboard) {\n await navigator.clipboard.writeText(text)\n return true\n }\n } catch {\n // Refused (an insecure origin, a denied permission): try the textarea.\n }\n if (typeof document === 'undefined' || typeof document.execCommand !== 'function') return false\n const area = document.createElement('textarea')\n area.value = text\n area.setAttribute('readonly', '')\n area.style.cssText = 'position:fixed;top:0;left:0;opacity:0;pointer-events:none'\n const active = document.activeElement as HTMLElement | null\n document.body.append(area)\n area.focus()\n area.select()\n let copied = false\n try {\n // oxlint-disable-next-line typescript/no-deprecated -- the one copy that works without the API\n copied = document.execCommand('copy')\n } catch {\n copied = false\n }\n area.remove()\n active?.focus()\n return copied\n}\n\n/** Saves `text` as a file named `name`, through a temporary `a[download]`. */\nexport function downloadFile(text: string, name: string, type = 'text/plain') {\n const url = URL.createObjectURL(new Blob([text], { type }))\n const link = document.createElement('a')\n link.href = url\n link.download = name\n link.style.display = 'none'\n document.body.append(link)\n link.click()\n link.remove()\n // Revoked a moment later: a browser that starts the download after this task still finds it.\n setTimeout(() => URL.revokeObjectURL(url), 1_000)\n}\n\n/** The file extension of a fence's language, for a code block saved without a title. */\nconst EXTENSIONS: Record<string, string> = {\n ts: 'ts',\n typescript: 'ts',\n tsx: 'tsx',\n js: 'js',\n javascript: 'js',\n jsx: 'jsx',\n json: 'json',\n sh: 'sh',\n bash: 'sh',\n shell: 'sh',\n zsh: 'sh',\n py: 'py',\n python: 'py',\n css: 'css',\n html: 'html',\n yaml: 'yaml',\n yml: 'yaml',\n sql: 'sql',\n md: 'md',\n markdown: 'md',\n}\n\n/**\n * The name a code block is saved under: the file its fence names (`title=\"src/app.ts\"`, its last\n * path segment), or `code.<ext>` from its language, `code.txt` when the language is unknown.\n */\nexport function codeFileName(title: string | undefined, language: string | undefined): string {\n const named = title?.split(/[\\\\/]/).pop()?.trim()\n if (named) return named\n return `code.${(language && EXTENSIONS[language.toLowerCase()]) || 'txt'}`\n}\n"],"mappings":";;;;;;;AAOA,eAAsB,SAAS,MAAgC;CAC7D,IAAI;EACF,IAAI,OAAO,cAAc,eAAe,UAAU,WAAW;GAC3D,MAAM,UAAU,UAAU,UAAU,IAAI;GACxC,OAAO;EACT;CACF,QAAQ,CAER;CACA,IAAI,OAAO,aAAa,eAAe,OAAO,SAAS,gBAAgB,YAAY,OAAO;CAC1F,MAAM,OAAO,SAAS,cAAc,UAAU;CAC9C,KAAK,QAAQ;CACb,KAAK,aAAa,YAAY,EAAE;CAChC,KAAK,MAAM,UAAU;CACrB,MAAM,SAAS,SAAS;CACxB,SAAS,KAAK,OAAO,IAAI;CACzB,KAAK,MAAM;CACX,KAAK,OAAO;CACZ,IAAI,SAAS;CACb,IAAI;EAEF,SAAS,SAAS,YAAY,MAAM;CACtC,QAAQ;EACN,SAAS;CACX;CACA,KAAK,OAAO;CACZ,QAAQ,MAAM;CACd,OAAO;AACT;;AAGA,SAAgB,aAAa,MAAc,MAAc,OAAO,cAAc;CAC5E,MAAM,MAAM,IAAI,gBAAgB,IAAI,KAAK,CAAC,IAAI,GAAG,EAAE,KAAK,CAAC,CAAC;CAC1D,MAAM,OAAO,SAAS,cAAc,GAAG;CACvC,KAAK,OAAO;CACZ,KAAK,WAAW;CAChB,KAAK,MAAM,UAAU;CACrB,SAAS,KAAK,OAAO,IAAI;CACzB,KAAK,MAAM;CACX,KAAK,OAAO;CAEZ,iBAAiB,IAAI,gBAAgB,GAAG,GAAG,GAAK;AAClD;;AAGA,MAAM,aAAqC;CACzC,IAAI;CACJ,YAAY;CACZ,KAAK;CACL,IAAI;CACJ,YAAY;CACZ,KAAK;CACL,MAAM;CACN,IAAI;CACJ,MAAM;CACN,OAAO;CACP,KAAK;CACL,IAAI;CACJ,QAAQ;CACR,KAAK;CACL,MAAM;CACN,MAAM;CACN,KAAK;CACL,KAAK;CACL,IAAI;CACJ,UAAU;AACZ;;;;;AAMA,SAAgB,aAAa,OAA2B,UAAsC;CAC5F,MAAM,QAAQ,OAAO,MAAM,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK;CAChD,IAAI,OAAO,OAAO;CAClB,OAAO,QAAS,YAAY,WAAW,SAAS,YAAY,MAAO;AACrE"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { hasRareEntity } from "../engine/entities.js";
|
|
2
|
+
import { isFenceBlock } from "./blocks.js";
|
|
3
|
+
import { escapeLiteralDollars } from "./dollars.js";
|
|
4
|
+
import { headingBases } from "./headings.js";
|
|
5
|
+
import { hasHtmlSyntax, hasMathSyntax } from "./syntax.js";
|
|
6
|
+
// src/core/block-facts.ts
|
|
7
|
+
/**
|
|
8
|
+
* What a renderer needs to know about each block, worked out once per block.
|
|
9
|
+
*
|
|
10
|
+
* The splitter hands back the blocks that did not change as the same objects, so anything
|
|
11
|
+
* computed from a block's text can be kept beside it: on the next token only the reply's end is
|
|
12
|
+
* scanned again. Each answer is the one a scan of the whole document gives, because every
|
|
13
|
+
* construct these scans look for lies inside one top-level block (a document where something
|
|
14
|
+
* resolves across blocks, footnotes or link definitions, is never split).
|
|
15
|
+
*/
|
|
16
|
+
function remembered(compute) {
|
|
17
|
+
const cache = /* @__PURE__ */ new WeakMap();
|
|
18
|
+
return (block) => {
|
|
19
|
+
if (cache.has(block)) return cache.get(block);
|
|
20
|
+
const value = compute(block);
|
|
21
|
+
cache.set(block, value);
|
|
22
|
+
return value;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** A fence whose language is `math`: a display formula, as GitHub writes one. */
|
|
26
|
+
const MATH_FENCE = /(?:^|\n) {0,3}(?:`{3,}|~{3,})[ \t]*math[ \t]*(?:\n|$)/;
|
|
27
|
+
/**
|
|
28
|
+
* Whether the block has a formula. A block that is one fenced code block has no prose to look in
|
|
29
|
+
* (only its opening line says whether it is a formula): a long fence that is still streaming is
|
|
30
|
+
* not scanned again on every token.
|
|
31
|
+
*/
|
|
32
|
+
const blockHasMath = remembered((block) => isFenceBlock(block) ? MATH_FENCE.test(block.source.slice(0, 40)) : hasMathSyntax(block.source) || MATH_FENCE.test(block.source));
|
|
33
|
+
/** Whether the block names a character reference outside the common ones (`↛`). */
|
|
34
|
+
const blockHasRareEntity = remembered((block) => !isFenceBlock(block) && block.source.includes("&") && hasRareEntity(block.source));
|
|
35
|
+
/** Whether the block has raw HTML in its prose. */
|
|
36
|
+
const blockHasHtml = remembered((block) => !isFenceBlock(block) && hasHtmlSyntax(block.source));
|
|
37
|
+
/**
|
|
38
|
+
* The block with its literal dollars escaped (`$5`), for a page that typesets `$…$`. The same
|
|
39
|
+
* block always gives back the same object, so a memoized renderer still skips it.
|
|
40
|
+
*/
|
|
41
|
+
const escapedBlock = remembered((block) => {
|
|
42
|
+
if (isFenceBlock(block)) return block;
|
|
43
|
+
const source = escapeLiteralDollars(block.source);
|
|
44
|
+
return source === block.source ? block : {
|
|
45
|
+
...block,
|
|
46
|
+
source
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
/** The headings in a block: where each starts in it, and the text its id is made from. */
|
|
50
|
+
const blockHeadings = remembered((block) => !isFenceBlock(block) && (block.source.includes("#") || /\n *(=+|-+) *(\n|$)/.test(block.source)) ? headingBases(block.source) : []);
|
|
51
|
+
/**
|
|
52
|
+
* Every heading's id across the blocks, keyed by where it starts in the document, numbered in
|
|
53
|
+
* document order (the second `## Setup` is `setup-1`) just as one parse of the whole would.
|
|
54
|
+
* Returned per block, so a block is handed only its own anchors.
|
|
55
|
+
*/
|
|
56
|
+
function headingIdsOfBlocks(blocks) {
|
|
57
|
+
const used = /* @__PURE__ */ new Set();
|
|
58
|
+
return blocks.map((block) => {
|
|
59
|
+
const ids = /* @__PURE__ */ new Map();
|
|
60
|
+
for (const { offset, base } of blockHeadings(block)) {
|
|
61
|
+
let id = base;
|
|
62
|
+
let suffix = 1;
|
|
63
|
+
while (used.has(id)) id = `${base}-${suffix++}`;
|
|
64
|
+
used.add(id);
|
|
65
|
+
ids.set(block.offset + offset, id);
|
|
66
|
+
}
|
|
67
|
+
return ids;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
export { blockHasHtml, blockHasMath, blockHasRareEntity, escapedBlock, headingIdsOfBlocks };
|
|
71
|
+
|
|
72
|
+
//# sourceMappingURL=block-facts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"block-facts.js","names":[],"sources":["../../src/core/block-facts.ts"],"sourcesContent":["/**\n * What a renderer needs to know about each block, worked out once per block.\n *\n * The splitter hands back the blocks that did not change as the same objects, so anything\n * computed from a block's text can be kept beside it: on the next token only the reply's end is\n * scanned again. Each answer is the one a scan of the whole document gives, because every\n * construct these scans look for lies inside one top-level block (a document where something\n * resolves across blocks, footnotes or link definitions, is never split).\n */\nimport { hasRareEntity } from '../engine/entities'\nimport { isFenceBlock, type MarkdownBlock } from './blocks'\nimport { escapeLiteralDollars } from './dollars'\nimport { headingBases } from './headings'\nimport { hasHtmlSyntax, hasMathSyntax } from './syntax'\n\nfunction remembered<T>(compute: (block: MarkdownBlock) => T) {\n const cache = new WeakMap<MarkdownBlock, T>()\n return (block: MarkdownBlock): T => {\n if (cache.has(block)) return cache.get(block) as T\n const value = compute(block)\n cache.set(block, value)\n return value\n }\n}\n\n/** A fence whose language is `math`: a display formula, as GitHub writes one. */\nconst MATH_FENCE = /(?:^|\\n) {0,3}(?:`{3,}|~{3,})[ \\t]*math[ \\t]*(?:\\n|$)/\n\n/**\n * Whether the block has a formula. A block that is one fenced code block has no prose to look in\n * (only its opening line says whether it is a formula): a long fence that is still streaming is\n * not scanned again on every token.\n */\nexport const blockHasMath = remembered((block) =>\n isFenceBlock(block)\n ? MATH_FENCE.test(block.source.slice(0, 40))\n : hasMathSyntax(block.source) || MATH_FENCE.test(block.source),\n)\n\n/** Whether the block names a character reference outside the common ones (`↛`). */\nexport const blockHasRareEntity = remembered(\n (block) => !isFenceBlock(block) && block.source.includes('&') && hasRareEntity(block.source),\n)\n\n/** Whether the block has raw HTML in its prose. */\nexport const blockHasHtml = remembered(\n (block) => !isFenceBlock(block) && hasHtmlSyntax(block.source),\n)\n\n/**\n * The block with its literal dollars escaped (`$5`), for a page that typesets `$…$`. The same\n * block always gives back the same object, so a memoized renderer still skips it.\n */\nexport const escapedBlock = remembered((block): MarkdownBlock => {\n if (isFenceBlock(block)) return block\n const source = escapeLiteralDollars(block.source)\n return source === block.source ? block : { ...block, source }\n})\n\n/** The headings in a block: where each starts in it, and the text its id is made from. */\nconst blockHeadings = remembered((block) =>\n !isFenceBlock(block) && (block.source.includes('#') || /\\n *(=+|-+) *(\\n|$)/.test(block.source))\n ? headingBases(block.source)\n : [],\n)\n\n/**\n * Every heading's id across the blocks, keyed by where it starts in the document, numbered in\n * document order (the second `## Setup` is `setup-1`) just as one parse of the whole would.\n * Returned per block, so a block is handed only its own anchors.\n */\nexport function headingIdsOfBlocks(blocks: readonly MarkdownBlock[]): Array<Map<number, string>> {\n const used = new Set<string>()\n return blocks.map((block) => {\n const ids = new Map<number, string>()\n for (const { offset, base } of blockHeadings(block)) {\n let id = base\n let suffix = 1\n while (used.has(id)) id = `${base}-${suffix++}`\n used.add(id)\n ids.set(block.offset + offset, id)\n }\n return ids\n })\n}\n"],"mappings":";;;;;;;;;;;;;;;AAeA,SAAS,WAAc,SAAsC;CAC3D,MAAM,wBAAQ,IAAI,QAA0B;CAC5C,QAAQ,UAA4B;EAClC,IAAI,MAAM,IAAI,KAAK,GAAG,OAAO,MAAM,IAAI,KAAK;EAC5C,MAAM,QAAQ,QAAQ,KAAK;EAC3B,MAAM,IAAI,OAAO,KAAK;EACtB,OAAO;CACT;AACF;;AAGA,MAAM,aAAa;;;;;;AAOnB,MAAa,eAAe,YAAY,UACtC,aAAa,KAAK,IACd,WAAW,KAAK,MAAM,OAAO,MAAM,GAAG,EAAE,CAAC,IACzC,cAAc,MAAM,MAAM,KAAK,WAAW,KAAK,MAAM,MAAM,CACjE;;AAGA,MAAa,qBAAqB,YAC/B,UAAU,CAAC,aAAa,KAAK,KAAK,MAAM,OAAO,SAAS,GAAG,KAAK,cAAc,MAAM,MAAM,CAC7F;;AAGA,MAAa,eAAe,YACzB,UAAU,CAAC,aAAa,KAAK,KAAK,cAAc,MAAM,MAAM,CAC/D;;;;;AAMA,MAAa,eAAe,YAAY,UAAyB;CAC/D,IAAI,aAAa,KAAK,GAAG,OAAO;CAChC,MAAM,SAAS,qBAAqB,MAAM,MAAM;CAChD,OAAO,WAAW,MAAM,SAAS,QAAQ;EAAE,GAAG;EAAO;CAAO;AAC9D,CAAC;;AAGD,MAAM,gBAAgB,YAAY,UAChC,CAAC,aAAa,KAAK,MAAM,MAAM,OAAO,SAAS,GAAG,KAAK,sBAAsB,KAAK,MAAM,MAAM,KAC1F,aAAa,MAAM,MAAM,IACzB,CAAC,CACP;;;;;;AAOA,SAAgB,mBAAmB,QAA8D;CAC/F,MAAM,uBAAO,IAAI,IAAY;CAC7B,OAAO,OAAO,KAAK,UAAU;EAC3B,MAAM,sBAAM,IAAI,IAAoB;EACpC,KAAK,MAAM,EAAE,QAAQ,UAAU,cAAc,KAAK,GAAG;GACnD,IAAI,KAAK;GACT,IAAI,SAAS;GACb,OAAO,KAAK,IAAI,EAAE,GAAG,KAAK,GAAG,KAAK,GAAG;GACrC,KAAK,IAAI,EAAE;GACX,IAAI,IAAI,MAAM,SAAS,QAAQ,EAAE;EACnC;EACA,OAAO;CACT,CAAC;AACH"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface MarkdownBlock {
|
|
2
|
+
/** The block's own source. */
|
|
3
|
+
source: string;
|
|
4
|
+
/** Where it starts in the document: characters before it, and its 1-based line. */
|
|
5
|
+
offset: number;
|
|
6
|
+
line: number;
|
|
7
|
+
}
|
|
8
|
+
/** Whether the splitter cut this block as one fenced code block. */
|
|
9
|
+
export declare const isFenceBlock: (block: MarkdownBlock) => boolean;
|
|
10
|
+
/** The top-level blocks of a document, cut where one parse of the whole would not join them. */
|
|
11
|
+
export declare function splitBlocks(input: string): MarkdownBlock[];
|
|
12
|
+
export interface BlockSplitter {
|
|
13
|
+
/** The same blocks as `splitBlocks(markdown)`; the ones that did not change are the same objects. */
|
|
14
|
+
split(markdown: string): MarkdownBlock[];
|
|
15
|
+
/** Characters scanned so far: the work the splitter has done. */
|
|
16
|
+
readonly lexed: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* `splitBlocks` for a document that grows at its end, as a streaming reply does. It remembers the
|
|
20
|
+
* blocks it cut last time and where its scan stopped: when the new text still starts with all of
|
|
21
|
+
* them but the last two, it keeps those, as the very same objects, and scans only the lines that
|
|
22
|
+
* are new. Any other text (a reply that was regenerated, or edited above its end) is split afresh.
|
|
23
|
+
* Each update then costs the new characters, not the reply: a stream is linear in its length.
|
|
24
|
+
*/
|
|
25
|
+
export declare function createBlockSplitter(): BlockSplitter;
|