@llui/markdown-editor 0.2.14 → 0.4.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.
Files changed (70) hide show
  1. package/dist/editor.d.ts +22 -2
  2. package/dist/editor.d.ts.map +1 -1
  3. package/dist/editor.js +45 -6
  4. package/dist/editor.js.map +1 -1
  5. package/dist/effects.d.ts +0 -1
  6. package/dist/effects.d.ts.map +1 -1
  7. package/dist/effects.js +4 -1
  8. package/dist/effects.js.map +1 -1
  9. package/dist/index.d.ts +9 -4
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +18 -4
  12. package/dist/index.js.map +1 -1
  13. package/dist/plugins/block-drag.d.ts +73 -0
  14. package/dist/plugins/block-drag.d.ts.map +1 -0
  15. package/dist/plugins/block-drag.js +760 -0
  16. package/dist/plugins/block-drag.js.map +1 -0
  17. package/dist/plugins/code-language.d.ts +60 -0
  18. package/dist/plugins/code-language.d.ts.map +1 -0
  19. package/dist/plugins/code-language.js +308 -0
  20. package/dist/plugins/code-language.js.map +1 -0
  21. package/dist/plugins/frontmatter.d.ts +45 -0
  22. package/dist/plugins/frontmatter.d.ts.map +1 -0
  23. package/dist/plugins/frontmatter.js +281 -0
  24. package/dist/plugins/frontmatter.js.map +1 -0
  25. package/dist/plugins/overlay.d.ts +1 -0
  26. package/dist/plugins/overlay.d.ts.map +1 -1
  27. package/dist/plugins/overlay.js +7 -0
  28. package/dist/plugins/overlay.js.map +1 -1
  29. package/dist/plugins/table.d.ts.map +1 -1
  30. package/dist/plugins/table.js +77 -15
  31. package/dist/plugins/table.js.map +1 -1
  32. package/dist/plugins/wikilink.d.ts +91 -0
  33. package/dist/plugins/wikilink.d.ts.map +1 -0
  34. package/dist/plugins/wikilink.js +467 -0
  35. package/dist/plugins/wikilink.js.map +1 -0
  36. package/dist/styles/block-drag.css +63 -0
  37. package/dist/styles/editor.css +64 -6
  38. package/dist/theme.d.ts +2 -5
  39. package/dist/theme.d.ts.map +1 -1
  40. package/dist/theme.js +15 -11
  41. package/dist/theme.js.map +1 -1
  42. package/dist/transformers/code.d.ts +24 -0
  43. package/dist/transformers/code.d.ts.map +1 -0
  44. package/dist/transformers/code.js +167 -0
  45. package/dist/transformers/code.js.map +1 -0
  46. package/dist/transformers/gfm.d.ts +9 -1
  47. package/dist/transformers/gfm.d.ts.map +1 -1
  48. package/dist/transformers/gfm.js +16 -4
  49. package/dist/transformers/gfm.js.map +1 -1
  50. package/dist/transformers/registry.d.ts +3 -0
  51. package/dist/transformers/registry.d.ts.map +1 -1
  52. package/dist/transformers/registry.js +26 -0
  53. package/dist/transformers/registry.js.map +1 -1
  54. package/package.json +45 -31
  55. package/src/editor.ts +77 -9
  56. package/src/effects.ts +4 -2
  57. package/src/index.ts +61 -9
  58. package/src/plugins/block-drag.ts +912 -0
  59. package/src/plugins/code-language.ts +378 -0
  60. package/src/plugins/frontmatter.ts +324 -0
  61. package/src/plugins/overlay.ts +7 -0
  62. package/src/plugins/table.ts +87 -13
  63. package/src/plugins/wikilink.ts +553 -0
  64. package/src/styles/block-drag.css +63 -0
  65. package/src/styles/editor.css +64 -6
  66. package/src/theme.ts +15 -11
  67. package/src/transformers/code.ts +174 -0
  68. package/src/transformers/gfm.ts +16 -4
  69. package/src/transformers/registry.ts +27 -0
  70. package/dist/__llui_deps.json +0 -274
