@pterodoc/core 0.2.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/LICENCE.md +10 -0
- package/README.md +12 -0
- package/lib/chunks/capture-BhdvHwrN.js +428 -0
- package/lib/chunks/capture-BhdvHwrN.js.map +1 -0
- package/lib/chunks/issues-DBsPh6ek.js +55 -0
- package/lib/chunks/issues-DBsPh6ek.js.map +1 -0
- package/lib/chunks/mime-2SUyOolJ.js +62 -0
- package/lib/chunks/mime-2SUyOolJ.js.map +1 -0
- package/lib/chunks/paths-Bh6UsRI9.js +177 -0
- package/lib/chunks/paths-Bh6UsRI9.js.map +1 -0
- package/lib/config/load.d.ts +123 -0
- package/lib/config/load.d.ts.map +1 -0
- package/lib/config/types.d.ts +136 -0
- package/lib/config/types.d.ts.map +1 -0
- package/lib/errors.d.ts +53 -0
- package/lib/errors.d.ts.map +1 -0
- package/lib/index.d.ts +23 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +807 -0
- package/lib/index.js.map +1 -0
- package/lib/model/capture.d.ts +37 -0
- package/lib/model/capture.d.ts.map +1 -0
- package/lib/model/index.d.ts +13 -0
- package/lib/model/index.d.ts.map +1 -0
- package/lib/model/index.js +7 -0
- package/lib/model/index.js.map +1 -0
- package/lib/model/reader.d.ts +25 -0
- package/lib/model/reader.d.ts.map +1 -0
- package/lib/model/tree.d.ts +81 -0
- package/lib/model/tree.d.ts.map +1 -0
- package/lib/model/types.d.ts +166 -0
- package/lib/model/types.d.ts.map +1 -0
- package/lib/render/admonitions.d.ts +26 -0
- package/lib/render/admonitions.d.ts.map +1 -0
- package/lib/render/blocks.d.ts +39 -0
- package/lib/render/blocks.d.ts.map +1 -0
- package/lib/render/code.d.ts +37 -0
- package/lib/render/code.d.ts.map +1 -0
- package/lib/render/components.d.ts +49 -0
- package/lib/render/components.d.ts.map +1 -0
- package/lib/render/excerpt.d.ts +9 -0
- package/lib/render/excerpt.d.ts.map +1 -0
- package/lib/render/images.d.ts +58 -0
- package/lib/render/images.d.ts.map +1 -0
- package/lib/render/index.d.ts +80 -0
- package/lib/render/index.d.ts.map +1 -0
- package/lib/render/index.js +1336 -0
- package/lib/render/index.js.map +1 -0
- package/lib/render/inline.d.ts +25 -0
- package/lib/render/inline.d.ts.map +1 -0
- package/lib/render/links.d.ts +45 -0
- package/lib/render/links.d.ts.map +1 -0
- package/lib/render/mdx.d.ts +37 -0
- package/lib/render/mdx.d.ts.map +1 -0
- package/lib/render/page.d.ts +85 -0
- package/lib/render/page.d.ts.map +1 -0
- package/lib/render/parse.d.ts +27 -0
- package/lib/render/parse.d.ts.map +1 -0
- package/lib/render/references.d.ts +20 -0
- package/lib/render/references.d.ts.map +1 -0
- package/lib/render/renderers.d.ts +57 -0
- package/lib/render/renderers.d.ts.map +1 -0
- package/lib/render/slug.d.ts +34 -0
- package/lib/render/slug.d.ts.map +1 -0
- package/lib/render/theme.d.ts +62 -0
- package/lib/render/theme.d.ts.map +1 -0
- package/lib/sync/artifacts.d.ts +46 -0
- package/lib/sync/artifacts.d.ts.map +1 -0
- package/lib/sync/plan.d.ts +59 -0
- package/lib/sync/plan.d.ts.map +1 -0
- package/lib/sync/run.d.ts +39 -0
- package/lib/sync/run.d.ts.map +1 -0
- package/lib/target/index.d.ts +8 -0
- package/lib/target/index.d.ts.map +1 -0
- package/lib/target/index.js +2 -0
- package/lib/target/index.js.map +1 -0
- package/lib/target/target.d.ts +152 -0
- package/lib/target/target.d.ts.map +1 -0
- package/lib/util/hash.d.ts +10 -0
- package/lib/util/hash.d.ts.map +1 -0
- package/lib/util/index.d.ts +14 -0
- package/lib/util/index.d.ts.map +1 -0
- package/lib/util/index.js +7 -0
- package/lib/util/index.js.map +1 -0
- package/lib/util/issues.d.ts +52 -0
- package/lib/util/issues.d.ts.map +1 -0
- package/lib/util/mime.d.ts +16 -0
- package/lib/util/mime.d.ts.map +1 -0
- package/lib/util/paths.d.ts +42 -0
- package/lib/util/paths.d.ts.map +1 -0
- package/lib/version.d.ts +6 -0
- package/lib/version.d.ts.map +1 -0
- package/package.json +43 -0
- package/src/config/load.ts +387 -0
- package/src/config/types.ts +137 -0
- package/src/errors.ts +77 -0
- package/src/globals.d.ts +10 -0
- package/src/index.ts +31 -0
- package/src/model/capture.ts +92 -0
- package/src/model/index.ts +25 -0
- package/src/model/reader.ts +37 -0
- package/src/model/tree.ts +416 -0
- package/src/model/types.ts +167 -0
- package/src/render/admonitions.ts +74 -0
- package/src/render/blocks.ts +79 -0
- package/src/render/code.ts +112 -0
- package/src/render/components.ts +230 -0
- package/src/render/excerpt.ts +16 -0
- package/src/render/images.ts +113 -0
- package/src/render/index.ts +129 -0
- package/src/render/inline.ts +62 -0
- package/src/render/links.ts +94 -0
- package/src/render/mdx.ts +178 -0
- package/src/render/page.ts +289 -0
- package/src/render/parse.ts +65 -0
- package/src/render/references.ts +69 -0
- package/src/render/renderers.ts +346 -0
- package/src/render/slug.ts +63 -0
- package/src/render/theme.ts +92 -0
- package/src/sync/artifacts.ts +70 -0
- package/src/sync/plan.ts +69 -0
- package/src/sync/run.ts +641 -0
- package/src/target/index.ts +18 -0
- package/src/target/target.ts +146 -0
- package/src/util/hash.ts +14 -0
- package/src/util/index.ts +23 -0
- package/src/util/issues.ts +86 -0
- package/src/util/mime.ts +47 -0
- package/src/util/paths.ts +101 -0
- package/src/version.ts +31 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown block nodes to Gutenberg blocks.
|
|
3
|
+
*
|
|
4
|
+
* One function per node type, dispatched from `renderBlock`. Everything that
|
|
5
|
+
* cannot be represented is reported as an issue; nothing is dropped quietly.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
Blockquote,
|
|
10
|
+
Code,
|
|
11
|
+
Heading,
|
|
12
|
+
Image,
|
|
13
|
+
List,
|
|
14
|
+
ListItem,
|
|
15
|
+
Paragraph,
|
|
16
|
+
PhrasingContent,
|
|
17
|
+
Root,
|
|
18
|
+
RootContent,
|
|
19
|
+
Table,
|
|
20
|
+
} from 'mdast';
|
|
21
|
+
import type { ContainerDirective } from 'mdast-util-directive';
|
|
22
|
+
import { toHast } from 'mdast-util-to-hast';
|
|
23
|
+
import { escapeText, joinBlocks, serializeBlock } from './blocks';
|
|
24
|
+
import { escapeShortcodesInHast, hastToHtml, renderInline } from './inline';
|
|
25
|
+
import { headingIdFor, type Slugger } from './slug';
|
|
26
|
+
import { renderCode } from './code';
|
|
27
|
+
import { renderAdmonition, DEFAULT_ADMONITION_KEYWORDS } from './admonitions';
|
|
28
|
+
import { renderJsxBlock, type JsxElement } from './components';
|
|
29
|
+
import type { Theme } from './theme';
|
|
30
|
+
import type { IssueCollector } from '../util/issues';
|
|
31
|
+
|
|
32
|
+
/** Everything a block renderer needs that is not the node itself. */
|
|
33
|
+
export interface RenderContext {
|
|
34
|
+
/** Class names and strings. */
|
|
35
|
+
theme: Theme;
|
|
36
|
+
/** The document's heading slugger. */
|
|
37
|
+
slugger: Slugger;
|
|
38
|
+
/** Whether Docusaurus keeps heading case when generating anchors. */
|
|
39
|
+
maintainCase: boolean;
|
|
40
|
+
/** Directive names that mean "admonition" on this site. */
|
|
41
|
+
admonitionKeywords: Set<string>;
|
|
42
|
+
/** Where problems are recorded. */
|
|
43
|
+
issues: IssueCollector;
|
|
44
|
+
/** Source file, for issue positions. */
|
|
45
|
+
file?: string | undefined;
|
|
46
|
+
/** The markdown the tree was parsed from, for reproducing unhandled nodes. */
|
|
47
|
+
source: string;
|
|
48
|
+
/** Media already uploaded, keyed by the URL as written in the source. */
|
|
49
|
+
media?: Map<string, { id: number; url: string }> | undefined;
|
|
50
|
+
/** What to do about JSX with no translation. */
|
|
51
|
+
onUnknownJsx?: 'report' | 'placeholder' | 'error';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A context with the defaults a caller usually wants. */
|
|
55
|
+
export function createRenderContext(
|
|
56
|
+
init: Partial<RenderContext> & Pick<RenderContext, 'theme' | 'slugger' | 'issues'>,
|
|
57
|
+
): RenderContext {
|
|
58
|
+
return {
|
|
59
|
+
maintainCase: false,
|
|
60
|
+
admonitionKeywords: new Set(DEFAULT_ADMONITION_KEYWORDS),
|
|
61
|
+
source: '',
|
|
62
|
+
...init,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** True when a list only holds shapes core's list block can carry. */
|
|
67
|
+
function listIsSimple(list: List): boolean {
|
|
68
|
+
return list.children.every(
|
|
69
|
+
(item) =>
|
|
70
|
+
item.type === 'listItem' &&
|
|
71
|
+
item.children.every(
|
|
72
|
+
(child, index) => (child.type === 'paragraph' && index === 0) || child.type === 'list',
|
|
73
|
+
),
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function renderListItem(item: ListItem, ctx: RenderContext): string {
|
|
78
|
+
const parts: string[] = [];
|
|
79
|
+
for (const child of item.children) {
|
|
80
|
+
if (child.type === 'paragraph') parts.push(renderInline(child.children));
|
|
81
|
+
else if (child.type === 'list') parts.push(renderList(child, ctx));
|
|
82
|
+
}
|
|
83
|
+
const marker = item.checked === true ? '☑ ' : item.checked === false ? '☐ ' : '';
|
|
84
|
+
return serializeBlock('list-item', undefined, `<li>${marker}${parts.join('')}</li>`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function renderList(node: List, ctx: RenderContext): string {
|
|
88
|
+
const tag = node.ordered ? 'ol' : 'ul';
|
|
89
|
+
const attributes: Record<string, unknown> = {};
|
|
90
|
+
if (node.ordered) attributes['ordered'] = true;
|
|
91
|
+
const start = node.ordered && typeof node.start === 'number' && node.start !== 1 ? node.start : null;
|
|
92
|
+
if (start !== null) attributes['start'] = start;
|
|
93
|
+
|
|
94
|
+
const items = node.children.map((item) => renderListItem(item as ListItem, ctx)).join('\n\n');
|
|
95
|
+
const open = `<${tag}${start !== null ? ` start="${start}"` : ''} class="wp-block-list">`;
|
|
96
|
+
return serializeBlock('list', attributes, `${open}${items}</${tag}>`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function renderTable(node: Table): string {
|
|
100
|
+
const align = node.align ?? [];
|
|
101
|
+
const cell = (content: { children?: PhrasingContent[] }, index: number, header: boolean): string => {
|
|
102
|
+
const tag = header ? 'th' : 'td';
|
|
103
|
+
const alignment = align[index];
|
|
104
|
+
const attrs = alignment ? ` class="has-text-align-${alignment}" data-align="${alignment}"` : '';
|
|
105
|
+
return `<${tag}${attrs}>${renderInline(content.children ?? [])}</${tag}>`;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const [head, ...body] = node.children;
|
|
109
|
+
const thead = head
|
|
110
|
+
? `<thead><tr>${head.children.map((c, i) => cell(c, i, true)).join('')}</tr></thead>`
|
|
111
|
+
: '';
|
|
112
|
+
const tbody = body.length
|
|
113
|
+
? `<tbody>${body
|
|
114
|
+
.map((row) => `<tr>${row.children.map((c, i) => cell(c, i, false)).join('')}</tr>`)
|
|
115
|
+
.join('')}</tbody>`
|
|
116
|
+
: '';
|
|
117
|
+
|
|
118
|
+
return serializeBlock(
|
|
119
|
+
'table',
|
|
120
|
+
{ hasFixedLayout: false },
|
|
121
|
+
`<figure class="wp-block-table"><table>${thead}${tbody}</table></figure>`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** A paragraph holding nothing but one image becomes an image block. */
|
|
126
|
+
function loneImage(node: Paragraph): Image | undefined {
|
|
127
|
+
const meaningful = node.children.filter(
|
|
128
|
+
(child) => !(child.type === 'text' && child.value.trim() === ''),
|
|
129
|
+
);
|
|
130
|
+
return meaningful.length === 1 && meaningful[0]!.type === 'image'
|
|
131
|
+
? (meaningful[0] as Image)
|
|
132
|
+
: undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function renderImageBlock(node: Image, ctx: RenderContext): string {
|
|
136
|
+
const uploaded = ctx.media?.get(node.url);
|
|
137
|
+
const src = uploaded?.url ?? node.url;
|
|
138
|
+
const attributes: Record<string, unknown> = { sizeSlug: 'large', linkDestination: 'none' };
|
|
139
|
+
if (uploaded) attributes['id'] = uploaded.id;
|
|
140
|
+
|
|
141
|
+
const classes = `wp-block-image size-large`;
|
|
142
|
+
const imgClass = uploaded ? ` class="wp-image-${uploaded.id}"` : '';
|
|
143
|
+
const caption = node.title
|
|
144
|
+
? `<figcaption class="wp-element-caption">${escapeText(node.title)}</figcaption>`
|
|
145
|
+
: '';
|
|
146
|
+
|
|
147
|
+
return serializeBlock(
|
|
148
|
+
'image',
|
|
149
|
+
attributes,
|
|
150
|
+
`<figure class="${classes}"><img src="${escapeText(src)}" alt="${escapeText(node.alt ?? '')}"${imgClass}/>${caption}</figure>`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Render one top-level markdown node.
|
|
156
|
+
*
|
|
157
|
+
* @param node The node.
|
|
158
|
+
* @param ctx Theme, slugger and issue collector.
|
|
159
|
+
*/
|
|
160
|
+
export function renderBlock(node: RootContent, ctx: RenderContext): string {
|
|
161
|
+
switch (node.type) {
|
|
162
|
+
case 'paragraph': {
|
|
163
|
+
const image = loneImage(node as Paragraph);
|
|
164
|
+
if (image) return renderImageBlock(image, ctx);
|
|
165
|
+
const html = renderInline((node as Paragraph).children);
|
|
166
|
+
if (html.trim() === '') return '';
|
|
167
|
+
return serializeBlock('paragraph', undefined, `<p>${html}</p>`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
case 'heading': {
|
|
171
|
+
const heading = node as Heading;
|
|
172
|
+
const { id, children } = headingIdFor(heading, ctx.slugger, ctx.maintainCase);
|
|
173
|
+
const level = heading.depth;
|
|
174
|
+
const attributes = level === 2 ? undefined : { level };
|
|
175
|
+
return serializeBlock(
|
|
176
|
+
'heading',
|
|
177
|
+
attributes,
|
|
178
|
+
`<h${level} class="wp-block-heading" id="${id}">${renderInline(children)}</h${level}>`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
case 'list': {
|
|
183
|
+
const list = node as List;
|
|
184
|
+
if (listIsSimple(list)) return renderList(list, ctx);
|
|
185
|
+
ctx.issues.add({
|
|
186
|
+
code: 'list-not-representable',
|
|
187
|
+
severity: 'info',
|
|
188
|
+
message: 'A list item holds blocks the WordPress list block cannot carry; kept as HTML.',
|
|
189
|
+
file: ctx.file,
|
|
190
|
+
line: node.position?.start.line,
|
|
191
|
+
});
|
|
192
|
+
const hast = toHast(list, { allowDangerousHtml: true });
|
|
193
|
+
const escaped = hast ? escapeShortcodesInHast([hast as never]) : [];
|
|
194
|
+
return serializeBlock('html', undefined, hastToHtml(escaped));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
case 'code':
|
|
198
|
+
return renderCode(node as Code, ctx.theme, ctx.issues, ctx.file);
|
|
199
|
+
|
|
200
|
+
case 'blockquote': {
|
|
201
|
+
const inner = joinBlocks(
|
|
202
|
+
(node as Blockquote).children.map((child) => renderBlock(child, ctx)),
|
|
203
|
+
);
|
|
204
|
+
return serializeBlock('quote', undefined, `<blockquote class="wp-block-quote">${inner}</blockquote>`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
case 'table':
|
|
208
|
+
return renderTable(node as Table);
|
|
209
|
+
|
|
210
|
+
case 'thematicBreak':
|
|
211
|
+
return serializeBlock(
|
|
212
|
+
'separator',
|
|
213
|
+
undefined,
|
|
214
|
+
'<hr class="wp-block-separator has-alpha-channel-opacity"/>',
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
case 'containerDirective': {
|
|
218
|
+
const directive = node as ContainerDirective;
|
|
219
|
+
const name = String(directive.name ?? '').toLowerCase();
|
|
220
|
+
if (ctx.admonitionKeywords.has(name)) {
|
|
221
|
+
return renderAdmonition(directive, ctx.theme, (child) => renderBlock(child, ctx));
|
|
222
|
+
}
|
|
223
|
+
ctx.issues.add({
|
|
224
|
+
code: 'directive-unknown',
|
|
225
|
+
severity: 'warning',
|
|
226
|
+
message: `":::${directive.name}" is not an admonition on this site; its content was kept without the wrapper.`,
|
|
227
|
+
file: ctx.file,
|
|
228
|
+
line: node.position?.start.line,
|
|
229
|
+
});
|
|
230
|
+
return joinBlocks((directive.children as RootContent[]).map((child) => renderBlock(child, ctx)));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
case 'mdxJsxFlowElement': {
|
|
234
|
+
const element = node as unknown as JsxElement;
|
|
235
|
+
const rendered = renderJsxBlock(element, {
|
|
236
|
+
theme: ctx.theme,
|
|
237
|
+
issues: ctx.issues,
|
|
238
|
+
source: ctx.source,
|
|
239
|
+
file: ctx.file,
|
|
240
|
+
renderChild: (child) => renderBlock(child, ctx),
|
|
241
|
+
});
|
|
242
|
+
if (rendered !== undefined) return rendered;
|
|
243
|
+
ctx.issues.add({
|
|
244
|
+
code: 'mdx-unknown-component',
|
|
245
|
+
severity: ctx.onUnknownJsx === 'error' ? 'error' : 'warning',
|
|
246
|
+
message: `<${element.name ?? 'fragment'}> is a React component, which a page cannot run, so it was left out.`,
|
|
247
|
+
file: ctx.file,
|
|
248
|
+
line: node.position?.start.line,
|
|
249
|
+
column: node.position?.start.column,
|
|
250
|
+
});
|
|
251
|
+
return ctx.onUnknownJsx === 'placeholder'
|
|
252
|
+
? serializeBlock('html', undefined, `<!-- pterodoc: <${element.name ?? 'fragment'}> omitted -->`)
|
|
253
|
+
: '';
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
case 'html': {
|
|
257
|
+
const value = (node as { value: string }).value;
|
|
258
|
+
// Comments carry no content; Docusaurus hides them too.
|
|
259
|
+
if (/^\s*<!--[\s\S]*-->\s*$/.test(value)) return '';
|
|
260
|
+
return serializeBlock('html', undefined, value);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
case 'image':
|
|
264
|
+
return renderImageBlock(node as Image, ctx);
|
|
265
|
+
|
|
266
|
+
case 'definition':
|
|
267
|
+
case 'yaml':
|
|
268
|
+
case 'footnoteDefinition':
|
|
269
|
+
return '';
|
|
270
|
+
|
|
271
|
+
default: {
|
|
272
|
+
const position = node.position;
|
|
273
|
+
if (position && ctx.source) {
|
|
274
|
+
const raw = ctx.source.slice(position.start.offset ?? 0, position.end.offset ?? 0);
|
|
275
|
+
ctx.issues.add({
|
|
276
|
+
code: 'node-unhandled',
|
|
277
|
+
severity: 'warning',
|
|
278
|
+
message: `A "${node.type}" node has no WordPress equivalent; its source was kept verbatim.`,
|
|
279
|
+
file: ctx.file,
|
|
280
|
+
line: position.start.line,
|
|
281
|
+
});
|
|
282
|
+
return serializeBlock('paragraph', undefined, `<p>${escapeText(raw)}</p>`);
|
|
283
|
+
}
|
|
284
|
+
ctx.issues.add({
|
|
285
|
+
code: 'node-unhandled',
|
|
286
|
+
severity: 'warning',
|
|
287
|
+
message: `A "${node.type}" node has no WordPress equivalent and was skipped.`,
|
|
288
|
+
file: ctx.file,
|
|
289
|
+
});
|
|
290
|
+
return '';
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Render a document body.
|
|
297
|
+
*
|
|
298
|
+
* @param root The parsed document.
|
|
299
|
+
* @param ctx Theme, slugger and issue collector.
|
|
300
|
+
* @param options `dedupeTitle` drops a leading H1 that repeats the page title.
|
|
301
|
+
*/
|
|
302
|
+
export function renderBody(
|
|
303
|
+
root: Root,
|
|
304
|
+
ctx: RenderContext,
|
|
305
|
+
options: { dedupeTitle?: boolean } = {},
|
|
306
|
+
): { body: string; firstParagraph: string } {
|
|
307
|
+
const children = [...root.children];
|
|
308
|
+
|
|
309
|
+
if (options.dedupeTitle !== false) {
|
|
310
|
+
// The theme prints the page title, so a body that opens with the same
|
|
311
|
+
// heading would show it twice. Skipping anything that renders to nothing
|
|
312
|
+
// first means a comment or a stripped import cannot hide the heading.
|
|
313
|
+
const firstVisible = children.findIndex(
|
|
314
|
+
(child) =>
|
|
315
|
+
!(child.type === 'html' && /^\s*<!--[\s\S]*-->\s*$/.test(child.value)) &&
|
|
316
|
+
child.type !== 'yaml' &&
|
|
317
|
+
child.type !== 'definition',
|
|
318
|
+
);
|
|
319
|
+
const candidate = firstVisible === -1 ? undefined : children[firstVisible];
|
|
320
|
+
if (candidate && candidate.type === 'heading' && candidate.depth === 1) {
|
|
321
|
+
children.splice(firstVisible, 1);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const firstParagraph = children.find((child) => child.type === 'paragraph');
|
|
326
|
+
const body = joinBlocks(children.map((child) => renderBlock(child, ctx)));
|
|
327
|
+
return {
|
|
328
|
+
body,
|
|
329
|
+
firstParagraph: firstParagraph ? plainText(firstParagraph as Paragraph) : '',
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Plain text of a node, for excerpts. */
|
|
334
|
+
function plainText(node: Paragraph): string {
|
|
335
|
+
const parts: string[] = [];
|
|
336
|
+
const walk = (child: { type: string; value?: string; children?: unknown[] }): void => {
|
|
337
|
+
if (typeof child.value === 'string' && (child.type === 'text' || child.type === 'inlineCode')) {
|
|
338
|
+
parts.push(child.value);
|
|
339
|
+
}
|
|
340
|
+
if (Array.isArray(child.children)) {
|
|
341
|
+
for (const grandChild of child.children) walk(grandChild as never);
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
walk(node as never);
|
|
345
|
+
return parts.join('');
|
|
346
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heading identifiers.
|
|
3
|
+
*
|
|
4
|
+
* The anchors have to match the ones Docusaurus generated, or every in-page
|
|
5
|
+
* link that survived the move would break. That is why `github-slugger` is
|
|
6
|
+
* pinned to the major Docusaurus itself depends on.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import GithubSlugger from 'github-slugger';
|
|
10
|
+
import { toString as mdastToString } from 'mdast-util-to-string';
|
|
11
|
+
import type { Heading, PhrasingContent, Text } from 'mdast';
|
|
12
|
+
|
|
13
|
+
/** A slugger scoped to one document, so repeated headings get `-1`, `-2` suffixes. */
|
|
14
|
+
export type Slugger = InstanceType<typeof GithubSlugger>;
|
|
15
|
+
|
|
16
|
+
/** Start a fresh slugger for a document. */
|
|
17
|
+
export function createSlugger(): Slugger {
|
|
18
|
+
return new GithubSlugger();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A heading's identifier and the content that remains once the id syntax is removed. */
|
|
22
|
+
export interface HeadingId {
|
|
23
|
+
/** The anchor for this heading. */
|
|
24
|
+
id: string;
|
|
25
|
+
/** The heading's children with any explicit `{#id}` stripped. */
|
|
26
|
+
children: PhrasingContent[];
|
|
27
|
+
/** Plain text of the heading, for a table of contents or a title. */
|
|
28
|
+
text: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Work out a heading's identifier.
|
|
33
|
+
*
|
|
34
|
+
* Docusaurus lets an explicit `{#id}` override the generated slug. Both paths
|
|
35
|
+
* feed the same slugger so that a later duplicate is still disambiguated.
|
|
36
|
+
*
|
|
37
|
+
* @param heading The heading node.
|
|
38
|
+
* @param slugger The document's slugger.
|
|
39
|
+
* @param maintainCase Whether Docusaurus is configured to keep heading case.
|
|
40
|
+
*/
|
|
41
|
+
export function headingIdFor(heading: Heading, slugger: Slugger, maintainCase = false): HeadingId {
|
|
42
|
+
const children = heading.children.map((child) => ({ ...child })) as PhrasingContent[];
|
|
43
|
+
const last = children[children.length - 1];
|
|
44
|
+
let explicit = '';
|
|
45
|
+
|
|
46
|
+
if (last && last.type === 'text') {
|
|
47
|
+
const text = last as Text;
|
|
48
|
+
const match = /\s*\{#([^}]+)\}\s*$/.exec(text.value);
|
|
49
|
+
if (match) {
|
|
50
|
+
explicit = match[1]!.trim();
|
|
51
|
+
text.value = text.value.slice(0, match.index).replace(/\s+$/, '');
|
|
52
|
+
if (text.value === '') children.pop();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const text = mdastToString({ type: 'root', children } as never);
|
|
57
|
+
if (explicit) {
|
|
58
|
+
// Register it so a later generated slug cannot collide with it.
|
|
59
|
+
slugger.slug(explicit, true);
|
|
60
|
+
return { id: explicit, children, text };
|
|
61
|
+
}
|
|
62
|
+
return { id: slugger.slug(text, maintainCase), children, text };
|
|
63
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every class name and every human-readable string the renderer emits.
|
|
3
|
+
*
|
|
4
|
+
* Kept in one place so a site can restyle the output without patching the
|
|
5
|
+
* renderer, and so a non-English site can translate it.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Human-readable strings, with `{placeholders}` substituted at use. */
|
|
9
|
+
export interface Strings {
|
|
10
|
+
/** Heading above a generated list of child pages. */
|
|
11
|
+
indexHeading: string;
|
|
12
|
+
/** Separator between breadcrumb entries. */
|
|
13
|
+
breadcrumbSeparator: string;
|
|
14
|
+
/** Link to the previous page; `{title}` is the page's title. */
|
|
15
|
+
previous: string;
|
|
16
|
+
/** Link to the next page; `{title}` is the page's title. */
|
|
17
|
+
next: string;
|
|
18
|
+
/** Fallback title for the documentation root. */
|
|
19
|
+
documentation: string;
|
|
20
|
+
/** Summary for a section page; `{count}` is the number of children. */
|
|
21
|
+
pageCount: string;
|
|
22
|
+
/** Singular form of `pageCount`. */
|
|
23
|
+
pageCountOne: string;
|
|
24
|
+
/** Banner on a version that is not the current one; `{label}` is the version label. */
|
|
25
|
+
versionBanner: string;
|
|
26
|
+
/** Placeholder left where content could not be represented; `{what}` names it. */
|
|
27
|
+
unsupportedNotice: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The strings used when a site configures none. */
|
|
31
|
+
export const DEFAULT_STRINGS: Strings = {
|
|
32
|
+
indexHeading: 'In this section',
|
|
33
|
+
breadcrumbSeparator: ' › ',
|
|
34
|
+
previous: '← {title}',
|
|
35
|
+
next: '{title} →',
|
|
36
|
+
documentation: 'Documentation',
|
|
37
|
+
pageCount: '{count} pages',
|
|
38
|
+
pageCountOne: '1 page',
|
|
39
|
+
versionBanner: 'This is documentation for {label}.',
|
|
40
|
+
unsupportedNotice: 'Content omitted: {what}',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Which block vocabulary to emit.
|
|
45
|
+
*
|
|
46
|
+
* `core` is core blocks and nothing else, which is what a site with no plugin
|
|
47
|
+
* installed can display. `plugin` additionally carries instructions the
|
|
48
|
+
* WordPress plugin understands, in block-comment attributes only — never in the
|
|
49
|
+
* saved markup, so the two are the same content to WordPress either way.
|
|
50
|
+
*/
|
|
51
|
+
export type BlockVocabulary = 'core' | 'plugin';
|
|
52
|
+
|
|
53
|
+
/** Class names and strings, resolved for one run. */
|
|
54
|
+
export interface Theme {
|
|
55
|
+
/** Prefix on every generated class name. */
|
|
56
|
+
readonly classPrefix: string;
|
|
57
|
+
/** Whether the WordPress plugin is expected to be there. */
|
|
58
|
+
readonly blocks: BlockVocabulary;
|
|
59
|
+
/** The resolved strings. */
|
|
60
|
+
readonly strings: Strings;
|
|
61
|
+
/** A prefixed class name: `cls('docs-nav')` with prefix `x` gives `x-docs-nav`. */
|
|
62
|
+
cls(name: string): string;
|
|
63
|
+
/** A string with `{placeholders}` filled in. */
|
|
64
|
+
text(key: keyof Strings, values?: Record<string, string | number>): string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Build a theme.
|
|
69
|
+
*
|
|
70
|
+
* @param options Class prefix and any string overrides.
|
|
71
|
+
*/
|
|
72
|
+
export function createTheme(options: {
|
|
73
|
+
classPrefix?: string;
|
|
74
|
+
strings?: Partial<Strings>;
|
|
75
|
+
blocks?: BlockVocabulary;
|
|
76
|
+
} = {}): Theme {
|
|
77
|
+
const classPrefix = options.classPrefix ?? 'pterodoc';
|
|
78
|
+
const strings: Strings = { ...DEFAULT_STRINGS, ...options.strings };
|
|
79
|
+
return {
|
|
80
|
+
classPrefix,
|
|
81
|
+
blocks: options.blocks ?? 'core',
|
|
82
|
+
strings,
|
|
83
|
+
cls(name: string): string {
|
|
84
|
+
return `${classPrefix}-${name}`;
|
|
85
|
+
},
|
|
86
|
+
text(key, values = {}): string {
|
|
87
|
+
return strings[key].replace(/\{(\w+)\}/g, (match, name: string) =>
|
|
88
|
+
name in values ? String(values[name]) : match,
|
|
89
|
+
);
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a run leaves behind.
|
|
3
|
+
*
|
|
4
|
+
* The rendered pages and the plan are how a change is reviewed before it is
|
|
5
|
+
* published, and how a run that failed part way through is diagnosed.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs/promises';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import type { RenderedPage } from '../target/target';
|
|
11
|
+
import type { Plan } from './plan';
|
|
12
|
+
|
|
13
|
+
/** A page, with where it came from and where it is going. */
|
|
14
|
+
export interface ManifestEntry {
|
|
15
|
+
path: string;
|
|
16
|
+
title: string;
|
|
17
|
+
slug: string;
|
|
18
|
+
parent: string | null;
|
|
19
|
+
menuOrder: number;
|
|
20
|
+
locale: string;
|
|
21
|
+
versionName: string;
|
|
22
|
+
file: string | null;
|
|
23
|
+
href: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Everything written to the output directory. */
|
|
27
|
+
export interface Artifacts {
|
|
28
|
+
pages: { page: RenderedPage; locale: string; versionName: string }[];
|
|
29
|
+
manifest: ManifestEntry[];
|
|
30
|
+
media: { hash: string; file: string; url: string | null; uploaded: boolean }[];
|
|
31
|
+
plan: Plan;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Where one page's rendered body is written. */
|
|
35
|
+
function pageFile(outDir: string, locale: string, versionName: string, treePath: string): string {
|
|
36
|
+
const parts = [outDir, 'pages', locale, versionName];
|
|
37
|
+
const name = treePath === '' ? 'index.html' : `${treePath}.html`;
|
|
38
|
+
return path.join(...parts, name);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Write the rendered pages, the manifest, the media map and the plan.
|
|
43
|
+
*
|
|
44
|
+
* Never called from a `finally`: a failure to write the artefacts must not
|
|
45
|
+
* replace the error that actually stopped the run.
|
|
46
|
+
*/
|
|
47
|
+
export async function writeArtifacts(
|
|
48
|
+
outDir: string,
|
|
49
|
+
artifacts: Artifacts,
|
|
50
|
+
options: { writePages: boolean },
|
|
51
|
+
): Promise<void> {
|
|
52
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
53
|
+
|
|
54
|
+
if (options.writePages) {
|
|
55
|
+
await fs.rm(path.join(outDir, 'pages'), { recursive: true, force: true });
|
|
56
|
+
for (const { page, locale, versionName } of artifacts.pages) {
|
|
57
|
+
const file = pageFile(outDir, locale, versionName, page.path);
|
|
58
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
59
|
+
await fs.writeFile(file, page.content, 'utf8');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const write = async (name: string, value: unknown): Promise<void> => {
|
|
64
|
+
await fs.writeFile(path.join(outDir, name), `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
await write('manifest.json', artifacts.manifest);
|
|
68
|
+
await write('media.json', artifacts.media);
|
|
69
|
+
await write('plan.json', artifacts.plan);
|
|
70
|
+
}
|
package/src/sync/plan.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** What a run intends to do, and what it did. */
|
|
2
|
+
|
|
3
|
+
import type { Issue } from '../util/issues';
|
|
4
|
+
|
|
5
|
+
/** One thing a run did, or would do. */
|
|
6
|
+
export interface Action {
|
|
7
|
+
/** What kind of change this is. */
|
|
8
|
+
op:
|
|
9
|
+
| 'create-root'
|
|
10
|
+
| 'create'
|
|
11
|
+
| 'update'
|
|
12
|
+
| 'unchanged'
|
|
13
|
+
| 'prune'
|
|
14
|
+
| 'upload-media'
|
|
15
|
+
| 'reuse-media';
|
|
16
|
+
/** Full path within the published tree, for every operation. */
|
|
17
|
+
path: string;
|
|
18
|
+
/** Target id, once it is known. */
|
|
19
|
+
id?: number | null;
|
|
20
|
+
/** Fields that differ, for an update. */
|
|
21
|
+
changed?: string[];
|
|
22
|
+
/** Source file, when the action belongs to a document. */
|
|
23
|
+
file?: string | null;
|
|
24
|
+
/** Locale this action belongs to. */
|
|
25
|
+
locale?: string;
|
|
26
|
+
/** Version this action belongs to. */
|
|
27
|
+
versionName?: string;
|
|
28
|
+
/** Whether a removal was actually carried out. */
|
|
29
|
+
applied?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The record of a run. */
|
|
33
|
+
export interface Plan {
|
|
34
|
+
/** When the run started. */
|
|
35
|
+
generatedAt: string;
|
|
36
|
+
/** Versions involved, for reproducing a report. */
|
|
37
|
+
versions: { pterodoc: string; docusaurus: string | null; node: string };
|
|
38
|
+
/** True when nothing was written. */
|
|
39
|
+
dryRun: boolean;
|
|
40
|
+
/** True when nothing was even read from the target. */
|
|
41
|
+
offline: boolean;
|
|
42
|
+
/** The target site, when there is one. */
|
|
43
|
+
site: string | null;
|
|
44
|
+
/** Where the documentation was published. */
|
|
45
|
+
rootPath: string;
|
|
46
|
+
/** Locales published. */
|
|
47
|
+
locales: string[];
|
|
48
|
+
/** Versions published. */
|
|
49
|
+
versionNames: string[];
|
|
50
|
+
/** Everything the run did, or would do. */
|
|
51
|
+
actions: Action[];
|
|
52
|
+
/** Everything worth telling the user. */
|
|
53
|
+
issues: Issue[];
|
|
54
|
+
/** How many of each operation. */
|
|
55
|
+
summary: Record<string, number>;
|
|
56
|
+
/** How many requests were made. */
|
|
57
|
+
requests: number;
|
|
58
|
+
/** How many files still need uploading; non-zero only on a dry run. */
|
|
59
|
+
mediaPending: number;
|
|
60
|
+
/** Set when writing the artefacts itself failed. */
|
|
61
|
+
artifactError: string | null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Count the actions by operation. */
|
|
65
|
+
export function summarise(actions: Action[]): Record<string, number> {
|
|
66
|
+
const summary: Record<string, number> = {};
|
|
67
|
+
for (const action of actions) summary[action.op] = (summary[action.op] ?? 0) + 1;
|
|
68
|
+
return summary;
|
|
69
|
+
}
|