@brillout/docpress 0.15.10-commit-f89d08f → 0.15.10-commit-05c2883
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/components/CodeSnippets/useSelectCodeLang.ts +1 -2
- package/components/CodeSnippets.css +66 -49
- package/components/CodeSnippets.tsx +26 -83
- package/components/Pre.css +51 -0
- package/components/Pre.tsx +79 -0
- package/components/useMDXComponents.tsx +13 -0
- package/css/button.css +23 -0
- package/css/code.css +3 -21
- package/css/index.css +1 -0
- package/dist/components/CodeSnippets/useSelectCodeLang.js +1 -2
- package/dist/components/CodeSnippets.d.ts +0 -6
- package/dist/components/CodeSnippets.js +17 -55
- package/dist/rehypeMetaToProps.d.ts +19 -0
- package/dist/rehypeMetaToProps.js +62 -0
- package/dist/remarkDetype.d.ts +4 -0
- package/dist/remarkDetype.js +146 -0
- package/dist/resolvePageContext.js +18 -13
- package/dist/vite.config.js +9 -6
- package/index.ts +14 -7
- package/package.json +5 -3
- package/rehypeMetaToProps.ts +69 -0
- package/remarkDetype.ts +172 -0
- package/resolvePageContext.ts +19 -15
- package/vite.config.ts +9 -6
- package/detypePlugin.ts +0 -159
- package/dist/detypePlugin.d.ts +0 -3
- package/dist/detypePlugin.js +0 -139
- package/dist/utils/getMagicString.d.ts +0 -9
- package/dist/utils/getMagicString.js +0 -13
- package/utils/getMagicString.ts +0 -17
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
export { remarkDetype };
|
|
2
|
+
import { visit } from 'unist-util-visit';
|
|
3
|
+
import { assertUsage } from './utils/assert.js';
|
|
4
|
+
import pc from '@brillout/picocolors';
|
|
5
|
+
import module from 'node:module';
|
|
6
|
+
// Cannot use `import { transform } from 'detype'` as it results in errors,
|
|
7
|
+
// and the package has no default export. Using `module.createRequire` instead.
|
|
8
|
+
const { transform: detype } = module.createRequire(import.meta.url)('detype');
|
|
9
|
+
const prettierOptions = {
|
|
10
|
+
semi: false,
|
|
11
|
+
singleQuote: true,
|
|
12
|
+
printWidth: 100,
|
|
13
|
+
trailingComma: 'none',
|
|
14
|
+
};
|
|
15
|
+
function remarkDetype() {
|
|
16
|
+
return async function transformer(tree, file) {
|
|
17
|
+
const code_nodes = [];
|
|
18
|
+
visit(tree, 'code', (node, index, parent) => {
|
|
19
|
+
if (!parent || typeof index === 'undefined')
|
|
20
|
+
return;
|
|
21
|
+
// Skip if `node.lang` is not ts, tsx, vue, or yaml
|
|
22
|
+
if (!['ts', 'tsx', 'vue', 'yaml'].includes(node.lang || ''))
|
|
23
|
+
return;
|
|
24
|
+
// Skip if 'ts-only' meta is present
|
|
25
|
+
if (node.meta?.includes('ts-only'))
|
|
26
|
+
return;
|
|
27
|
+
// Collect this node for post-processing
|
|
28
|
+
code_nodes.push({ codeBlock: node, index, parent });
|
|
29
|
+
});
|
|
30
|
+
for (const node of code_nodes.reverse()) {
|
|
31
|
+
if (node.codeBlock.lang === 'yaml') {
|
|
32
|
+
transformYaml(node);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
await transformTsToJs(node, file);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function transformYaml(node) {
|
|
41
|
+
const { codeBlock, index, parent } = node;
|
|
42
|
+
const codeBlockContentJs = replaceFileNameSuffixes(codeBlock.value);
|
|
43
|
+
// Skip wrapping if the YAML code block hasn't changed
|
|
44
|
+
if (codeBlockContentJs === codeBlock.value)
|
|
45
|
+
return;
|
|
46
|
+
const { position, ...rest } = codeBlock;
|
|
47
|
+
// Create a new code node for the JS version based on the modified YAML
|
|
48
|
+
const yamlJsCode = {
|
|
49
|
+
...rest,
|
|
50
|
+
value: codeBlockContentJs,
|
|
51
|
+
};
|
|
52
|
+
// Wrap both the original YAML and `yamlJsCode` with <CodeSnippets>
|
|
53
|
+
const yamlContainer = {
|
|
54
|
+
type: 'mdxJsxFlowElement',
|
|
55
|
+
name: 'CodeSnippets',
|
|
56
|
+
children: [yamlJsCode, codeBlock],
|
|
57
|
+
attributes: [],
|
|
58
|
+
};
|
|
59
|
+
parent.children.splice(index, 1, yamlContainer);
|
|
60
|
+
}
|
|
61
|
+
async function transformTsToJs(node, file) {
|
|
62
|
+
const { codeBlock, index, parent } = node;
|
|
63
|
+
let codeBlockContentJs = replaceFileNameSuffixes(codeBlock.value);
|
|
64
|
+
// Remove TypeScript from the TS/TSX/Vue code node
|
|
65
|
+
try {
|
|
66
|
+
codeBlockContentJs = await detype(codeBlockContentJs, `some-dummy-filename.${codeBlock.lang}`, {
|
|
67
|
+
customizeBabelConfig(config) {
|
|
68
|
+
// Add `onlyRemoveTypeImports: true` to the internal `@babel/preset-typescript` config
|
|
69
|
+
// https://github.com/cyco130/detype/blob/46ec867e9efd31d31a312a215ca169bd6bff4726/src/transform.ts#L206
|
|
70
|
+
assertUsage(config.presets && config.presets.length === 1, 'Unexpected Babel config presets');
|
|
71
|
+
config.presets = [[config.presets[0], { onlyRemoveTypeImports: true }]];
|
|
72
|
+
},
|
|
73
|
+
removeTsComments: true,
|
|
74
|
+
prettierOptions,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
// Log errors and return original content instead of throwing
|
|
79
|
+
console.error(pc.red(error.message));
|
|
80
|
+
console.error([
|
|
81
|
+
`Failed to transform the code block in: ${pc.bold(pc.blue(file.path))}.`,
|
|
82
|
+
"This likely happened due to invalid TypeScript syntax (see detype's error message above). You can either:",
|
|
83
|
+
'- Fix the code block syntax',
|
|
84
|
+
'- Set the code block language to js instead of ts',
|
|
85
|
+
'- Use magic comments https://github.com/brillout/docpress#code-block-meta-and-comments',
|
|
86
|
+
].join('\n') + '\n');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// Clean up both JS and TS code contents: correct diff comments (for js only) and apply custom magic comment replacements
|
|
90
|
+
codeBlockContentJs = cleanUpCode(codeBlockContentJs.trimEnd(), true);
|
|
91
|
+
codeBlock.value = cleanUpCode(codeBlock.value);
|
|
92
|
+
// No wrapping needed if JS and TS code are still identical
|
|
93
|
+
if (codeBlockContentJs === codeBlock.value)
|
|
94
|
+
return;
|
|
95
|
+
const { position, lang, ...rest } = codeBlock;
|
|
96
|
+
const jsCode = {
|
|
97
|
+
...rest,
|
|
98
|
+
// The jsCode lang should be js|jsx|vue
|
|
99
|
+
lang: lang.replace('t', 'j'),
|
|
100
|
+
value: codeBlockContentJs,
|
|
101
|
+
};
|
|
102
|
+
// Wrap both the original `codeBlock` and `jsCode` with <CodeSnippets>
|
|
103
|
+
const container = {
|
|
104
|
+
type: 'mdxJsxFlowElement',
|
|
105
|
+
name: 'CodeSnippets',
|
|
106
|
+
children: [jsCode, codeBlock],
|
|
107
|
+
attributes: [],
|
|
108
|
+
};
|
|
109
|
+
parent.children.splice(index, 1, container);
|
|
110
|
+
}
|
|
111
|
+
// Replace all '.ts' extensions with '.js'
|
|
112
|
+
function replaceFileNameSuffixes(codeBlockValue) {
|
|
113
|
+
return codeBlockValue.replaceAll('.ts', '.js');
|
|
114
|
+
}
|
|
115
|
+
function cleanUpCode(code, isJsCode = false) {
|
|
116
|
+
if (isJsCode) {
|
|
117
|
+
code = correctCodeDiffComments(code);
|
|
118
|
+
}
|
|
119
|
+
return processMagicComments(code);
|
|
120
|
+
}
|
|
121
|
+
function processMagicComments(code) {
|
|
122
|
+
// @detype-replace DummyLayout Layout
|
|
123
|
+
const renameCommentRE = /^\/\/\s@detype-replace\s([^ ]+) ([^ ]+)\n/gm;
|
|
124
|
+
const matches = Array.from(code.matchAll(renameCommentRE));
|
|
125
|
+
if (matches.length) {
|
|
126
|
+
for (let i = matches.length - 1; i >= 0; i--) {
|
|
127
|
+
const match = matches[i];
|
|
128
|
+
const [fullMatch, renameFrom, renameTo] = match;
|
|
129
|
+
code = code.split(fullMatch).join('').replaceAll(renameFrom, renameTo);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
code = code.replaceAll('// @detype-uncomment ', '');
|
|
133
|
+
return code;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Correct code diff comments that detype() unexpectedly reformatted (using Prettier and Babel internally)
|
|
137
|
+
* after removing TypeScript.
|
|
138
|
+
* See https://github.com/brillout/docpress/pull/47#issuecomment-3263953899
|
|
139
|
+
* @param code Transformed Javascript code.
|
|
140
|
+
* @returns The corrected code.
|
|
141
|
+
*/
|
|
142
|
+
function correctCodeDiffComments(code) {
|
|
143
|
+
// Expected to match the code diff comments: `// [!code ++]` and `// [!code --]` started with newline and optional spaces
|
|
144
|
+
const codeDiffRE = /\n\s*\/\/\s\[!code.+\]/g;
|
|
145
|
+
return code.replaceAll(codeDiffRE, (codeDiff) => codeDiff.trimStart());
|
|
146
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { resolvePageContext };
|
|
2
|
-
import { assert } from './utils/assert';
|
|
2
|
+
import { assert, assertUsage } from './utils/assert';
|
|
3
3
|
import { jsxToTextContent } from './utils/jsxToTextContent';
|
|
4
4
|
import pc from '@brillout/picocolors';
|
|
5
5
|
import { parseMarkdownMini } from './parseMarkdownMini';
|
|
@@ -104,6 +104,15 @@ function getTitles(activeHeading, urlPathname, config) {
|
|
|
104
104
|
return { documentTitle, isLandingPage, pageTitle };
|
|
105
105
|
}
|
|
106
106
|
function getActiveHeading(headingsResolved, headingsDetachedResolved, urlPathname) {
|
|
107
|
+
const URLs = '\n' +
|
|
108
|
+
[...headingsResolved, ...headingsDetachedResolved]
|
|
109
|
+
.filter(Boolean)
|
|
110
|
+
.map((h) => h.url)
|
|
111
|
+
.sort()
|
|
112
|
+
.map((url) => ` ${url}`)
|
|
113
|
+
.join('\n');
|
|
114
|
+
const errNotFound = `URL ${pc.bold(urlPathname)} not found in following URLs:${URLs}`;
|
|
115
|
+
const errFoundTwice = `URL ${pc.bold(urlPathname)} found twice in following URLs:${URLs}`;
|
|
107
116
|
let activeHeading = null;
|
|
108
117
|
let activeCategoryName = 'Miscellaneous';
|
|
109
118
|
let headingCategory;
|
|
@@ -113,6 +122,7 @@ function getActiveHeading(headingsResolved, headingsDetachedResolved, urlPathnam
|
|
|
113
122
|
headingCategory = heading.title;
|
|
114
123
|
}
|
|
115
124
|
if (heading.url === urlPathname) {
|
|
125
|
+
assertUsage(!activeHeading, errFoundTwice);
|
|
116
126
|
activeHeading = heading;
|
|
117
127
|
assert(headingCategory);
|
|
118
128
|
activeCategoryName = headingCategory;
|
|
@@ -122,19 +132,14 @@ function getActiveHeading(headingsResolved, headingsDetachedResolved, urlPathnam
|
|
|
122
132
|
}
|
|
123
133
|
const isDetachedPage = !activeHeading;
|
|
124
134
|
if (!activeHeading) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
.filter(Boolean)
|
|
132
|
-
.map((h) => h.url)
|
|
133
|
-
.sort()
|
|
134
|
-
.map((url) => ` ${url}`)
|
|
135
|
-
.join('\n'),
|
|
136
|
-
].join('\n'));
|
|
135
|
+
const found = headingsDetachedResolved.filter(({ url }) => urlPathname === url);
|
|
136
|
+
if (found.length > 0) {
|
|
137
|
+
assertUsage(found.length === 1, errFoundTwice);
|
|
138
|
+
assertUsage(!activeHeading, errFoundTwice);
|
|
139
|
+
activeHeading = found[0];
|
|
140
|
+
}
|
|
137
141
|
}
|
|
142
|
+
assertUsage(activeHeading, errNotFound);
|
|
138
143
|
if (activeHeading.category)
|
|
139
144
|
activeCategoryName = activeHeading.category;
|
|
140
145
|
return { activeHeading, isDetachedPage, activeCategoryName };
|
package/dist/vite.config.js
CHANGED
|
@@ -2,20 +2,23 @@ export { config as viteConfig };
|
|
|
2
2
|
import mdx from '@mdx-js/rollup';
|
|
3
3
|
import react from '@vitejs/plugin-react-swc';
|
|
4
4
|
import { parsePageSections } from './parsePageSections.js';
|
|
5
|
-
import { detypePlugin } from './detypePlugin.js';
|
|
6
5
|
import rehypePrettyCode from 'rehype-pretty-code';
|
|
7
6
|
import remarkGfm from 'remark-gfm';
|
|
8
7
|
import { transformerNotationDiff } from '@shikijs/transformers';
|
|
8
|
+
import { remarkDetype } from './remarkDetype.js';
|
|
9
|
+
import { rehypeMetaToProps } from './rehypeMetaToProps.js';
|
|
9
10
|
const root = process.cwd();
|
|
10
|
-
const prettyCode = [
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
const prettyCode = [
|
|
12
|
+
rehypePrettyCode,
|
|
13
|
+
{ theme: 'github-light', keepBackground: false, transformers: [transformerNotationDiff()] },
|
|
14
|
+
];
|
|
15
|
+
const rehypePlugins = [prettyCode, [rehypeMetaToProps]];
|
|
16
|
+
const remarkPlugins = [remarkGfm, remarkDetype];
|
|
13
17
|
const config = {
|
|
14
18
|
root,
|
|
15
19
|
plugins: [
|
|
16
20
|
parsePageSections(),
|
|
17
|
-
|
|
18
|
-
mdx({ rehypePlugins, remarkPlugins }),
|
|
21
|
+
mdx({ rehypePlugins, remarkPlugins, providerImportSource: '@brillout/docpress' }),
|
|
19
22
|
// @vitejs/plugin-react-swc needs to be added *after* the mdx plugins
|
|
20
23
|
react(),
|
|
21
24
|
],
|
package/index.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/**********/
|
|
2
|
+
/* PUBLIC */
|
|
3
|
+
/**********/
|
|
1
4
|
export {
|
|
2
5
|
CodeBlockTransformer,
|
|
3
6
|
Link,
|
|
@@ -6,19 +9,23 @@ export {
|
|
|
6
9
|
FileRemoved,
|
|
7
10
|
ImportMeta,
|
|
8
11
|
Emoji,
|
|
9
|
-
CodeSnippets,
|
|
10
|
-
CodeSnippet,
|
|
11
12
|
TypescriptOnly,
|
|
12
13
|
} from './components'
|
|
13
14
|
export { MenuToggle } from './Layout'
|
|
14
|
-
|
|
15
|
-
// The only place usePageContext() is used at:
|
|
16
|
-
// https://github.com/vikejs/vike/blob/0b1b109f64aafbed23a1c2ac2630e6146a270ec0/packages/vike.dev/components/CommunityNote.tsx#L4
|
|
17
|
-
export { usePageContext } from './renderer/usePageContext'
|
|
18
|
-
|
|
19
15
|
export * from './components/Note'
|
|
20
16
|
export * from './icons/index'
|
|
21
17
|
export { assert } from './utils/assert'
|
|
22
18
|
export { parseMarkdownMini } from './parseMarkdownMini'
|
|
23
19
|
export type { Config } from './types/Config'
|
|
24
20
|
export type { HeadingDefinition, HeadingDetachedDefinition } from './types/Heading'
|
|
21
|
+
// The only place usePageContext() is used at:
|
|
22
|
+
// https://github.com/vikejs/vike/blob/0b1b109f64aafbed23a1c2ac2630e6146a270ec0/packages/vike.dev/components/CommunityNote.tsx#L4
|
|
23
|
+
export { usePageContext } from './renderer/usePageContext'
|
|
24
|
+
|
|
25
|
+
// The following are used internally by DocPress — users (should) never use these exports
|
|
26
|
+
/************/
|
|
27
|
+
/* INTERNAL */
|
|
28
|
+
/************/
|
|
29
|
+
// We provide our own `useMDXComponents()` to enable MDX component injection by setting `providerImportSource` to '@brillout/docpress'.
|
|
30
|
+
// https://mdxjs.com/guides/injecting-components/
|
|
31
|
+
export { useMDXComponents } from './components/useMDXComponents'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brillout/docpress",
|
|
3
|
-
"version": "0.15.10-commit-
|
|
3
|
+
"version": "0.15.10-commit-05c2883",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@brillout/picocolors": "^1.0.10",
|
|
@@ -11,11 +11,11 @@
|
|
|
11
11
|
"@mdx-js/rollup": "3.0.1",
|
|
12
12
|
"@shikijs/transformers": "1.2.0",
|
|
13
13
|
"@vitejs/plugin-react-swc": "^3.10.2",
|
|
14
|
-
"detype": "^1.1.
|
|
15
|
-
"magic-string": "^0.30.18",
|
|
14
|
+
"detype": "^1.1.3",
|
|
16
15
|
"rehype-pretty-code": "0.13.0",
|
|
17
16
|
"remark-gfm": "4.0.0",
|
|
18
17
|
"shiki": "1.2.0",
|
|
18
|
+
"unist-util-visit": "^5.0.0",
|
|
19
19
|
"vite": "^6.3.5"
|
|
20
20
|
},
|
|
21
21
|
"peerDependencies": {
|
|
@@ -60,6 +60,8 @@
|
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@brillout/release-me": "^0.4.8",
|
|
63
|
+
"@types/hast": "^3.0.4",
|
|
64
|
+
"@types/mdast": "^4.0.4",
|
|
63
65
|
"@types/node": "^22.5.5",
|
|
64
66
|
"@types/react": "^18.3.8",
|
|
65
67
|
"@types/react-dom": "^18.3.0"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export { rehypeMetaToProps }
|
|
2
|
+
|
|
3
|
+
import { visit } from 'unist-util-visit'
|
|
4
|
+
import type { ElementData, Root } from 'hast'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Rehype plugin to extract metadata from `<code>` blocks in markdown
|
|
8
|
+
* and attach them as props to the parent `<pre>` element.
|
|
9
|
+
*
|
|
10
|
+
* This allows using those props inside a custom `<Pre>` component.
|
|
11
|
+
*
|
|
12
|
+
* Example:
|
|
13
|
+
* ~~~mdx
|
|
14
|
+
* ```js foo="bar" hide_copy='true'
|
|
15
|
+
* export function add(a, b) {
|
|
16
|
+
* return a + b
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
* ~~~
|
|
20
|
+
* These props are then added to the `<pre>` element
|
|
21
|
+
*/
|
|
22
|
+
function rehypeMetaToProps() {
|
|
23
|
+
return (tree: Root) => {
|
|
24
|
+
visit(tree, 'element', (node, _index, parent) => {
|
|
25
|
+
if (node.tagName === 'code' && parent?.type === 'element' && parent.tagName === 'pre') {
|
|
26
|
+
const props = parseMetaString(node.data?.meta)
|
|
27
|
+
parent.properties ??= {}
|
|
28
|
+
parent.properties = { ...parent.properties, ...props }
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Minimal parser for a metadata string into key-value pairs.
|
|
36
|
+
*
|
|
37
|
+
* Supports simple patterns: key or key="value".
|
|
38
|
+
*
|
|
39
|
+
* Keys must contain only letters, dashes, or underscores (no digits).
|
|
40
|
+
* Keys are converted to kebab-case. Values default to "true" if missing.
|
|
41
|
+
*
|
|
42
|
+
* Example:
|
|
43
|
+
* parseMetaString('foo fooBar="value"')
|
|
44
|
+
* => { foo: 'true', foo_bar: 'value' }
|
|
45
|
+
*
|
|
46
|
+
* @param metaString - The input metadata string.
|
|
47
|
+
* @returns A plain object of parsed key-value pairs.
|
|
48
|
+
*/
|
|
49
|
+
function parseMetaString(metaString: ElementData['meta']): Record<string, string> {
|
|
50
|
+
if (!metaString) return {}
|
|
51
|
+
|
|
52
|
+
const props: Record<string, string> = {}
|
|
53
|
+
|
|
54
|
+
const keyValuePairRE = /([a-zA-Z_-]+)(?:="([^"]*)")?(?=\s|$)/g
|
|
55
|
+
for (const match of metaString.matchAll(keyValuePairRE)) {
|
|
56
|
+
let [_, key, value] = match
|
|
57
|
+
props[kebabCase(key)] = value || 'true'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return props
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Simple function to convert a camelCase or PascalCase string to kebab-case.
|
|
64
|
+
function kebabCase(str: string) {
|
|
65
|
+
return str
|
|
66
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
67
|
+
.replace('_', '-')
|
|
68
|
+
.toLowerCase()
|
|
69
|
+
}
|
package/remarkDetype.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
export { remarkDetype }
|
|
2
|
+
|
|
3
|
+
import type { Root, Parent, Code } from 'mdast'
|
|
4
|
+
import type { VFile } from '@mdx-js/mdx/internal-create-format-aware-processors'
|
|
5
|
+
import { visit } from 'unist-util-visit'
|
|
6
|
+
import { assertUsage } from './utils/assert.js'
|
|
7
|
+
import pc from '@brillout/picocolors'
|
|
8
|
+
import module from 'node:module'
|
|
9
|
+
// Cannot use `import { transform } from 'detype'` as it results in errors,
|
|
10
|
+
// and the package has no default export. Using `module.createRequire` instead.
|
|
11
|
+
const { transform: detype } = module.createRequire(import.meta.url)('detype') as typeof import('detype')
|
|
12
|
+
|
|
13
|
+
const prettierOptions: NonNullable<Parameters<typeof detype>[2]>['prettierOptions'] = {
|
|
14
|
+
semi: false,
|
|
15
|
+
singleQuote: true,
|
|
16
|
+
printWidth: 100,
|
|
17
|
+
trailingComma: 'none',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type CodeNode = {
|
|
21
|
+
codeBlock: Code
|
|
22
|
+
index: number
|
|
23
|
+
parent: Parent
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function remarkDetype() {
|
|
27
|
+
return async function transformer(tree: Root, file: VFile) {
|
|
28
|
+
const code_nodes: CodeNode[] = []
|
|
29
|
+
|
|
30
|
+
visit(tree, 'code', (node, index, parent) => {
|
|
31
|
+
if (!parent || typeof index === 'undefined') return
|
|
32
|
+
// Skip if `node.lang` is not ts, tsx, vue, or yaml
|
|
33
|
+
if (!['ts', 'tsx', 'vue', 'yaml'].includes(node.lang || '')) return
|
|
34
|
+
|
|
35
|
+
// Skip if 'ts-only' meta is present
|
|
36
|
+
if (node.meta?.includes('ts-only')) return
|
|
37
|
+
|
|
38
|
+
// Collect this node for post-processing
|
|
39
|
+
code_nodes.push({ codeBlock: node, index, parent })
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
for (const node of code_nodes.reverse()) {
|
|
43
|
+
if (node.codeBlock.lang === 'yaml') {
|
|
44
|
+
transformYaml(node)
|
|
45
|
+
} else {
|
|
46
|
+
await transformTsToJs(node, file)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function transformYaml(node: CodeNode) {
|
|
53
|
+
const { codeBlock, index, parent } = node
|
|
54
|
+
const codeBlockContentJs = replaceFileNameSuffixes(codeBlock.value)
|
|
55
|
+
|
|
56
|
+
// Skip wrapping if the YAML code block hasn't changed
|
|
57
|
+
if (codeBlockContentJs === codeBlock.value) return
|
|
58
|
+
|
|
59
|
+
const { position, ...rest } = codeBlock
|
|
60
|
+
|
|
61
|
+
// Create a new code node for the JS version based on the modified YAML
|
|
62
|
+
const yamlJsCode: Code = {
|
|
63
|
+
...rest,
|
|
64
|
+
value: codeBlockContentJs,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Wrap both the original YAML and `yamlJsCode` with <CodeSnippets>
|
|
68
|
+
const yamlContainer = {
|
|
69
|
+
type: 'mdxJsxFlowElement' as const,
|
|
70
|
+
name: 'CodeSnippets',
|
|
71
|
+
children: [yamlJsCode, codeBlock],
|
|
72
|
+
attributes: [],
|
|
73
|
+
}
|
|
74
|
+
parent.children.splice(index, 1, yamlContainer)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function transformTsToJs(node: CodeNode, file: VFile) {
|
|
78
|
+
const { codeBlock, index, parent } = node
|
|
79
|
+
let codeBlockContentJs = replaceFileNameSuffixes(codeBlock.value)
|
|
80
|
+
|
|
81
|
+
// Remove TypeScript from the TS/TSX/Vue code node
|
|
82
|
+
try {
|
|
83
|
+
codeBlockContentJs = await detype(codeBlockContentJs, `some-dummy-filename.${codeBlock.lang}`, {
|
|
84
|
+
customizeBabelConfig(config) {
|
|
85
|
+
// Add `onlyRemoveTypeImports: true` to the internal `@babel/preset-typescript` config
|
|
86
|
+
// https://github.com/cyco130/detype/blob/46ec867e9efd31d31a312a215ca169bd6bff4726/src/transform.ts#L206
|
|
87
|
+
assertUsage(config.presets && config.presets.length === 1, 'Unexpected Babel config presets')
|
|
88
|
+
config.presets = [[config.presets[0], { onlyRemoveTypeImports: true }]]
|
|
89
|
+
},
|
|
90
|
+
removeTsComments: true,
|
|
91
|
+
prettierOptions,
|
|
92
|
+
})
|
|
93
|
+
} catch (error) {
|
|
94
|
+
// Log errors and return original content instead of throwing
|
|
95
|
+
console.error(pc.red((error as SyntaxError).message))
|
|
96
|
+
console.error(
|
|
97
|
+
[
|
|
98
|
+
`Failed to transform the code block in: ${pc.bold(pc.blue(file.path))}.`,
|
|
99
|
+
"This likely happened due to invalid TypeScript syntax (see detype's error message above). You can either:",
|
|
100
|
+
'- Fix the code block syntax',
|
|
101
|
+
'- Set the code block language to js instead of ts',
|
|
102
|
+
'- Use magic comments https://github.com/brillout/docpress#code-block-meta-and-comments',
|
|
103
|
+
].join('\n') + '\n',
|
|
104
|
+
)
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Clean up both JS and TS code contents: correct diff comments (for js only) and apply custom magic comment replacements
|
|
109
|
+
codeBlockContentJs = cleanUpCode(codeBlockContentJs.trimEnd(), true)
|
|
110
|
+
codeBlock.value = cleanUpCode(codeBlock.value)
|
|
111
|
+
|
|
112
|
+
// No wrapping needed if JS and TS code are still identical
|
|
113
|
+
if (codeBlockContentJs === codeBlock.value) return
|
|
114
|
+
|
|
115
|
+
const { position, lang, ...rest } = codeBlock
|
|
116
|
+
|
|
117
|
+
const jsCode: Code = {
|
|
118
|
+
...rest,
|
|
119
|
+
// The jsCode lang should be js|jsx|vue
|
|
120
|
+
lang: lang!.replace('t', 'j'),
|
|
121
|
+
value: codeBlockContentJs,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Wrap both the original `codeBlock` and `jsCode` with <CodeSnippets>
|
|
125
|
+
const container = {
|
|
126
|
+
type: 'mdxJsxFlowElement' as const,
|
|
127
|
+
name: 'CodeSnippets',
|
|
128
|
+
children: [jsCode, codeBlock],
|
|
129
|
+
attributes: [],
|
|
130
|
+
}
|
|
131
|
+
parent.children.splice(index, 1, container)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Replace all '.ts' extensions with '.js'
|
|
135
|
+
function replaceFileNameSuffixes(codeBlockValue: string) {
|
|
136
|
+
return codeBlockValue.replaceAll('.ts', '.js')
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function cleanUpCode(code: string, isJsCode: boolean = false) {
|
|
140
|
+
if (isJsCode) {
|
|
141
|
+
code = correctCodeDiffComments(code)
|
|
142
|
+
}
|
|
143
|
+
return processMagicComments(code)
|
|
144
|
+
}
|
|
145
|
+
function processMagicComments(code: string) {
|
|
146
|
+
// @detype-replace DummyLayout Layout
|
|
147
|
+
const renameCommentRE = /^\/\/\s@detype-replace\s([^ ]+) ([^ ]+)\n/gm
|
|
148
|
+
const matches = Array.from(code.matchAll(renameCommentRE))
|
|
149
|
+
|
|
150
|
+
if (matches.length) {
|
|
151
|
+
for (let i = matches.length - 1; i >= 0; i--) {
|
|
152
|
+
const match = matches[i]
|
|
153
|
+
const [fullMatch, renameFrom, renameTo] = match
|
|
154
|
+
code = code.split(fullMatch).join('').replaceAll(renameFrom, renameTo)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
code = code.replaceAll('// @detype-uncomment ', '')
|
|
158
|
+
|
|
159
|
+
return code
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Correct code diff comments that detype() unexpectedly reformatted (using Prettier and Babel internally)
|
|
163
|
+
* after removing TypeScript.
|
|
164
|
+
* See https://github.com/brillout/docpress/pull/47#issuecomment-3263953899
|
|
165
|
+
* @param code Transformed Javascript code.
|
|
166
|
+
* @returns The corrected code.
|
|
167
|
+
*/
|
|
168
|
+
function correctCodeDiffComments(code: string) {
|
|
169
|
+
// Expected to match the code diff comments: `// [!code ++]` and `// [!code --]` started with newline and optional spaces
|
|
170
|
+
const codeDiffRE = /\n\s*\/\/\s\[!code.+\]/g
|
|
171
|
+
return code.replaceAll(codeDiffRE, (codeDiff) => codeDiff.trimStart())
|
|
172
|
+
}
|
package/resolvePageContext.ts
CHANGED
|
@@ -13,7 +13,7 @@ import type {
|
|
|
13
13
|
HeadingDetachedResolved,
|
|
14
14
|
StringArray,
|
|
15
15
|
} from './types/Heading'
|
|
16
|
-
import { assert } from './utils/assert'
|
|
16
|
+
import { assert, assertUsage } from './utils/assert'
|
|
17
17
|
import { jsxToTextContent } from './utils/jsxToTextContent'
|
|
18
18
|
import pc from '@brillout/picocolors'
|
|
19
19
|
import { parseMarkdownMini } from './parseMarkdownMini'
|
|
@@ -149,6 +149,16 @@ function getActiveHeading(
|
|
|
149
149
|
headingsDetachedResolved: HeadingDetachedResolved[],
|
|
150
150
|
urlPathname: string,
|
|
151
151
|
) {
|
|
152
|
+
const URLs =
|
|
153
|
+
'\n' +
|
|
154
|
+
[...headingsResolved, ...headingsDetachedResolved]
|
|
155
|
+
.filter(Boolean)
|
|
156
|
+
.map((h) => h.url)
|
|
157
|
+
.sort()
|
|
158
|
+
.map((url) => ` ${url}`)
|
|
159
|
+
.join('\n')
|
|
160
|
+
const errNotFound = `URL ${pc.bold(urlPathname)} not found in following URLs:${URLs}`
|
|
161
|
+
const errFoundTwice = `URL ${pc.bold(urlPathname)} found twice in following URLs:${URLs}`
|
|
152
162
|
let activeHeading: HeadingResolved | HeadingDetachedResolved | null = null
|
|
153
163
|
let activeCategoryName = 'Miscellaneous'
|
|
154
164
|
let headingCategory: string | undefined
|
|
@@ -158,6 +168,7 @@ function getActiveHeading(
|
|
|
158
168
|
headingCategory = heading.title
|
|
159
169
|
}
|
|
160
170
|
if (heading.url === urlPathname) {
|
|
171
|
+
assertUsage(!activeHeading, errFoundTwice)
|
|
161
172
|
activeHeading = heading
|
|
162
173
|
assert(headingCategory)
|
|
163
174
|
activeCategoryName = headingCategory
|
|
@@ -167,21 +178,14 @@ function getActiveHeading(
|
|
|
167
178
|
}
|
|
168
179
|
const isDetachedPage = !activeHeading
|
|
169
180
|
if (!activeHeading) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
[
|
|
175
|
-
|
|
176
|
-
[...headingsResolved, ...headingsDetachedResolved]
|
|
177
|
-
.filter(Boolean)
|
|
178
|
-
.map((h) => h.url)
|
|
179
|
-
.sort()
|
|
180
|
-
.map((url) => ` ${url}`)
|
|
181
|
-
.join('\n'),
|
|
182
|
-
].join('\n'),
|
|
183
|
-
)
|
|
181
|
+
const found = headingsDetachedResolved.filter(({ url }) => urlPathname === url)
|
|
182
|
+
if (found.length > 0) {
|
|
183
|
+
assertUsage(found.length === 1, errFoundTwice)
|
|
184
|
+
assertUsage(!activeHeading, errFoundTwice)
|
|
185
|
+
activeHeading = found[0]!
|
|
186
|
+
}
|
|
184
187
|
}
|
|
188
|
+
assertUsage(activeHeading, errNotFound)
|
|
185
189
|
if (activeHeading.category) activeCategoryName = activeHeading.category
|
|
186
190
|
return { activeHeading, isDetachedPage, activeCategoryName }
|
|
187
191
|
}
|
package/vite.config.ts
CHANGED
|
@@ -4,22 +4,25 @@ import mdx from '@mdx-js/rollup'
|
|
|
4
4
|
import react from '@vitejs/plugin-react-swc'
|
|
5
5
|
import type { PluginOption, UserConfig } from 'vite'
|
|
6
6
|
import { parsePageSections } from './parsePageSections.js'
|
|
7
|
-
import { detypePlugin } from './detypePlugin.js'
|
|
8
7
|
import rehypePrettyCode from 'rehype-pretty-code'
|
|
9
8
|
import remarkGfm from 'remark-gfm'
|
|
10
9
|
import { transformerNotationDiff } from '@shikijs/transformers'
|
|
10
|
+
import { remarkDetype } from './remarkDetype.js'
|
|
11
|
+
import { rehypeMetaToProps } from './rehypeMetaToProps.js'
|
|
11
12
|
|
|
12
13
|
const root = process.cwd()
|
|
13
|
-
const prettyCode = [
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
const prettyCode = [
|
|
15
|
+
rehypePrettyCode,
|
|
16
|
+
{ theme: 'github-light', keepBackground: false, transformers: [transformerNotationDiff()] },
|
|
17
|
+
]
|
|
18
|
+
const rehypePlugins: any = [prettyCode, [rehypeMetaToProps]]
|
|
19
|
+
const remarkPlugins = [remarkGfm, remarkDetype]
|
|
16
20
|
|
|
17
21
|
const config: UserConfig = {
|
|
18
22
|
root,
|
|
19
23
|
plugins: [
|
|
20
24
|
parsePageSections(),
|
|
21
|
-
|
|
22
|
-
mdx({ rehypePlugins, remarkPlugins }) as PluginOption,
|
|
25
|
+
mdx({ rehypePlugins, remarkPlugins, providerImportSource: '@brillout/docpress' }) as PluginOption,
|
|
23
26
|
// @vitejs/plugin-react-swc needs to be added *after* the mdx plugins
|
|
24
27
|
react(),
|
|
25
28
|
],
|