@@ -0,0 +1,91 @@
1
+ import { TextNode, type EditorConfig, type LexicalEditor, type LexicalNode, type LexicalUpdateJSON, type NodeKey, type SerializedTextNode, type Spread } from 'lexical';
2
+ import type { MarkdownPlugin } from './types.js';
3
+ /** A parsed wikilink. `alias` is `null` when the target is shown verbatim. */
4
+ export interface WikiLink {
5
+ target: string;
6
+ alias: string | null;
7
+ }
8
+ /**
9
+ * Parse the content BETWEEN the brackets. Returns `null` when the content is not
10
+ * a valid wikilink body.
11
+ *
12
+ * Deliberate choices, each load-bearing for exact round-tripping:
13
+ * * split on the FIRST `|` only, so `[[a|b|c]]` has alias `b|c` and re-exports
14
+ * byte-identically;
15
+ * * an EMPTY alias (`[[a|]]`) normalizes to no alias — the alternative
16
+ * (keeping `alias: ''`) would render a zero-width, unclickable node;
17
+ * * NO trimming. `[[ a ]]` keeps its spaces, because trimming would make
18
+ * import→export lossy. Presentation trimming is the host's call in
19
+ * `onNavigate`/`resolve`, not the document's.
20
+ */
21
+ export declare function parseWikiLinkInner(inner: string): WikiLink | null;
22
+ /** Sanitize a target. Returns `null` when nothing usable survives. */
23
+ export declare function sanitizeWikiLinkTarget(raw: string): string | null;
24
+ /** Sanitize an alias. Returns `null` when nothing usable survives. */
25
+ export declare function sanitizeWikiLinkAlias(raw: string | null): string | null;
26
+ /**
27
+ * Serialize a wikilink back to markdown. Inverse of {@link parseWikiLinkInner}
28
+ * for every link built through this module's constructors — see
29
+ * {@link sanitizeWikiLinkTarget} for why that qualifier is load-bearing.
30
+ */
31
+ export declare function formatWikiLink(link: WikiLink): string;
32
+ export type SerializedWikiLinkNode = Spread<{
33
+ target: string;
34
+ alias: string | null;
35
+ }, SerializedTextNode>;
36
+ /**
37
+ * An atomic inline wikilink. Extends `TextNode` so the caret, selection and
38
+ * text formats behave exactly as they do for prose, while `token` mode keeps it
39
+ * indivisible: the user can delete it or move past it, but never edit its
40
+ * interior into a state where the visible alias disagrees with `__target`.
41
+ */
42
+ export declare class WikiLinkNode extends TextNode {
43
+ __target: string;
44
+ __alias: string | null;
45
+ static getType(): string;
46
+ static clone(node: WikiLinkNode): WikiLinkNode;
47
+ constructor(target: string, alias: string | null, text?: string, key?: NodeKey);
48
+ static importJSON(serializedNode: SerializedWikiLinkNode): WikiLinkNode;
49
+ updateFromJSON(serializedNode: LexicalUpdateJSON<SerializedWikiLinkNode>): this;
50
+ exportJSON(): SerializedWikiLinkNode;
51
+ createDOM(config: EditorConfig, editor?: LexicalEditor): HTMLElement;
52
+ updateDOM(prevNode: this, dom: HTMLElement, config: EditorConfig): boolean;
53
+ getTarget(): string;
54
+ /**
55
+ * Repoint the link. The display text follows only when there is no alias.
56
+ * The value is sanitized (see {@link sanitizeWikiLinkTarget}); a target with
57
+ * nothing representable in it leaves the link untouched rather than producing
58
+ * one that corrupts the document on export.
59
+ */
60
+ setTarget(target: string): this;
61
+ getAlias(): string | null;
62
+ /** Set (or clear, with `null`) the alias; the display text follows. */
63
+ setAlias(alias: string | null): this;
64
+ getLink(): WikiLink;
65
+ canInsertTextBefore(): boolean;
66
+ canInsertTextAfter(): boolean;
67
+ }
68
+ /**
69
+ * Build a wikilink node. `target`/`alias` are sanitized to values the `[[…]]`
70
+ * syntax can express (see {@link sanitizeWikiLinkTarget}); a target with nothing
71
+ * usable left falls back to the literal text `Page` rather than yielding an
72
+ * invisible token.
73
+ */
74
+ export declare function $createWikiLinkNode(target: string, alias?: string | null): WikiLinkNode;
75
+ export declare function $isWikiLinkNode(node: LexicalNode | null | undefined): node is WikiLinkNode;
76
+ export interface WikiLinkPluginOptions {
77
+ /**
78
+ * Called when the user activates a wikilink. This is the host's resolution
79
+ * seam: `@llui/markdown-editor` knows nothing about what a target names.
80
+ *
81
+ * The notification travels the same route as every other plugin event —
82
+ * `ctx.emit` → the editor's update loop → this plugin's reducer → an effect —
83
+ * rather than a raw DOM event, so an activation is an ordinary TEA message
84
+ * that shows up in devtools, replay and agent traces.
85
+ */
86
+ onNavigate?: (link: WikiLink) => void;
87
+ /** Text used as the target when the insert command runs with no selection. */
88
+ placeholderTarget?: string;
89
+ }
90
+ export declare function wikilinkPlugin(opts?: WikiLinkPluginOptions): MarkdownPlugin;
91
+ //# sourceMappingURL=wikilink.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wikilink.d.ts","sourceRoot":"","sources":["../../src/plugins/wikilink.ts"],"names":[],"mappings":"AAiEA,OAAO,EAQL,QAAQ,EACR,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,OAAO,EACZ,KAAK,kBAAkB,EACvB,KAAK,MAAM,EACZ,MAAM,SAAS,CAAA;AAKhB,OAAO,KAAK,EAAe,cAAc,EAAE,MAAM,YAAY,CAAA;AAI7D,8EAA8E;AAC9E,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CACrB;AAkBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAajE;AAwCD,sEAAsE;AACtE,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGjE;AAED,sEAAsE;AACtE,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAMvE;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAErD;AAmBD,MAAM,MAAM,sBAAsB,GAAG,MAAM,CACzC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EACxC,kBAAkB,CACnB,CAAA;AAED;;;;;GAKG;AACH,qBAAa,YAAa,SAAQ,QAAQ;IACxC,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IAEtB,MAAM,CAAC,OAAO,IAAI,MAAM;IAIxB,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,YAAY,GAAG,YAAY;gBAIlC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO;IAM9E,MAAM,CAAC,UAAU,CAAC,cAAc,EAAE,sBAAsB,GAAG,YAAY;IAMvE,cAAc,CAAC,cAAc,EAAE,iBAAiB,CAAC,sBAAsB,CAAC,GAAG,IAAI;IAQ/E,UAAU,IAAI,sBAAsB;IAIpC,SAAS,CAAC,MAAM,EAAE,YAAY,EAAE,MAAM,CAAC,EAAE,aAAa,GAAG,WAAW;IAgBpE,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO;IAc1E,SAAS,IAAI,MAAM;IAInB;;;;;OAKG;IACH,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAS/B,QAAQ,IAAI,MAAM,GAAG,IAAI;IAIzB,uEAAuE;IACvE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAQpC,OAAO,IAAI,QAAQ;IAOnB,mBAAmB,IAAI,OAAO;IAI9B,kBAAkB,IAAI,OAAO;CAG9B;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,GAAE,MAAM,GAAG,IAAW,GAAG,YAAY,CAK7F;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,IAAI,YAAY,CAE1F;AAuGD,MAAM,WAAW,qBAAqB;IACpC;;;;;;;;OAQG;IACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,IAAI,CAAA;IACrC,8EAA8E;IAC9E,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B;AAED,wBAAgB,cAAc,CAAC,IAAI,GAAE,qBAA0B,GAAG,cAAc,CA2E/E"}
@@ -0,0 +1,467 @@
1
+ // Wikilink plugin — `[[target]]` / `[[target|alias]]` inline references.
2
+ //
3
+ // The ALIAS is what the reader sees; the TARGET is what the link points at.
4
+ // Semantics are lifted from lance's markdown-it inline rule: a wikilink opens
5
+ // with `[[`, closes at the FIRST `]]`, splits on the FIRST `|`, and is rejected
6
+ // when the inner content is empty or contains a nested `[[`.
7
+ //
8
+ // ── Why a custom TextNode and NOT `@lexical/link` ────────────────────────────
9
+ //
10
+ // Reusing the link infrastructure looks tempting (a wikilink IS a link) but is
11
+ // wrong here on four counts:
12
+ //
13
+ // 1. A `LinkNode` is an ELEMENT node: the display text lives in its children,
14
+ // the destination in `getURL()`. A wikilink's target is not a URL — it is
15
+ // a document name resolved by the host. Storing it in `url` means every
16
+ // wikilink flows through this package's link-security layer
17
+ // (`sanitizeLinkUrl` + the global `registerLinkSanitizer` node transform,
18
+ // see `../security.ts`), which exists precisely to rewrite/unwrap hrefs it
19
+ // does not recognize. A wikilink would have to be carved out of the one
20
+ // enforcement point the editor relies on — a security seam we refuse to cut.
21
+ // 2. Because the display text is a child text node, editing it desyncs the
22
+ // alias from the target silently: type inside `[[Page|alias]]` and you get
23
+ // a link whose text no longer matches anything the exporter can round-trip.
24
+ // A `TextNode` subclass in `token` mode is ATOMIC — it is selected, moved
25
+ // and deleted as one unit, so target/alias can never drift.
26
+ // 3. `$toggleLink`, the link dialog, the floating link toolbar and autolink
27
+ // would all treat a wikilink as an ordinary hyperlink and happily rewrite it.
28
+ // 4. Both directions of the markdown conversion then live in ONE text-match
29
+ // transformer over ONE node type, instead of being split across the LINK
30
+ // transformer with a sentinel-scheme escape hatch.
31
+ //
32
+ // A `DecoratorNode` (the `LLuiDecoratorNode` bridge used by math/mermaid) was
33
+ // the other candidate and is also wrong: a decorator is an opaque, non-editable
34
+ // island. A wikilink is inline prose — the caret must move through and around
35
+ // it naturally, and it must inherit the surrounding text format. `TextNode` is
36
+ // the node kind that gives that for free.
37
+ //
38
+ // ── Transformer ordering ─────────────────────────────────────────────────────
39
+ //
40
+ // `transformers/registry.ts` ranks by type (text-match last) and, within a rank,
41
+ // by plugin array order. `@lexical/markdown` resolves competing text-match
42
+ // transformers with `findOutermostTextMatchTransformer`, which prefers the
43
+ // EARLIEST/outermost match and falls back to array order on a tie.
44
+ //
45
+ // * Typing (`registerMarkdownShortcuts`) indexes transformers by their single
46
+ // `trigger` character. Ours is `]`, LINK's is `)`, so they can never collide.
47
+ // * On IMPORT, LINK's `importRegExp` requires a `](`, which `[[a]]` and
48
+ // `[[a|b]]` never contain — plain wikilinks are safe at any position.
49
+ // * The ambiguity is a wikilink on a line that also contains a `](` — either
50
+ // immediately (`[[a|b]](c)`) or LATER on the same line
51
+ // (`**[[A]]** and [b](url)`). LINK's import pattern has a LAZY label
52
+ // (`\[(.+?)\]\(…\)`), so it can match starting at the WIKILINK's own `[` and
53
+ // span across to that later `](`. Both rules then match at the same start
54
+ // index, and a tie is broken by array order.
55
+ //
56
+ // That made fidelity depend on plugin order, and the failure was silent: with
57
+ // `wikilinkPlugin()` listed after `corePlugin()`, LINK swallowed the `**`
58
+ // delimiters and re-exported them ESCAPED, losing the bold outright.
59
+ //
60
+ // This is now resolved STRUCTURALLY, not by documentation: the transformer
61
+ // declares `setTransformerPrecedence(WIKILINK_TRANSFORMER, -10)` (see the
62
+ // bottom of this file), so the registry orders it ahead of LINK in EITHER
63
+ // plugin order. `test/composition.test.ts` pins both orders. To get the
64
+ // CommonMark reading of `[[a|b]](c)` instead, omit `wikilinkPlugin()`.
65
+ import { $applyNodeReplacement, $getNearestNodeFromDOMNode, $getSelection, $isRangeSelection, CLICK_COMMAND, COMMAND_PRIORITY_LOW, KEY_ENTER_COMMAND, TextNode, } from 'lexical';
66
+ import { mergeRegister } from '@lexical/utils';
67
+ import { setTransformerPrecedence } from '../transformers/registry.js';
68
+ import { definePluginUI } from './ui.js';
69
+ const PLUGIN = 'wikilink';
70
+ // ── Parsing ──────────────────────────────────────────────────────────────────
71
+ /**
72
+ * Matches `[[inner]]` where `inner` is non-empty and contains neither `[[` nor
73
+ * `]]`. Encoding both rejections IN the pattern (rather than validating inside
74
+ * `replace`) is what reproduces lance's scan-and-retry behaviour: markdown-it
75
+ * returns `false` at a bad `[[` and re-attempts one character later, so
76
+ * `[[a[[b]]` yields the INNER `[[b]]`. A regex that matched greedily at index 0
77
+ * and then bailed in `replace` would swallow the valid inner link instead.
78
+ */
79
+ const WIKILINK_RE_SOURCE = String.raw `\[\[((?:(?!\[\[|\]\])[\s\S])+)\]\]`;
80
+ const WIKILINK_IMPORT_RE = new RegExp(WIKILINK_RE_SOURCE);
81
+ /** Same pattern anchored at the caret, for the live typing shortcut. */
82
+ const WIKILINK_TYPING_RE = new RegExp(`${WIKILINK_RE_SOURCE}$`);
83
+ /**
84
+ * Parse the content BETWEEN the brackets. Returns `null` when the content is not
85
+ * a valid wikilink body.
86
+ *
87
+ * Deliberate choices, each load-bearing for exact round-tripping:
88
+ * * split on the FIRST `|` only, so `[[a|b|c]]` has alias `b|c` and re-exports
89
+ * byte-identically;
90
+ * * an EMPTY alias (`[[a|]]`) normalizes to no alias — the alternative
91
+ * (keeping `alias: ''`) would render a zero-width, unclickable node;
92
+ * * NO trimming. `[[ a ]]` keeps its spaces, because trimming would make
93
+ * import→export lossy. Presentation trimming is the host's call in
94
+ * `onNavigate`/`resolve`, not the document's.
95
+ */
96
+ export function parseWikiLinkInner(inner) {
97
+ if (inner.length === 0 || inner.includes('[['))
98
+ return null;
99
+ const pipe = inner.indexOf('|');
100
+ const target = pipe >= 0 ? inner.slice(0, pipe) : inner;
101
+ // A blank target is as unusable as an empty one: `[[ ]]` would build a
102
+ // token-mode node whose only glyph is a space — invisible, unclickable and
103
+ // (being a token) uneditable except by deleting it wholesale. Same rejection
104
+ // the empty case gets. Note this is a BLANK check, not a trim: a target that
105
+ // has any non-space content keeps its spaces verbatim, so `[[ a ]]` still
106
+ // round-trips byte-identically.
107
+ if (target.trim().length === 0)
108
+ return null;
109
+ const rawAlias = pipe >= 0 ? inner.slice(pipe + 1) : null;
110
+ return { target, alias: rawAlias !== null && rawAlias.trim().length > 0 ? rawAlias : null };
111
+ }
112
+ /**
113
+ * Constrain a target/alias to the values the `[[…]]` syntax can actually
114
+ * express, so that {@link formatWikiLink} is INJECTIVE.
115
+ *
116
+ * Without this, `formatWikiLink` is not the inverse it claims to be: a target
117
+ * of `a|b` emits `[[a|b]]`, which reads back as target `a` with alias `b` — the
118
+ * link silently repoints itself and its visible text changes on the next load.
119
+ * A target of `a]]b` emits `[[a]]b]]`, which reads back as a wikilink to `a`
120
+ * followed by the literal text `b]]`.
121
+ *
122
+ * ── Why sanitize rather than introduce an escape dialect ─────────────────────
123
+ *
124
+ * Backslash-escaping `|`/`[`/`]` would make every value representable, but it
125
+ * forks the syntax away from the markdown-it/Obsidian rule this plugin exists
126
+ * to match: `[[a\|b]]` is not a wikilink to `a|b` anywhere else in the
127
+ * ecosystem, it is a wikilink to the literal `a\` with alias `b`. Emitting it
128
+ * would corrupt the document for every OTHER reader of the same file, which is
129
+ * strictly worse than declining to represent a target no wikilink dialect can
130
+ * express. Documents are shared; our in-memory model is not.
131
+ *
132
+ * So the unrepresentable characters are removed at the point a link is BUILT
133
+ * (`$createWikiLinkNode`, `setTarget`, `setAlias`, the insert command) — every
134
+ * route by which a value can enter the document. Everything downstream may then
135
+ * assume `parse(format(link)) === link`.
136
+ */
137
+ function sanitizePart(raw, stripPipe) {
138
+ let out = raw
139
+ // Markdown is line-oriented: a newline inside `[[…]]` cannot survive an
140
+ // export/import cycle at all (neither half of the split matches).
141
+ .replace(/\s+/g, ' ')
142
+ // `[[` would open a nested link (which the import rule rejects outright);
143
+ // `]]` would close this one early and spill the remainder as literal text.
144
+ .replace(/\[\[+/g, '[')
145
+ .replace(/\]\]+/g, ']');
146
+ if (stripPipe)
147
+ out = out.replace(/\|/g, '');
148
+ return out;
149
+ }
150
+ /** Sanitize a target. Returns `null` when nothing usable survives. */
151
+ export function sanitizeWikiLinkTarget(raw) {
152
+ const out = sanitizePart(raw, true);
153
+ return out.trim().length > 0 ? out : null;
154
+ }
155
+ /** Sanitize an alias. Returns `null` when nothing usable survives. */
156
+ export function sanitizeWikiLinkAlias(raw) {
157
+ if (raw === null)
158
+ return null;
159
+ // A pipe is legal in an alias: the parser splits on the FIRST pipe only, so
160
+ // `[[a|b|c]]` unambiguously means alias `b|c` and re-exports byte-identically.
161
+ const out = sanitizePart(raw, false);
162
+ return out.trim().length > 0 ? out : null;
163
+ }
164
+ /**
165
+ * Serialize a wikilink back to markdown. Inverse of {@link parseWikiLinkInner}
166
+ * for every link built through this module's constructors — see
167
+ * {@link sanitizeWikiLinkTarget} for why that qualifier is load-bearing.
168
+ */
169
+ export function formatWikiLink(link) {
170
+ return link.alias === null ? `[[${link.target}]]` : `[[${link.target}|${link.alias}]]`;
171
+ }
172
+ /** The text a wikilink displays: the alias when present, else the target. */
173
+ function displayText(link) {
174
+ return link.alias ?? link.target;
175
+ }
176
+ /**
177
+ * What a screen reader announces. An aliased link must name BOTH halves: the
178
+ * visible text is the alias, so the destination is otherwise unhearable.
179
+ */
180
+ function ariaLabel(link) {
181
+ return link.alias === null
182
+ ? `${link.target}, wiki link`
183
+ : `${link.alias}, wiki link to ${link.target}`;
184
+ }
185
+ /**
186
+ * An atomic inline wikilink. Extends `TextNode` so the caret, selection and
187
+ * text formats behave exactly as they do for prose, while `token` mode keeps it
188
+ * indivisible: the user can delete it or move past it, but never edit its
189
+ * interior into a state where the visible alias disagrees with `__target`.
190
+ */
191
+ export class WikiLinkNode extends TextNode {
192
+ __target;
193
+ __alias;
194
+ static getType() {
195
+ return 'wikilink';
196
+ }
197
+ static clone(node) {
198
+ return new WikiLinkNode(node.__target, node.__alias, node.__text, node.__key);
199
+ }
200
+ constructor(target, alias, text, key) {
201
+ super(text ?? displayText({ target, alias }), key);
202
+ this.__target = target;
203
+ this.__alias = alias;
204
+ }
205
+ static importJSON(serializedNode) {
206
+ return $createWikiLinkNode(serializedNode.target, serializedNode.alias).updateFromJSON(serializedNode);
207
+ }
208
+ updateFromJSON(serializedNode) {
209
+ const self = super.updateFromJSON(serializedNode);
210
+ const writable = self.getWritable();
211
+ writable.__target = serializedNode.target;
212
+ writable.__alias = serializedNode.alias;
213
+ return writable;
214
+ }
215
+ exportJSON() {
216
+ return { ...super.exportJSON(), alias: this.getAlias(), target: this.getTarget() };
217
+ }
218
+ createDOM(config, editor) {
219
+ const dom = super.createDOM(config, editor);
220
+ // `data-scope` / `data-part` is this package's styling contract (see the
221
+ // other plugins); `data-wikilink` additionally carries the target so a host
222
+ // stylesheet can react to it and the click handler can find the element.
223
+ dom.setAttribute('data-scope', 'md-wikilink');
224
+ dom.setAttribute('data-part', 'link');
225
+ dom.setAttribute('data-wikilink', this.__target);
226
+ if (this.__alias !== null)
227
+ dom.setAttribute('data-wikilink-alias', this.__alias);
228
+ // Without a role, assistive tech reads a wikilink as ordinary prose, and an
229
+ // aliased one gives no way to hear where it points.
230
+ dom.setAttribute('role', 'link');
231
+ dom.setAttribute('aria-label', ariaLabel(this.getLink()));
232
+ return dom;
233
+ }
234
+ updateDOM(prevNode, dom, config) {
235
+ const recreated = super.updateDOM(prevNode, dom, config);
236
+ if (recreated)
237
+ return true;
238
+ if (prevNode.__target !== this.__target)
239
+ dom.setAttribute('data-wikilink', this.__target);
240
+ if (prevNode.__alias !== this.__alias) {
241
+ if (this.__alias === null)
242
+ dom.removeAttribute('data-wikilink-alias');
243
+ else
244
+ dom.setAttribute('data-wikilink-alias', this.__alias);
245
+ }
246
+ if (prevNode.__target !== this.__target || prevNode.__alias !== this.__alias) {
247
+ dom.setAttribute('aria-label', ariaLabel(this.getLink()));
248
+ }
249
+ return false;
250
+ }
251
+ getTarget() {
252
+ return this.getLatest().__target;
253
+ }
254
+ /**
255
+ * Repoint the link. The display text follows only when there is no alias.
256
+ * The value is sanitized (see {@link sanitizeWikiLinkTarget}); a target with
257
+ * nothing representable in it leaves the link untouched rather than producing
258
+ * one that corrupts the document on export.
259
+ */
260
+ setTarget(target) {
261
+ const clean = sanitizeWikiLinkTarget(target);
262
+ if (clean === null)
263
+ return this.getWritable();
264
+ const writable = this.getWritable();
265
+ writable.__target = clean;
266
+ if (writable.__alias === null)
267
+ writable.setTextContent(clean);
268
+ return writable;
269
+ }
270
+ getAlias() {
271
+ return this.getLatest().__alias;
272
+ }
273
+ /** Set (or clear, with `null`) the alias; the display text follows. */
274
+ setAlias(alias) {
275
+ const normalized = sanitizeWikiLinkAlias(alias);
276
+ const writable = this.getWritable();
277
+ writable.__alias = normalized;
278
+ writable.setTextContent(displayText({ target: writable.__target, alias: normalized }));
279
+ return writable;
280
+ }
281
+ getLink() {
282
+ const self = this.getLatest();
283
+ return { target: self.__target, alias: self.__alias };
284
+ }
285
+ // An atomic token: typing against either edge must produce a sibling text
286
+ // node, never extend the wikilink's text (which would desync it from target).
287
+ canInsertTextBefore() {
288
+ return false;
289
+ }
290
+ canInsertTextAfter() {
291
+ return false;
292
+ }
293
+ }
294
+ /**
295
+ * Build a wikilink node. `target`/`alias` are sanitized to values the `[[…]]`
296
+ * syntax can express (see {@link sanitizeWikiLinkTarget}); a target with nothing
297
+ * usable left falls back to the literal text `Page` rather than yielding an
298
+ * invisible token.
299
+ */
300
+ export function $createWikiLinkNode(target, alias = null) {
301
+ const cleanTarget = sanitizeWikiLinkTarget(target) ?? 'Page';
302
+ return $applyNodeReplacement(new WikiLinkNode(cleanTarget, sanitizeWikiLinkAlias(alias)).setMode('token'));
303
+ }
304
+ export function $isWikiLinkNode(node) {
305
+ return node instanceof WikiLinkNode;
306
+ }
307
+ // ── Transformer ──────────────────────────────────────────────────────────────
308
+ const WIKILINK_TRANSFORMER = {
309
+ dependencies: [WikiLinkNode],
310
+ // The THIRD parameter is `exportFormat`, not the `selection` that
311
+ // `ElementTransformer['export']` takes — see `TextMatchTransformer` in
312
+ // @lexical/markdown's MarkdownTransformers.ts, and the call site in
313
+ // MarkdownExport.ts `$exportChildren`, which supplies
314
+ // `(textNode, textContent) => exportTextFormat(...)`.
315
+ //
316
+ // Ignoring it exports a bold wikilink as a bare one, and — worse — TEARS any
317
+ // surrounding formatted run in two, because `$exportChildren` emits our
318
+ // unformatted string in the middle of it: `a **b [[P]] c** d` came back as
319
+ // `a **b** [[P]] **c** d`.
320
+ export: (node, _exportChildren, exportFormat) => {
321
+ if (!$isWikiLinkNode(node))
322
+ return null;
323
+ const markdown = formatWikiLink(node.getLink());
324
+ // Only route through `exportFormat` when there IS a format to apply.
325
+ // `exportTextFormat` backslash-escapes `*_\`~\\` in its input, which for an
326
+ // unformatted link would rewrite targets containing those characters; the
327
+ // raw string keeps the byte-exact round-trip the plugin guarantees.
328
+ return node.getFormat() === 0 ? markdown : exportFormat(node, markdown);
329
+ },
330
+ importRegExp: WIKILINK_IMPORT_RE,
331
+ regExp: WIKILINK_TYPING_RE,
332
+ trigger: ']',
333
+ replace: (textNode, match) => {
334
+ const link = parseWikiLinkInner(match[1] ?? '');
335
+ if (!link)
336
+ return;
337
+ // `LexicalNode.replace()` does NOT carry `__format`/`__style` onto the
338
+ // replacement (verified in lexical@0.48.0 LexicalNode.ts) and
339
+ // `$createWikiLinkNode` builds a node with format 0, so without this the
340
+ // node silently loses bold/italic/strikethrough/underline — falsifying this
341
+ // module's own stated reason for subclassing TextNode ("it must inherit the
342
+ // surrounding text format").
343
+ const replacement = $createWikiLinkNode(link.target, link.alias);
344
+ replacement.setFormat(textNode.getFormat());
345
+ replacement.setStyle(textNode.getStyle());
346
+ textNode.replace(replacement);
347
+ // Returning nothing (rather than the new node) stops `importTextTransformers`
348
+ // from recursing INTO the wikilink: an alias is literal text, so `[[a|**b**]]`
349
+ // must display `**b**`, not bold `b`.
350
+ },
351
+ type: 'text-match',
352
+ };
353
+ // A wikilink must beat upstream's LINK when both match at the SAME index, in
354
+ // EITHER plugin order. LINK's import pattern is `\[(.+?)\]\(…\)` with a LAZY
355
+ // label, so on a line like `**[[A]]** and [b](url)` it matches starting at the
356
+ // wikilink's own `[`, spanning all the way to the later `](`. Tied start index →
357
+ // resolved by array order → listing `wikilinkPlugin()` after `corePlugin()`
358
+ // handed the span to LINK, which swallowed the `**` delimiters and re-exported
359
+ // them ESCAPED: the bold was silently lost.
360
+ //
361
+ // Declaring the precedence makes the wikilink reading structural rather than a
362
+ // documented ordering requirement a consumer can get wrong. The CommonMark
363
+ // reading of `[[a|b]](c)` is still reachable — omit `wikilinkPlugin()`.
364
+ setTransformerPrecedence(WIKILINK_TRANSFORMER, -10);
365
+ /** Resolve the wikilink a click landed on, if any. Must run in an editor scope. */
366
+ function $wikiLinkFromEvent(event) {
367
+ const target = event.target;
368
+ if (!(target instanceof Node))
369
+ return null;
370
+ const direct = $getNearestNodeFromDOMNode(target);
371
+ if ($isWikiLinkNode(direct))
372
+ return direct;
373
+ // A click can land on the inner text node of a formatted wikilink; climb to
374
+ // the nearest element carrying our marker attribute and map that back.
375
+ const element = target instanceof Element ? target : target.parentElement;
376
+ const marked = element?.closest('[data-wikilink]');
377
+ if (!marked)
378
+ return null;
379
+ const node = $getNearestNodeFromDOMNode(marked);
380
+ return $isWikiLinkNode(node) ? node : null;
381
+ }
382
+ /**
383
+ * The wikilink the caret is on or immediately beside, if any. Must run in an
384
+ * editor scope. A token-mode node is selected as a whole, so the anchor lands
385
+ * either ON it or on an adjacent sibling.
386
+ */
387
+ function $selectedWikiLink() {
388
+ const selection = $getSelection();
389
+ if (!$isRangeSelection(selection))
390
+ return null;
391
+ const anchor = selection.anchor.getNode();
392
+ if ($isWikiLinkNode(anchor))
393
+ return anchor;
394
+ for (const candidate of [anchor.getPreviousSibling(), anchor.getNextSibling()]) {
395
+ if ($isWikiLinkNode(candidate))
396
+ return candidate;
397
+ }
398
+ return null;
399
+ }
400
+ export function wikilinkPlugin(opts = {}) {
401
+ const placeholderTarget = opts.placeholderTarget ?? 'Page';
402
+ const item = {
403
+ id: PLUGIN,
404
+ label: 'Wiki link',
405
+ icon: 'wikilink',
406
+ group: 'inline',
407
+ keywords: ['wiki', 'wikilink', 'backlink', 'reference', '[['],
408
+ run: (editor) => editor.update(() => {
409
+ const selection = $getSelection();
410
+ if (!$isRangeSelection(selection))
411
+ return;
412
+ // A range selection's text can span blocks (newlines) and can contain
413
+ // `|` or `]]` — none of which a `[[…]]` can carry. Sanitizing here, at
414
+ // the boundary where user input enters the document, is what stops the
415
+ // command from minting a link that destroys itself on the next reload.
416
+ const selected = sanitizeWikiLinkTarget(selection.getTextContent());
417
+ // Selected prose becomes the alias of a same-named target, so the
418
+ // sentence reads unchanged and the author only has to fix the target.
419
+ const node = $createWikiLinkNode(selected ?? placeholderTarget, null);
420
+ selection.insertNodes([node]);
421
+ node.selectNext(0, 0);
422
+ }),
423
+ surfaces: ['toolbar', 'floating', 'slash', 'context'],
424
+ };
425
+ return {
426
+ name: PLUGIN,
427
+ nodes: [WikiLinkNode],
428
+ transformers: [WIKILINK_TRANSFORMER],
429
+ items: [item],
430
+ register: (editor, ctx) => {
431
+ const activate = (node) => {
432
+ const { target, alias } = node.getLink();
433
+ ctx.emit({ type: 'plugin', name: PLUGIN, msg: { type: 'activate', target, alias } });
434
+ return true;
435
+ };
436
+ return mergeRegister(editor.registerCommand(CLICK_COMMAND, (event) => {
437
+ const node = $wikiLinkFromEvent(event);
438
+ if (!node)
439
+ return false;
440
+ event.preventDefault();
441
+ return activate(node);
442
+ }, COMMAND_PRIORITY_LOW),
443
+ // Keyboard parity. A wikilink is token-mode text, so the caret can rest
444
+ // on it, but CLICK_COMMAND is unreachable without a pointer — activation
445
+ // was mouse-only. Mod+Enter (rather than bare Enter) keeps the ordinary
446
+ // paragraph-splitting Enter intact while the caret sits beside a link.
447
+ editor.registerCommand(KEY_ENTER_COMMAND, (event) => {
448
+ if (!event || !(event.metaKey || event.ctrlKey))
449
+ return false;
450
+ const node = $selectedWikiLink();
451
+ if (!node)
452
+ return false;
453
+ event.preventDefault();
454
+ return activate(node);
455
+ }, COMMAND_PRIORITY_LOW));
456
+ },
457
+ ui: definePluginUI({
458
+ init: () => ({ last: null }),
459
+ update: (_state, msg) => {
460
+ const link = { target: msg.target, alias: msg.alias };
461
+ return [{ last: link }, [{ type: 'navigate', link }]];
462
+ },
463
+ onEffect: (effect) => opts.onNavigate?.(effect.link),
464
+ }),
465
+ };
466
+ }
467
+ //# sourceMappingURL=wikilink.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wikilink.js","sourceRoot":"","sources":["../../src/plugins/wikilink.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,63 @@
1
+ /* Block drag-and-drop reordering (`blockDragPlugin`).
2
+ *
3
+ * Geometry is set inline by the plugin (measured, therefore dynamic); everything
4
+ * here is appearance only. Import alongside `editor.css`:
5
+ * import '@llui/markdown-editor/styles/block-drag.css'
6
+ */
7
+
8
+ [data-scope='md-block-drag'][data-part='handle'] {
9
+ display: flex;
10
+ align-items: center;
11
+ }
12
+
13
+ [data-scope='md-block-drag'][data-part='grip'] {
14
+ display: flex;
15
+ align-items: center;
16
+ justify-content: center;
17
+ width: 22px;
18
+ height: 22px;
19
+ padding: 0;
20
+ border: 0;
21
+ border-radius: 4px;
22
+ background: transparent;
23
+ color: var(--md-muted, #9aa0a6);
24
+ font-size: 14px;
25
+ line-height: 1;
26
+ cursor: grab;
27
+ opacity: 0.7;
28
+ }
29
+
30
+ [data-scope='md-block-drag'][data-part='grip']:hover {
31
+ background: var(--md-hover, rgba(127, 127, 127, 0.14));
32
+ opacity: 1;
33
+ }
34
+
35
+ /* Keyboard users must be able to see the grip they have focused. */
36
+ [data-scope='md-block-drag'][data-part='grip']:focus-visible {
37
+ outline: 2px solid var(--md-accent, #3b82f6);
38
+ outline-offset: 1px;
39
+ opacity: 1;
40
+ }
41
+
42
+ /* Grab mode is a live, invisible-by-default mode — give it a strong affordance. */
43
+ [data-scope='md-block-drag'][data-part='grip'][data-grabbed] {
44
+ background: var(--md-accent, #3b82f6);
45
+ color: #fff;
46
+ cursor: grabbing;
47
+ opacity: 1;
48
+ }
49
+
50
+ [data-scope='md-block-drag'][data-part='indicator'] {
51
+ height: 2px;
52
+ border-radius: 1px;
53
+ background: var(--md-accent, #3b82f6);
54
+ pointer-events: none;
55
+ }
56
+
57
+ @media (prefers-reduced-motion: no-preference) {
58
+ [data-scope='md-block-drag'][data-part='grip'] {
59
+ transition:
60
+ opacity 120ms ease,
61
+ background-color 120ms ease;
62
+ }
63
+ }