@llui/markdown-editor 0.2.14 → 0.3.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 +7 -0
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +33 -4
- package/dist/editor.js.map +1 -1
- package/dist/effects.d.ts +0 -1
- package/dist/effects.d.ts.map +1 -1
- package/dist/effects.js +4 -1
- package/dist/effects.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/plugins/table.d.ts.map +1 -1
- package/dist/plugins/table.js +77 -15
- package/dist/plugins/table.js.map +1 -1
- package/dist/styles/editor.css +0 -6
- package/dist/theme.d.ts +2 -5
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +15 -11
- package/dist/theme.js.map +1 -1
- package/dist/transformers/gfm.d.ts +9 -1
- package/dist/transformers/gfm.d.ts.map +1 -1
- package/dist/transformers/gfm.js +9 -2
- package/dist/transformers/gfm.js.map +1 -1
- package/package.json +9 -9
- package/src/editor.ts +50 -5
- package/src/effects.ts +4 -2
- package/src/index.ts +3 -8
- package/src/plugins/table.ts +87 -13
- package/src/styles/editor.css +0 -6
- package/src/theme.ts +15 -11
- package/src/transformers/gfm.ts +10 -2
- package/dist/__llui_deps.json +0 -274
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llui/markdown-editor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -33,10 +33,10 @@
|
|
|
33
33
|
"@lexical/selection": "^0.46.0",
|
|
34
34
|
"@lexical/utils": "^0.46.0",
|
|
35
35
|
"@lexical/code-core": "^0.46.0",
|
|
36
|
-
"@llui/dom": "^0.
|
|
37
|
-
"@llui/
|
|
38
|
-
"@llui/
|
|
39
|
-
"@llui/components": "^0.
|
|
36
|
+
"@llui/dom": "^0.12.0",
|
|
37
|
+
"@llui/markdown": "^0.12.0",
|
|
38
|
+
"@llui/lexical": "^0.3.0",
|
|
39
|
+
"@llui/components": "^0.13.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"lexical": "^0.46.0",
|
|
@@ -51,10 +51,10 @@
|
|
|
51
51
|
"typescript": "^6.0.0",
|
|
52
52
|
"vitest": "^4.1.2",
|
|
53
53
|
"@lexical/code-core": "^0.46.0",
|
|
54
|
-
"@llui/dom": "0.
|
|
55
|
-
"@llui/markdown": "0.
|
|
56
|
-
"@llui/components": "0.
|
|
57
|
-
"@llui/lexical": "0.
|
|
54
|
+
"@llui/dom": "0.12.0",
|
|
55
|
+
"@llui/markdown": "0.12.0",
|
|
56
|
+
"@llui/components": "0.13.0",
|
|
57
|
+
"@llui/lexical": "0.3.0"
|
|
58
58
|
},
|
|
59
59
|
"sideEffects": [
|
|
60
60
|
"./dist/styles/editor.css"
|
package/src/editor.ts
CHANGED
|
@@ -4,7 +4,14 @@
|
|
|
4
4
|
// live editor through effects, and COMPOSES plugin UI extensions (each plugin's
|
|
5
5
|
// state slice + reducer + view + effects) into the single component.
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
$getRoot,
|
|
9
|
+
$setSelection,
|
|
10
|
+
COMMAND_PRIORITY_CRITICAL,
|
|
11
|
+
FORMAT_TEXT_COMMAND,
|
|
12
|
+
type EditorThemeClasses,
|
|
13
|
+
type LexicalEditor,
|
|
14
|
+
} from 'lexical'
|
|
8
15
|
import {
|
|
9
16
|
$convertFromMarkdownString,
|
|
10
17
|
$convertToMarkdownString,
|
|
@@ -109,13 +116,31 @@ function defaultPlugins(): MarkdownPlugin[] {
|
|
|
109
116
|
return [corePlugin(), linkPlugin()]
|
|
110
117
|
}
|
|
111
118
|
|
|
119
|
+
/** Swallow the underline text-format command. `registerRichText` wires Cmd+U to
|
|
120
|
+
* FORMAT_TEXT 'underline', but the GFM markdown dialect this editor serializes
|
|
121
|
+
* has no underline representation, so an applied underline would be silently
|
|
122
|
+
* stripped on save. Intercepting at CRITICAL priority (ahead of rich-text) keeps
|
|
123
|
+
* the WYSIWYG surface and the serialized dialect in lock-step: underline can be
|
|
124
|
+
* neither applied nor lost. Returns a disposer. */
|
|
125
|
+
export function blockUnderlineFormat(editor: LexicalEditor): () => void {
|
|
126
|
+
return editor.registerCommand(
|
|
127
|
+
FORMAT_TEXT_COMMAND,
|
|
128
|
+
(payload) => payload === 'underline',
|
|
129
|
+
COMMAND_PRIORITY_CRITICAL,
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
112
133
|
/** The per-mount `send` closure — the WeakMap key that ties a live editor to the
|
|
113
134
|
* one mount it belongs to. */
|
|
114
135
|
type EditorSend = (msg: EditorMsg) => void
|
|
115
136
|
|
|
116
|
-
/** Per-mount live state: the Lexical editor captured by that mount's `onReady
|
|
137
|
+
/** Per-mount live state: the Lexical editor captured by that mount's `onReady`,
|
|
138
|
+
* plus the last markdown delivered to the consumer's `onChange` (dedup key). */
|
|
117
139
|
interface MountContext {
|
|
118
140
|
editor: LexicalEditor | null
|
|
141
|
+
/** Last value handed to `config.onChange` for this mount — guards against
|
|
142
|
+
* double / redundant consumer notifications. `undefined` = nothing emitted yet. */
|
|
143
|
+
lastChange: string | undefined
|
|
119
144
|
}
|
|
120
145
|
|
|
121
146
|
/**
|
|
@@ -148,14 +173,17 @@ export function markdownEditor(
|
|
|
148
173
|
const contextFor = (send: EditorSend): MountContext => {
|
|
149
174
|
let ctx = mountContexts.get(send)
|
|
150
175
|
if (!ctx) {
|
|
151
|
-
ctx = { editor: null }
|
|
176
|
+
ctx = { editor: null, lastChange: undefined }
|
|
152
177
|
mountContexts.set(send, ctx)
|
|
153
178
|
}
|
|
154
179
|
return ctx
|
|
155
180
|
}
|
|
156
181
|
|
|
157
182
|
const baseOnEffect = makeOnEffect((api) => contextFor(api.send).editor, itemsById, {
|
|
158
|
-
|
|
183
|
+
// NB: consumer `onChange` is delivered DIRECTLY from the foreign onChange
|
|
184
|
+
// wrapper (see `view` below), NOT through this effect path — the dispose-time
|
|
185
|
+
// debounce flush runs after the TEA loop is disposed, and a `send` from a
|
|
186
|
+
// disposed loop is dropped, so effect-routed delivery loses the final edit.
|
|
159
187
|
onFormatChange: config.onFormatChange,
|
|
160
188
|
applyValue: (editor, value) =>
|
|
161
189
|
editor.update(
|
|
@@ -273,6 +301,10 @@ export function markdownEditor(
|
|
|
273
301
|
: {}),
|
|
274
302
|
register: (editor) => {
|
|
275
303
|
const disposers = [
|
|
304
|
+
// Keep the WYSIWYG surface and the serialized GFM dialect in lock-step:
|
|
305
|
+
// underline (Cmd+U) has no markdown representation, so it is swallowed
|
|
306
|
+
// rather than applied-then-silently-stripped on save.
|
|
307
|
+
blockUnderlineFormat(editor),
|
|
276
308
|
registerMarkdownShortcuts(editor, transformers),
|
|
277
309
|
// Global scheme allowlist for links: the single backstop covering the
|
|
278
310
|
// link dialog, pasted/imported markdown, and the typed `[x](url)`
|
|
@@ -297,7 +329,20 @@ export function markdownEditor(
|
|
|
297
329
|
}
|
|
298
330
|
config.onReady?.(editor)
|
|
299
331
|
},
|
|
300
|
-
onChange: (value) =>
|
|
332
|
+
onChange: (value) => {
|
|
333
|
+
// Deliver to the consumer DIRECTLY, independent of the TEA loop: the
|
|
334
|
+
// dispose-time debounce flush runs AFTER the component is disposed, and a
|
|
335
|
+
// `send` from a disposed loop is dropped — so routing consumer delivery
|
|
336
|
+
// through `send` would lose the last debounce window of typing on unmount.
|
|
337
|
+
// Deduped per mount so the same value isn't delivered twice.
|
|
338
|
+
if (value !== mount.lastChange) {
|
|
339
|
+
mount.lastChange = value
|
|
340
|
+
config.onChange?.(value)
|
|
341
|
+
}
|
|
342
|
+
// Mirror into TEA state (value/dirty) while the loop is alive; dropped
|
|
343
|
+
// silently after dispose, which is fine — the consumer was already told.
|
|
344
|
+
send({ type: 'markdownChanged', value })
|
|
345
|
+
},
|
|
301
346
|
onSelectionChange: (ctx) => {
|
|
302
347
|
const format = computeFormatState(ctx.editor, ctx)
|
|
303
348
|
const text = ctx.editor.getEditorState().read(() => $getRoot().getTextContent())
|
package/src/effects.ts
CHANGED
|
@@ -12,7 +12,6 @@ export interface EffectApi {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export interface EffectConfig {
|
|
15
|
-
onChange?: (markdown: string) => void
|
|
16
15
|
onFormatChange?: (format: FormatState) => void
|
|
17
16
|
/** Push markdown into the live editor (deserialize), without echoing onChange. */
|
|
18
17
|
applyValue: (editor: LexicalEditor, value: string) => void
|
|
@@ -41,7 +40,10 @@ export function makeOnEffect(
|
|
|
41
40
|
return
|
|
42
41
|
}
|
|
43
42
|
case 'emitChange': {
|
|
44
|
-
|
|
43
|
+
// Consumer `onChange` delivery moved to the foreign onChange wrapper (see
|
|
44
|
+
// editor.ts) so it survives dispose — the loop is torn down before the
|
|
45
|
+
// dispose-time debounce flush runs, and a `send`-routed effect would be
|
|
46
|
+
// dropped. This effect now only signals that state changed; no side effect.
|
|
45
47
|
return
|
|
46
48
|
}
|
|
47
49
|
case 'emitFormat': {
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ export {
|
|
|
7
7
|
type CollabHooks,
|
|
8
8
|
type CollabFactory,
|
|
9
9
|
markdownEditor,
|
|
10
|
+
blockUnderlineFormat,
|
|
10
11
|
} from './editor.js'
|
|
11
12
|
|
|
12
13
|
export {
|
|
@@ -68,18 +69,12 @@ export { tablePlugin } from './plugins/table.js'
|
|
|
68
69
|
|
|
69
70
|
export { $insertMarkdownAtSelection, registerMarkdownPaste } from './paste.js'
|
|
70
71
|
|
|
71
|
-
export { GFM_NODES, GFM_TRANSFORMERS } from './transformers/gfm.js'
|
|
72
|
+
export { GFM_NODES, GFM_TRANSFORMERS, HIGHLIGHT_TRANSFORMER } from './transformers/gfm.js'
|
|
72
73
|
export { buildTransformers, orderTransformers } from './transformers/registry.js'
|
|
73
74
|
|
|
74
75
|
export { computeFormatState } from './format.js'
|
|
75
76
|
|
|
76
|
-
export {
|
|
77
|
-
STRIKETHROUGH_CLASS,
|
|
78
|
-
UNDERLINE_CLASS,
|
|
79
|
-
UNDERLINE_STRIKETHROUGH_CLASS,
|
|
80
|
-
defaultTheme,
|
|
81
|
-
mergeTheme,
|
|
82
|
-
} from './theme.js'
|
|
77
|
+
export { STRIKETHROUGH_CLASS, defaultTheme, mergeTheme } from './theme.js'
|
|
83
78
|
|
|
84
79
|
export {
|
|
85
80
|
type ToolbarItemParts,
|
package/src/plugins/table.ts
CHANGED
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
$createParagraphNode,
|
|
10
|
-
$createTextNode,
|
|
11
10
|
$getSelection,
|
|
12
11
|
$isRangeSelection,
|
|
12
|
+
type ElementFormatType,
|
|
13
|
+
type ElementNode,
|
|
13
14
|
type LexicalNode,
|
|
14
15
|
} from 'lexical'
|
|
15
16
|
import { $insertNodeToNearestRoot, mergeRegister } from '@lexical/utils'
|
|
@@ -30,12 +31,26 @@ import {
|
|
|
30
31
|
TableNode,
|
|
31
32
|
TableRowNode,
|
|
32
33
|
} from '@lexical/table'
|
|
33
|
-
import
|
|
34
|
+
import {
|
|
35
|
+
$convertFromMarkdownString,
|
|
36
|
+
LINK,
|
|
37
|
+
type MultilineElementTransformer,
|
|
38
|
+
type Transformer,
|
|
39
|
+
} from '@lexical/markdown'
|
|
34
40
|
import { button, text } from '@llui/dom'
|
|
35
41
|
import { definePluginUI } from './ui.js'
|
|
36
42
|
import { OVERLAY_Z, hideOverlay, onViewportChange, overlayRoot } from './overlay.js'
|
|
43
|
+
import { INLINE_TEXT_TRANSFORMERS } from '../transformers/gfm.js'
|
|
37
44
|
import type { MarkdownPlugin } from './types.js'
|
|
38
45
|
|
|
46
|
+
/** Transformers used to parse/serialize a single table cell's INLINE content
|
|
47
|
+
* (bold/italic/strikethrough/inline-code and links). No block/element or
|
|
48
|
+
* multiline transformers, so a cell's markdown is always a single inline run —
|
|
49
|
+
* this is what makes bold/link cells survive an export∘import round-trip instead
|
|
50
|
+
* of being flattened to bare text. `LINK` needs `LinkNode`, which the core plugin
|
|
51
|
+
* registers (tables are used alongside it). */
|
|
52
|
+
const CELL_TRANSFORMERS: readonly Transformer[] = [...INLINE_TEXT_TRANSFORMERS, LINK]
|
|
53
|
+
|
|
39
54
|
interface TableToolsState {
|
|
40
55
|
open: boolean
|
|
41
56
|
x: number
|
|
@@ -107,26 +122,71 @@ function isSeparator(line: string | undefined): boolean {
|
|
|
107
122
|
return !!line && /^\s*\|?[\s:|-]*-[\s:|-]*\|?\s*$/.test(line) && line.includes('-')
|
|
108
123
|
}
|
|
109
124
|
|
|
125
|
+
/** Is `line` a GFM table row? GFM makes the leading and trailing pipes OPTIONAL
|
|
126
|
+
* (`a | b` is as valid as `| a | b |`); the only requirement is at least one
|
|
127
|
+
* unescaped cell delimiter. The old `\|.*\|` test rejected valid pipe-less-edge
|
|
128
|
+
* rows, truncating imported tables. */
|
|
110
129
|
function isRow(line: string | undefined): boolean {
|
|
111
|
-
|
|
130
|
+
if (line === undefined) return false
|
|
131
|
+
const trimmed = line.trim()
|
|
132
|
+
return trimmed !== '' && /(?<!\\)\|/.test(trimmed)
|
|
112
133
|
}
|
|
113
134
|
|
|
114
|
-
|
|
135
|
+
/** Map a GFM separator cell (`:---`, `:---:`, `---:`, `---`) to a cell alignment. */
|
|
136
|
+
function parseAlignment(separatorCell: string): ElementFormatType {
|
|
137
|
+
const t = separatorCell.trim()
|
|
138
|
+
const left = t.startsWith(':')
|
|
139
|
+
const right = t.endsWith(':')
|
|
140
|
+
if (left && right) return 'center'
|
|
141
|
+
if (right) return 'right'
|
|
142
|
+
if (left) return 'left'
|
|
143
|
+
return ''
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Map a cell's alignment back to its GFM separator token. */
|
|
147
|
+
function alignmentSeparator(format: ElementFormatType): string {
|
|
148
|
+
switch (format) {
|
|
149
|
+
case 'center':
|
|
150
|
+
return ':---:'
|
|
151
|
+
case 'right':
|
|
152
|
+
case 'end':
|
|
153
|
+
return '---:'
|
|
154
|
+
case 'left':
|
|
155
|
+
case 'start':
|
|
156
|
+
return ':---'
|
|
157
|
+
default:
|
|
158
|
+
return '---'
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Build a cell from its inline markdown, parsing bold/italic/link/etc. through
|
|
163
|
+
* {@link CELL_TRANSFORMERS} (not a bare text node) and applying its column's
|
|
164
|
+
* alignment. */
|
|
165
|
+
function makeCell(cellMarkdown: string, header: boolean, align: ElementFormatType): TableCellNode {
|
|
115
166
|
const cell = $createTableCellNode(
|
|
116
167
|
header ? TableCellHeaderStates.COLUMN : TableCellHeaderStates.NO_STATUS,
|
|
117
168
|
)
|
|
118
|
-
cell.
|
|
169
|
+
// Parse the cell's inline markdown into a paragraph of formatted text nodes.
|
|
170
|
+
$convertFromMarkdownString(cellMarkdown, [...CELL_TRANSFORMERS], cell)
|
|
171
|
+
// A blank cell yields no paragraph — keep the cell a well-formed single-paragraph.
|
|
172
|
+
if (cell.getChildrenSize() === 0) cell.append($createParagraphNode())
|
|
173
|
+
if (align !== '') cell.setFormat(align)
|
|
119
174
|
return cell
|
|
120
175
|
}
|
|
121
176
|
|
|
122
|
-
function buildTable(
|
|
177
|
+
function buildTable(
|
|
178
|
+
header: string[],
|
|
179
|
+
body: string[][],
|
|
180
|
+
aligns: readonly ElementFormatType[],
|
|
181
|
+
): TableNode {
|
|
123
182
|
const table = $createTableNode()
|
|
124
183
|
const headerRow = $createTableRowNode()
|
|
125
|
-
|
|
184
|
+
header.forEach((h, i) => headerRow.append(makeCell(h, true, aligns[i] ?? '')))
|
|
126
185
|
table.append(headerRow)
|
|
127
186
|
for (const row of body) {
|
|
128
187
|
const tr = $createTableRowNode()
|
|
129
|
-
for (let i = 0; i < header.length; i++)
|
|
188
|
+
for (let i = 0; i < header.length; i++)
|
|
189
|
+
tr.append(makeCell(row[i] ?? '', false, aligns[i] ?? ''))
|
|
130
190
|
table.append(tr)
|
|
131
191
|
}
|
|
132
192
|
return table
|
|
@@ -134,29 +194,42 @@ function buildTable(header: string[], body: string[][]): TableNode {
|
|
|
134
194
|
|
|
135
195
|
const TABLE_TRANSFORMER: MultilineElementTransformer = {
|
|
136
196
|
dependencies: [TableNode, TableRowNode, TableCellNode],
|
|
137
|
-
export: (node: LexicalNode): string | null => {
|
|
197
|
+
export: (node: LexicalNode, traverseChildren: (node: ElementNode) => string): string | null => {
|
|
138
198
|
if (!$isTableNode(node)) return null
|
|
139
199
|
const rows = node.getChildren() as TableRowNode[]
|
|
140
200
|
if (rows.length === 0) return null
|
|
141
201
|
const lines: string[] = []
|
|
142
202
|
rows.forEach((row, ri) => {
|
|
143
203
|
const cells = row.getChildren() as TableCellNode[]
|
|
144
|
-
|
|
204
|
+
// Serialize each cell's INLINE content through the full transformer set
|
|
205
|
+
// (so bold/italic/links survive), then escape `|` and flatten newlines so
|
|
206
|
+
// the cell can't break the row.
|
|
207
|
+
const texts = cells.map((c) =>
|
|
208
|
+
traverseChildren(c).replace(/\|/g, '\\|').replace(/\r?\n/g, ' ').trim(),
|
|
209
|
+
)
|
|
145
210
|
lines.push('| ' + texts.join(' | ') + ' |')
|
|
146
|
-
if (ri === 0)
|
|
211
|
+
if (ri === 0) {
|
|
212
|
+
// Reconstruct the alignment row from the header cells' alignments.
|
|
213
|
+
const seps = cells.map((c) => alignmentSeparator(c.getFormatType()))
|
|
214
|
+
lines.push('| ' + seps.join(' | ') + ' |')
|
|
215
|
+
}
|
|
147
216
|
})
|
|
148
217
|
return lines.join('\n')
|
|
149
218
|
},
|
|
150
|
-
|
|
219
|
+
// Leading/trailing pipes are optional in GFM; require only one internal pipe.
|
|
220
|
+
// A false positive is harmless — the header is only accepted when the NEXT line
|
|
221
|
+
// is a delimiter row (checked below).
|
|
222
|
+
regExpStart: /^\s*\|?.*(?<!\\)\|.*\|?\s*$/,
|
|
151
223
|
handleImportAfterStartMatch: ({ lines, rootNode, startLineIndex }) => {
|
|
152
224
|
if (!isSeparator(lines[startLineIndex + 1])) return null // header must be followed by a separator
|
|
225
|
+
const aligns = splitRow(lines[startLineIndex + 1]!).map(parseAlignment)
|
|
153
226
|
let end = startLineIndex + 2
|
|
154
227
|
const body: string[][] = []
|
|
155
228
|
while (end < lines.length && isRow(lines[end]) && !isSeparator(lines[end])) {
|
|
156
229
|
body.push(splitRow(lines[end]!))
|
|
157
230
|
end++
|
|
158
231
|
}
|
|
159
|
-
rootNode.append(buildTable(splitRow(lines[startLineIndex]!), body))
|
|
232
|
+
rootNode.append(buildTable(splitRow(lines[startLineIndex]!), body, aligns))
|
|
160
233
|
return [true, end - 1]
|
|
161
234
|
},
|
|
162
235
|
// Import is handled manually above; a multiline transformer still needs replace.
|
|
@@ -274,6 +347,7 @@ export function tablePlugin(): MarkdownPlugin {
|
|
|
274
347
|
['', ''],
|
|
275
348
|
['', ''],
|
|
276
349
|
],
|
|
350
|
+
['', ''],
|
|
277
351
|
),
|
|
278
352
|
),
|
|
279
353
|
),
|
package/src/styles/editor.css
CHANGED
|
@@ -83,12 +83,6 @@
|
|
|
83
83
|
[data-lexical-editor] .md-strikethrough {
|
|
84
84
|
text-decoration: line-through;
|
|
85
85
|
}
|
|
86
|
-
[data-lexical-editor] .md-underline {
|
|
87
|
-
text-decoration: underline;
|
|
88
|
-
}
|
|
89
|
-
[data-lexical-editor] .md-underline-strikethrough {
|
|
90
|
-
text-decoration: underline line-through;
|
|
91
|
-
}
|
|
92
86
|
|
|
93
87
|
/* Task-list items (li[role="checkbox"]) with a clickable checkbox. */
|
|
94
88
|
[data-lexical-editor] li[role='checkbox'] {
|
package/src/theme.ts
CHANGED
|
@@ -2,10 +2,19 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Lexical styles most inline formats through semantic tags that carry
|
|
4
4
|
// browser-default styling — bold→<strong>, italic→<em>, code→<code>. But
|
|
5
|
-
// `strikethrough`
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
5
|
+
// `strikethrough` renders as a bare <span> whose ONLY styling hook is a
|
|
6
|
+
// `theme.text.strikethrough` class. With no theme the format is applied to the
|
|
7
|
+
// model but is visually invisible. This default theme supplies that class name;
|
|
8
|
+
// the bundled `styles/editor.css` gives it its `text-decoration`.
|
|
9
|
+
//
|
|
10
|
+
// `underline` is deliberately NOT themed here: the GFM markdown dialect this
|
|
11
|
+
// editor serializes has no underline representation (Lexical's text-format
|
|
12
|
+
// transformers require a SYMMETRIC delimiter, and there is no standard symmetric
|
|
13
|
+
// underline syntax), so an applied underline would be silently stripped on save.
|
|
14
|
+
// To keep the WYSIWYG surface and the serialized dialect in lock-step, the
|
|
15
|
+
// underline command is also intercepted at the editor seam (see editor.ts) — so
|
|
16
|
+
// underline can be neither applied nor lost. Add it back (theme + transformer +
|
|
17
|
+
// command) only alongside a dialect that can round-trip it.
|
|
9
18
|
//
|
|
10
19
|
// Consumers can override any entry via `markdownEditor({ theme })` — the user's
|
|
11
20
|
// theme is merged over this default (see `mergeTheme`).
|
|
@@ -13,22 +22,17 @@
|
|
|
13
22
|
import type { EditorThemeClasses } from 'lexical'
|
|
14
23
|
|
|
15
24
|
export const STRIKETHROUGH_CLASS = 'md-strikethrough'
|
|
16
|
-
export const UNDERLINE_CLASS = 'md-underline'
|
|
17
|
-
export const UNDERLINE_STRIKETHROUGH_CLASS = 'md-underline-strikethrough'
|
|
18
25
|
|
|
19
26
|
/** The class hooks Lexical needs for text-decoration formats it renders as a
|
|
20
|
-
* plain <span>.
|
|
21
|
-
* the case where both apply — both want `text-decoration`.) */
|
|
27
|
+
* plain <span>. */
|
|
22
28
|
export const defaultTheme: EditorThemeClasses = {
|
|
23
29
|
text: {
|
|
24
30
|
strikethrough: STRIKETHROUGH_CLASS,
|
|
25
|
-
underline: UNDERLINE_CLASS,
|
|
26
|
-
underlineStrikethrough: UNDERLINE_STRIKETHROUGH_CLASS,
|
|
27
31
|
},
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
/** Merge a consumer theme over the default. `text` is merged per-key so a
|
|
31
|
-
* consumer overriding (say) `strikethrough` keeps the default
|
|
35
|
+
* consumer overriding (say) `strikethrough` keeps the other default entries.
|
|
32
36
|
*
|
|
33
37
|
* Always returns a FRESH theme (never the shared `defaultTheme` singleton):
|
|
34
38
|
* Lexical caches resolved class arrays by MUTATING the `text` object it is
|
package/src/transformers/gfm.ts
CHANGED
|
@@ -39,9 +39,18 @@ export const GFM_NODES: ReadonlyArray<Klass<LexicalNode>> = [
|
|
|
39
39
|
LinkNode,
|
|
40
40
|
]
|
|
41
41
|
|
|
42
|
+
/** The `==highlight==` transformer. NOT part of the default GFM set: `==..==` is
|
|
43
|
+
* not GFM, so exporting it produces non-standard markdown other renderers won't
|
|
44
|
+
* understand. Offered as an opt-in a consumer can add to a plugin's transformers. */
|
|
45
|
+
export const HIGHLIGHT_TRANSFORMER: Transformer = HIGHLIGHT
|
|
46
|
+
|
|
42
47
|
/** Inline text-format transformers (no block nodes, no node registration). These
|
|
43
48
|
* are the only transformers a single-block / inline-only editor needs; `LINK` is
|
|
44
|
-
* kept separate since it requires `LinkNode` to be registered.
|
|
49
|
+
* kept separate since it requires `LinkNode` to be registered.
|
|
50
|
+
*
|
|
51
|
+
* `HIGHLIGHT` is deliberately excluded: it round-trips as the non-GFM `==..==`
|
|
52
|
+
* syntax, so it would silently emit markdown outside the editor's stated dialect.
|
|
53
|
+
* Opt in with {@link HIGHLIGHT_TRANSFORMER}. */
|
|
45
54
|
export const INLINE_TEXT_TRANSFORMERS: readonly Transformer[] = [
|
|
46
55
|
BOLD_ITALIC_STAR,
|
|
47
56
|
BOLD_ITALIC_UNDERSCORE,
|
|
@@ -51,7 +60,6 @@ export const INLINE_TEXT_TRANSFORMERS: readonly Transformer[] = [
|
|
|
51
60
|
ITALIC_UNDERSCORE,
|
|
52
61
|
STRIKETHROUGH,
|
|
53
62
|
INLINE_CODE,
|
|
54
|
-
HIGHLIGHT,
|
|
55
63
|
]
|
|
56
64
|
|
|
57
65
|
/** Markdown ↔ node transformers for the GFM superset. */
|