@byline/cli 2.1.3 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +28 -0
- package/dist/commands/setup.js.map +1 -1
- package/dist/manifest/deps.d.ts.map +1 -1
- package/dist/manifest/deps.js +34 -0
- package/dist/manifest/deps.js.map +1 -1
- package/dist/templates/byline/i18n.ts +1 -1
- package/dist/templates/byline-examples/admin.config.ts +12 -0
- package/dist/templates/byline-examples/collections/media/components/media-list-view.module.css +1 -1
- package/dist/templates/byline-examples/collections/news/admin.tsx +7 -2
- package/dist/templates/byline-examples/collections/pages/admin.tsx +2 -1
- package/dist/templates/byline-examples/fields/ai-text.ts +47 -0
- package/dist/templates/byline-examples/fields/ai-textarea.ts +50 -0
- package/dist/templates/byline-examples/fields/ai-widgets/ai-field-label.tsx +48 -0
- package/dist/templates/byline-examples/fields/ai-widgets/ai-field-panel.tsx +42 -0
- package/dist/templates/byline-examples/fields/ai-widgets/ai-panel-store.ts +52 -0
- package/dist/templates/byline-examples/fields/lexical-richtext-ai.tsx +82 -0
- package/dist/templates/byline-examples/fields/lexical-richtext-compact.ts +8 -4
- package/dist/templates/byline-examples/i18n.ts +1 -1
- package/dist/templates/byline-examples/plugins/ai/ai-plugin-text.tsx +58 -0
- package/dist/templates/byline-examples/scripts/import-docs.ts +272 -0
- package/dist/templates/byline-examples/scripts/lib/frontmatter.ts +136 -0
- package/dist/templates/byline-examples/scripts/lib/mdast-to-lexical.ts +497 -0
- package/dist/templates/byline-examples/scripts/lib/strip-leading-h1.ts +31 -0
- package/dist/templates/migrations/{0000_slimy_lilandra.sql → 0000_cold_red_wolf.sql} +40 -34
- package/dist/templates/migrations/meta/0000_snapshot.json +100 -68
- package/dist/templates/migrations/meta/_journal.json +2 -2
- package/package.json +1 -1
- package/dist/templates/byline-examples/collections/docs/components/feature-formatter.tsx +0 -10
- package/dist/templates/byline-examples/collections/docs-categories/admin.tsx +0 -78
- package/dist/templates/byline-examples/collections/docs-categories/components/.gitkeep +0 -0
- package/dist/templates/byline-examples/collections/docs-categories/hooks/.gitkeep +0 -0
- package/dist/templates/byline-examples/collections/docs-categories/index.ts +0 -10
- package/dist/templates/byline-examples/collections/docs-categories/schema.ts +0 -33
- package/dist/templates/byline-examples/seeds/doc-categories.ts +0 -71
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* mdast → Lexical SerializedEditorState mapper.
|
|
11
|
+
*
|
|
12
|
+
* Pure, headless-editor-free conversion. The output JSON is shaped to
|
|
13
|
+
* match what the runtime serializer in
|
|
14
|
+
* `apps/webapp/src/ui/byline/components/richtext-lexical/serialize`
|
|
15
|
+
* expects to render (heading.tag, list.tag + list.listType, code
|
|
16
|
+
* children of type 'code-highlight' interspersed with 'linebreak',
|
|
17
|
+
* link.attributes, etc.).
|
|
18
|
+
*
|
|
19
|
+
* Inline format is a bitmask on each text node (Lexical convention) —
|
|
20
|
+
* we accumulate it down the walk so `**_foo_**` collapses to a single
|
|
21
|
+
* text node with format = BOLD | ITALIC instead of nested wrappers.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type {
|
|
25
|
+
Blockquote,
|
|
26
|
+
Code,
|
|
27
|
+
Content,
|
|
28
|
+
Delete,
|
|
29
|
+
Emphasis,
|
|
30
|
+
Heading,
|
|
31
|
+
Image,
|
|
32
|
+
InlineCode,
|
|
33
|
+
Link,
|
|
34
|
+
List,
|
|
35
|
+
ListItem,
|
|
36
|
+
Paragraph,
|
|
37
|
+
PhrasingContent,
|
|
38
|
+
Root,
|
|
39
|
+
Strong,
|
|
40
|
+
Table,
|
|
41
|
+
TableCell,
|
|
42
|
+
TableRow,
|
|
43
|
+
Text,
|
|
44
|
+
ThematicBreak,
|
|
45
|
+
} from 'mdast'
|
|
46
|
+
|
|
47
|
+
// Lexical inline format bits — kept in sync with
|
|
48
|
+
// apps/webapp/src/ui/byline/components/richtext-lexical/serialize/richtext-node-formats.ts
|
|
49
|
+
const IS_BOLD = 1
|
|
50
|
+
const IS_ITALIC = 1 << 1
|
|
51
|
+
const IS_STRIKETHROUGH = 1 << 2
|
|
52
|
+
const IS_CODE = 1 << 4
|
|
53
|
+
|
|
54
|
+
export interface LexicalRoot {
|
|
55
|
+
root: {
|
|
56
|
+
type: 'root'
|
|
57
|
+
children: LexicalNode[]
|
|
58
|
+
direction: 'ltr' | null
|
|
59
|
+
format: ''
|
|
60
|
+
indent: 0
|
|
61
|
+
version: 1
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface LexicalNode {
|
|
66
|
+
type: string
|
|
67
|
+
version: number
|
|
68
|
+
children?: LexicalNode[]
|
|
69
|
+
[k: string]: unknown
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface MdastToLexicalWarning {
|
|
73
|
+
kind: 'unsupported-node' | 'dropped-html' | 'dropped-image'
|
|
74
|
+
detail: string
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface MdastToLexicalResult {
|
|
78
|
+
state: LexicalRoot
|
|
79
|
+
warnings: MdastToLexicalWarning[]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Convert an mdast tree to a Lexical SerializedEditorState. Returns the
|
|
84
|
+
* state alongside a list of warnings (unsupported node types, dropped
|
|
85
|
+
* HTML/image/table nodes) so callers can decide whether to fail the
|
|
86
|
+
* import.
|
|
87
|
+
*/
|
|
88
|
+
export function mdastToLexical(root: Root): MdastToLexicalResult {
|
|
89
|
+
const warnings: MdastToLexicalWarning[] = []
|
|
90
|
+
const children = walkBlocks(root.children, warnings)
|
|
91
|
+
return {
|
|
92
|
+
state: {
|
|
93
|
+
root: {
|
|
94
|
+
type: 'root',
|
|
95
|
+
children: children.length > 0 ? children : [emptyParagraph()],
|
|
96
|
+
direction: 'ltr',
|
|
97
|
+
format: '',
|
|
98
|
+
indent: 0,
|
|
99
|
+
version: 1,
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
warnings,
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function walkBlocks(nodes: Content[], warnings: MdastToLexicalWarning[]): LexicalNode[] {
|
|
107
|
+
const out: LexicalNode[] = []
|
|
108
|
+
for (const node of nodes) {
|
|
109
|
+
const converted = blockNode(node, warnings)
|
|
110
|
+
if (converted) out.push(converted)
|
|
111
|
+
}
|
|
112
|
+
return out
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function blockNode(node: Content, warnings: MdastToLexicalWarning[]): LexicalNode | null {
|
|
116
|
+
switch (node.type) {
|
|
117
|
+
case 'paragraph':
|
|
118
|
+
return paragraphNode(node, warnings)
|
|
119
|
+
case 'heading':
|
|
120
|
+
return headingNode(node, warnings)
|
|
121
|
+
case 'list':
|
|
122
|
+
return listNode(node, warnings)
|
|
123
|
+
case 'blockquote':
|
|
124
|
+
return blockquoteNode(node, warnings)
|
|
125
|
+
case 'code':
|
|
126
|
+
return codeNode(node)
|
|
127
|
+
case 'thematicBreak':
|
|
128
|
+
return horizontalRuleNode(node)
|
|
129
|
+
case 'html':
|
|
130
|
+
warnings.push({
|
|
131
|
+
kind: 'dropped-html',
|
|
132
|
+
detail: `dropped HTML node: ${truncate((node as { value: string }).value)}`,
|
|
133
|
+
})
|
|
134
|
+
return null
|
|
135
|
+
case 'image':
|
|
136
|
+
warnings.push({
|
|
137
|
+
kind: 'dropped-image',
|
|
138
|
+
detail: `dropped image (alt=${(node as Image).alt ?? ''} url=${(node as Image).url})`,
|
|
139
|
+
})
|
|
140
|
+
return null
|
|
141
|
+
case 'table':
|
|
142
|
+
return tableNode(node as Table, warnings)
|
|
143
|
+
default:
|
|
144
|
+
warnings.push({
|
|
145
|
+
kind: 'unsupported-node',
|
|
146
|
+
detail: `dropped block-level node of type '${(node as { type: string }).type}'`,
|
|
147
|
+
})
|
|
148
|
+
return null
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function paragraphNode(node: Paragraph, warnings: MdastToLexicalWarning[]): LexicalNode {
|
|
153
|
+
return {
|
|
154
|
+
type: 'paragraph',
|
|
155
|
+
version: 1,
|
|
156
|
+
direction: 'ltr',
|
|
157
|
+
format: '',
|
|
158
|
+
indent: 0,
|
|
159
|
+
textFormat: 0,
|
|
160
|
+
textStyle: '',
|
|
161
|
+
children: walkInlines(node.children, 0, warnings),
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function headingNode(node: Heading, warnings: MdastToLexicalWarning[]): LexicalNode {
|
|
166
|
+
return {
|
|
167
|
+
type: 'heading',
|
|
168
|
+
version: 1,
|
|
169
|
+
direction: 'ltr',
|
|
170
|
+
format: '',
|
|
171
|
+
indent: 0,
|
|
172
|
+
tag: `h${node.depth}`,
|
|
173
|
+
children: walkInlines(node.children, 0, warnings),
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function listNode(node: List, warnings: MdastToLexicalWarning[]): LexicalNode {
|
|
178
|
+
const ordered = node.ordered === true
|
|
179
|
+
const start = ordered ? (node.start ?? 1) : 1
|
|
180
|
+
return {
|
|
181
|
+
type: 'list',
|
|
182
|
+
version: 1,
|
|
183
|
+
direction: 'ltr',
|
|
184
|
+
format: '',
|
|
185
|
+
indent: 0,
|
|
186
|
+
listType: ordered ? 'number' : 'bullet',
|
|
187
|
+
tag: ordered ? 'ol' : 'ul',
|
|
188
|
+
start,
|
|
189
|
+
children: node.children.map((item, index) =>
|
|
190
|
+
listItemNode(item, ordered ? start + index : index + 1, warnings)
|
|
191
|
+
),
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function listItemNode(
|
|
196
|
+
node: ListItem,
|
|
197
|
+
value: number,
|
|
198
|
+
warnings: MdastToLexicalWarning[]
|
|
199
|
+
): LexicalNode {
|
|
200
|
+
// mdast wraps list-item content in implicit paragraph(s). Lexical
|
|
201
|
+
// listitem expects inline children directly, so peel single-paragraph
|
|
202
|
+
// wrappers. Nested lists are passed through as block children.
|
|
203
|
+
const children: LexicalNode[] = []
|
|
204
|
+
for (const child of node.children) {
|
|
205
|
+
if (child.type === 'paragraph') {
|
|
206
|
+
children.push(...walkInlines(child.children, 0, warnings))
|
|
207
|
+
} else if (child.type === 'list') {
|
|
208
|
+
children.push(listNode(child, warnings))
|
|
209
|
+
} else {
|
|
210
|
+
const block = blockNode(child as Content, warnings)
|
|
211
|
+
if (block) children.push(block)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
type: 'listitem',
|
|
216
|
+
version: 1,
|
|
217
|
+
direction: 'ltr',
|
|
218
|
+
format: '',
|
|
219
|
+
indent: 0,
|
|
220
|
+
value,
|
|
221
|
+
children,
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function blockquoteNode(node: Blockquote, warnings: MdastToLexicalWarning[]): LexicalNode {
|
|
226
|
+
// Lexical's quote node holds inline children, not blocks. mdast
|
|
227
|
+
// blockquotes wrap paragraphs — flatten single-paragraph blockquotes,
|
|
228
|
+
// and for multi-paragraph quotes inject a linebreak between them.
|
|
229
|
+
const inline: LexicalNode[] = []
|
|
230
|
+
let first = true
|
|
231
|
+
for (const child of node.children) {
|
|
232
|
+
if (child.type === 'paragraph') {
|
|
233
|
+
if (!first) inline.push({ type: 'linebreak', version: 1 })
|
|
234
|
+
inline.push(...walkInlines(child.children, 0, warnings))
|
|
235
|
+
first = false
|
|
236
|
+
} else {
|
|
237
|
+
warnings.push({
|
|
238
|
+
kind: 'unsupported-node',
|
|
239
|
+
detail: `blockquote contained non-paragraph child '${child.type}' — dropped`,
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
type: 'quote',
|
|
245
|
+
version: 1,
|
|
246
|
+
direction: 'ltr',
|
|
247
|
+
format: '',
|
|
248
|
+
indent: 0,
|
|
249
|
+
children: inline,
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Markdown fence shorthands → prism-react-renderer language ids. Prism
|
|
254
|
+
// bundles `typescript` / `tsx` / `bash` etc. but not the common shorthand
|
|
255
|
+
// forms most authors write. Without normalization the imported language
|
|
256
|
+
// is unrecognised and falls back to plain — which is the "imported code
|
|
257
|
+
// doesn't highlight until I cut-paste it back" symptom (the editor's
|
|
258
|
+
// own CodeNode normalises on insert, which is why the round-trip fixes
|
|
259
|
+
// it).
|
|
260
|
+
const LANG_ALIASES: Record<string, string> = {
|
|
261
|
+
ts: 'typescript',
|
|
262
|
+
js: 'javascript',
|
|
263
|
+
sh: 'bash',
|
|
264
|
+
shell: 'bash',
|
|
265
|
+
zsh: 'bash',
|
|
266
|
+
yml: 'yaml',
|
|
267
|
+
jsonc: 'json',
|
|
268
|
+
py: 'python',
|
|
269
|
+
rb: 'ruby',
|
|
270
|
+
md: 'markdown',
|
|
271
|
+
html: 'markup',
|
|
272
|
+
xml: 'markup',
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function normalizeCodeLang(lang: string | null | undefined): string | null {
|
|
276
|
+
if (lang == null) return null
|
|
277
|
+
const trimmed = lang.trim().toLowerCase()
|
|
278
|
+
if (trimmed.length === 0) return null
|
|
279
|
+
return LANG_ALIASES[trimmed] ?? trimmed
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// @lexical/table's TableCellHeaderStates: NO_STATUS=0, ROW=1, COLUMN=2, BOTH=3.
|
|
283
|
+
// Markdown GFM tables only ever produce a ROW header (the first row).
|
|
284
|
+
const HEADER_STATE_NONE = 0
|
|
285
|
+
const HEADER_STATE_ROW = 1
|
|
286
|
+
|
|
287
|
+
function tableNode(node: Table, warnings: MdastToLexicalWarning[]): LexicalNode {
|
|
288
|
+
return {
|
|
289
|
+
type: 'table',
|
|
290
|
+
version: 1,
|
|
291
|
+
direction: 'ltr',
|
|
292
|
+
format: '',
|
|
293
|
+
indent: 0,
|
|
294
|
+
children: node.children.map((row, rowIndex) => tableRowNode(row, rowIndex === 0, warnings)),
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function tableRowNode(
|
|
299
|
+
row: TableRow,
|
|
300
|
+
isHeaderRow: boolean,
|
|
301
|
+
warnings: MdastToLexicalWarning[]
|
|
302
|
+
): LexicalNode {
|
|
303
|
+
return {
|
|
304
|
+
type: 'tablerow',
|
|
305
|
+
version: 1,
|
|
306
|
+
direction: 'ltr',
|
|
307
|
+
format: '',
|
|
308
|
+
indent: 0,
|
|
309
|
+
children: row.children.map((cell) => tableCellNode(cell, isHeaderRow, warnings)),
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function tableCellNode(
|
|
314
|
+
cell: TableCell,
|
|
315
|
+
isHeaderRow: boolean,
|
|
316
|
+
warnings: MdastToLexicalWarning[]
|
|
317
|
+
): LexicalNode {
|
|
318
|
+
// Lexical's TableCellNode expects block-level children (the editor
|
|
319
|
+
// typing flow inserts a paragraph and writes text into it), so wrap
|
|
320
|
+
// mdast's inline cell content in a single paragraph. Empty cells
|
|
321
|
+
// get an empty paragraph so the node still has structurally valid
|
|
322
|
+
// children.
|
|
323
|
+
const inline = walkInlines(cell.children, 0, warnings)
|
|
324
|
+
return {
|
|
325
|
+
type: 'tablecell',
|
|
326
|
+
version: 1,
|
|
327
|
+
direction: 'ltr',
|
|
328
|
+
format: '',
|
|
329
|
+
indent: 0,
|
|
330
|
+
headerState: isHeaderRow ? HEADER_STATE_ROW : HEADER_STATE_NONE,
|
|
331
|
+
colSpan: 1,
|
|
332
|
+
rowSpan: 1,
|
|
333
|
+
children: [
|
|
334
|
+
{
|
|
335
|
+
type: 'paragraph',
|
|
336
|
+
version: 1,
|
|
337
|
+
direction: 'ltr',
|
|
338
|
+
format: '',
|
|
339
|
+
indent: 0,
|
|
340
|
+
textFormat: 0,
|
|
341
|
+
textStyle: '',
|
|
342
|
+
children: inline,
|
|
343
|
+
},
|
|
344
|
+
],
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function codeNode(node: Code): LexicalNode {
|
|
349
|
+
// @lexical/code stores fenced code as a `code` element whose children
|
|
350
|
+
// are `code-highlight` text segments interspersed with `linebreak`s
|
|
351
|
+
// (one per line). At import time we don't tokenize — emit one
|
|
352
|
+
// code-highlight per line and let the runtime re-highlight on render.
|
|
353
|
+
const lines = node.value.split('\n')
|
|
354
|
+
const children: LexicalNode[] = []
|
|
355
|
+
lines.forEach((line, i) => {
|
|
356
|
+
if (i > 0) children.push({ type: 'linebreak', version: 1 })
|
|
357
|
+
if (line.length > 0) {
|
|
358
|
+
children.push({
|
|
359
|
+
type: 'code-highlight',
|
|
360
|
+
version: 1,
|
|
361
|
+
detail: 0,
|
|
362
|
+
format: 0,
|
|
363
|
+
mode: 'normal',
|
|
364
|
+
style: '',
|
|
365
|
+
text: line,
|
|
366
|
+
})
|
|
367
|
+
}
|
|
368
|
+
})
|
|
369
|
+
return {
|
|
370
|
+
type: 'code',
|
|
371
|
+
version: 1,
|
|
372
|
+
direction: 'ltr',
|
|
373
|
+
format: '',
|
|
374
|
+
indent: 0,
|
|
375
|
+
language: normalizeCodeLang(node.lang),
|
|
376
|
+
children,
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function horizontalRuleNode(_node: ThematicBreak): LexicalNode {
|
|
381
|
+
return { type: 'horizontalrule', version: 1 }
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function walkInlines(
|
|
385
|
+
nodes: PhrasingContent[],
|
|
386
|
+
format: number,
|
|
387
|
+
warnings: MdastToLexicalWarning[]
|
|
388
|
+
): LexicalNode[] {
|
|
389
|
+
const out: LexicalNode[] = []
|
|
390
|
+
for (const node of nodes) {
|
|
391
|
+
const converted = inlineNode(node, format, warnings)
|
|
392
|
+
if (Array.isArray(converted)) out.push(...converted)
|
|
393
|
+
else if (converted) out.push(converted)
|
|
394
|
+
}
|
|
395
|
+
return out
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function inlineNode(
|
|
399
|
+
node: PhrasingContent,
|
|
400
|
+
format: number,
|
|
401
|
+
warnings: MdastToLexicalWarning[]
|
|
402
|
+
): LexicalNode | LexicalNode[] | null {
|
|
403
|
+
switch (node.type) {
|
|
404
|
+
case 'text':
|
|
405
|
+
return textNode((node as Text).value, format)
|
|
406
|
+
case 'strong':
|
|
407
|
+
return walkInlines((node as Strong).children, format | IS_BOLD, warnings)
|
|
408
|
+
case 'emphasis':
|
|
409
|
+
return walkInlines((node as Emphasis).children, format | IS_ITALIC, warnings)
|
|
410
|
+
case 'delete':
|
|
411
|
+
return walkInlines((node as Delete).children, format | IS_STRIKETHROUGH, warnings)
|
|
412
|
+
case 'inlineCode':
|
|
413
|
+
return textNode((node as InlineCode).value, format | IS_CODE)
|
|
414
|
+
case 'link':
|
|
415
|
+
return linkNode(node as Link, format, warnings)
|
|
416
|
+
case 'break':
|
|
417
|
+
return { type: 'linebreak', version: 1 }
|
|
418
|
+
case 'image':
|
|
419
|
+
warnings.push({
|
|
420
|
+
kind: 'dropped-image',
|
|
421
|
+
detail: `dropped inline image (alt=${(node as Image).alt ?? ''} url=${(node as Image).url})`,
|
|
422
|
+
})
|
|
423
|
+
return null
|
|
424
|
+
case 'html':
|
|
425
|
+
warnings.push({
|
|
426
|
+
kind: 'dropped-html',
|
|
427
|
+
detail: `dropped inline HTML: ${truncate((node as { value: string }).value)}`,
|
|
428
|
+
})
|
|
429
|
+
return null
|
|
430
|
+
default:
|
|
431
|
+
warnings.push({
|
|
432
|
+
kind: 'unsupported-node',
|
|
433
|
+
detail: `dropped inline node of type '${(node as { type: string }).type}'`,
|
|
434
|
+
})
|
|
435
|
+
return null
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function textNode(text: string, format: number): LexicalNode {
|
|
440
|
+
return {
|
|
441
|
+
type: 'text',
|
|
442
|
+
version: 1,
|
|
443
|
+
detail: 0,
|
|
444
|
+
format,
|
|
445
|
+
mode: 'normal',
|
|
446
|
+
style: '',
|
|
447
|
+
text: collapseSoftNewlines(text),
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Hard-wrapped markdown paragraphs (common in `/docs`) leave the
|
|
452
|
+
// source newlines inside mdast text values. HTML would normally
|
|
453
|
+
// collapse those, but Lexical's editor view treats embedded `\n` as
|
|
454
|
+
// visible breaks — paragraphs render with mid-sentence wraps at the
|
|
455
|
+
// original 80-char column rather than reflowing to the container.
|
|
456
|
+
// Collapse a single newline plus any surrounding horizontal whitespace
|
|
457
|
+
// to one space; runs of plain spaces (which the author may have
|
|
458
|
+
// chosen) are left alone.
|
|
459
|
+
function collapseSoftNewlines(text: string): string {
|
|
460
|
+
return text.replace(/[ \t]*\n[ \t]*/g, ' ')
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function linkNode(node: Link, format: number, warnings: MdastToLexicalWarning[]): LexicalNode {
|
|
464
|
+
const newTab = node.url.startsWith('http://') || node.url.startsWith('https://')
|
|
465
|
+
return {
|
|
466
|
+
type: 'link',
|
|
467
|
+
version: 2,
|
|
468
|
+
direction: 'ltr',
|
|
469
|
+
format: '',
|
|
470
|
+
indent: 0,
|
|
471
|
+
attributes: {
|
|
472
|
+
linkType: 'custom',
|
|
473
|
+
url: node.url,
|
|
474
|
+
newTab,
|
|
475
|
+
rel: newTab ? 'noopener' : null,
|
|
476
|
+
},
|
|
477
|
+
children: walkInlines(node.children, format, warnings),
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function emptyParagraph(): LexicalNode {
|
|
482
|
+
return {
|
|
483
|
+
type: 'paragraph',
|
|
484
|
+
version: 1,
|
|
485
|
+
direction: 'ltr',
|
|
486
|
+
format: '',
|
|
487
|
+
indent: 0,
|
|
488
|
+
textFormat: 0,
|
|
489
|
+
textStyle: '',
|
|
490
|
+
children: [],
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function truncate(s: string, max = 40): string {
|
|
495
|
+
const trimmed = s.trim().replace(/\s+/g, ' ')
|
|
496
|
+
return trimmed.length > max ? `${trimmed.slice(0, max)}…` : trimmed
|
|
497
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Drop the body's leading H1 when its text matches the frontmatter
|
|
11
|
+
* `title`. Frontmatter is the source of truth for the page title; if
|
|
12
|
+
* the body also opens with the same H1 the rendered doc would display
|
|
13
|
+
* the title twice.
|
|
14
|
+
*
|
|
15
|
+
* The match is case-insensitive and ignores surrounding whitespace, so
|
|
16
|
+
* minor presentation differences ("Authn / Authz" vs "authn / authz")
|
|
17
|
+
* don't leak a duplicate heading. Anything else — a different H1, a
|
|
18
|
+
* leading H2, no leading heading at all — is left untouched.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { Root } from 'mdast'
|
|
22
|
+
import { toString as mdastToString } from 'mdast-util-to-string'
|
|
23
|
+
|
|
24
|
+
export function stripLeadingH1IfMatches(root: Root, title: string): Root {
|
|
25
|
+
const first = root.children[0]
|
|
26
|
+
if (!first || first.type !== 'heading' || first.depth !== 1) return root
|
|
27
|
+
const headingText = mdastToString(first).trim().toLowerCase()
|
|
28
|
+
const wanted = title.trim().toLowerCase()
|
|
29
|
+
if (headingText !== wanted) return root
|
|
30
|
+
return { ...root, children: root.children.slice(1) }
|
|
31
|
+
}
|