@brillout/docpress 0.16.5 → 0.16.7-commit-fc2ed19
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/Layout.tsx +6 -2
- package/code-blocks/components/ChoiceGroup.css +48 -0
- package/code-blocks/components/ChoiceGroup.tsx +85 -0
- package/code-blocks/components/CodeSnippets.tsx +3 -13
- package/code-blocks/components/Pre.tsx +27 -3
- package/code-blocks/hooks/useLocalStorage.ts +39 -0
- package/code-blocks/hooks/useMDXComponents.tsx +2 -0
- package/code-blocks/hooks/useRestoreScroll.ts +31 -0
- package/code-blocks/hooks/useSelectCodeLang.ts +2 -51
- package/code-blocks/hooks/useSelectedChoice.ts +34 -0
- package/code-blocks/rehypeMetaToProps.ts +32 -21
- package/code-blocks/remarkChoiceGroup.ts +112 -0
- package/code-blocks/remarkDetype.ts +10 -3
- package/code-blocks/remarkPkgManager.ts +45 -0
- package/code-blocks/utils/generateChoiceGroup.ts +87 -0
- package/dist/code-blocks/rehypeMetaToProps.d.ts +17 -11
- package/dist/code-blocks/rehypeMetaToProps.js +26 -21
- package/dist/code-blocks/remarkChoiceGroup.d.ts +8 -0
- package/dist/code-blocks/remarkChoiceGroup.js +84 -0
- package/dist/code-blocks/remarkDetype.js +8 -3
- package/dist/code-blocks/remarkPkgManager.d.ts +3 -0
- package/dist/code-blocks/remarkPkgManager.js +38 -0
- package/dist/code-blocks/utils/generateChoiceGroup.d.ts +9 -0
- package/dist/code-blocks/utils/generateChoiceGroup.js +66 -0
- package/dist/types/Config.d.ts +9 -1
- package/dist/vite.config.js +4 -1
- package/package.json +4 -1
- package/renderer/onRenderHtml.tsx +42 -4
- package/types/Config.ts +12 -1
- package/vite.config.ts +4 -1
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export { remarkPkgManager }
|
|
2
|
+
|
|
3
|
+
import type { Code, Root } from 'mdast'
|
|
4
|
+
import { visit } from 'unist-util-visit'
|
|
5
|
+
import convert from 'npm-to-yarn'
|
|
6
|
+
import { parseMetaString } from './rehypeMetaToProps.js'
|
|
7
|
+
import { generateChoiceGroup } from './utils/generateChoiceGroup.js'
|
|
8
|
+
|
|
9
|
+
const PKG_MANAGERS = ['pnpm', 'yarn', 'bun'] as const
|
|
10
|
+
|
|
11
|
+
function remarkPkgManager() {
|
|
12
|
+
return function (tree: Root) {
|
|
13
|
+
visit(tree, 'code', (node, index, parent) => {
|
|
14
|
+
if (!parent || typeof index === 'undefined') return
|
|
15
|
+
if (!['sh', 'shell'].includes(node.lang || '')) return
|
|
16
|
+
if (node.value.indexOf('npm') === -1 && node.value.indexOf('npx') === -1) return
|
|
17
|
+
|
|
18
|
+
let choice: string | undefined = undefined
|
|
19
|
+
const nodes = new Map<string, Code>()
|
|
20
|
+
|
|
21
|
+
if (node.meta) {
|
|
22
|
+
const meta = parseMetaString(node.meta, ['choice'])
|
|
23
|
+
choice = meta.props['choice']
|
|
24
|
+
node.meta = meta.rest
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
nodes.set('npm', node)
|
|
28
|
+
|
|
29
|
+
for (const pm of PKG_MANAGERS) {
|
|
30
|
+
nodes.set(pm, {
|
|
31
|
+
type: node.type,
|
|
32
|
+
lang: node.lang,
|
|
33
|
+
meta: node.meta,
|
|
34
|
+
value: convert(node.value, pm),
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const groupedNodes = [...nodes].map(([name, node]) => ({ value: name, children: [node] }))
|
|
39
|
+
const replacement = generateChoiceGroup(groupedNodes)
|
|
40
|
+
|
|
41
|
+
replacement.data ??= { choice }
|
|
42
|
+
parent.children.splice(index, 1, replacement)
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export { generateChoiceGroup }
|
|
2
|
+
export type { CodeChoice }
|
|
3
|
+
|
|
4
|
+
import type { BlockContent, DefinitionContent } from 'mdast'
|
|
5
|
+
import type { MdxJsxAttribute, MdxJsxFlowElement } from 'mdast-util-mdx-jsx'
|
|
6
|
+
|
|
7
|
+
type CodeChoice = {
|
|
8
|
+
value: string
|
|
9
|
+
children: (BlockContent | DefinitionContent)[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function generateChoiceGroup(codeChoices: CodeChoice[]): MdxJsxFlowElement {
|
|
13
|
+
const attributes: MdxJsxAttribute[] = []
|
|
14
|
+
const children: MdxJsxFlowElement[] = []
|
|
15
|
+
|
|
16
|
+
attributes.push({
|
|
17
|
+
type: 'mdxJsxAttribute',
|
|
18
|
+
name: 'choices',
|
|
19
|
+
value: {
|
|
20
|
+
type: 'mdxJsxAttributeValueExpression',
|
|
21
|
+
value: '',
|
|
22
|
+
data: {
|
|
23
|
+
estree: {
|
|
24
|
+
type: 'Program',
|
|
25
|
+
sourceType: 'module',
|
|
26
|
+
comments: [],
|
|
27
|
+
body: [
|
|
28
|
+
{
|
|
29
|
+
type: 'ExpressionStatement',
|
|
30
|
+
expression: {
|
|
31
|
+
type: 'ArrayExpression',
|
|
32
|
+
// @ts-ignore: Missing properties in type definition
|
|
33
|
+
elements: codeChoices.map((choice) => ({
|
|
34
|
+
type: 'Literal',
|
|
35
|
+
value: choice.value,
|
|
36
|
+
})),
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
for (const codeChoice of codeChoices) {
|
|
46
|
+
const classNames = ['choice']
|
|
47
|
+
if (findHasJsToggle(codeChoice.children[0])) {
|
|
48
|
+
classNames.push('has-toggle')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
children.push({
|
|
52
|
+
type: 'mdxJsxFlowElement',
|
|
53
|
+
name: 'div',
|
|
54
|
+
attributes: [
|
|
55
|
+
{ type: 'mdxJsxAttribute', name: 'id', value: codeChoice.value },
|
|
56
|
+
{ type: 'mdxJsxAttribute', name: 'className', value: classNames.join(' ') },
|
|
57
|
+
],
|
|
58
|
+
children: codeChoice.children.every((node) => node.type === 'containerDirective')
|
|
59
|
+
? codeChoice.children.flatMap((node) => [...node.children])
|
|
60
|
+
: codeChoice.children,
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
type: 'mdxJsxFlowElement',
|
|
66
|
+
name: 'ChoiceGroup',
|
|
67
|
+
attributes,
|
|
68
|
+
children,
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function findHasJsToggle(node: BlockContent | DefinitionContent) {
|
|
73
|
+
if (node.type === 'containerDirective' && node.name === 'Choice') {
|
|
74
|
+
return (
|
|
75
|
+
node.children[0].type === 'mdxJsxFlowElement' &&
|
|
76
|
+
node.children[0].name === 'CodeSnippets' &&
|
|
77
|
+
node.children[0].attributes.every(
|
|
78
|
+
(attribute) => attribute.type !== 'mdxJsxAttribute' || attribute.name !== 'hideToggle',
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
return (
|
|
83
|
+
node.type === 'mdxJsxFlowElement' &&
|
|
84
|
+
node.name === 'CodeSnippets' &&
|
|
85
|
+
node.attributes.every((attribute) => attribute.type !== 'mdxJsxAttribute' || attribute.name !== 'hideToggle')
|
|
86
|
+
)
|
|
87
|
+
}
|
|
@@ -18,18 +18,24 @@ import type { ElementData, Root } from 'hast';
|
|
|
18
18
|
*/
|
|
19
19
|
declare function rehypeMetaToProps(): (tree: Root) => void;
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
21
|
+
* Simple parser for a metadata string into key-value pairs and a remaining unparsed string.
|
|
22
22
|
*
|
|
23
|
-
* Supports simple patterns: key or key=
|
|
23
|
+
* Supports simple patterns: key or key=value.
|
|
24
24
|
*
|
|
25
|
-
* Keys must contain only letters, dashes, or underscores (no digits).
|
|
26
|
-
* Keys are converted to kebab-case. Values default to "true" if missing.
|
|
25
|
+
* - Keys must contain only letters, dashes, or underscores (no digits).
|
|
26
|
+
* - Keys are converted to kebab-case. Values default to "true" if missing.
|
|
27
|
+
* - Keys and values are stored in `props`.
|
|
28
|
+
* - If `propNames` is provided, only keys included in that list are extracted.
|
|
29
|
+
* - Unextracted tokens remain in `rest`.
|
|
27
30
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* @returns A plain object of parsed key-value pairs.
|
|
31
|
+
* @param meta - The input metadata string.
|
|
32
|
+
* @param propNames - Optional whitelist of property names to extract.
|
|
33
|
+
* @returns An object containing:
|
|
34
|
+
* - `props`: a map of extracted properties
|
|
35
|
+
* - `rest`: the remaining metadata string after extraction
|
|
34
36
|
*/
|
|
35
|
-
declare function parseMetaString(
|
|
37
|
+
declare function parseMetaString<Name extends string = string>(meta: ElementData['meta'], propNames?: Name[]): PropsType<Name>;
|
|
38
|
+
interface PropsType<Name extends string = string> {
|
|
39
|
+
props: Partial<Record<Name, string>>;
|
|
40
|
+
rest: string;
|
|
41
|
+
}
|
|
@@ -20,38 +20,43 @@ function rehypeMetaToProps() {
|
|
|
20
20
|
return (tree) => {
|
|
21
21
|
visit(tree, 'element', (node, _index, parent) => {
|
|
22
22
|
if (node.tagName === 'code' && parent?.type === 'element' && parent.tagName === 'pre') {
|
|
23
|
-
const
|
|
23
|
+
const meta = parseMetaString(node.data?.meta);
|
|
24
24
|
parent.properties ??= {};
|
|
25
|
-
parent.properties = { ...parent.properties, ...props };
|
|
25
|
+
parent.properties = { ...parent.properties, ...meta.props };
|
|
26
26
|
}
|
|
27
27
|
});
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
|
-
*
|
|
31
|
+
* Simple parser for a metadata string into key-value pairs and a remaining unparsed string.
|
|
32
32
|
*
|
|
33
|
-
* Supports simple patterns: key or key=
|
|
33
|
+
* Supports simple patterns: key or key=value.
|
|
34
34
|
*
|
|
35
|
-
* Keys must contain only letters, dashes, or underscores (no digits).
|
|
36
|
-
* Keys are converted to kebab-case. Values default to "true" if missing.
|
|
35
|
+
* - Keys must contain only letters, dashes, or underscores (no digits).
|
|
36
|
+
* - Keys are converted to kebab-case. Values default to "true" if missing.
|
|
37
|
+
* - Keys and values are stored in `props`.
|
|
38
|
+
* - If `propNames` is provided, only keys included in that list are extracted.
|
|
39
|
+
* - Unextracted tokens remain in `rest`.
|
|
37
40
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* @returns A plain object of parsed key-value pairs.
|
|
41
|
+
* @param meta - The input metadata string.
|
|
42
|
+
* @param propNames - Optional whitelist of property names to extract.
|
|
43
|
+
* @returns An object containing:
|
|
44
|
+
* - `props`: a map of extracted properties
|
|
45
|
+
* - `rest`: the remaining metadata string after extraction
|
|
44
46
|
*/
|
|
45
|
-
function parseMetaString(
|
|
46
|
-
if (!
|
|
47
|
-
return {};
|
|
47
|
+
function parseMetaString(meta, propNames) {
|
|
48
|
+
if (!meta)
|
|
49
|
+
return { props: {}, rest: '' };
|
|
50
|
+
let str = meta;
|
|
51
|
+
const keyValuePairRE = /(?<name>[a-zA-Z_-]+)(?:=([^"'\s]+))?/g;
|
|
48
52
|
const props = {};
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
props[kebabCase(
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
str = str.replaceAll(keyValuePairRE, (match, name, value) => {
|
|
54
|
+
if (propNames && !propNames.includes(name))
|
|
55
|
+
return match;
|
|
56
|
+
props[kebabCase(name)] = value || 'true';
|
|
57
|
+
return '';
|
|
58
|
+
});
|
|
59
|
+
return { props, rest: str.trim() };
|
|
55
60
|
}
|
|
56
61
|
// Simple function to convert a camelCase or PascalCase string to kebab-case.
|
|
57
62
|
function kebabCase(str) {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
export { remarkChoiceGroup };
|
|
2
|
+
import { visit } from 'unist-util-visit';
|
|
3
|
+
import { parseMetaString } from './rehypeMetaToProps.js';
|
|
4
|
+
import { generateChoiceGroup } from './utils/generateChoiceGroup.js';
|
|
5
|
+
function remarkChoiceGroup() {
|
|
6
|
+
return function (tree) {
|
|
7
|
+
visit(tree, (node) => {
|
|
8
|
+
if (node.type === 'code') {
|
|
9
|
+
if (!node.meta)
|
|
10
|
+
return;
|
|
11
|
+
const meta = parseMetaString(node.meta, ['choice']);
|
|
12
|
+
const { choice } = meta.props;
|
|
13
|
+
node.meta = meta.rest;
|
|
14
|
+
if (choice)
|
|
15
|
+
node.data ??= { choice };
|
|
16
|
+
}
|
|
17
|
+
if (node.type === 'containerDirective' && node.name === 'Choice') {
|
|
18
|
+
if (!node.attributes)
|
|
19
|
+
return;
|
|
20
|
+
const { id: choice } = node.attributes;
|
|
21
|
+
if (choice) {
|
|
22
|
+
node.data ??= { choice };
|
|
23
|
+
node.attributes = {};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
const replaced = new WeakSet();
|
|
28
|
+
visit(tree, (node) => {
|
|
29
|
+
if (!('children' in node) || replaced.has(node))
|
|
30
|
+
return 'skip';
|
|
31
|
+
if (node.type === 'mdxJsxFlowElement')
|
|
32
|
+
return 'skip';
|
|
33
|
+
let start = -1;
|
|
34
|
+
let end = 0;
|
|
35
|
+
const process = () => {
|
|
36
|
+
if (start === -1 || start === end)
|
|
37
|
+
return;
|
|
38
|
+
const nodes = node.children.slice(start, end);
|
|
39
|
+
const groupedNodes = groupByNodeType(nodes);
|
|
40
|
+
const replacements = [];
|
|
41
|
+
for (const groupedNode of groupedNodes) {
|
|
42
|
+
const replacement = generateChoiceGroup(groupedNode);
|
|
43
|
+
replacements.push(replacement);
|
|
44
|
+
replaced.add(replacement);
|
|
45
|
+
}
|
|
46
|
+
node.children.splice(start, end - start, ...replacements);
|
|
47
|
+
end = start;
|
|
48
|
+
start = -1;
|
|
49
|
+
};
|
|
50
|
+
for (; end < node.children.length; end++) {
|
|
51
|
+
const child = node.children[end];
|
|
52
|
+
if (!['code', 'mdxJsxFlowElement', 'containerDirective'].includes(child.type)) {
|
|
53
|
+
process();
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (!child.data?.choice) {
|
|
57
|
+
process();
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (start === -1)
|
|
61
|
+
start = end;
|
|
62
|
+
}
|
|
63
|
+
process();
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function groupByNodeType(nodes) {
|
|
68
|
+
const groupedNodes = new Set();
|
|
69
|
+
const filters = [...new Set(nodes.flat().map((node) => (node.type === 'code' ? node.lang : node.name)))];
|
|
70
|
+
filters.map((filter) => {
|
|
71
|
+
const nodesByChoice = new Map();
|
|
72
|
+
nodes
|
|
73
|
+
.filter((node) => (node.type === 'code' ? node.lang : node.name) === filter)
|
|
74
|
+
.map((node) => {
|
|
75
|
+
const choice = node.data.choice;
|
|
76
|
+
const nodes = nodesByChoice.get(choice) ?? [];
|
|
77
|
+
nodes.push(node);
|
|
78
|
+
node.data = {};
|
|
79
|
+
nodesByChoice.set(choice, nodes);
|
|
80
|
+
});
|
|
81
|
+
groupedNodes.add([...nodesByChoice].map(([name, nodes]) => ({ value: name, children: nodes })));
|
|
82
|
+
});
|
|
83
|
+
return [...groupedNodes];
|
|
84
|
+
}
|
|
@@ -65,7 +65,10 @@ function transformYaml(node) {
|
|
|
65
65
|
}
|
|
66
66
|
async function transformTsToJs(node, file) {
|
|
67
67
|
const { codeBlock, index, parent } = node;
|
|
68
|
-
const
|
|
68
|
+
const meta = parseMetaString(codeBlock.meta, ['max-width', 'choice']);
|
|
69
|
+
const maxWidth = Number(meta.props['max-width']);
|
|
70
|
+
const { choice } = meta.props;
|
|
71
|
+
codeBlock.meta = meta.rest;
|
|
69
72
|
let codeBlockReplacedJs = replaceFileNameSuffixes(codeBlock.value);
|
|
70
73
|
let codeBlockContentJs = '';
|
|
71
74
|
// Remove TypeScript from the TS/TSX/Vue code node
|
|
@@ -113,8 +116,8 @@ async function transformTsToJs(node, file) {
|
|
|
113
116
|
// Add `hideToggle` attribute (prop) to `CodeSnippets` if the only change was replacing `.ts` with `.js`
|
|
114
117
|
if (codeBlockReplacedJs === codeBlockContentJs) {
|
|
115
118
|
attributes.push({
|
|
116
|
-
name: 'hideToggle',
|
|
117
119
|
type: 'mdxJsxAttribute',
|
|
120
|
+
name: 'hideToggle',
|
|
118
121
|
});
|
|
119
122
|
}
|
|
120
123
|
// Wrap both the original `codeBlock` and `jsCode` with <CodeSnippets>
|
|
@@ -124,6 +127,8 @@ async function transformTsToJs(node, file) {
|
|
|
124
127
|
children: [jsCode, codeBlock],
|
|
125
128
|
attributes,
|
|
126
129
|
};
|
|
130
|
+
if (choice)
|
|
131
|
+
container.data ??= { choice };
|
|
127
132
|
parent.children.splice(index, 1, container);
|
|
128
133
|
}
|
|
129
134
|
// Replace all '.ts' extensions with '.js'
|
|
@@ -138,7 +143,7 @@ function cleanUpCode(code, isJsCode = false) {
|
|
|
138
143
|
}
|
|
139
144
|
function processMagicComments(code) {
|
|
140
145
|
// @detype-replace DummyLayout Layout
|
|
141
|
-
const renameCommentRE =
|
|
146
|
+
const renameCommentRE = /^\s*\/\/\s@detype-replace\s([^ ]+) ([^ ]+)\n/gm;
|
|
142
147
|
const matches = Array.from(code.matchAll(renameCommentRE));
|
|
143
148
|
if (matches.length) {
|
|
144
149
|
for (let i = matches.length - 1; i >= 0; i--) {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export { remarkPkgManager };
|
|
2
|
+
import { visit } from 'unist-util-visit';
|
|
3
|
+
import convert from 'npm-to-yarn';
|
|
4
|
+
import { parseMetaString } from './rehypeMetaToProps.js';
|
|
5
|
+
import { generateChoiceGroup } from './utils/generateChoiceGroup.js';
|
|
6
|
+
const PKG_MANAGERS = ['pnpm', 'yarn', 'bun'];
|
|
7
|
+
function remarkPkgManager() {
|
|
8
|
+
return function (tree) {
|
|
9
|
+
visit(tree, 'code', (node, index, parent) => {
|
|
10
|
+
if (!parent || typeof index === 'undefined')
|
|
11
|
+
return;
|
|
12
|
+
if (!['sh', 'shell'].includes(node.lang || ''))
|
|
13
|
+
return;
|
|
14
|
+
if (node.value.indexOf('npm') === -1 && node.value.indexOf('npx') === -1)
|
|
15
|
+
return;
|
|
16
|
+
let choice = undefined;
|
|
17
|
+
const nodes = new Map();
|
|
18
|
+
if (node.meta) {
|
|
19
|
+
const meta = parseMetaString(node.meta, ['choice']);
|
|
20
|
+
choice = meta.props['choice'];
|
|
21
|
+
node.meta = meta.rest;
|
|
22
|
+
}
|
|
23
|
+
nodes.set('npm', node);
|
|
24
|
+
for (const pm of PKG_MANAGERS) {
|
|
25
|
+
nodes.set(pm, {
|
|
26
|
+
type: node.type,
|
|
27
|
+
lang: node.lang,
|
|
28
|
+
meta: node.meta,
|
|
29
|
+
value: convert(node.value, pm),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
const groupedNodes = [...nodes].map(([name, node]) => ({ value: name, children: [node] }));
|
|
33
|
+
const replacement = generateChoiceGroup(groupedNodes);
|
|
34
|
+
replacement.data ??= { choice };
|
|
35
|
+
parent.children.splice(index, 1, replacement);
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { generateChoiceGroup };
|
|
2
|
+
export type { CodeChoice };
|
|
3
|
+
import type { BlockContent, DefinitionContent } from 'mdast';
|
|
4
|
+
import type { MdxJsxFlowElement } from 'mdast-util-mdx-jsx';
|
|
5
|
+
type CodeChoice = {
|
|
6
|
+
value: string;
|
|
7
|
+
children: (BlockContent | DefinitionContent)[];
|
|
8
|
+
};
|
|
9
|
+
declare function generateChoiceGroup(codeChoices: CodeChoice[]): MdxJsxFlowElement;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export { generateChoiceGroup };
|
|
2
|
+
function generateChoiceGroup(codeChoices) {
|
|
3
|
+
const attributes = [];
|
|
4
|
+
const children = [];
|
|
5
|
+
attributes.push({
|
|
6
|
+
type: 'mdxJsxAttribute',
|
|
7
|
+
name: 'choices',
|
|
8
|
+
value: {
|
|
9
|
+
type: 'mdxJsxAttributeValueExpression',
|
|
10
|
+
value: '',
|
|
11
|
+
data: {
|
|
12
|
+
estree: {
|
|
13
|
+
type: 'Program',
|
|
14
|
+
sourceType: 'module',
|
|
15
|
+
comments: [],
|
|
16
|
+
body: [
|
|
17
|
+
{
|
|
18
|
+
type: 'ExpressionStatement',
|
|
19
|
+
expression: {
|
|
20
|
+
type: 'ArrayExpression',
|
|
21
|
+
// @ts-ignore: Missing properties in type definition
|
|
22
|
+
elements: codeChoices.map((choice) => ({
|
|
23
|
+
type: 'Literal',
|
|
24
|
+
value: choice.value,
|
|
25
|
+
})),
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
for (const codeChoice of codeChoices) {
|
|
34
|
+
const classNames = ['choice'];
|
|
35
|
+
if (findHasJsToggle(codeChoice.children[0])) {
|
|
36
|
+
classNames.push('has-toggle');
|
|
37
|
+
}
|
|
38
|
+
children.push({
|
|
39
|
+
type: 'mdxJsxFlowElement',
|
|
40
|
+
name: 'div',
|
|
41
|
+
attributes: [
|
|
42
|
+
{ type: 'mdxJsxAttribute', name: 'id', value: codeChoice.value },
|
|
43
|
+
{ type: 'mdxJsxAttribute', name: 'className', value: classNames.join(' ') },
|
|
44
|
+
],
|
|
45
|
+
children: codeChoice.children.every((node) => node.type === 'containerDirective')
|
|
46
|
+
? codeChoice.children.flatMap((node) => [...node.children])
|
|
47
|
+
: codeChoice.children,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
type: 'mdxJsxFlowElement',
|
|
52
|
+
name: 'ChoiceGroup',
|
|
53
|
+
attributes,
|
|
54
|
+
children,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function findHasJsToggle(node) {
|
|
58
|
+
if (node.type === 'containerDirective' && node.name === 'Choice') {
|
|
59
|
+
return (node.children[0].type === 'mdxJsxFlowElement' &&
|
|
60
|
+
node.children[0].name === 'CodeSnippets' &&
|
|
61
|
+
node.children[0].attributes.every((attribute) => attribute.type !== 'mdxJsxAttribute' || attribute.name !== 'hideToggle'));
|
|
62
|
+
}
|
|
63
|
+
return (node.type === 'mdxJsxFlowElement' &&
|
|
64
|
+
node.name === 'CodeSnippets' &&
|
|
65
|
+
node.attributes.every((attribute) => attribute.type !== 'mdxJsxAttribute' || attribute.name !== 'hideToggle'));
|
|
66
|
+
}
|
package/dist/types/Config.d.ts
CHANGED
|
@@ -7,7 +7,10 @@ type Config = {
|
|
|
7
7
|
/** Sets `<meta name="description" content="${tagline}" />` */
|
|
8
8
|
tagline: string;
|
|
9
9
|
logo: string;
|
|
10
|
-
favicon?: string
|
|
10
|
+
favicon?: string | {
|
|
11
|
+
browser: string;
|
|
12
|
+
google: string;
|
|
13
|
+
};
|
|
11
14
|
banner?: string;
|
|
12
15
|
github: string;
|
|
13
16
|
discord?: string;
|
|
@@ -34,6 +37,7 @@ type Config = {
|
|
|
34
37
|
navLogoStyle?: React.CSSProperties;
|
|
35
38
|
navLogoTextStyle?: React.CSSProperties;
|
|
36
39
|
globalNote?: React.ReactNode;
|
|
40
|
+
choices?: Record<string, Choice>;
|
|
37
41
|
};
|
|
38
42
|
/** Order in Algolia search results */
|
|
39
43
|
type Category = string | {
|
|
@@ -41,3 +45,7 @@ type Category = string | {
|
|
|
41
45
|
/** Hide from Algolia search */
|
|
42
46
|
hide?: boolean;
|
|
43
47
|
};
|
|
48
|
+
type Choice = {
|
|
49
|
+
choices: string[];
|
|
50
|
+
default: string;
|
|
51
|
+
};
|
package/dist/vite.config.js
CHANGED
|
@@ -4,10 +4,13 @@ import react from '@vitejs/plugin-react-swc';
|
|
|
4
4
|
import { parsePageSections } from './parsePageSections.js';
|
|
5
5
|
import rehypePrettyCode from 'rehype-pretty-code';
|
|
6
6
|
import remarkGfm from 'remark-gfm';
|
|
7
|
+
import remarkDirective from 'remark-directive';
|
|
7
8
|
import { transformerNotationDiff } from '@shikijs/transformers';
|
|
8
9
|
import { rehypeMetaToProps } from './code-blocks/rehypeMetaToProps.js';
|
|
9
10
|
import { remarkDetype } from './code-blocks/remarkDetype.js';
|
|
10
11
|
import { shikiTransformerAutoLinks } from './code-blocks/shikiTransformerAutoLinks.js';
|
|
12
|
+
import { remarkPkgManager } from './code-blocks/remarkPkgManager.js';
|
|
13
|
+
import { remarkChoiceGroup } from './code-blocks/remarkChoiceGroup.js';
|
|
11
14
|
const root = process.cwd();
|
|
12
15
|
const prettyCode = [
|
|
13
16
|
rehypePrettyCode,
|
|
@@ -18,7 +21,7 @@ const prettyCode = [
|
|
|
18
21
|
},
|
|
19
22
|
];
|
|
20
23
|
const rehypePlugins = [prettyCode, [rehypeMetaToProps]];
|
|
21
|
-
const remarkPlugins = [remarkGfm, remarkDetype];
|
|
24
|
+
const remarkPlugins = [remarkGfm, remarkDirective, remarkDetype, remarkPkgManager, remarkChoiceGroup];
|
|
22
25
|
const config = {
|
|
23
26
|
root,
|
|
24
27
|
plugins: [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brillout/docpress",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.7-commit-fc2ed19",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@brillout/picocolors": "^1.0.10",
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
"@shikijs/transformers": "1.2.0",
|
|
13
13
|
"@vitejs/plugin-react-swc": "^3.10.2",
|
|
14
14
|
"detype": "^1.1.3",
|
|
15
|
+
"npm-to-yarn": "^3.0.1",
|
|
15
16
|
"rehype-pretty-code": "0.13.0",
|
|
17
|
+
"remark-directive": "^4.0.0",
|
|
16
18
|
"remark-gfm": "4.0.0",
|
|
17
19
|
"shiki": "1.2.0",
|
|
18
20
|
"unist-util-visit": "^5.0.0",
|
|
@@ -65,6 +67,7 @@
|
|
|
65
67
|
"@types/node": "^24.10.0",
|
|
66
68
|
"@types/react": "^19.2.2",
|
|
67
69
|
"@types/react-dom": "^19.2.2",
|
|
70
|
+
"mdast-util-directive": "^3.1.0",
|
|
68
71
|
"mdast-util-mdx-jsx": "^3.2.0"
|
|
69
72
|
},
|
|
70
73
|
"repository": "https://github.com/brillout/docpress",
|
|
@@ -18,16 +18,13 @@ async function onRenderHtml(pageContext: PageContextServer): Promise<any> {
|
|
|
18
18
|
|
|
19
19
|
const pageHtml = ReactDOMServer.renderToString(page)
|
|
20
20
|
|
|
21
|
-
const faviconUrl = pageContext.globalContext.config.docpress.favicon ?? pageContext.globalContext.config.docpress.logo
|
|
22
|
-
assert(faviconUrl)
|
|
23
|
-
|
|
24
21
|
const { documentTitle } = pageContext.resolved
|
|
25
22
|
assert(documentTitle)
|
|
26
23
|
return escapeInject`<!DOCTYPE html>
|
|
27
24
|
<html>
|
|
28
25
|
<head>
|
|
29
26
|
<meta charset="UTF-8" />
|
|
30
|
-
|
|
27
|
+
${getFaviconTags(pageContext.globalContext.config.docpress)}
|
|
31
28
|
<title>${documentTitle}</title>
|
|
32
29
|
${descriptionTag}
|
|
33
30
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
@@ -98,3 +95,44 @@ function getOpenGraphTags(url: string, documentTitle: string, config: Config) {
|
|
|
98
95
|
${metaTwitter}
|
|
99
96
|
`
|
|
100
97
|
}
|
|
98
|
+
|
|
99
|
+
// Resources:
|
|
100
|
+
// - https://www.google.com/s2/favicons?domain=vike.dev
|
|
101
|
+
// - https://stackoverflow.com/questions/59568586/favicon-don%c2%b4t-show-up-in-google-search-result/59577456#59577456
|
|
102
|
+
// - https://developers.google.com/search/docs/appearance/favicon-in-search
|
|
103
|
+
//
|
|
104
|
+
// Examples:
|
|
105
|
+
// - Nice looking on Goolge Search Results:
|
|
106
|
+
// https://www.wikipedia.org
|
|
107
|
+
// - Single PNG:
|
|
108
|
+
// https://rubyonrails.org
|
|
109
|
+
// - Favicon shown in browser is different than favicon shown in Google:
|
|
110
|
+
// https://evilmartians.com
|
|
111
|
+
// Shown in Google: <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
|
112
|
+
// Shown in Browser: <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
|
113
|
+
function getFaviconTags(config: Config) {
|
|
114
|
+
const { faviconBrowser, faviconGoogle } = getFavicons(config)
|
|
115
|
+
assert(faviconBrowser)
|
|
116
|
+
const faviconTagGoogle = !faviconGoogle
|
|
117
|
+
? ''
|
|
118
|
+
: escapeInject`
|
|
119
|
+
<link rel="apple-touch-icon" href="${faviconGoogle}" />
|
|
120
|
+
`
|
|
121
|
+
return escapeInject`
|
|
122
|
+
<link rel="icon" href="${faviconBrowser}" type="image/svg+xml" />
|
|
123
|
+
${faviconTagGoogle}
|
|
124
|
+
`
|
|
125
|
+
}
|
|
126
|
+
function getFavicons(config: Config) {
|
|
127
|
+
let faviconBrowser: string
|
|
128
|
+
let faviconGoogle: null | string = null
|
|
129
|
+
if (!config.favicon) {
|
|
130
|
+
faviconBrowser = config.logo
|
|
131
|
+
} else if (typeof config.favicon === 'string') {
|
|
132
|
+
faviconBrowser = config.favicon
|
|
133
|
+
} else {
|
|
134
|
+
faviconBrowser = config.favicon.browser
|
|
135
|
+
faviconGoogle = config.favicon.google
|
|
136
|
+
}
|
|
137
|
+
return { faviconBrowser, faviconGoogle }
|
|
138
|
+
}
|
package/types/Config.ts
CHANGED
|
@@ -9,7 +9,12 @@ type Config = {
|
|
|
9
9
|
/** Sets `<meta name="description" content="${tagline}" />` */
|
|
10
10
|
tagline: string
|
|
11
11
|
logo: string
|
|
12
|
-
favicon?:
|
|
12
|
+
favicon?:
|
|
13
|
+
| string
|
|
14
|
+
| {
|
|
15
|
+
browser: string
|
|
16
|
+
google: string
|
|
17
|
+
}
|
|
13
18
|
banner?: string
|
|
14
19
|
|
|
15
20
|
github: string
|
|
@@ -43,6 +48,7 @@ type Config = {
|
|
|
43
48
|
navLogoTextStyle?: React.CSSProperties
|
|
44
49
|
|
|
45
50
|
globalNote?: React.ReactNode
|
|
51
|
+
choices?: Record<string, Choice>
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
/** Order in Algolia search results */
|
|
@@ -53,3 +59,8 @@ type Category =
|
|
|
53
59
|
/** Hide from Algolia search */
|
|
54
60
|
hide?: boolean
|
|
55
61
|
}
|
|
62
|
+
|
|
63
|
+
type Choice = {
|
|
64
|
+
choices: string[]
|
|
65
|
+
default: string
|
|
66
|
+
}
|