@latentic/live-markdown 0.0.1 → 0.1.0
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/CHANGELOG.md +48 -0
- package/README.md +71 -5
- package/dist/index.d.ts +1 -2
- package/dist/index.js +192 -62
- package/dist/index.js.map +1 -1
- package/dist/styles.css +7 -2
- package/package.json +43 -17
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0] - 2026-08-24
|
|
4
|
+
|
|
5
|
+
First release from the editor's own repository, and the first with CI on more
|
|
6
|
+
than one platform.
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- **A `[…]` keeps its brackets until it resolves to a link.** Lezer emits a
|
|
11
|
+
`Link` node for every bracketed span; CommonMark only makes `[label]` a link
|
|
12
|
+
when the document also carries a `[label]: url` definition. Reading the node
|
|
13
|
+
as the answer hid the brackets out of ordinary prose — `arr[0]` rendered as
|
|
14
|
+
`arr0`, `see [3]` as `see 3` — and stripped every optional argument from a
|
|
15
|
+
pasted LaTeX block (`\begin{tikzpicture}[scale=0.42]`). Unresolved, the
|
|
16
|
+
brackets are content: visible, unstyled, and no longer atomic to the caret.
|
|
17
|
+
This also matches what a CommonMark exporter produces for the same input.
|
|
18
|
+
|
|
19
|
+
- **A table cell renders what the paragraph above it renders.** Cells walk the
|
|
20
|
+
Lezer tree, and four constructs have no node in it — wikilinks,
|
|
21
|
+
`==highlight==`, footnote references and `$math$` are found by scanning text.
|
|
22
|
+
Cells showed `$x^2$` as source, and mangled two of the others outright
|
|
23
|
+
(`[[Some Note]]` → `[Some Note]`). Features now contribute a pattern and its
|
|
24
|
+
markup through `inlineScanRulesFacet`, and the cell renderer carves those
|
|
25
|
+
spans out before walking the tree.
|
|
26
|
+
|
|
27
|
+
- **A parse budget that holds on more than the fastest machine.** `treeAt` spent
|
|
28
|
+
at most 50ms driving the parse to a queried position, against a worst case
|
|
29
|
+
measured at 43ms — seven milliseconds of headroom. On a slower machine
|
|
30
|
+
`ensureSyntaxTree` returned null, the lookup fell back to the viewport-limited
|
|
31
|
+
tree, and a delete crossed a fence boundary the guard should have walled.
|
|
32
|
+
Now 150ms.
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- `inlineScanRulesFacet` and `scanInline` — the seam for a construct Lezer does
|
|
37
|
+
not parse, so any renderer can see it, not just the decoration plugins.
|
|
38
|
+
- `linkResolves` / `linkHasVisibleLabel` (`core/linkResolution`).
|
|
39
|
+
|
|
40
|
+
### Documentation
|
|
41
|
+
|
|
42
|
+
- `styles.css` still called the package `ai-editor`, and both it and the README
|
|
43
|
+
promised that mathematics needs no CSS import. It does — KaTeX renders
|
|
44
|
+
through its own stylesheet, which no CodeMirror theme can supply.
|
|
45
|
+
|
|
46
|
+
## [0.0.1] - 2026-08-23
|
|
47
|
+
|
|
48
|
+
Initial publish, extracted from the Compose monorepo.
|
package/README.md
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
# @latentic/live-markdown
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@latentic/live-markdown)
|
|
4
|
+
[](https://github.com/getlatentic/live-markdown/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE)
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
**WYSIWYG markdown editing for React — where the markdown is still the document.**
|
|
8
|
+
|
|
9
|
+
A markdown editor built on [CodeMirror 6](https://codemirror.net/). Headings, tables, LaTeX math, Mermaid diagrams, images, footnotes and code blocks render inline as you type — there is no split-screen preview pane to sync, and no syntax left on screen to read past.
|
|
10
|
+
|
|
11
|
+
And unlike rich-text editors built on ProseMirror, Tiptap, Slate or Lexical, there is no intermediate document model to translate through. `value` in and `onChange` out are the same markdown string, byte for byte, YAML frontmatter included.
|
|
6
12
|
|
|
7
13
|
```tsx
|
|
8
14
|
import { CodeMirrorMarkdownEditor } from "@latentic/live-markdown";
|
|
@@ -25,7 +31,7 @@ Most markdown editors fall into two camps:
|
|
|
25
31
|
| **Raw editors** | CodeMirror, Monaco, Ace | Fast, plain text — but users see `## Heading`, not a heading |
|
|
26
32
|
| **Rich editors** | Tiptap, ProseMirror, Slate, Lexical | WYSIWYG — but the internal model is a custom AST, not markdown. Round-tripping to `.md` is lossy or fragile |
|
|
27
33
|
|
|
28
|
-
**@latentic/live-markdown sits in between.** The source of truth is the raw markdown string. CodeMirror parses it with Lezer, and a decoration engine replaces syntax tokens with rendered widgets in real time — `## Heading` becomes a styled heading, `- item` becomes a bullet, `` becomes an inline image. You
|
|
34
|
+
**@latentic/live-markdown sits in between.** The source of truth is the raw markdown string. CodeMirror parses it with Lezer, and a decoration engine replaces syntax tokens with rendered widgets in real time — `## Heading` becomes a styled heading, `- item` becomes a bullet, `` becomes an inline image. You edit the rendered form directly — no syntax on screen, no preview pane to keep in sync — while `value` in and `onChange` out stays a plain markdown string. No AST translation, no serialization bugs, no format lock-in.
|
|
29
35
|
|
|
30
36
|
The boundary semantics this demands — what every keystroke does at every construct edge — are specified in [docs/interaction-spec.md](docs/interaction-spec.md) and enforced by its conformance matrix (`interactionMatrix.test.ts`); block-level behaviors are specified executably in `src/codemirror/features/*.feature`.
|
|
31
37
|
|
|
@@ -38,11 +44,14 @@ The boundary semantics this demands — what every keystroke does at every const
|
|
|
38
44
|
| Round-trip fidelity | Byte-for-byte | Lossy (serializer) | Byte-for-byte | MDX subset | Lossy |
|
|
39
45
|
| YAML frontmatter | Preserved, never stripped | Plugin (varies) | No | Plugin | No |
|
|
40
46
|
| LaTeX math | Inline KaTeX | Plugin | No | Plugin | No |
|
|
47
|
+
| Mermaid diagrams | Rendered inline | Plugin | No | Plugin | No |
|
|
41
48
|
| Large files (1 MB+) | Fast (CodeMirror) | Slow (DOM-per-node) | Fast | Slow | Slow |
|
|
42
49
|
| Tables | GFM, cell navigation | Plugin | Basic | Plugin | Slash command |
|
|
43
50
|
| Image paste/drop | Built-in | Plugin | No | Plugin | Plugin |
|
|
44
51
|
| Framework | React | React / Vue / vanilla | Vanilla / adapters | React | React |
|
|
45
|
-
| Bundle size |
|
|
52
|
+
| Bundle size | 57 KB gzipped* | ~120 KB+ | ~40 KB | ~150 KB+ | ~200 KB+ |
|
|
53
|
+
|
|
54
|
+
\* Measured on the published bundle. CodeMirror, KaTeX and Mermaid are external, so you pay for a renderer only where your documents use one — the other figures are the projects' own published numbers and are not all drawn on the same basis.
|
|
46
55
|
|
|
47
56
|
### Key differentiators
|
|
48
57
|
|
|
@@ -72,6 +81,7 @@ The boundary semantics this demands — what every keystroke does at every const
|
|
|
72
81
|
- **Horizontal rules**
|
|
73
82
|
- **Footnotes** — inline marker with hover preview
|
|
74
83
|
- **LaTeX math** — inline `$...$` and display `$$...$$` via KaTeX
|
|
84
|
+
- **Mermaid diagrams** — a ```` ```mermaid ```` fence renders as a live diagram; click to select, double-click for the source
|
|
75
85
|
- **Wikilinks** — `[[Page]]` and `[[Page|alias]]` with Cmd/Ctrl-click navigation
|
|
76
86
|
- **Links** — Cmd/Ctrl-click to open, auto-detection
|
|
77
87
|
|
|
@@ -102,7 +112,7 @@ pnpm add @latentic/live-markdown
|
|
|
102
112
|
|
|
103
113
|
Peer dependencies: `react` and `react-dom` (18+).
|
|
104
114
|
|
|
105
|
-
All *in-editor* styling (headings, code, lists, tables, image widgets,
|
|
115
|
+
All *in-editor* styling (headings, code, lists, tables, image widgets, links) ships with the editor as a CodeMirror theme and applies automatically — no CSS import needed. For the outer container layout (so the editor fills its parent and scrolls), import the small stylesheet once:
|
|
106
116
|
|
|
107
117
|
```ts
|
|
108
118
|
import "@latentic/live-markdown/styles.css";
|
|
@@ -110,6 +120,12 @@ import "@latentic/live-markdown/styles.css";
|
|
|
110
120
|
|
|
111
121
|
Skip it if your app already lays the editor out as a flex child.
|
|
112
122
|
|
|
123
|
+
Mathematics is the exception. KaTeX renders through its own stylesheet, which no CodeMirror theme can supply — without it `$x$` still typesets, just unstyled (a fraction stops stacking). If your documents use math:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
import "katex/dist/katex.min.css";
|
|
127
|
+
```
|
|
128
|
+
|
|
113
129
|
---
|
|
114
130
|
|
|
115
131
|
## Usage
|
|
@@ -254,12 +270,62 @@ The decoration engine walks the Lezer syntax tree on every document change, asks
|
|
|
254
270
|
- [x] Host-environment seams (image storage/resolution, link opening) with browser defaults
|
|
255
271
|
- [x] Self-themed editor surface (CodeMirror theme ships with the package)
|
|
256
272
|
- [x] Public extension API for contributing node rules (`MarkdownExtension.rules` → `nodeRulesFacet`)
|
|
273
|
+
- [ ] [Ship the engine without React](https://github.com/getlatentic/live-markdown/issues/1) — the editor is React only at its edge: of 195 source files, one component and one `type ReactNode` import name it. Splitting `/react` into a subpath entry frees the engine for any framework, or none
|
|
257
274
|
- [ ] Frontmatter as a toggleable extension (default on, disable via prop)
|
|
258
275
|
- [ ] Collaborative editing (CM6 collab extension)
|
|
259
276
|
- [ ] Slash commands (`/` menu for inserting blocks)
|
|
260
277
|
|
|
261
278
|
---
|
|
262
279
|
|
|
280
|
+
## Development
|
|
281
|
+
|
|
282
|
+
```sh
|
|
283
|
+
pnpm install
|
|
284
|
+
pnpm exec playwright install webkit # once — for the browser tier below
|
|
285
|
+
pnpm check # typecheck + build + both test tiers
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Tests run in two tiers, and the split is deliberate:
|
|
289
|
+
|
|
290
|
+
| tier | command | what it is for |
|
|
291
|
+
|---|---|---|
|
|
292
|
+
| default | `pnpm test` | everything that does not need layout — jsdom and node |
|
|
293
|
+
| browser | `pnpm test:browser` | real WebKit, for what jsdom cannot judge |
|
|
294
|
+
|
|
295
|
+
jsdom has no layout engine, so caret geometry, click placement, drawn selection
|
|
296
|
+
and KaTeX metrics all pass in it whatever they do on screen. Anything that
|
|
297
|
+
depends on where a box actually lands belongs in a `*.browser.test.ts`.
|
|
298
|
+
|
|
299
|
+
`pnpm build` is part of the gate rather than a release step: `tsc --noEmit`
|
|
300
|
+
never emits, so the errors that appear only when rolling up declarations — an
|
|
301
|
+
exported anonymous class, an inferred type naming a transitive dependency by
|
|
302
|
+
its package-manager path — stay invisible until a publish fails.
|
|
303
|
+
|
|
304
|
+
### Releasing
|
|
305
|
+
|
|
306
|
+
Publishing is caused by green CI, not preceded by it. `cargo`-style local
|
|
307
|
+
publishing is not wired up on purpose: a published version cannot be taken
|
|
308
|
+
back.
|
|
309
|
+
|
|
310
|
+
```sh
|
|
311
|
+
# 1. bump the version in package.json, land it on main
|
|
312
|
+
# 2. tag the merged commit
|
|
313
|
+
git tag v0.1.0 && git push origin v0.1.0
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
The tag triggers `publish.yml`, which refuses to build at all if the tag is not
|
|
317
|
+
an ancestor of `main` or disagrees with `package.json`, runs the full CI
|
|
318
|
+
workflow against the tagged commit as a job it `needs`, and only then publishes.
|
|
319
|
+
|
|
320
|
+
There is no npm token anywhere — not in the repository, not on a laptop.
|
|
321
|
+
Publishing uses [trusted publishing](https://docs.npmjs.com/trusted-publishers):
|
|
322
|
+
npm accepts a short-lived OIDC token minted for this workflow in this
|
|
323
|
+
repository, so the credential cannot be leaked, reused, or forgotten about, and
|
|
324
|
+
[provenance](https://docs.npmjs.com/generating-provenance-statements) is
|
|
325
|
+
generated automatically from it.
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
263
329
|
## License
|
|
264
330
|
|
|
265
331
|
[MIT](LICENSE)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
|
-
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
4
3
|
import * as _codemirror_view from '@codemirror/view';
|
|
5
4
|
import { Decoration, ViewPlugin, DecorationSet, ViewUpdate, EditorView, Command, KeyBinding } from '@codemirror/view';
|
|
6
5
|
import * as _codemirror_state from '@codemirror/state';
|
|
@@ -406,7 +405,7 @@ interface CodeMirrorMarkdownEditorProps {
|
|
|
406
405
|
*/
|
|
407
406
|
onFlushReady?: (flush: (() => void) | null) => void;
|
|
408
407
|
}
|
|
409
|
-
declare function CodeMirrorMarkdownEditorInner({ mode, onChange, value, workspaceRoot, filePath, linkTargets, onNavigateToLink, toolbar, selectionActions, resolveImageSrc, saveImageBytes, onOpenExternalUrl, onCommentOnExcerpt, renderClipboardHtml, onAfterContentSwap, onFlushReady, }: CodeMirrorMarkdownEditorProps):
|
|
408
|
+
declare function CodeMirrorMarkdownEditorInner({ mode, onChange, value, workspaceRoot, filePath, linkTargets, onNavigateToLink, toolbar, selectionActions, resolveImageSrc, saveImageBytes, onOpenExternalUrl, onCommentOnExcerpt, renderClipboardHtml, onAfterContentSwap, onFlushReady, }: CodeMirrorMarkdownEditorProps): react.JSX.Element;
|
|
410
409
|
/**
|
|
411
410
|
* Memoised export — same reason as the Tiptap editor. AppShell
|
|
412
411
|
* re-renders on every chat-thread token; without memoisation each
|
package/dist/index.js
CHANGED
|
@@ -356,7 +356,7 @@ var verbatimPasteKeymap = keymap.of([
|
|
|
356
356
|
}
|
|
357
357
|
]);
|
|
358
358
|
var markdownPaste = [pasteHandler, verbatimPasteKeymap];
|
|
359
|
-
var PARSE_BUDGET_MS =
|
|
359
|
+
var PARSE_BUDGET_MS = 150;
|
|
360
360
|
function treeAt(state, pos) {
|
|
361
361
|
return ensureSyntaxTree(state, pos + 1, PARSE_BUDGET_MS) ?? syntaxTree(state);
|
|
362
362
|
}
|
|
@@ -1514,6 +1514,38 @@ var nodeRulesFacet = Facet.define({
|
|
|
1514
1514
|
return Object.assign({}, ...values);
|
|
1515
1515
|
}
|
|
1516
1516
|
});
|
|
1517
|
+
var inlineScanRulesFacet = Facet.define({
|
|
1518
|
+
combine: (values) => values
|
|
1519
|
+
});
|
|
1520
|
+
function scanInline(rules, text, at = 0) {
|
|
1521
|
+
const found = [];
|
|
1522
|
+
for (const rule of rules) {
|
|
1523
|
+
rule.pattern.lastIndex = 0;
|
|
1524
|
+
let match;
|
|
1525
|
+
while ((match = rule.pattern.exec(text)) !== null) {
|
|
1526
|
+
found.push({
|
|
1527
|
+
from: at + match.index,
|
|
1528
|
+
to: at + match.index + match[0].length,
|
|
1529
|
+
html: rule.render(match)
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
found.sort((a, b) => a.from - b.from || b.to - a.to);
|
|
1534
|
+
const kept = [];
|
|
1535
|
+
for (const span of found) {
|
|
1536
|
+
const last = kept[kept.length - 1];
|
|
1537
|
+
if (!last || span.from >= last.to) kept.push(span);
|
|
1538
|
+
}
|
|
1539
|
+
return kept;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
// src/codemirror/core/htmlEscape.ts
|
|
1543
|
+
function escapeText(value) {
|
|
1544
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1545
|
+
}
|
|
1546
|
+
function escapeAttr(value) {
|
|
1547
|
+
return escapeText(value).replace(/"/g, """);
|
|
1548
|
+
}
|
|
1517
1549
|
var SLICE_MS = 10;
|
|
1518
1550
|
var GAP_MS = 25;
|
|
1519
1551
|
var parseToEnd = ViewPlugin.fromClass(
|
|
@@ -1955,6 +1987,73 @@ var imageRule = (ctx) => {
|
|
|
1955
1987
|
})
|
|
1956
1988
|
};
|
|
1957
1989
|
};
|
|
1990
|
+
var CODE_NODES = /* @__PURE__ */ new Set(["FencedCode", "CodeBlock", "InlineCode"]);
|
|
1991
|
+
function viewportTree(view) {
|
|
1992
|
+
return ensureSyntaxTree(view.state, view.viewport.to, 100) ?? syntaxTree(view.state);
|
|
1993
|
+
}
|
|
1994
|
+
function docTree(state) {
|
|
1995
|
+
return ensureSyntaxTree(state, state.doc.length, 20) ?? syntaxTree(state);
|
|
1996
|
+
}
|
|
1997
|
+
function inCode(tree, pos) {
|
|
1998
|
+
let node = tree.resolveInner(pos, 1);
|
|
1999
|
+
for (; node; node = node.parent) {
|
|
2000
|
+
if (CODE_NODES.has(node.name)) return true;
|
|
2001
|
+
}
|
|
2002
|
+
return false;
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
// src/codemirror/core/linkResolution.ts
|
|
2006
|
+
var DEFINITION_CONTAINERS = /* @__PURE__ */ new Set([
|
|
2007
|
+
"Document",
|
|
2008
|
+
"Blockquote",
|
|
2009
|
+
"BulletList",
|
|
2010
|
+
"OrderedList",
|
|
2011
|
+
"ListItem"
|
|
2012
|
+
]);
|
|
2013
|
+
function normalizeLabel(label) {
|
|
2014
|
+
return label.trim().replace(/\s+/g, " ").toLowerCase();
|
|
2015
|
+
}
|
|
2016
|
+
function labelInner(state, label) {
|
|
2017
|
+
return state.sliceDoc(label.from + 1, label.to - 1);
|
|
2018
|
+
}
|
|
2019
|
+
var definedLabels = /* @__PURE__ */ new WeakMap();
|
|
2020
|
+
function definitions(state) {
|
|
2021
|
+
const cached = definedLabels.get(state);
|
|
2022
|
+
if (cached) return cached;
|
|
2023
|
+
const labels = /* @__PURE__ */ new Set();
|
|
2024
|
+
docTree(state).iterate({
|
|
2025
|
+
enter: (node) => {
|
|
2026
|
+
if (node.name !== "LinkReference") return DEFINITION_CONTAINERS.has(node.name);
|
|
2027
|
+
const label = node.node.getChild("LinkLabel");
|
|
2028
|
+
if (label) labels.add(normalizeLabel(labelInner(state, label)));
|
|
2029
|
+
return false;
|
|
2030
|
+
}
|
|
2031
|
+
});
|
|
2032
|
+
definedLabels.set(state, labels);
|
|
2033
|
+
return labels;
|
|
2034
|
+
}
|
|
2035
|
+
function ownLabel(state, link) {
|
|
2036
|
+
let open = null;
|
|
2037
|
+
let close = null;
|
|
2038
|
+
for (let child = link.firstChild; child; child = child.nextSibling) {
|
|
2039
|
+
if (child.name !== "LinkMark") continue;
|
|
2040
|
+
const mark2 = state.sliceDoc(child.from, child.to);
|
|
2041
|
+
if (mark2 === "[" && !open) open = child;
|
|
2042
|
+
else if (mark2 === "]" && !close) close = child;
|
|
2043
|
+
}
|
|
2044
|
+
if (!open || !close || close.from <= open.to) return null;
|
|
2045
|
+
return state.sliceDoc(open.to, close.from);
|
|
2046
|
+
}
|
|
2047
|
+
function linkResolves(link, state) {
|
|
2048
|
+
if (link.getChild("URL")) return true;
|
|
2049
|
+
const reference = link.getChild("LinkLabel");
|
|
2050
|
+
const explicit = reference ? labelInner(state, reference) : "";
|
|
2051
|
+
const key = explicit.trim() === "" ? ownLabel(state, link) ?? "" : explicit;
|
|
2052
|
+
return key.trim() !== "" && definitions(state).has(normalizeLabel(key));
|
|
2053
|
+
}
|
|
2054
|
+
function linkHasVisibleLabel(link, state) {
|
|
2055
|
+
return (ownLabel(state, link) ?? "").trim() !== "";
|
|
2056
|
+
}
|
|
1958
2057
|
var BulletWidget = class extends WidgetType {
|
|
1959
2058
|
/**
|
|
1960
2059
|
* Eq returns true if two widgets are interchangeable — when CM6
|
|
@@ -2068,19 +2167,8 @@ var taskMarkerRule = (ctx) => {
|
|
|
2068
2167
|
};
|
|
2069
2168
|
|
|
2070
2169
|
// src/codemirror/core/registry.ts
|
|
2071
|
-
function
|
|
2072
|
-
|
|
2073
|
-
if (!link) return false;
|
|
2074
|
-
let bracketOpen = null;
|
|
2075
|
-
let bracketClose = null;
|
|
2076
|
-
for (let child = link.firstChild; child; child = child.nextSibling) {
|
|
2077
|
-
if (child.name !== "LinkMark") continue;
|
|
2078
|
-
const mark2 = ctx.state.sliceDoc(child.from, child.to);
|
|
2079
|
-
if (mark2 === "[" && !bracketOpen) bracketOpen = child;
|
|
2080
|
-
else if (mark2 === "]" && !bracketClose) bracketClose = child;
|
|
2081
|
-
}
|
|
2082
|
-
if (!bracketOpen || !bracketClose || bracketClose.from <= bracketOpen.to) return false;
|
|
2083
|
-
return ctx.state.sliceDoc(bracketOpen.to, bracketClose.from).trim() !== "";
|
|
2170
|
+
function parentLink(ctx) {
|
|
2171
|
+
return ctx.parentName === "Link" ? ctx.node.parent : null;
|
|
2084
2172
|
}
|
|
2085
2173
|
var NODE_RULES = {
|
|
2086
2174
|
// ----- Structural wrappers (never directly styled) -----
|
|
@@ -2111,7 +2199,11 @@ var NODE_RULES = {
|
|
|
2111
2199
|
Emphasis: mark("cm-emphasis"),
|
|
2112
2200
|
StrongEmphasis: mark("cm-strong"),
|
|
2113
2201
|
InlineCode: mark("cm-inline-code"),
|
|
2114
|
-
Link:
|
|
2202
|
+
// A `Link` node is not yet a link: Lezer emits one for every `[…]`, and the
|
|
2203
|
+
// reference lookup CommonMark requires is left to us. Unresolved, it is the
|
|
2204
|
+
// literal brackets the author typed — most `[…]` in prose, and every optional
|
|
2205
|
+
// argument in a LaTeX block.
|
|
2206
|
+
Link: (ctx) => linkResolves(ctx.node, ctx.state) ? { paint: "mark", className: "cm-link" } : none,
|
|
2115
2207
|
Image: imageRule,
|
|
2116
2208
|
// `` → inline `<img>` widget
|
|
2117
2209
|
// ----- Inline literal sub-nodes (rendered inside their parent) -----
|
|
@@ -2121,8 +2213,9 @@ var NODE_RULES = {
|
|
|
2121
2213
|
// Bare GFM autolinks and <angle> autolinks emit the SAME node name, and
|
|
2122
2214
|
// there the URL IS the content — same class of bug when hidden.
|
|
2123
2215
|
URL: (ctx) => {
|
|
2124
|
-
|
|
2125
|
-
|
|
2216
|
+
const link = parentLink(ctx);
|
|
2217
|
+
if (!link) return { paint: "mark", className: "cm-link" };
|
|
2218
|
+
return linkHasVisibleLabel(link, ctx.state) ? { paint: "hide" } : { paint: "mark", className: "cm-link" };
|
|
2126
2219
|
},
|
|
2127
2220
|
LinkLabel: raw("visible inside Link; parent mark styles it"),
|
|
2128
2221
|
LinkTitle: hideAlways(),
|
|
@@ -2153,8 +2246,12 @@ var NODE_RULES = {
|
|
|
2153
2246
|
// `*` / `_`
|
|
2154
2247
|
CodeMark: hideAlways(),
|
|
2155
2248
|
// backticks for inline / fence pairs for blocks
|
|
2156
|
-
|
|
2157
|
-
//
|
|
2249
|
+
// `[`/`]`/`(`/`)` — chrome only where they really are chrome. An unresolved
|
|
2250
|
+
// link's brackets are content, and hiding them rewrote the document on screen.
|
|
2251
|
+
LinkMark: (ctx) => {
|
|
2252
|
+
const link = parentLink(ctx);
|
|
2253
|
+
return link && !linkResolves(link, ctx.state) ? none : { paint: "hide" };
|
|
2254
|
+
},
|
|
2158
2255
|
QuoteMark: hideAlways(),
|
|
2159
2256
|
// `>`
|
|
2160
2257
|
ListMark: listMarkRule,
|
|
@@ -2902,20 +2999,6 @@ function slugKey(value) {
|
|
|
2902
2999
|
}
|
|
2903
3000
|
return raw2.split("-").filter(Boolean).join("-");
|
|
2904
3001
|
}
|
|
2905
|
-
var CODE_NODES = /* @__PURE__ */ new Set(["FencedCode", "CodeBlock", "InlineCode"]);
|
|
2906
|
-
function viewportTree(view) {
|
|
2907
|
-
return ensureSyntaxTree(view.state, view.viewport.to, 100) ?? syntaxTree(view.state);
|
|
2908
|
-
}
|
|
2909
|
-
function docTree(state) {
|
|
2910
|
-
return ensureSyntaxTree(state, state.doc.length, 20) ?? syntaxTree(state);
|
|
2911
|
-
}
|
|
2912
|
-
function inCode(tree, pos) {
|
|
2913
|
-
let node = tree.resolveInner(pos, 1);
|
|
2914
|
-
for (; node; node = node.parent) {
|
|
2915
|
-
if (CODE_NODES.has(node.name)) return true;
|
|
2916
|
-
}
|
|
2917
|
-
return false;
|
|
2918
|
-
}
|
|
2919
3002
|
var WIKILINK_RE = /\[\[([^\]\n]+?)\]\]/g;
|
|
2920
3003
|
var HIDE = Decoration.replace({});
|
|
2921
3004
|
var linkMark = Decoration.mark({ class: "cm-wikilink" });
|
|
@@ -3073,12 +3156,6 @@ var clickModel = EditorView.domEventHandlers({
|
|
|
3073
3156
|
});
|
|
3074
3157
|
|
|
3075
3158
|
// src/codemirror/table/tableInline.ts
|
|
3076
|
-
function escapeText(value) {
|
|
3077
|
-
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
3078
|
-
}
|
|
3079
|
-
function escapeAttr(value) {
|
|
3080
|
-
return escapeText(value).replace(/"/g, """);
|
|
3081
|
-
}
|
|
3082
3159
|
function codeText(state, node) {
|
|
3083
3160
|
let innerFrom = node.from;
|
|
3084
3161
|
let innerTo = node.to;
|
|
@@ -3154,8 +3231,26 @@ function renderRange(state, parent, from, to) {
|
|
|
3154
3231
|
if (pos < to) html += state.sliceDoc(pos, to);
|
|
3155
3232
|
return html;
|
|
3156
3233
|
}
|
|
3234
|
+
function straddlesChild(cell, from, to) {
|
|
3235
|
+
for (let child = cell.firstChild; child; child = child.nextSibling) {
|
|
3236
|
+
if (child.from < from && child.to > from) return true;
|
|
3237
|
+
if (child.from < to && child.to > to) return true;
|
|
3238
|
+
}
|
|
3239
|
+
return false;
|
|
3240
|
+
}
|
|
3157
3241
|
function renderInlineCell(state, cell) {
|
|
3158
|
-
|
|
3242
|
+
const rules = state.facet(inlineScanRulesFacet);
|
|
3243
|
+
const tree = docTree(state);
|
|
3244
|
+
let html = "";
|
|
3245
|
+
let pos = cell.from;
|
|
3246
|
+
for (const span of scanInline(rules, state.sliceDoc(cell.from, cell.to), cell.from)) {
|
|
3247
|
+
if (span.from < pos) continue;
|
|
3248
|
+
if (inCode(tree, span.from) || inCode(tree, span.to - 1)) continue;
|
|
3249
|
+
if (straddlesChild(cell, span.from, span.to)) continue;
|
|
3250
|
+
html += renderRange(state, cell, pos, span.from) + span.html;
|
|
3251
|
+
pos = span.to;
|
|
3252
|
+
}
|
|
3253
|
+
return (html + renderRange(state, cell, pos, cell.to)).trim();
|
|
3159
3254
|
}
|
|
3160
3255
|
|
|
3161
3256
|
// src/codemirror/table/tableModel.ts
|
|
@@ -3256,8 +3351,9 @@ var CELL_SANITIZE_CONFIG = {
|
|
|
3256
3351
|
ALLOWED_ATTR: ["href", "class"],
|
|
3257
3352
|
RETURN_TRUSTED_TYPE: false
|
|
3258
3353
|
};
|
|
3259
|
-
function renderCellInto(el, source) {
|
|
3354
|
+
function renderCellInto(el, source, rules) {
|
|
3260
3355
|
el.innerHTML = DOMPurify.sanitize(source, CELL_SANITIZE_CONFIG);
|
|
3356
|
+
for (const rule of rules) rule.hydrate?.(el);
|
|
3261
3357
|
}
|
|
3262
3358
|
function tableV2Sync(surface) {
|
|
3263
3359
|
return EditorView.updateListener.of((update) => {
|
|
@@ -3268,8 +3364,8 @@ function tableV2Sync(surface) {
|
|
|
3268
3364
|
}
|
|
3269
3365
|
|
|
3270
3366
|
// src/codemirror/tablev2/tableWidgetV2.ts
|
|
3271
|
-
function fillCell(el, cell, row, col) {
|
|
3272
|
-
renderCellInto(el, cell.html);
|
|
3367
|
+
function fillCell(el, cell, row, col, rules) {
|
|
3368
|
+
renderCellInto(el, cell.html, rules);
|
|
3273
3369
|
el.dataset.row = String(row);
|
|
3274
3370
|
el.dataset.col = String(col);
|
|
3275
3371
|
el.dataset.cellFrom = String(cell.from);
|
|
@@ -3297,7 +3393,8 @@ var TableWidgetV2 = class extends WidgetType {
|
|
|
3297
3393
|
eq(other) {
|
|
3298
3394
|
return other.sourceFrom === this.sourceFrom && other.sourceTo === this.sourceTo && JSON.stringify(other.data) === JSON.stringify(this.data);
|
|
3299
3395
|
}
|
|
3300
|
-
toDOM() {
|
|
3396
|
+
toDOM(view) {
|
|
3397
|
+
const rules = view.state.facet(inlineScanRulesFacet);
|
|
3301
3398
|
const wrap = document.createElement("div");
|
|
3302
3399
|
wrap.className = "cm-tablev2-wrap cm-table-wrap";
|
|
3303
3400
|
wrap.dataset.tablev2From = String(this.sourceFrom);
|
|
@@ -3308,7 +3405,7 @@ var TableWidgetV2 = class extends WidgetType {
|
|
|
3308
3405
|
const headRow = document.createElement("tr");
|
|
3309
3406
|
this.data.header.forEach((cell, col) => {
|
|
3310
3407
|
const th = document.createElement("th");
|
|
3311
|
-
fillCell(th, cell, 0, col);
|
|
3408
|
+
fillCell(th, cell, 0, col, rules);
|
|
3312
3409
|
const align = this.data.alignments[col];
|
|
3313
3410
|
if (align) th.style.textAlign = align;
|
|
3314
3411
|
headRow.appendChild(th);
|
|
@@ -3320,7 +3417,7 @@ var TableWidgetV2 = class extends WidgetType {
|
|
|
3320
3417
|
const tr = document.createElement("tr");
|
|
3321
3418
|
cells2.forEach((cell, col) => {
|
|
3322
3419
|
const td = document.createElement("td");
|
|
3323
|
-
fillCell(td, cell, r + 1, col);
|
|
3420
|
+
fillCell(td, cell, r + 1, col, rules);
|
|
3324
3421
|
const align = this.data.alignments[col];
|
|
3325
3422
|
if (align) td.style.textAlign = align;
|
|
3326
3423
|
tr.appendChild(td);
|
|
@@ -3331,7 +3428,8 @@ var TableWidgetV2 = class extends WidgetType {
|
|
|
3331
3428
|
wrap.appendChild(table);
|
|
3332
3429
|
return wrap;
|
|
3333
3430
|
}
|
|
3334
|
-
updateDOM(dom) {
|
|
3431
|
+
updateDOM(dom, view) {
|
|
3432
|
+
const rules = view.state.facet(inlineScanRulesFacet);
|
|
3335
3433
|
if (dom.dataset.tablev2From === void 0) return false;
|
|
3336
3434
|
const rows = dom.querySelectorAll("tr");
|
|
3337
3435
|
if (rows.length !== 1 + this.data.rows.length) return false;
|
|
@@ -3345,7 +3443,7 @@ var TableWidgetV2 = class extends WidgetType {
|
|
|
3345
3443
|
for (let r = 0; r < grid.length; r++) {
|
|
3346
3444
|
const cells2 = rows[r].children;
|
|
3347
3445
|
grid[r].forEach((cell, col) => {
|
|
3348
|
-
fillCell(cells2[col], cell, r, col);
|
|
3446
|
+
fillCell(cells2[col], cell, r, col, rules);
|
|
3349
3447
|
});
|
|
3350
3448
|
}
|
|
3351
3449
|
return true;
|
|
@@ -4293,12 +4391,19 @@ var highlightPlugin = ViewPlugin.fromClass(
|
|
|
4293
4391
|
}
|
|
4294
4392
|
);
|
|
4295
4393
|
|
|
4394
|
+
// src/codemirror/highlight/highlightScanRule.ts
|
|
4395
|
+
var highlightScanRule = {
|
|
4396
|
+
name: "highlight",
|
|
4397
|
+
pattern: HIGHLIGHT_RE,
|
|
4398
|
+
render: (match) => `<span class="cm-highlight">${escapeText(match[1] ?? "")}</span>`
|
|
4399
|
+
};
|
|
4400
|
+
|
|
4296
4401
|
// src/codemirror/extensions/highlightExtension.ts
|
|
4297
4402
|
var highlightExtension = {
|
|
4298
4403
|
name: "@compose/highlight",
|
|
4299
4404
|
version: "0.1.0",
|
|
4300
4405
|
description: "Renders `==text==` with a yellow highlight background.",
|
|
4301
|
-
extensions: [highlightPlugin]
|
|
4406
|
+
extensions: [highlightPlugin, inlineScanRulesFacet.of(highlightScanRule)]
|
|
4302
4407
|
};
|
|
4303
4408
|
var FOOTNOTE_REF_RE = /(?<!\])\[\^([^\]\s]+)\](?!:)/g;
|
|
4304
4409
|
var FOOTNOTE_DEF_LINE_RE = /^\[\^([^\]\s]+)\]:\s/;
|
|
@@ -4365,13 +4470,27 @@ var footnotePlugin = ViewPlugin.fromClass(
|
|
|
4365
4470
|
}
|
|
4366
4471
|
);
|
|
4367
4472
|
|
|
4473
|
+
// src/codemirror/footnote/footnoteScanRule.ts
|
|
4474
|
+
var footnoteScanRule = {
|
|
4475
|
+
name: "footnote",
|
|
4476
|
+
pattern: FOOTNOTE_REF_RE,
|
|
4477
|
+
render: (match) => `<span class="cm-footnote-ref">${escapeText(match[1] ?? "")}</span>`
|
|
4478
|
+
};
|
|
4479
|
+
|
|
4368
4480
|
// src/codemirror/extensions/footnoteExtension.ts
|
|
4369
4481
|
var footnoteExtension = {
|
|
4370
4482
|
name: "@compose/footnote",
|
|
4371
4483
|
version: "0.1.0",
|
|
4372
4484
|
description: "Renders `[^id]` references and `[^id]:` definitions with tooltip jump.",
|
|
4373
|
-
extensions: [footnotePlugin]
|
|
4485
|
+
extensions: [footnotePlugin, inlineScanRulesFacet.of(footnoteScanRule)]
|
|
4374
4486
|
};
|
|
4487
|
+
function renderMathInto(el, tex, displayMode) {
|
|
4488
|
+
try {
|
|
4489
|
+
katex.render(tex, el, { displayMode, throwOnError: false, output: "html" });
|
|
4490
|
+
} catch {
|
|
4491
|
+
el.textContent = tex;
|
|
4492
|
+
}
|
|
4493
|
+
}
|
|
4375
4494
|
var MathWidget = class extends WidgetType {
|
|
4376
4495
|
constructor(tex, displayMode) {
|
|
4377
4496
|
super();
|
|
@@ -4384,15 +4503,7 @@ var MathWidget = class extends WidgetType {
|
|
|
4384
4503
|
toDOM(_view) {
|
|
4385
4504
|
const span = document.createElement(this.displayMode ? "div" : "span");
|
|
4386
4505
|
span.className = this.displayMode ? "cm-math-block" : "cm-math-inline";
|
|
4387
|
-
|
|
4388
|
-
katex.render(this.tex, span, {
|
|
4389
|
-
displayMode: this.displayMode,
|
|
4390
|
-
throwOnError: false,
|
|
4391
|
-
output: "html"
|
|
4392
|
-
});
|
|
4393
|
-
} catch {
|
|
4394
|
-
span.textContent = this.tex;
|
|
4395
|
-
}
|
|
4506
|
+
renderMathInto(span, this.tex, this.displayMode);
|
|
4396
4507
|
return span;
|
|
4397
4508
|
}
|
|
4398
4509
|
ignoreEvent() {
|
|
@@ -4465,12 +4576,24 @@ var mathPlugin = StateField.define({
|
|
|
4465
4576
|
]
|
|
4466
4577
|
});
|
|
4467
4578
|
|
|
4579
|
+
// src/codemirror/math/mathScanRule.ts
|
|
4580
|
+
var mathScanRule = {
|
|
4581
|
+
name: "math",
|
|
4582
|
+
pattern: INLINE_MATH_RE,
|
|
4583
|
+
render: (match) => `<span class="cm-math-inline">${escapeText(match[1] ?? "")}</span>`,
|
|
4584
|
+
hydrate: (root) => {
|
|
4585
|
+
for (const el of root.querySelectorAll(".cm-math-inline")) {
|
|
4586
|
+
renderMathInto(el, el.textContent ?? "", false);
|
|
4587
|
+
}
|
|
4588
|
+
}
|
|
4589
|
+
};
|
|
4590
|
+
|
|
4468
4591
|
// src/codemirror/extensions/mathExtension.ts
|
|
4469
4592
|
var mathExtension = {
|
|
4470
4593
|
name: "@compose/math",
|
|
4471
4594
|
version: "0.1.0",
|
|
4472
4595
|
description: "Renders `$x$` inline and `$$x$$` block math via KaTeX.",
|
|
4473
|
-
extensions: [mathPlugin]
|
|
4596
|
+
extensions: [mathPlugin, inlineScanRulesFacet.of(mathScanRule)]
|
|
4474
4597
|
};
|
|
4475
4598
|
|
|
4476
4599
|
// src/codemirror/extensions/mermaidExtension.ts
|
|
@@ -4778,7 +4901,7 @@ var InlineCellSurface = class {
|
|
|
4778
4901
|
}
|
|
4779
4902
|
const model = modelAt(view.state, s.tableFrom);
|
|
4780
4903
|
const cell = model ? cellAt(model, s.ref.row, s.ref.col) : null;
|
|
4781
|
-
if (cell) renderCellInto(s.el, cell.html);
|
|
4904
|
+
if (cell) renderCellInto(s.el, cell.html, view.state.facet(inlineScanRulesFacet));
|
|
4782
4905
|
}
|
|
4783
4906
|
cancel() {
|
|
4784
4907
|
const s = this.edit;
|
|
@@ -5385,12 +5508,19 @@ function tableExtension() {
|
|
|
5385
5508
|
};
|
|
5386
5509
|
}
|
|
5387
5510
|
|
|
5511
|
+
// src/codemirror/wikilink/wikilinkScanRule.ts
|
|
5512
|
+
var wikilinkScanRule = {
|
|
5513
|
+
name: "wikilink",
|
|
5514
|
+
pattern: WIKILINK_RE,
|
|
5515
|
+
render: (match) => `<span class="cm-wikilink">${escapeText(parseWikilinkBody(match[1] ?? "").label)}</span>`
|
|
5516
|
+
};
|
|
5517
|
+
|
|
5388
5518
|
// src/codemirror/extensions/wikilinkExtension.ts
|
|
5389
5519
|
var wikilinkExtension = {
|
|
5390
5520
|
name: "@compose/wikilink",
|
|
5391
5521
|
version: "0.1.0",
|
|
5392
5522
|
description: "Renders `[[target]]` / `[[target|alias]]` as clickable links.",
|
|
5393
|
-
extensions: [wikilinkPlugin]
|
|
5523
|
+
extensions: [wikilinkPlugin, inlineScanRulesFacet.of(wikilinkScanRule)]
|
|
5394
5524
|
};
|
|
5395
5525
|
var programmaticSwap = Annotation.define();
|
|
5396
5526
|
var AUTOSAVE_DEBOUNCE_MS = 500;
|