@llui/markdown-editor 0.3.0 → 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.
- package/dist/editor.d.ts +15 -2
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +12 -2
- package/dist/editor.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -1
- package/dist/index.js.map +1 -1
- package/dist/plugins/block-drag.d.ts +73 -0
- package/dist/plugins/block-drag.d.ts.map +1 -0
- package/dist/plugins/block-drag.js +760 -0
- package/dist/plugins/block-drag.js.map +1 -0
- package/dist/plugins/code-language.d.ts +60 -0
- package/dist/plugins/code-language.d.ts.map +1 -0
- package/dist/plugins/code-language.js +308 -0
- package/dist/plugins/code-language.js.map +1 -0
- package/dist/plugins/frontmatter.d.ts +45 -0
- package/dist/plugins/frontmatter.d.ts.map +1 -0
- package/dist/plugins/frontmatter.js +281 -0
- package/dist/plugins/frontmatter.js.map +1 -0
- package/dist/plugins/overlay.d.ts +1 -0
- package/dist/plugins/overlay.d.ts.map +1 -1
- package/dist/plugins/overlay.js +7 -0
- package/dist/plugins/overlay.js.map +1 -1
- package/dist/plugins/wikilink.d.ts +91 -0
- package/dist/plugins/wikilink.d.ts.map +1 -0
- package/dist/plugins/wikilink.js +467 -0
- package/dist/plugins/wikilink.js.map +1 -0
- package/dist/styles/block-drag.css +63 -0
- package/dist/styles/editor.css +64 -0
- package/dist/transformers/code.d.ts +24 -0
- package/dist/transformers/code.d.ts.map +1 -0
- package/dist/transformers/code.js +167 -0
- package/dist/transformers/code.js.map +1 -0
- package/dist/transformers/gfm.d.ts.map +1 -1
- package/dist/transformers/gfm.js +7 -2
- package/dist/transformers/gfm.js.map +1 -1
- package/dist/transformers/registry.d.ts +3 -0
- package/dist/transformers/registry.d.ts.map +1 -1
- package/dist/transformers/registry.js +26 -0
- package/dist/transformers/registry.js.map +1 -1
- package/package.json +40 -26
- package/src/editor.ts +27 -4
- package/src/index.ts +58 -1
- package/src/plugins/block-drag.ts +912 -0
- package/src/plugins/code-language.ts +378 -0
- package/src/plugins/frontmatter.ts +324 -0
- package/src/plugins/overlay.ts +7 -0
- package/src/plugins/wikilink.ts +553 -0
- package/src/styles/block-drag.css +63 -0
- package/src/styles/editor.css +64 -0
- package/src/transformers/code.ts +174 -0
- package/src/transformers/gfm.ts +6 -2
- package/src/transformers/registry.ts +27 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
// YAML frontmatter plugin — the leading `---` … `---` metadata block.
|
|
2
|
+
//
|
|
3
|
+
// ## Why the block is an OPAQUE STRING, not parsed YAML
|
|
4
|
+
//
|
|
5
|
+
// Deliberate choice, and the requirement that drove it is round-trip fidelity:
|
|
6
|
+
// what the author wrote must come back out byte-for-byte. Two options exist —
|
|
7
|
+
// parse YAML properly, or don't parse it at all — and only the second is
|
|
8
|
+
// achievable here:
|
|
9
|
+
//
|
|
10
|
+
// - Parsing correctly means a real YAML 1.2 implementation (anchors, aliases,
|
|
11
|
+
// tags, flow/block collections, the five scalar styles, folded/literal block
|
|
12
|
+
// scalars with their indentation-indicator and chomping rules). Nothing
|
|
13
|
+
// smaller is *correct*, and a correct parser is not enough by itself: a
|
|
14
|
+
// round-trip also needs a comment- and style-preserving EMITTER, because
|
|
15
|
+
// `parse` → `stringify` normalizes quoting, key order, indentation and drops
|
|
16
|
+
// comments outright. That is a large dependency (this package deliberately
|
|
17
|
+
// dropped Prism from `@lexical/code` for exactly this reason — see
|
|
18
|
+
// `code-language.ts`) to buy a feature the editor does not need: it never
|
|
19
|
+
// interprets frontmatter, it only has to preserve it.
|
|
20
|
+
//
|
|
21
|
+
// - The naive middle ground — split each line on the first `:` into a
|
|
22
|
+
// `Record<string, string>` — is what the reference implementation this was
|
|
23
|
+
// modelled on does, and it is strictly WORSE than not parsing. Nested maps,
|
|
24
|
+
// sequences, block scalars, quoted values containing `:`, comments and blank
|
|
25
|
+
// lines are all silently flattened or destroyed. A lossy parse presented as
|
|
26
|
+
// structured data is a data-loss bug wearing a feature's clothes.
|
|
27
|
+
//
|
|
28
|
+
// So the block is stored verbatim as a single `source` string and re-emitted
|
|
29
|
+
// between two fences. Consumers that *want* structure own that choice: hand
|
|
30
|
+
// `$getFrontmatter()` to whatever YAML library they already ship, or supply
|
|
31
|
+
// `render` to draw a preview. The editor stays honest about not understanding it.
|
|
32
|
+
//
|
|
33
|
+
// The one unavoidable deviation from byte-for-byte: `$convertFromMarkdownString`
|
|
34
|
+
// runs `normalizeMarkdown`, which `trimEnd()`s every line BEFORE any transformer
|
|
35
|
+
// is consulted, so trailing whitespace inside the block is lost. That happens
|
|
36
|
+
// upstream of every transformer and cannot be intercepted from one. YAML treats
|
|
37
|
+
// trailing spaces as insignificant, so nothing meaningful changes. Likewise a
|
|
38
|
+
// body that is empty or only blank lines normalizes to the canonical `---\n---`.
|
|
39
|
+
//
|
|
40
|
+
// ## The `---` collision with `hrPlugin`
|
|
41
|
+
//
|
|
42
|
+
// `hrPlugin`'s ELEMENT transformer matches `/^(---|\*\*\*|___)\s*$/`, so before
|
|
43
|
+
// this plugin existed a leading frontmatter block parsed as a horizontal rule
|
|
44
|
+
// followed by loose prose. Telling consumers to drop `hrPlugin` is not a fix:
|
|
45
|
+
// `---` is legitimately both things, and which one it is depends on POSITION and
|
|
46
|
+
// CLOSURE, not on which plugin is loaded.
|
|
47
|
+
//
|
|
48
|
+
// The resolution is structural, not ordering-based. `@lexical/markdown`'s
|
|
49
|
+
// importer tries EVERY multiline-element transformer on a line before ANY
|
|
50
|
+
// element transformer (`$importMultiline` runs first in `$importMarkdownNodes`;
|
|
51
|
+
// export mirrors it with `[...multilineElement, ...element]`). This is therefore
|
|
52
|
+
// a `multiline-element` transformer, which means it is consulted ahead of the
|
|
53
|
+
// `hr` element transformer no matter where the two plugins sit in the array —
|
|
54
|
+
// plugin ORDER cannot change the outcome, and neither can loading or omitting
|
|
55
|
+
// `hrPlugin`.
|
|
56
|
+
//
|
|
57
|
+
// Claiming the line is then narrowed by `handleImportAfterStartMatch`, which
|
|
58
|
+
// returns `null` — "not mine, try the next transformer" — unless BOTH hold:
|
|
59
|
+
//
|
|
60
|
+
// 1. the fence is on line 0 (frontmatter is only frontmatter at the very top
|
|
61
|
+
// of the document), and
|
|
62
|
+
// 2. a closing `---` line exists somewhere below it.
|
|
63
|
+
//
|
|
64
|
+
// Declining falls through to `hrPlugin` (when loaded), so a `---` that is
|
|
65
|
+
// genuinely a thematic break — mid-document, or an unclosed one on line 1 —
|
|
66
|
+
// still becomes an `<hr>`. `replace` returns `false` so that TYPING `---` in the
|
|
67
|
+
// editor is likewise left to the hr shortcut; frontmatter is only ever created
|
|
68
|
+
// by importing markdown or by calling `$setFrontmatter`.
|
|
69
|
+
//
|
|
70
|
+
// The block closes at the FIRST subsequent line that is exactly `---`, which is
|
|
71
|
+
// the rule Jekyll / gray-matter / Hugo apply. A `---` *inside* a value (e.g.
|
|
72
|
+
// `title: a --- b`) is not a whole line and is preserved untouched.
|
|
73
|
+
|
|
74
|
+
import { $getRoot, type LexicalEditor, type LexicalNode } from 'lexical'
|
|
75
|
+
import type { MultilineElementTransformer } from '@lexical/markdown'
|
|
76
|
+
import {
|
|
77
|
+
$createLLuiDecoratorNode,
|
|
78
|
+
$isLLuiDecoratorNode,
|
|
79
|
+
LLuiDecoratorNode,
|
|
80
|
+
decoratorBridge,
|
|
81
|
+
} from '@llui/lexical'
|
|
82
|
+
import { div, textarea, type Mountable, type Signal } from '@llui/dom'
|
|
83
|
+
import type { MarkdownPlugin } from './types.js'
|
|
84
|
+
import { renderedPreview, type PreviewRender } from './_preview.js'
|
|
85
|
+
|
|
86
|
+
/** The decorator bridge id for the frontmatter block. */
|
|
87
|
+
export const FRONTMATTER_BRIDGE_TYPE = 'frontmatter'
|
|
88
|
+
|
|
89
|
+
/** The frontmatter node's payload: the block body, verbatim, with no fences. */
|
|
90
|
+
export interface FrontmatterData {
|
|
91
|
+
/** The raw text between the opening and closing `---`. Never interpreted. */
|
|
92
|
+
source: string
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isFrontmatterData(value: unknown): value is FrontmatterData {
|
|
96
|
+
return (
|
|
97
|
+
typeof value === 'object' &&
|
|
98
|
+
value !== null &&
|
|
99
|
+
typeof (value as FrontmatterData).source === 'string'
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** A line consisting solely of `---` (trailing tabs/spaces tolerated — they are
|
|
104
|
+
* not content, and `normalizeMarkdown` has usually already trimmed them). */
|
|
105
|
+
const FENCE = /^---[ \t]*$/
|
|
106
|
+
|
|
107
|
+
/** Render the fences back around an opaque body. An empty (or blank-only) body
|
|
108
|
+
* collapses to the canonical two-line form so the result stays idempotent. */
|
|
109
|
+
export function serializeFrontmatter(source: string): string {
|
|
110
|
+
return source === '' ? '---\n---' : `---\n${source}\n---`
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The index of the closing fence, or `-1` when `lines` does not open a
|
|
115
|
+
* frontmatter block (no fence on line 0) or never closes one.
|
|
116
|
+
*
|
|
117
|
+
* The single definition of "is this frontmatter?", shared by the importer and
|
|
118
|
+
* {@link splitFrontmatter} so the two can never disagree. A returned index is
|
|
119
|
+
* always `>= 1`, which is the property the importer's anti-rewind invariant
|
|
120
|
+
* rests on (see `handleImportAfterStartMatch`).
|
|
121
|
+
*/
|
|
122
|
+
function closingFenceIndex(lines: readonly string[]): number {
|
|
123
|
+
const first = lines[0]
|
|
124
|
+
if (first === undefined || !FENCE.test(first)) return -1
|
|
125
|
+
for (let i = 1; i < lines.length; i++) {
|
|
126
|
+
const line = lines[i]
|
|
127
|
+
if (line !== undefined && FENCE.test(line)) return i
|
|
128
|
+
}
|
|
129
|
+
return -1
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Split a leading frontmatter block off a markdown string: `[body, rest]`, or
|
|
134
|
+
* `null` when the document has none (no line-0 fence, or no closing fence).
|
|
135
|
+
* Exported because a consumer often needs the metadata BEFORE building an
|
|
136
|
+
* editor — the same predicate the importer uses, so the two never disagree.
|
|
137
|
+
*/
|
|
138
|
+
export function splitFrontmatter(markdown: string): [source: string, rest: string] | null {
|
|
139
|
+
const lines = markdown.split('\n')
|
|
140
|
+
const end = closingFenceIndex(lines)
|
|
141
|
+
if (end === -1) return null
|
|
142
|
+
return [lines.slice(1, end).join('\n'), lines.slice(end + 1).join('\n')]
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── Document accessors (run inside `editor.read` / `editor.update`) ──────────
|
|
146
|
+
|
|
147
|
+
/** The frontmatter node, if the document starts with one. */
|
|
148
|
+
function $frontmatterNode(): LexicalNode | null {
|
|
149
|
+
const first = $getRoot().getFirstChild()
|
|
150
|
+
return $isLLuiDecoratorNode(first) && first.getBridgeType() === FRONTMATTER_BRIDGE_TYPE
|
|
151
|
+
? first
|
|
152
|
+
: null
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The document's frontmatter body, or `null` when it has none. */
|
|
156
|
+
export function $getFrontmatter(): string | null {
|
|
157
|
+
const node = $frontmatterNode()
|
|
158
|
+
if (!node || !$isLLuiDecoratorNode(node)) return null
|
|
159
|
+
const data = node.getData()
|
|
160
|
+
return isFrontmatterData(data) ? data.source : null
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Set (or, with `null`, remove) the document's frontmatter. The block is always
|
|
165
|
+
* kept as the FIRST child of the root — it is only frontmatter there, and the
|
|
166
|
+
* exporter relies on that position (see the transformer's `export`).
|
|
167
|
+
*/
|
|
168
|
+
export function $setFrontmatter(source: string | null): void {
|
|
169
|
+
const existing = $frontmatterNode()
|
|
170
|
+
if (source === null) {
|
|
171
|
+
existing?.remove()
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
if (existing && $isLLuiDecoratorNode(existing)) {
|
|
175
|
+
existing.setData({ source })
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
const node = $createLLuiDecoratorNode(FRONTMATTER_BRIDGE_TYPE, { source })
|
|
179
|
+
const root = $getRoot()
|
|
180
|
+
const first = root.getFirstChild()
|
|
181
|
+
if (first) first.insertBefore(node)
|
|
182
|
+
else root.append(node)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── Transformer ─────────────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
const FRONTMATTER_TRANSFORMER: MultilineElementTransformer = {
|
|
188
|
+
dependencies: [LLuiDecoratorNode],
|
|
189
|
+
export: (node: LexicalNode): string | null => {
|
|
190
|
+
if (!$isLLuiDecoratorNode(node) || node.getBridgeType() !== FRONTMATTER_BRIDGE_TYPE) return null
|
|
191
|
+
const data = node.getData()
|
|
192
|
+
if (!isFrontmatterData(data)) return null
|
|
193
|
+
// Only the document's FIRST block can be emitted as fences: `---` anywhere
|
|
194
|
+
// else re-imports as a thematic break, which would silently destroy the
|
|
195
|
+
// metadata on the next round-trip. A stray block (dragged out of position,
|
|
196
|
+
// pasted into a quote, …) degrades to a visible ```yaml code fence instead —
|
|
197
|
+
// lossy in node type, but the text survives and the author can see why.
|
|
198
|
+
if ($getRoot().getFirstChild() !== node) return '```yaml\n' + data.source + '\n```'
|
|
199
|
+
return serializeFrontmatter(data.source)
|
|
200
|
+
},
|
|
201
|
+
regExpStart: FENCE,
|
|
202
|
+
// Never consulted: `handleImportAfterStartMatch` below always returns a
|
|
203
|
+
// decision, so the default open/close scan is not reached. Declared because
|
|
204
|
+
// the shape requires the pair to be meaningful together.
|
|
205
|
+
regExpEnd: FENCE,
|
|
206
|
+
handleImportAfterStartMatch: ({ lines, rootNode, startLineIndex }) => {
|
|
207
|
+
// Frontmatter exists only at the very top of the document. Anywhere else a
|
|
208
|
+
// `---` is a thematic break — decline so `hrPlugin` (or plain prose) gets it.
|
|
209
|
+
//
|
|
210
|
+
// INVARIANT — do not relax this guard without also fixing the scan below.
|
|
211
|
+
// `$importMarkdownNodes` assigns `i = shiftedIndex` from the returned tuple,
|
|
212
|
+
// so a returned index <= `startLineIndex` REWINDS its loop and the importer
|
|
213
|
+
// spins forever (not a wrong parse — a hang). The scan starts at line 1, so
|
|
214
|
+
// pinning `startLineIndex` to 0 is what makes every returned index strictly
|
|
215
|
+
// greater. A positional relaxation must start the scan at `startLineIndex + 1`.
|
|
216
|
+
if (startLineIndex !== 0) return null
|
|
217
|
+
const end = closingFenceIndex(lines)
|
|
218
|
+
if (end !== -1) {
|
|
219
|
+
// Verbatim body: no trimming, no interpretation, blank lines preserved.
|
|
220
|
+
const source = lines.slice(1, end).join('\n')
|
|
221
|
+
rootNode.append($createLLuiDecoratorNode(FRONTMATTER_BRIDGE_TYPE, { source }))
|
|
222
|
+
return [true, end]
|
|
223
|
+
}
|
|
224
|
+
// Unclosed: this is a lone `---` on line 1, i.e. a horizontal rule.
|
|
225
|
+
return null
|
|
226
|
+
},
|
|
227
|
+
// Typing `---` must never produce frontmatter — returning false hands the
|
|
228
|
+
// markdown shortcut back to `hrPlugin`. Use `$setFrontmatter` to create one.
|
|
229
|
+
replace: () => false,
|
|
230
|
+
type: 'multiline-element',
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export { FRONTMATTER_TRANSFORMER }
|
|
234
|
+
|
|
235
|
+
// ── Plugin ──────────────────────────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
export interface FrontmatterPluginOptions {
|
|
238
|
+
/** Render the raw block to a preview (e.g. parse with your own YAML library
|
|
239
|
+
* and draw a table). Return a DOM `Node` (mounted directly) or a **trusted**
|
|
240
|
+
* HTML string — see `renderedPreview`'s security note. */
|
|
241
|
+
render?: PreviewRender
|
|
242
|
+
/** Accessible label for the raw-source editor (default `'Frontmatter'`). */
|
|
243
|
+
label?: string
|
|
244
|
+
/** Placeholder shown for an empty block (default `'key: value'`). */
|
|
245
|
+
placeholder?: string
|
|
246
|
+
/** Show the raw source editor. Set false for a `render`-only presentation
|
|
247
|
+
* (the block still round-trips; it just isn't editable in place). Default true. */
|
|
248
|
+
editable?: boolean
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Keep editor-level key/paste handling out of the frontmatter island. */
|
|
252
|
+
const stop = (e: Event): void => e.stopPropagation()
|
|
253
|
+
|
|
254
|
+
export function frontmatterPlugin(opts: FrontmatterPluginOptions = {}): MarkdownPlugin {
|
|
255
|
+
const editable = opts.editable ?? true
|
|
256
|
+
|
|
257
|
+
const bridge = decoratorBridge<FrontmatterData>(FRONTMATTER_BRIDGE_TYPE, (data, api) => {
|
|
258
|
+
const children: Mountable[] = []
|
|
259
|
+
if (editable) {
|
|
260
|
+
children.push(
|
|
261
|
+
// A <textarea>, NOT a contenteditable div: newlines are load-bearing in
|
|
262
|
+
// YAML, and a contenteditable's `textContent` drops the <br>/<div> line
|
|
263
|
+
// structure the browser inserts, so committing from one silently
|
|
264
|
+
// flattens a multi-line block into a single line.
|
|
265
|
+
textarea({
|
|
266
|
+
'data-scope': 'md-frontmatter',
|
|
267
|
+
'data-part': 'source',
|
|
268
|
+
'aria-label': opts.label ?? 'Frontmatter',
|
|
269
|
+
placeholder: opts.placeholder ?? 'key: value',
|
|
270
|
+
spellcheck: 'false',
|
|
271
|
+
autocapitalize: 'off',
|
|
272
|
+
autocomplete: 'off',
|
|
273
|
+
value: data.at('source') as Signal<string>,
|
|
274
|
+
// Grow with the content so the whole block stays visible.
|
|
275
|
+
rows: (data.at('source') as Signal<string>).map((source) =>
|
|
276
|
+
String(Math.max(1, source.split('\n').length)),
|
|
277
|
+
),
|
|
278
|
+
onKeyDown: stop,
|
|
279
|
+
onBeforeInput: stop,
|
|
280
|
+
onPaste: stop,
|
|
281
|
+
// Commit on blur only: while the caret is inside, the node data is
|
|
282
|
+
// deliberately stale so the reactive `value` binding cannot rewrite
|
|
283
|
+
// what is being typed.
|
|
284
|
+
onBlur: (e: FocusEvent) => {
|
|
285
|
+
const source = (e.target as HTMLTextAreaElement).value
|
|
286
|
+
if (source !== data.peek().source) api.update({ source })
|
|
287
|
+
},
|
|
288
|
+
}),
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
if (opts.render) {
|
|
292
|
+
children.push(renderedPreview(data.at('source') as Signal<string>, opts.render))
|
|
293
|
+
}
|
|
294
|
+
return [
|
|
295
|
+
div(
|
|
296
|
+
{ 'data-scope': 'md-frontmatter', 'data-part': 'root', contenteditable: 'false' },
|
|
297
|
+
children,
|
|
298
|
+
),
|
|
299
|
+
]
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
name: 'frontmatter',
|
|
304
|
+
nodes: [LLuiDecoratorNode],
|
|
305
|
+
decorators: [bridge],
|
|
306
|
+
transformers: [FRONTMATTER_TRANSFORMER],
|
|
307
|
+
items: [
|
|
308
|
+
{
|
|
309
|
+
id: 'frontmatter',
|
|
310
|
+
label: 'Frontmatter',
|
|
311
|
+
icon: 'frontmatter',
|
|
312
|
+
group: 'insert',
|
|
313
|
+
keywords: ['frontmatter', 'yaml', 'metadata', 'front matter'],
|
|
314
|
+
// Idempotent: a document already carrying frontmatter is left alone
|
|
315
|
+
// rather than having its metadata blanked.
|
|
316
|
+
run: (editor: LexicalEditor) =>
|
|
317
|
+
editor.update(() => {
|
|
318
|
+
if ($getFrontmatter() === null) $setFrontmatter('')
|
|
319
|
+
}),
|
|
320
|
+
surfaces: ['slash', 'context'],
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
}
|
|
324
|
+
}
|
package/src/plugins/overlay.ts
CHANGED
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
// 61 — context menu — pointer-anchored
|
|
19
19
|
// 62 — floating selection toolbar — selection-anchored bubble
|
|
20
20
|
// 63 — table tools — element-anchored, sits over a table
|
|
21
|
+
// 64 — code-block language badge — element-anchored, may sit INSIDE a
|
|
22
|
+
// table cell, so it must clear 63
|
|
21
23
|
// The values live with each plugin's view; this comment is the source of truth.
|
|
22
24
|
|
|
23
25
|
import {
|
|
@@ -37,6 +39,11 @@ export const OVERLAY_Z = {
|
|
|
37
39
|
contextMenu: 61,
|
|
38
40
|
floatingToolbar: 62,
|
|
39
41
|
tableTools: 63,
|
|
42
|
+
// Strictly above `tableTools`: a fenced code block can live inside a table
|
|
43
|
+
// cell, in which case both surfaces are open at once. At an equal z-index the
|
|
44
|
+
// clickable one was decided by portal insertion order — i.e. by plugin array
|
|
45
|
+
// order — which is exactly the no-tie rule this scale exists to prevent.
|
|
46
|
+
codeLanguage: 64,
|
|
40
47
|
} as const
|
|
41
48
|
|
|
42
49
|
const toMountables = (r: Renderable): Mountable[] => [...r]
|