@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,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The site model: what the rest of pterodoc knows about a Docusaurus site.
|
|
3
|
+
*
|
|
4
|
+
* Every field here is derived from Docusaurus's own loaded state rather than
|
|
5
|
+
* re-read from disk, so ordering, permalinks, versions and locales are the
|
|
6
|
+
* ones the site itself would render.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { MarkdownFormat } from '../render';
|
|
10
|
+
|
|
11
|
+
/** A sidebar entry that points at a document. */
|
|
12
|
+
export interface SidebarDocItem {
|
|
13
|
+
type: 'doc' | 'ref';
|
|
14
|
+
id: string;
|
|
15
|
+
label?: string | undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** A sidebar entry that points at a URL. */
|
|
19
|
+
export interface SidebarLinkItem {
|
|
20
|
+
type: 'link';
|
|
21
|
+
href: string;
|
|
22
|
+
label: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** A sidebar entry that is raw markup. */
|
|
26
|
+
export interface SidebarHtmlItem {
|
|
27
|
+
type: 'html';
|
|
28
|
+
value: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** What a category's own page is, when it has one. */
|
|
32
|
+
export type SidebarCategoryLink =
|
|
33
|
+
| { type: 'doc'; id: string }
|
|
34
|
+
| { type: 'generated-index'; slug: string; permalink: string; title?: string; description?: string };
|
|
35
|
+
|
|
36
|
+
/** A sidebar entry that groups other entries. */
|
|
37
|
+
export interface SidebarCategoryItem {
|
|
38
|
+
type: 'category';
|
|
39
|
+
label: string;
|
|
40
|
+
items: SidebarItem[];
|
|
41
|
+
link?: SidebarCategoryLink | undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Any resolved sidebar entry. Autogenerated entries are already expanded. */
|
|
45
|
+
export type SidebarItem = SidebarDocItem | SidebarLinkItem | SidebarHtmlItem | SidebarCategoryItem;
|
|
46
|
+
|
|
47
|
+
/** Where a neighbouring page sits. */
|
|
48
|
+
export interface DocNeighbour {
|
|
49
|
+
title: string;
|
|
50
|
+
permalink: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** One document. */
|
|
54
|
+
export interface Doc {
|
|
55
|
+
/** Docusaurus's document id, unique within a version. */
|
|
56
|
+
id: string;
|
|
57
|
+
/** Name of the version this document belongs to. */
|
|
58
|
+
versionName: string;
|
|
59
|
+
/** Title, from front matter or the first heading. */
|
|
60
|
+
title: string;
|
|
61
|
+
/** Front matter description; may be empty. */
|
|
62
|
+
description: string;
|
|
63
|
+
/** Source path as Docusaurus records it, e.g. `@site/docs/intro.md`. */
|
|
64
|
+
sourceAliased: string;
|
|
65
|
+
/** Absolute path of the source file. */
|
|
66
|
+
sourceAbsolutePath: string;
|
|
67
|
+
/** Source path relative to the site directory, for messages. */
|
|
68
|
+
sourceRelativePath: string;
|
|
69
|
+
/** Directory of the source below the version's content path; `.` at the root. */
|
|
70
|
+
sourceDirName: string;
|
|
71
|
+
/** URL below the version, without the base URL. */
|
|
72
|
+
slug: string;
|
|
73
|
+
/** Full URL path, including base URL and version path. */
|
|
74
|
+
permalink: string;
|
|
75
|
+
/** Position in the published tree, relative to the version root; '' is the root. */
|
|
76
|
+
treePath: string;
|
|
77
|
+
/** Docusaurus excludes drafts from production builds. */
|
|
78
|
+
draft: boolean;
|
|
79
|
+
/** Unlisted documents are reachable but hidden from navigation. */
|
|
80
|
+
unlisted: boolean;
|
|
81
|
+
/** The document's front matter, as parsed by Docusaurus. */
|
|
82
|
+
frontMatter: Record<string, unknown>;
|
|
83
|
+
/** Sidebar this document belongs to, when it belongs to one. */
|
|
84
|
+
sidebarName?: string | undefined;
|
|
85
|
+
/** Position within an autogenerated slice. */
|
|
86
|
+
sidebarPosition?: number | undefined;
|
|
87
|
+
/** The previous page in sidebar order. */
|
|
88
|
+
previous?: DocNeighbour | undefined;
|
|
89
|
+
/** The next page in sidebar order. */
|
|
90
|
+
next?: DocNeighbour | undefined;
|
|
91
|
+
/** Tags, kept for a future taxonomy mapping. */
|
|
92
|
+
tags: { label: string; permalink: string }[];
|
|
93
|
+
/** Which flavour the body is written in. */
|
|
94
|
+
format: MarkdownFormat;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** One version of one docs instance. */
|
|
98
|
+
export interface DocsVersion {
|
|
99
|
+
/** Version name, e.g. `current` or `1.0.0`. */
|
|
100
|
+
name: string;
|
|
101
|
+
/** Human label, e.g. `Next` or `1.0.0`. */
|
|
102
|
+
label: string;
|
|
103
|
+
/** True for the version served at the docs root. */
|
|
104
|
+
isLast: boolean;
|
|
105
|
+
/** URL path prefix for this version, including the base URL. */
|
|
106
|
+
pathPrefix: string;
|
|
107
|
+
/** Absolute path of this version's content directory. */
|
|
108
|
+
contentPath: string;
|
|
109
|
+
/** Absolute path of the localised content directory, preferred for assets. */
|
|
110
|
+
contentPathLocalized: string;
|
|
111
|
+
/** Docusaurus's own judgement that this version is unmaintained or unreleased. */
|
|
112
|
+
banner: 'unmaintained' | 'unreleased' | null;
|
|
113
|
+
/** Whether Docusaurus asks search engines to ignore this version. */
|
|
114
|
+
noIndex: boolean;
|
|
115
|
+
/** Resolved sidebars, keyed by sidebar id. */
|
|
116
|
+
sidebars: Record<string, SidebarItem[]>;
|
|
117
|
+
/** Published documents, drafts already removed. */
|
|
118
|
+
docs: Doc[];
|
|
119
|
+
/** How many documents were dropped for being drafts. */
|
|
120
|
+
draftCount: number;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** One instance of the docs plugin. */
|
|
124
|
+
export interface DocsInstance {
|
|
125
|
+
/** Plugin instance id; `default` when the site names none. */
|
|
126
|
+
id: string;
|
|
127
|
+
/** Route base path this instance serves under. */
|
|
128
|
+
routeBasePath: string;
|
|
129
|
+
/** Content directory name, relative to the site. */
|
|
130
|
+
contentDirName: string;
|
|
131
|
+
/** Directive names this site treats as admonitions. */
|
|
132
|
+
admonitionKeywords: string[];
|
|
133
|
+
/** Whether the site renders breadcrumbs. */
|
|
134
|
+
breadcrumbs: boolean;
|
|
135
|
+
/** Versions, in the order Docusaurus loaded them. */
|
|
136
|
+
versions: DocsVersion[];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Everything pterodoc knows about a site, for one locale. */
|
|
140
|
+
export interface SiteModel {
|
|
141
|
+
/** Absolute path of the Docusaurus site directory. */
|
|
142
|
+
siteDir: string;
|
|
143
|
+
/** Site URL, without a path. */
|
|
144
|
+
url: string;
|
|
145
|
+
/** Base URL path, with leading and trailing slashes. */
|
|
146
|
+
baseUrl: string;
|
|
147
|
+
/** The site's trailing-slash policy; undefined means Docusaurus decides per URL. */
|
|
148
|
+
trailingSlash: boolean | undefined;
|
|
149
|
+
/** Locale this model was loaded for. */
|
|
150
|
+
locale: string;
|
|
151
|
+
/** The site's default locale. */
|
|
152
|
+
defaultLocale: string;
|
|
153
|
+
/** Every locale the site declares. */
|
|
154
|
+
locales: string[];
|
|
155
|
+
/** Markdown flavour the site defaults to. */
|
|
156
|
+
markdownFormat: 'md' | 'mdx' | 'detect';
|
|
157
|
+
/** Whether heading anchors keep their case. */
|
|
158
|
+
maintainCase: boolean;
|
|
159
|
+
/** The site's title, used when the docs root has no document of its own. */
|
|
160
|
+
siteTitle: string;
|
|
161
|
+
/** Absolute paths of the static directories, for resolving assets. */
|
|
162
|
+
staticDirs: string[];
|
|
163
|
+
/** Version of Docusaurus that produced this model. */
|
|
164
|
+
docusaurusVersion: string;
|
|
165
|
+
/** Docs plugin instances. */
|
|
166
|
+
instances: DocsInstance[];
|
|
167
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docusaurus admonitions.
|
|
3
|
+
*
|
|
4
|
+
* `:::note` and its siblings are container directives. WordPress has no
|
|
5
|
+
* equivalent block, so each becomes a group with stable classes a theme can
|
|
6
|
+
* style, opened by a bold label the way Docusaurus renders one.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ContainerDirective } from 'mdast-util-directive';
|
|
10
|
+
import type { RootContent } from 'mdast';
|
|
11
|
+
import { toString as mdastToString } from 'mdast-util-to-string';
|
|
12
|
+
import { joinBlocks, serializeBlock } from './blocks';
|
|
13
|
+
import { renderInline } from './inline';
|
|
14
|
+
import type { Theme } from './theme';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The keywords Docusaurus recognises when a site configures none.
|
|
18
|
+
*
|
|
19
|
+
* Taken from the docs plugin's own defaults rather than guessed: a site that
|
|
20
|
+
* writes `:::success` gets an admonition on Docusaurus and must get one here.
|
|
21
|
+
*/
|
|
22
|
+
export const DEFAULT_ADMONITION_KEYWORDS = [
|
|
23
|
+
'secondary',
|
|
24
|
+
'info',
|
|
25
|
+
'success',
|
|
26
|
+
'danger',
|
|
27
|
+
'note',
|
|
28
|
+
'tip',
|
|
29
|
+
'warning',
|
|
30
|
+
'important',
|
|
31
|
+
'caution',
|
|
32
|
+
] as const;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Render an admonition.
|
|
36
|
+
*
|
|
37
|
+
* @param node The container directive.
|
|
38
|
+
* @param theme Class names and strings.
|
|
39
|
+
* @param renderChild How to render each block inside the admonition.
|
|
40
|
+
*/
|
|
41
|
+
export function renderAdmonition(
|
|
42
|
+
node: ContainerDirective,
|
|
43
|
+
theme: Theme,
|
|
44
|
+
renderChild: (child: RootContent) => string,
|
|
45
|
+
): string {
|
|
46
|
+
const type = String(node.name ?? '').toLowerCase();
|
|
47
|
+
const children = [...(node.children as RootContent[])];
|
|
48
|
+
|
|
49
|
+
let label = type.charAt(0).toUpperCase() + type.slice(1);
|
|
50
|
+
const first = children[0] as { data?: { directiveLabel?: boolean } } | undefined;
|
|
51
|
+
if (first?.data?.directiveLabel) {
|
|
52
|
+
label = mdastToString(children[0] as never);
|
|
53
|
+
children.shift();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const base = theme.cls('admonition');
|
|
57
|
+
const className = `${base} ${base}-${type}`;
|
|
58
|
+
const titleClass = theme.cls('admonition-title');
|
|
59
|
+
|
|
60
|
+
const inner = joinBlocks([
|
|
61
|
+
serializeBlock(
|
|
62
|
+
'paragraph',
|
|
63
|
+
{ className: titleClass },
|
|
64
|
+
`<p class="${titleClass}"><strong>${renderInline([{ type: 'text', value: label }])}</strong></p>`,
|
|
65
|
+
),
|
|
66
|
+
...children.map(renderChild),
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
return serializeBlock(
|
|
70
|
+
'group',
|
|
71
|
+
{ className },
|
|
72
|
+
`<div class="wp-block-group ${className}">${inner}</div>`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gutenberg block serialisation.
|
|
3
|
+
*
|
|
4
|
+
* WordPress stores post content as HTML annotated with block comments, and the
|
|
5
|
+
* editor re-parses that markup and compares it with what the block's own save
|
|
6
|
+
* function would produce. Everything here therefore follows core's output
|
|
7
|
+
* closely: the attribute encoding, the class names and the whitespace.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Attributes a block carries in its opening comment. */
|
|
11
|
+
export type BlockAttributes = Record<string, unknown>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Encode block attributes the way WordPress encodes them, so the editor sees
|
|
15
|
+
* markup it would have written itself.
|
|
16
|
+
*/
|
|
17
|
+
export function serializeAttrs(attributes: BlockAttributes): string {
|
|
18
|
+
return JSON.stringify(attributes)
|
|
19
|
+
.replace(/--/g, '\\u002d\\u002d')
|
|
20
|
+
.replace(/</g, '\\u003c')
|
|
21
|
+
.replace(/>/g, '\\u003e')
|
|
22
|
+
.replace(/&/g, '\\u0026')
|
|
23
|
+
.replace(/\\"/g, '\\u0022');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Wrap inner markup in a block comment pair.
|
|
28
|
+
*
|
|
29
|
+
* @param name Block name without the `core/` prefix.
|
|
30
|
+
* @param attributes Omitted from the output when empty.
|
|
31
|
+
* @param inner The block's stored markup.
|
|
32
|
+
*/
|
|
33
|
+
export function serializeBlock(
|
|
34
|
+
name: string,
|
|
35
|
+
attributes: BlockAttributes | undefined,
|
|
36
|
+
inner: string,
|
|
37
|
+
): string {
|
|
38
|
+
const attrs =
|
|
39
|
+
attributes && Object.keys(attributes).length > 0 ? ` ${serializeAttrs(attributes)}` : '';
|
|
40
|
+
return `<!-- wp:${name}${attrs} -->\n${inner}\n<!-- /wp:${name} -->`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A dynamic block: it renders on the server and stores no inner markup.
|
|
45
|
+
*/
|
|
46
|
+
export function serializeVoidBlock(name: string, attributes?: BlockAttributes): string {
|
|
47
|
+
const attrs =
|
|
48
|
+
attributes && Object.keys(attributes).length > 0 ? ` ${serializeAttrs(attributes)}` : '';
|
|
49
|
+
return `<!-- wp:${name}${attrs} /-->`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Join sibling blocks the way WordPress does. */
|
|
53
|
+
export function joinBlocks(blocks: Array<string | undefined | null>): string {
|
|
54
|
+
return blocks.filter((block): block is string => typeof block === 'string' && block !== '').join('\n\n');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Escape text destined for a `<code>` element.
|
|
59
|
+
*
|
|
60
|
+
* `[` becomes an entity because WordPress expands shortcodes inside code as
|
|
61
|
+
* happily as anywhere else, and documentation is full of bracketed samples.
|
|
62
|
+
*/
|
|
63
|
+
export function escapeCode(text: string): string {
|
|
64
|
+
return text
|
|
65
|
+
.replace(/&/g, '&')
|
|
66
|
+
.replace(/</g, '<')
|
|
67
|
+
.replace(/>/g, '>')
|
|
68
|
+
.replace(/\[/g, '[');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Escape text for use in an HTML text node or attribute value. */
|
|
72
|
+
export function escapeText(text: string): string {
|
|
73
|
+
return String(text)
|
|
74
|
+
.replace(/&/g, '&')
|
|
75
|
+
.replace(/</g, '<')
|
|
76
|
+
.replace(/>/g, '>')
|
|
77
|
+
.replace(/"/g, '"')
|
|
78
|
+
.replace(/\[/g, '[');
|
|
79
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code blocks.
|
|
3
|
+
*
|
|
4
|
+
* Docusaurus carries extra instructions in the fence's metastring — a title,
|
|
5
|
+
* highlighted line ranges, line numbers. WordPress core's code block has
|
|
6
|
+
* nowhere to put them, so the title becomes a visible caption, line numbers
|
|
7
|
+
* become a class a theme can act on, and anything left over is reported rather
|
|
8
|
+
* than dropped in silence.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Code } from 'mdast';
|
|
12
|
+
import { escapeCode, joinBlocks, serializeBlock } from './blocks';
|
|
13
|
+
import type { Theme } from './theme';
|
|
14
|
+
import type { IssueCollector } from '../util/issues';
|
|
15
|
+
|
|
16
|
+
/** What a fence's metastring asked for. */
|
|
17
|
+
export interface CodeMeta {
|
|
18
|
+
/** Title shown above the block. */
|
|
19
|
+
title?: string;
|
|
20
|
+
/** True when the fence asked for line numbers. */
|
|
21
|
+
showLineNumbers: boolean;
|
|
22
|
+
/** Highlighted line ranges, exactly as written. */
|
|
23
|
+
highlight?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Read a fence's metastring.
|
|
28
|
+
*
|
|
29
|
+
* @param meta The text after the language on the opening fence.
|
|
30
|
+
*/
|
|
31
|
+
export function parseCodeMeta(meta: string | null | undefined): CodeMeta {
|
|
32
|
+
const source = meta ?? '';
|
|
33
|
+
const title = /(?:^|\s)title=(?:"([^"]*)"|'([^']*)'|([^\s]+))/.exec(source);
|
|
34
|
+
const highlight = /\{([\d,\s-]+)\}/.exec(source);
|
|
35
|
+
return {
|
|
36
|
+
...(title ? { title: title[1] ?? title[2] ?? title[3] ?? '' } : {}),
|
|
37
|
+
showLineNumbers: /(?:^|\s)showLineNumbers(?:\s|=|$)/.test(source),
|
|
38
|
+
...(highlight ? { highlight: highlight[1]!.trim() } : {}),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Render a fenced code block.
|
|
44
|
+
*
|
|
45
|
+
* @param node The code node.
|
|
46
|
+
* @param theme Class names and strings.
|
|
47
|
+
* @param issues Where unrepresentable instructions are reported.
|
|
48
|
+
* @param file Source file, for the issue's position.
|
|
49
|
+
*/
|
|
50
|
+
export function renderCode(
|
|
51
|
+
node: Code,
|
|
52
|
+
theme: Theme,
|
|
53
|
+
issues?: IssueCollector,
|
|
54
|
+
file?: string,
|
|
55
|
+
): string {
|
|
56
|
+
const lang = (node.lang ?? '').toLowerCase();
|
|
57
|
+
const meta = parseCodeMeta(node.meta);
|
|
58
|
+
|
|
59
|
+
if (lang === 'mermaid') {
|
|
60
|
+
// Entities are decoded again when mermaid reads the element's text, so
|
|
61
|
+
// escaping keeps `A[Start]` away from the shortcode parser without
|
|
62
|
+
// changing the diagram.
|
|
63
|
+
return serializeBlock('html', undefined, `<pre class="mermaid">${escapeCode(node.value)}</pre>`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const classes: string[] = [];
|
|
67
|
+
if (lang) classes.push(`language-${lang}`);
|
|
68
|
+
if (meta.showLineNumbers) classes.push(theme.cls('line-numbers'));
|
|
69
|
+
|
|
70
|
+
const className = classes.join(' ');
|
|
71
|
+
const preClasses = className ? `wp-block-code ${className}` : 'wp-block-code';
|
|
72
|
+
|
|
73
|
+
// Highlighted lines have no core equivalent, so with core blocks alone they
|
|
74
|
+
// can only be reported. The plugin can render them, and reads the range from
|
|
75
|
+
// the block comment — never from the markup, which stays exactly what core's
|
|
76
|
+
// own save function would have written.
|
|
77
|
+
const carried = theme.blocks === 'plugin' && meta.highlight !== undefined;
|
|
78
|
+
|
|
79
|
+
const attributes: Record<string, unknown> = {};
|
|
80
|
+
if (className) attributes['className'] = className;
|
|
81
|
+
if (carried) attributes['pterodocHighlight'] = meta.highlight;
|
|
82
|
+
|
|
83
|
+
const code = serializeBlock(
|
|
84
|
+
'code',
|
|
85
|
+
Object.keys(attributes).length > 0 ? attributes : undefined,
|
|
86
|
+
`<pre class="${preClasses}"><code>${escapeCode(node.value)}</code></pre>`,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
if (meta.highlight && !carried) {
|
|
90
|
+
issues?.add({
|
|
91
|
+
code: 'code-highlight-dropped',
|
|
92
|
+
severity: 'info',
|
|
93
|
+
message: `Highlighted lines {${meta.highlight}} have no WordPress equivalent and were not carried over. Install the pterodoc WordPress plugin and set render.blocks to 'plugin' to keep them.`,
|
|
94
|
+
file,
|
|
95
|
+
line: node.position?.start.line,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!meta.title) return code;
|
|
100
|
+
|
|
101
|
+
const caption = serializeBlock(
|
|
102
|
+
'paragraph',
|
|
103
|
+
{ className: theme.cls('code-title') },
|
|
104
|
+
`<p class="${theme.cls('code-title')}">${escapeCode(meta.title)}</p>`,
|
|
105
|
+
);
|
|
106
|
+
const groupClass = theme.cls('code-group');
|
|
107
|
+
return serializeBlock(
|
|
108
|
+
'group',
|
|
109
|
+
{ className: groupClass },
|
|
110
|
+
`<div class="wp-block-group ${groupClass}">${joinBlocks([caption, code])}</div>`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The MDX component table.
|
|
3
|
+
*
|
|
4
|
+
* Docusaurus ships a small set of components that documentation actually uses.
|
|
5
|
+
* Each one here has a WordPress block that carries the same meaning; anything
|
|
6
|
+
* else is reported by the lowering pass rather than guessed at.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { RootContent } from 'mdast';
|
|
10
|
+
import { escapeText, joinBlocks, serializeBlock } from './blocks';
|
|
11
|
+
import { escapeCode } from './blocks';
|
|
12
|
+
import type { Theme } from './theme';
|
|
13
|
+
import type { IssueCollector } from '../util/issues';
|
|
14
|
+
import { HTML_ELEMENTS } from './mdx';
|
|
15
|
+
|
|
16
|
+
/** A JSX element as mdast holds it. */
|
|
17
|
+
export interface JsxElement {
|
|
18
|
+
type: string;
|
|
19
|
+
name?: string | null;
|
|
20
|
+
attributes?: {
|
|
21
|
+
type: string;
|
|
22
|
+
name?: string;
|
|
23
|
+
value?: unknown;
|
|
24
|
+
}[];
|
|
25
|
+
children?: RootContent[];
|
|
26
|
+
position?: { start: { line: number; column: number; offset?: number }; end: { offset?: number } };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Read a string attribute, ignoring expression values we cannot evaluate. */
|
|
30
|
+
export function attribute(element: JsxElement, name: string): string | undefined {
|
|
31
|
+
for (const attr of element.attributes ?? []) {
|
|
32
|
+
if (attr.type !== 'mdxJsxAttribute' || attr.name !== name) continue;
|
|
33
|
+
if (typeof attr.value === 'string') return attr.value;
|
|
34
|
+
const value = attr.value as { type?: string; value?: string } | null;
|
|
35
|
+
if (value && typeof value.value === 'string') return value.value;
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** What a component renderer is given. */
|
|
42
|
+
export interface ComponentContext {
|
|
43
|
+
theme: Theme;
|
|
44
|
+
issues: IssueCollector;
|
|
45
|
+
source: string;
|
|
46
|
+
file?: string | undefined;
|
|
47
|
+
/** Render an ordinary markdown node. */
|
|
48
|
+
renderChild: (node: RootContent) => string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A details block, which is what a collapsible section becomes. */
|
|
52
|
+
function detailsBlock(summary: string, inner: string): string {
|
|
53
|
+
return serializeBlock(
|
|
54
|
+
'details',
|
|
55
|
+
undefined,
|
|
56
|
+
`<details class="wp-block-details"><summary>${escapeText(summary)}</summary>${inner}</details>`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Render `<TabItem>` as one collapsible section. */
|
|
61
|
+
function renderTabItem(element: JsxElement, ctx: ComponentContext): string {
|
|
62
|
+
const label = attribute(element, 'label') ?? attribute(element, 'value') ?? 'Tab';
|
|
63
|
+
const inner = joinBlocks((element.children ?? []).map(ctx.renderChild));
|
|
64
|
+
return detailsBlock(label, inner);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Render `<Tabs>`.
|
|
69
|
+
*
|
|
70
|
+
* Tabs need JavaScript that a published page does not have, so each tab
|
|
71
|
+
* becomes a collapsible section instead. All of the content survives, and it
|
|
72
|
+
* stays readable without any script at all.
|
|
73
|
+
*/
|
|
74
|
+
function renderTabs(element: JsxElement, ctx: ComponentContext): string {
|
|
75
|
+
const items = (element.children ?? []).filter(
|
|
76
|
+
(child) => (child as JsxElement).name === 'TabItem',
|
|
77
|
+
) as JsxElement[];
|
|
78
|
+
|
|
79
|
+
const rendered = items.map((item) => renderTabItem(item, ctx));
|
|
80
|
+
const className = ctx.theme.cls('tabs');
|
|
81
|
+
return serializeBlock(
|
|
82
|
+
'group',
|
|
83
|
+
{ className },
|
|
84
|
+
`<div class="wp-block-group ${className}">${joinBlocks(rendered)}</div>`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Render `<Details>` or a raw `<details>` element. */
|
|
89
|
+
function renderDetails(element: JsxElement, ctx: ComponentContext): string {
|
|
90
|
+
const children = [...(element.children ?? [])];
|
|
91
|
+
let summary = attribute(element, 'summary') ?? '';
|
|
92
|
+
|
|
93
|
+
const summaryIndex = children.findIndex((child) => (child as JsxElement).name === 'summary');
|
|
94
|
+
if (summaryIndex !== -1) {
|
|
95
|
+
const node = children[summaryIndex] as JsxElement;
|
|
96
|
+
summary = plainText(node, ctx.source);
|
|
97
|
+
children.splice(summaryIndex, 1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return detailsBlock(summary || 'Details', joinBlocks(children.map(ctx.renderChild)));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Render `<CodeBlock language="ts" title="x">`. */
|
|
104
|
+
function renderCodeBlock(element: JsxElement, ctx: ComponentContext): string {
|
|
105
|
+
const language = attribute(element, 'language') ?? '';
|
|
106
|
+
const title = attribute(element, 'title');
|
|
107
|
+
// The children parse as ordinary markdown, so the code arrives as text
|
|
108
|
+
// inside paragraphs rather than as one raw string.
|
|
109
|
+
const value = textContent(element.children ?? []);
|
|
110
|
+
|
|
111
|
+
const classes = language ? `wp-block-code language-${language}` : 'wp-block-code';
|
|
112
|
+
const code = serializeBlock(
|
|
113
|
+
'code',
|
|
114
|
+
language ? { className: `language-${language}` } : undefined,
|
|
115
|
+
`<pre class="${classes}"><code>${escapeCode(value)}</code></pre>`,
|
|
116
|
+
);
|
|
117
|
+
if (!title) return code;
|
|
118
|
+
|
|
119
|
+
const groupClass = ctx.theme.cls('code-group');
|
|
120
|
+
const titleClass = ctx.theme.cls('code-title');
|
|
121
|
+
return serializeBlock(
|
|
122
|
+
'group',
|
|
123
|
+
{ className: groupClass },
|
|
124
|
+
`<div class="wp-block-group ${groupClass}">${joinBlocks([
|
|
125
|
+
serializeBlock('paragraph', { className: titleClass }, `<p class="${titleClass}">${escapeText(title)}</p>`),
|
|
126
|
+
code,
|
|
127
|
+
])}</div>`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Render `<Admonition type="tip" title="…">`. */
|
|
132
|
+
function renderAdmonitionElement(element: JsxElement, ctx: ComponentContext): string {
|
|
133
|
+
const type = (attribute(element, 'type') ?? 'note').toLowerCase();
|
|
134
|
+
const label = attribute(element, 'title') ?? type.charAt(0).toUpperCase() + type.slice(1);
|
|
135
|
+
const base = ctx.theme.cls('admonition');
|
|
136
|
+
const className = `${base} ${base}-${type}`;
|
|
137
|
+
const titleClass = ctx.theme.cls('admonition-title');
|
|
138
|
+
|
|
139
|
+
const inner = joinBlocks([
|
|
140
|
+
serializeBlock(
|
|
141
|
+
'paragraph',
|
|
142
|
+
{ className: titleClass },
|
|
143
|
+
`<p class="${titleClass}"><strong>${escapeText(label)}</strong></p>`,
|
|
144
|
+
),
|
|
145
|
+
...(element.children ?? []).map(ctx.renderChild),
|
|
146
|
+
]);
|
|
147
|
+
return serializeBlock(
|
|
148
|
+
'group',
|
|
149
|
+
{ className },
|
|
150
|
+
`<div class="wp-block-group ${className}">${inner}</div>`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The text a run of nodes carries.
|
|
156
|
+
*
|
|
157
|
+
* Block-level children are separated by a blank line, which is what keeps the
|
|
158
|
+
* lines of a code block apart once MDX has parsed them into paragraphs.
|
|
159
|
+
*/
|
|
160
|
+
function textContent(nodes: RootContent[]): string {
|
|
161
|
+
const blocks: string[] = [];
|
|
162
|
+
for (const node of nodes) {
|
|
163
|
+
const parts: string[] = [];
|
|
164
|
+
const walk = (child: { type?: string; value?: string; children?: unknown[] }): void => {
|
|
165
|
+
if (typeof child.value === 'string' && child.type !== 'html') parts.push(child.value);
|
|
166
|
+
for (const grandChild of (child.children ?? []) as never[]) walk(grandChild);
|
|
167
|
+
};
|
|
168
|
+
walk(node as never);
|
|
169
|
+
const text = parts.join('');
|
|
170
|
+
if (text !== '') blocks.push(text);
|
|
171
|
+
}
|
|
172
|
+
return blocks.join('\n\n');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** The text inside an element, for a summary. */
|
|
176
|
+
function plainText(element: JsxElement, source: string): string {
|
|
177
|
+
const parts: string[] = [];
|
|
178
|
+
const walk = (node: { type?: string; value?: string; children?: unknown[] }): void => {
|
|
179
|
+
if (typeof node.value === 'string' && node.type === 'text') parts.push(node.value);
|
|
180
|
+
for (const child of (node.children ?? []) as never[]) walk(child);
|
|
181
|
+
};
|
|
182
|
+
walk(element as never);
|
|
183
|
+
const text = parts.join('').trim();
|
|
184
|
+
if (text) return text;
|
|
185
|
+
const start = element.position?.start.offset;
|
|
186
|
+
const end = element.position?.end.offset;
|
|
187
|
+
return start !== undefined && end !== undefined
|
|
188
|
+
? source.slice(start, end).replace(/<[^>]*>/g, '').trim()
|
|
189
|
+
: '';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Render a block-level JSX element.
|
|
194
|
+
*
|
|
195
|
+
* @returns The block markup, or undefined when nothing here can render it.
|
|
196
|
+
*/
|
|
197
|
+
export function renderJsxBlock(element: JsxElement, ctx: ComponentContext): string | undefined {
|
|
198
|
+
const name = element.name ?? '';
|
|
199
|
+
|
|
200
|
+
switch (name) {
|
|
201
|
+
case 'Tabs':
|
|
202
|
+
return renderTabs(element, ctx);
|
|
203
|
+
case 'TabItem':
|
|
204
|
+
return renderTabItem(element, ctx);
|
|
205
|
+
case 'Details':
|
|
206
|
+
case 'details':
|
|
207
|
+
return renderDetails(element, ctx);
|
|
208
|
+
case 'CodeBlock':
|
|
209
|
+
return renderCodeBlock(element, ctx);
|
|
210
|
+
case 'Admonition':
|
|
211
|
+
return renderAdmonitionElement(element, ctx);
|
|
212
|
+
default:
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (name === '') {
|
|
217
|
+
// A fragment contributes nothing of its own.
|
|
218
|
+
return joinBlocks((element.children ?? []).map(ctx.renderChild));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (HTML_ELEMENTS.has(name)) {
|
|
222
|
+
const start = element.position?.start.offset;
|
|
223
|
+
const end = element.position?.end.offset;
|
|
224
|
+
if (start !== undefined && end !== undefined) {
|
|
225
|
+
return serializeBlock('html', undefined, ctx.source.slice(start, end));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Summaries used for excerpts, SEO descriptions and index listings. */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shorten text to at most `max` characters, breaking on a word.
|
|
5
|
+
*
|
|
6
|
+
* @param text The full text.
|
|
7
|
+
* @param max Longest result, before the ellipsis.
|
|
8
|
+
*/
|
|
9
|
+
export function excerptFrom(text: string, max = 160): string {
|
|
10
|
+
const clean = String(text ?? '').replace(/\s+/g, ' ').trim();
|
|
11
|
+
if (clean.length <= max) return clean;
|
|
12
|
+
const cut = clean.slice(0, max);
|
|
13
|
+
const lastSpace = cut.lastIndexOf(' ');
|
|
14
|
+
const kept = lastSpace > max * 0.6 ? cut.slice(0, lastSpace) : cut;
|
|
15
|
+
return `${kept.replace(/[\s,;:.]+$/, '')}…`;
|
|
16
|
+
}
|