@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.
@@ -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
- activeHeading = headingsDetachedResolved.find(({ url }) => urlPathname === url) ?? null;
126
- }
127
- if (!activeHeading) {
128
- throw new Error([
129
- `URL ${pc.bold(urlPathname)} not found in following URLs:`,
130
- [...headingsResolved, ...headingsDetachedResolved]
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 };
@@ -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 = [rehypePrettyCode, { theme: 'github-light', transformers: [transformerNotationDiff()] }];
11
- const rehypePlugins = [prettyCode];
12
- const remarkPlugins = [remarkGfm];
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
- detypePlugin(),
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-f89d08f",
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.2",
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
+ }
@@ -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
+ }
@@ -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
- activeHeading = headingsDetachedResolved.find(({ url }) => urlPathname === url) ?? null
171
- }
172
- if (!activeHeading) {
173
- throw new Error(
174
- [
175
- `URL ${pc.bold(urlPathname)} not found in following URLs:`,
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 = [rehypePrettyCode, { theme: 'github-light', transformers: [transformerNotationDiff()] }]
14
- const rehypePlugins: any = [prettyCode]
15
- const remarkPlugins = [remarkGfm]
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
- detypePlugin(),
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
  ],