@brillout/docpress 0.15.11 → 0.15.13
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/ExternalLinks.tsx +2 -2
- package/Layout.tsx +3 -0
- package/autoScrollNav.ts +3 -3
- package/{components → code-blocks/components}/CodeSnippets.css +1 -5
- package/{components → code-blocks/components}/CodeSnippets.tsx +3 -2
- package/{components → code-blocks/components}/Pre.tsx +1 -3
- package/{components → code-blocks/hooks}/useMDXComponents.tsx +2 -2
- package/{components/CodeSnippets → code-blocks/hooks}/useSelectCodeLang.ts +14 -0
- package/{rehypeMetaToProps.ts → code-blocks/rehypeMetaToProps.ts} +2 -2
- package/{remarkDetype.ts → code-blocks/remarkDetype.ts} +31 -11
- package/code-blocks/shikiTransformerAutoLinks.ts +61 -0
- package/components/index.ts +0 -1
- package/css/index.css +0 -1
- package/dist/code-blocks/rehypeMetaToProps.d.ts +35 -0
- package/dist/{rehypeMetaToProps.js → code-blocks/rehypeMetaToProps.js} +2 -2
- package/dist/{remarkDetype.js → code-blocks/remarkDetype.js} +25 -7
- package/dist/code-blocks/shikiTransformerAutoLinks.d.ts +8 -0
- package/dist/code-blocks/shikiTransformerAutoLinks.js +51 -0
- package/dist/components/index.d.ts +0 -1
- package/dist/components/index.js +0 -1
- package/dist/types/Config.d.ts +1 -0
- package/dist/vite.config.js +8 -3
- package/index.ts +3 -11
- package/package.json +3 -2
- package/types/Config.ts +1 -0
- package/vite.config.ts +8 -3
- package/dist/components/CodeSnippets/useSelectCodeLang.d.ts +0 -7
- package/dist/components/CodeSnippets/useSelectCodeLang.js +0 -50
- package/dist/components/CodeSnippets.d.ts +0 -11
- package/dist/components/CodeSnippets.js +0 -35
- package/dist/rehypeMetaToProps.d.ts +0 -19
- /package/{components → code-blocks/components}/Pre.css +0 -0
- /package/dist/{remarkDetype.d.ts → code-blocks/remarkDetype.d.ts} +0 -0
package/ExternalLinks.tsx
CHANGED
|
@@ -13,7 +13,7 @@ import '@docsearch/css'
|
|
|
13
13
|
|
|
14
14
|
function ExternalLinks(props: { style?: React.CSSProperties }) {
|
|
15
15
|
const pageContext = usePageContext()
|
|
16
|
-
const { github, discord, bluesky, linkedin, i18n, twitter } = pageContext.globalContext.config.docpress
|
|
16
|
+
const { github, discord, bluesky, linkedin, i18n, twitter, changelog } = pageContext.globalContext.config.docpress
|
|
17
17
|
const iconI18n = !i18n ? null : (
|
|
18
18
|
<LinkIcon
|
|
19
19
|
className="decolorize-4"
|
|
@@ -39,7 +39,7 @@ function ExternalLinks(props: { style?: React.CSSProperties }) {
|
|
|
39
39
|
<LinkIcon className="decolorize-6" icon={iconLinkedin} href={`https://www.linkedin.com/company/${linkedin}`} />
|
|
40
40
|
)}
|
|
41
41
|
<LinkIcon className="decolorize-4" icon={iconGithub} href={github} />
|
|
42
|
-
<ChangelogButton />
|
|
42
|
+
{changelog !== false && <ChangelogButton />}
|
|
43
43
|
</div>
|
|
44
44
|
)
|
|
45
45
|
}
|
package/Layout.tsx
CHANGED
|
@@ -15,6 +15,7 @@ import { ExternalLinks } from './ExternalLinks'
|
|
|
15
15
|
import { coseMenuModalOnMouseLeave, openMenuModal, toggleMenuModal } from './MenuModal/toggleMenuModal'
|
|
16
16
|
import { MenuModal } from './MenuModal'
|
|
17
17
|
import { autoScrollNav_SSR } from './autoScrollNav'
|
|
18
|
+
import { initializeJsToggle_SSR } from './code-blocks/hooks/useSelectCodeLang'
|
|
18
19
|
import { SearchLink } from './docsearch/SearchLink'
|
|
19
20
|
import { navigate } from 'vike/client/router'
|
|
20
21
|
import { css } from './utils/css'
|
|
@@ -78,6 +79,8 @@ function Layout({ children }: { children: React.ReactNode }) {
|
|
|
78
79
|
<NavHead />
|
|
79
80
|
{content}
|
|
80
81
|
</div>
|
|
82
|
+
{/* Early toggling, to avoid layout jumps */}
|
|
83
|
+
<script dangerouslySetInnerHTML={{ __html: initializeJsToggle_SSR }}></script>
|
|
81
84
|
</div>
|
|
82
85
|
)
|
|
83
86
|
}
|
package/autoScrollNav.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { autoScrollNav }
|
|
2
|
-
export
|
|
2
|
+
export { autoScrollNav_SSR }
|
|
3
3
|
|
|
4
|
-
// - We cannot use TypeScript
|
|
4
|
+
// - WARNING: We cannot use TypeScript here because we serialize the function.
|
|
5
5
|
// - We have to save & restore `document.documentElement.scrollTop` because scrollIntoView() scrolls the main view. (I don't know why).
|
|
6
6
|
// - Failed alternatives:
|
|
7
7
|
// - scrollIntoViewIfNeeded() (https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoViewIfNeeded) would work (it doesn't scroll the main view) but Firefox doesn't support it.
|
|
@@ -17,7 +17,7 @@ export const autoScrollNav_SSR = `autoScrollNav();${autoScrollNav.toString()}`
|
|
|
17
17
|
// // ...
|
|
18
18
|
// document.documentElement.style.overflow = overflowOriginal
|
|
19
19
|
// ```
|
|
20
|
-
|
|
20
|
+
const autoScrollNav_SSR = `autoScrollNav();${autoScrollNav.toString()}`
|
|
21
21
|
function autoScrollNav() {
|
|
22
22
|
const nav = document.querySelector('#nav-left .navigation-content')
|
|
23
23
|
if (!nav) return
|
|
@@ -8,11 +8,6 @@
|
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
/* Hide language toggle for YAML */
|
|
12
|
-
&:has(code[data-language='yaml']) .code-lang-toggle {
|
|
13
|
-
display: none !important;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
11
|
/* Language toggle styles */
|
|
17
12
|
.code-lang-toggle {
|
|
18
13
|
position: absolute !important;
|
|
@@ -28,6 +23,7 @@
|
|
|
28
23
|
margin: 0;
|
|
29
24
|
padding: 0 4px;
|
|
30
25
|
height: 25px;
|
|
26
|
+
width: 59px;
|
|
31
27
|
display: flex;
|
|
32
28
|
background-color: #f7f7f7;
|
|
33
29
|
opacity: 0;
|
|
@@ -5,7 +5,7 @@ export { TypescriptOnly }
|
|
|
5
5
|
export { CodeSnippets }
|
|
6
6
|
|
|
7
7
|
import React, { useEffect, useRef } from 'react'
|
|
8
|
-
import { useSelectCodeLang } from '
|
|
8
|
+
import { useSelectCodeLang } from '../hooks/useSelectCodeLang'
|
|
9
9
|
import './CodeSnippets.css'
|
|
10
10
|
|
|
11
11
|
/** Only show if TypeScript is selected */
|
|
@@ -14,7 +14,7 @@ function TypescriptOnly({ children }: { children: React.ReactNode }) {
|
|
|
14
14
|
return <div style={{ display: codeLangSelected === 'ts' ? 'block' : 'none' }}>{children}</div>
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
function CodeSnippets({ children }: { children: React.ReactNode }) {
|
|
17
|
+
function CodeSnippets({ children, hideToggle = false }: { children: React.ReactNode; hideToggle: boolean }) {
|
|
18
18
|
const [codeLangSelected, selectCodeLang] = useSelectCodeLang()
|
|
19
19
|
const prevPositionRef = useRef<null | { top: number; el: Element }>(null)
|
|
20
20
|
|
|
@@ -35,6 +35,7 @@ function CodeSnippets({ children }: { children: React.ReactNode }) {
|
|
|
35
35
|
type="checkbox"
|
|
36
36
|
name="code-lang-toggle"
|
|
37
37
|
className="code-lang-toggle raised"
|
|
38
|
+
style={{ display: hideToggle ? 'none' : undefined }}
|
|
38
39
|
checked={codeLangSelected === 'ts'}
|
|
39
40
|
onChange={onChange}
|
|
40
41
|
title="Toggle language"
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
export { Pre }
|
|
2
2
|
|
|
3
3
|
import React, { ComponentPropsWithoutRef, useState } from 'react'
|
|
4
|
-
/* Importing it here chokes the tests. I don't know why.
|
|
5
4
|
import './Pre.css'
|
|
6
|
-
//*/
|
|
7
5
|
|
|
8
6
|
function Pre({ children, ...props }: ComponentPropsWithoutRef<'pre'> & { 'hide-menu'?: string }) {
|
|
9
7
|
return (
|
|
@@ -26,7 +24,7 @@ function CopyButton() {
|
|
|
26
24
|
const icon =
|
|
27
25
|
isSuccess === null ? (
|
|
28
26
|
// Copy icon
|
|
29
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke="
|
|
27
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke="black" strokeWidth="2">
|
|
30
28
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
|
31
29
|
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
|
32
30
|
</svg>
|
|
@@ -2,8 +2,8 @@ export { useMDXComponents }
|
|
|
2
2
|
|
|
3
3
|
import React from 'react'
|
|
4
4
|
import type { UseMdxComponents } from '@mdx-js/mdx'
|
|
5
|
-
import { Pre } from '
|
|
6
|
-
import { CodeSnippets } from '
|
|
5
|
+
import { Pre } from '../components/Pre.js'
|
|
6
|
+
import { CodeSnippets } from '../components/CodeSnippets.js'
|
|
7
7
|
|
|
8
8
|
const useMDXComponents: UseMdxComponents = () => {
|
|
9
9
|
return {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { useSelectCodeLang }
|
|
2
|
+
export { initializeJsToggle_SSR }
|
|
2
3
|
|
|
3
4
|
import { useState, useEffect, useCallback } from 'react'
|
|
4
5
|
import { assertWarning } from '../../utils/assert'
|
|
@@ -53,6 +54,19 @@ function useSelectCodeLang() {
|
|
|
53
54
|
return [codeLangSelected, selectCodeLang] as const
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
// WARNING: We cannot use the variables storageKey nor codeLangDefaultClient here: closures
|
|
58
|
+
// don't work because we serialize the function.
|
|
59
|
+
// WARNING: We cannot use TypeScript here, for the same reason.
|
|
60
|
+
const initializeJsToggle_SSR = `initializeJsToggle();${initializeJsToggle.toString()};`
|
|
61
|
+
function initializeJsToggle() {
|
|
62
|
+
const codeLangSelected = localStorage.getItem('docpress:code-lang') ?? 'js'
|
|
63
|
+
if (codeLangSelected === 'js') {
|
|
64
|
+
const inputs = document.querySelectorAll('.code-lang-toggle')
|
|
65
|
+
// @ts-ignore
|
|
66
|
+
for (const input of inputs) input.checked = false
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
56
70
|
declare global {
|
|
57
71
|
interface WindowEventMap {
|
|
58
72
|
'code-lang-storage': CustomEvent
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { rehypeMetaToProps }
|
|
1
|
+
export { rehypeMetaToProps, parseMetaString }
|
|
2
2
|
|
|
3
3
|
import { visit } from 'unist-util-visit'
|
|
4
4
|
import type { ElementData, Root } from 'hast'
|
|
@@ -51,7 +51,7 @@ function parseMetaString(metaString: ElementData['meta']): Record<string, string
|
|
|
51
51
|
|
|
52
52
|
const props: Record<string, string> = {}
|
|
53
53
|
|
|
54
|
-
const keyValuePairRE = /([a-zA-Z_-]+)(?:=
|
|
54
|
+
const keyValuePairRE = /([a-zA-Z_-]+)(?:=([^"'\s]+))?(?=\s|$)/g
|
|
55
55
|
for (const match of metaString.matchAll(keyValuePairRE)) {
|
|
56
56
|
let [_, key, value] = match
|
|
57
57
|
props[kebabCase(key)] = value || 'true'
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
export { remarkDetype }
|
|
2
2
|
|
|
3
3
|
import type { Root, Parent, Code } from 'mdast'
|
|
4
|
+
import type { MdxJsxFlowElement } from 'mdast-util-mdx-jsx'
|
|
4
5
|
import type { VFile } from '@mdx-js/mdx/internal-create-format-aware-processors'
|
|
5
6
|
import { visit } from 'unist-util-visit'
|
|
6
|
-
import { assertUsage } from '
|
|
7
|
+
import { assertUsage } from '../utils/assert.js'
|
|
8
|
+
import { parseMetaString } from './rehypeMetaToProps.js'
|
|
7
9
|
import pc from '@brillout/picocolors'
|
|
8
10
|
import module from 'node:module'
|
|
9
11
|
// Cannot use `import { transform } from 'detype'` as it results in errors,
|
|
@@ -13,7 +15,6 @@ const { transform: detype } = module.createRequire(import.meta.url)('detype') as
|
|
|
13
15
|
const prettierOptions: NonNullable<Parameters<typeof detype>[2]>['prettierOptions'] = {
|
|
14
16
|
semi: false,
|
|
15
17
|
singleQuote: true,
|
|
16
|
-
printWidth: 100,
|
|
17
18
|
trailingComma: 'none',
|
|
18
19
|
}
|
|
19
20
|
|
|
@@ -65,22 +66,29 @@ function transformYaml(node: CodeNode) {
|
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
// Wrap both the original YAML and `yamlJsCode` with <CodeSnippets>
|
|
68
|
-
const yamlContainer = {
|
|
69
|
-
type: 'mdxJsxFlowElement'
|
|
69
|
+
const yamlContainer: MdxJsxFlowElement = {
|
|
70
|
+
type: 'mdxJsxFlowElement',
|
|
70
71
|
name: 'CodeSnippets',
|
|
71
72
|
children: [yamlJsCode, codeBlock],
|
|
72
|
-
attributes: [
|
|
73
|
+
attributes: [
|
|
74
|
+
{
|
|
75
|
+
name: 'hideToggle',
|
|
76
|
+
type: 'mdxJsxAttribute',
|
|
77
|
+
},
|
|
78
|
+
],
|
|
73
79
|
}
|
|
74
80
|
parent.children.splice(index, 1, yamlContainer)
|
|
75
81
|
}
|
|
76
82
|
|
|
77
83
|
async function transformTsToJs(node: CodeNode, file: VFile) {
|
|
78
84
|
const { codeBlock, index, parent } = node
|
|
79
|
-
|
|
85
|
+
const maxWidth = Number(parseMetaString(codeBlock.meta)['max-width'])
|
|
86
|
+
let codeBlockReplacedJs = replaceFileNameSuffixes(codeBlock.value)
|
|
87
|
+
let codeBlockContentJs = ''
|
|
80
88
|
|
|
81
89
|
// Remove TypeScript from the TS/TSX/Vue code node
|
|
82
90
|
try {
|
|
83
|
-
codeBlockContentJs = await detype(
|
|
91
|
+
codeBlockContentJs = await detype(codeBlockReplacedJs, `some-dummy-filename.${codeBlock.lang}`, {
|
|
84
92
|
customizeBabelConfig(config) {
|
|
85
93
|
// Add `onlyRemoveTypeImports: true` to the internal `@babel/preset-typescript` config
|
|
86
94
|
// https://github.com/cyco130/detype/blob/46ec867e9efd31d31a312a215ca169bd6bff4726/src/transform.ts#L206
|
|
@@ -88,7 +96,10 @@ async function transformTsToJs(node: CodeNode, file: VFile) {
|
|
|
88
96
|
config.presets = [[config.presets[0], { onlyRemoveTypeImports: true }]]
|
|
89
97
|
},
|
|
90
98
|
removeTsComments: true,
|
|
91
|
-
prettierOptions
|
|
99
|
+
prettierOptions: {
|
|
100
|
+
...prettierOptions,
|
|
101
|
+
printWidth: maxWidth ? maxWidth : 99,
|
|
102
|
+
},
|
|
92
103
|
})
|
|
93
104
|
} catch (error) {
|
|
94
105
|
// Log errors and return original content instead of throwing
|
|
@@ -113,6 +124,7 @@ async function transformTsToJs(node: CodeNode, file: VFile) {
|
|
|
113
124
|
if (codeBlockContentJs === codeBlock.value) return
|
|
114
125
|
|
|
115
126
|
const { position, lang, ...rest } = codeBlock
|
|
127
|
+
const attributes: MdxJsxFlowElement['attributes'] = []
|
|
116
128
|
|
|
117
129
|
const jsCode: Code = {
|
|
118
130
|
...rest,
|
|
@@ -121,12 +133,20 @@ async function transformTsToJs(node: CodeNode, file: VFile) {
|
|
|
121
133
|
value: codeBlockContentJs,
|
|
122
134
|
}
|
|
123
135
|
|
|
136
|
+
// Add `hideToggle` attribute (prop) to `CodeSnippets` if the only change was replacing `.ts` with `.js`
|
|
137
|
+
if (codeBlockReplacedJs === codeBlockContentJs) {
|
|
138
|
+
attributes.push({
|
|
139
|
+
name: 'hideToggle',
|
|
140
|
+
type: 'mdxJsxAttribute',
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
124
144
|
// Wrap both the original `codeBlock` and `jsCode` with <CodeSnippets>
|
|
125
|
-
const container = {
|
|
126
|
-
type: 'mdxJsxFlowElement'
|
|
145
|
+
const container: MdxJsxFlowElement = {
|
|
146
|
+
type: 'mdxJsxFlowElement',
|
|
127
147
|
name: 'CodeSnippets',
|
|
128
148
|
children: [jsCode, codeBlock],
|
|
129
|
-
attributes
|
|
149
|
+
attributes,
|
|
130
150
|
}
|
|
131
151
|
parent.children.splice(index, 1, container)
|
|
132
152
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export { shikiTransformerAutoLinks }
|
|
2
|
+
|
|
3
|
+
import type { ShikiTransformer } from 'shiki'
|
|
4
|
+
|
|
5
|
+
const linkRE = /https:\/\/[^\s]*[^.,\s"'`]/g
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A Shiki transformer that converts plain HTTPS URLs in code blocks into clickable `<a>` links.
|
|
9
|
+
*
|
|
10
|
+
* Inspired by `@jcayzac/shiki-transformer-autolinks`, but tailored for a narrower use case.
|
|
11
|
+
*/
|
|
12
|
+
function shikiTransformerAutoLinks(): ShikiTransformer {
|
|
13
|
+
return {
|
|
14
|
+
name: 'docpress-shiki-autolinks',
|
|
15
|
+
span(span) {
|
|
16
|
+
if (span.children.length !== 1) return
|
|
17
|
+
let child = span.children[0]
|
|
18
|
+
if (child.type !== 'text') return
|
|
19
|
+
|
|
20
|
+
const links: { href: string; index: number }[] = []
|
|
21
|
+
const matches = Array.from(child.value.matchAll(linkRE))
|
|
22
|
+
|
|
23
|
+
// Filter out URLs that contain `${...}`. e.g. `https://star-wars.brillout.com/api/films/${id}.json`.
|
|
24
|
+
const filtered = matches.filter(([href]) => !href.includes('${'))
|
|
25
|
+
if (filtered.length === 0) return
|
|
26
|
+
|
|
27
|
+
for (const match of filtered) {
|
|
28
|
+
const [href] = match
|
|
29
|
+
links.unshift({ href, index: match.index })
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const newChildren: typeof span.children = []
|
|
33
|
+
for (const { href, index } of links) {
|
|
34
|
+
const postIndex = index + href.length
|
|
35
|
+
const postValue = child.value.slice(postIndex)
|
|
36
|
+
|
|
37
|
+
if (postValue.length > 0) {
|
|
38
|
+
newChildren.unshift({ type: 'text', value: postValue })
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
newChildren.unshift({
|
|
42
|
+
type: 'element',
|
|
43
|
+
tagName: 'a',
|
|
44
|
+
properties: { href },
|
|
45
|
+
children: [{ type: 'text', value: href }],
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
child = {
|
|
49
|
+
type: 'text',
|
|
50
|
+
value: child.value.slice(0, index),
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (child.value.length > 0) {
|
|
55
|
+
newChildren.unshift(child)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
span.children = newChildren
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
}
|
package/components/index.ts
CHANGED
package/css/index.css
CHANGED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export { rehypeMetaToProps, parseMetaString };
|
|
2
|
+
import type { ElementData, Root } from 'hast';
|
|
3
|
+
/**
|
|
4
|
+
* Rehype plugin to extract metadata from `<code>` blocks in markdown
|
|
5
|
+
* and attach them as props to the parent `<pre>` element.
|
|
6
|
+
*
|
|
7
|
+
* This allows using those props inside a custom `<Pre>` component.
|
|
8
|
+
*
|
|
9
|
+
* Example:
|
|
10
|
+
* ~~~mdx
|
|
11
|
+
* ```js foo="bar" hide_copy='true'
|
|
12
|
+
* export function add(a, b) {
|
|
13
|
+
* return a + b
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
* ~~~
|
|
17
|
+
* These props are then added to the `<pre>` element
|
|
18
|
+
*/
|
|
19
|
+
declare function rehypeMetaToProps(): (tree: Root) => void;
|
|
20
|
+
/**
|
|
21
|
+
* Minimal parser for a metadata string into key-value pairs.
|
|
22
|
+
*
|
|
23
|
+
* Supports simple patterns: key or key="value".
|
|
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.
|
|
27
|
+
*
|
|
28
|
+
* Example:
|
|
29
|
+
* parseMetaString('foo fooBar="value"')
|
|
30
|
+
* => { foo: 'true', foo_bar: 'value' }
|
|
31
|
+
*
|
|
32
|
+
* @param metaString - The input metadata string.
|
|
33
|
+
* @returns A plain object of parsed key-value pairs.
|
|
34
|
+
*/
|
|
35
|
+
declare function parseMetaString(metaString: ElementData['meta']): Record<string, string>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { rehypeMetaToProps };
|
|
1
|
+
export { rehypeMetaToProps, parseMetaString };
|
|
2
2
|
import { visit } from 'unist-util-visit';
|
|
3
3
|
/**
|
|
4
4
|
* Rehype plugin to extract metadata from `<code>` blocks in markdown
|
|
@@ -46,7 +46,7 @@ function parseMetaString(metaString) {
|
|
|
46
46
|
if (!metaString)
|
|
47
47
|
return {};
|
|
48
48
|
const props = {};
|
|
49
|
-
const keyValuePairRE = /([a-zA-Z_-]+)(?:=
|
|
49
|
+
const keyValuePairRE = /([a-zA-Z_-]+)(?:=([^"'\s]+))?(?=\s|$)/g;
|
|
50
50
|
for (const match of metaString.matchAll(keyValuePairRE)) {
|
|
51
51
|
let [_, key, value] = match;
|
|
52
52
|
props[kebabCase(key)] = value || 'true';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { remarkDetype };
|
|
2
2
|
import { visit } from 'unist-util-visit';
|
|
3
|
-
import { assertUsage } from '
|
|
3
|
+
import { assertUsage } from '../utils/assert.js';
|
|
4
|
+
import { parseMetaString } from './rehypeMetaToProps.js';
|
|
4
5
|
import pc from '@brillout/picocolors';
|
|
5
6
|
import module from 'node:module';
|
|
6
7
|
// Cannot use `import { transform } from 'detype'` as it results in errors,
|
|
@@ -9,7 +10,6 @@ const { transform: detype } = module.createRequire(import.meta.url)('detype');
|
|
|
9
10
|
const prettierOptions = {
|
|
10
11
|
semi: false,
|
|
11
12
|
singleQuote: true,
|
|
12
|
-
printWidth: 100,
|
|
13
13
|
trailingComma: 'none',
|
|
14
14
|
};
|
|
15
15
|
function remarkDetype() {
|
|
@@ -54,16 +54,23 @@ function transformYaml(node) {
|
|
|
54
54
|
type: 'mdxJsxFlowElement',
|
|
55
55
|
name: 'CodeSnippets',
|
|
56
56
|
children: [yamlJsCode, codeBlock],
|
|
57
|
-
attributes: [
|
|
57
|
+
attributes: [
|
|
58
|
+
{
|
|
59
|
+
name: 'hideToggle',
|
|
60
|
+
type: 'mdxJsxAttribute',
|
|
61
|
+
},
|
|
62
|
+
],
|
|
58
63
|
};
|
|
59
64
|
parent.children.splice(index, 1, yamlContainer);
|
|
60
65
|
}
|
|
61
66
|
async function transformTsToJs(node, file) {
|
|
62
67
|
const { codeBlock, index, parent } = node;
|
|
63
|
-
|
|
68
|
+
const maxWidth = Number(parseMetaString(codeBlock.meta)['max-width']);
|
|
69
|
+
let codeBlockReplacedJs = replaceFileNameSuffixes(codeBlock.value);
|
|
70
|
+
let codeBlockContentJs = '';
|
|
64
71
|
// Remove TypeScript from the TS/TSX/Vue code node
|
|
65
72
|
try {
|
|
66
|
-
codeBlockContentJs = await detype(
|
|
73
|
+
codeBlockContentJs = await detype(codeBlockReplacedJs, `some-dummy-filename.${codeBlock.lang}`, {
|
|
67
74
|
customizeBabelConfig(config) {
|
|
68
75
|
// Add `onlyRemoveTypeImports: true` to the internal `@babel/preset-typescript` config
|
|
69
76
|
// https://github.com/cyco130/detype/blob/46ec867e9efd31d31a312a215ca169bd6bff4726/src/transform.ts#L206
|
|
@@ -71,7 +78,10 @@ async function transformTsToJs(node, file) {
|
|
|
71
78
|
config.presets = [[config.presets[0], { onlyRemoveTypeImports: true }]];
|
|
72
79
|
},
|
|
73
80
|
removeTsComments: true,
|
|
74
|
-
prettierOptions
|
|
81
|
+
prettierOptions: {
|
|
82
|
+
...prettierOptions,
|
|
83
|
+
printWidth: maxWidth ? maxWidth : 99,
|
|
84
|
+
},
|
|
75
85
|
});
|
|
76
86
|
}
|
|
77
87
|
catch (error) {
|
|
@@ -93,18 +103,26 @@ async function transformTsToJs(node, file) {
|
|
|
93
103
|
if (codeBlockContentJs === codeBlock.value)
|
|
94
104
|
return;
|
|
95
105
|
const { position, lang, ...rest } = codeBlock;
|
|
106
|
+
const attributes = [];
|
|
96
107
|
const jsCode = {
|
|
97
108
|
...rest,
|
|
98
109
|
// The jsCode lang should be js|jsx|vue
|
|
99
110
|
lang: lang.replace('t', 'j'),
|
|
100
111
|
value: codeBlockContentJs,
|
|
101
112
|
};
|
|
113
|
+
// Add `hideToggle` attribute (prop) to `CodeSnippets` if the only change was replacing `.ts` with `.js`
|
|
114
|
+
if (codeBlockReplacedJs === codeBlockContentJs) {
|
|
115
|
+
attributes.push({
|
|
116
|
+
name: 'hideToggle',
|
|
117
|
+
type: 'mdxJsxAttribute',
|
|
118
|
+
});
|
|
119
|
+
}
|
|
102
120
|
// Wrap both the original `codeBlock` and `jsCode` with <CodeSnippets>
|
|
103
121
|
const container = {
|
|
104
122
|
type: 'mdxJsxFlowElement',
|
|
105
123
|
name: 'CodeSnippets',
|
|
106
124
|
children: [jsCode, codeBlock],
|
|
107
|
-
attributes
|
|
125
|
+
attributes,
|
|
108
126
|
};
|
|
109
127
|
parent.children.splice(index, 1, container);
|
|
110
128
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { shikiTransformerAutoLinks };
|
|
2
|
+
import type { ShikiTransformer } from 'shiki';
|
|
3
|
+
/**
|
|
4
|
+
* A Shiki transformer that converts plain HTTPS URLs in code blocks into clickable `<a>` links.
|
|
5
|
+
*
|
|
6
|
+
* Inspired by `@jcayzac/shiki-transformer-autolinks`, but tailored for a narrower use case.
|
|
7
|
+
*/
|
|
8
|
+
declare function shikiTransformerAutoLinks(): ShikiTransformer;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export { shikiTransformerAutoLinks };
|
|
2
|
+
const linkRE = /https:\/\/[^\s]*[^.,\s"'`]/g;
|
|
3
|
+
/**
|
|
4
|
+
* A Shiki transformer that converts plain HTTPS URLs in code blocks into clickable `<a>` links.
|
|
5
|
+
*
|
|
6
|
+
* Inspired by `@jcayzac/shiki-transformer-autolinks`, but tailored for a narrower use case.
|
|
7
|
+
*/
|
|
8
|
+
function shikiTransformerAutoLinks() {
|
|
9
|
+
return {
|
|
10
|
+
name: 'docpress-shiki-autolinks',
|
|
11
|
+
span(span) {
|
|
12
|
+
if (span.children.length !== 1)
|
|
13
|
+
return;
|
|
14
|
+
let child = span.children[0];
|
|
15
|
+
if (child.type !== 'text')
|
|
16
|
+
return;
|
|
17
|
+
const links = [];
|
|
18
|
+
const matches = Array.from(child.value.matchAll(linkRE));
|
|
19
|
+
// Filter out URLs that contain `${...}`. e.g. `https://star-wars.brillout.com/api/films/${id}.json`.
|
|
20
|
+
const filtered = matches.filter(([href]) => !href.includes('${'));
|
|
21
|
+
if (filtered.length === 0)
|
|
22
|
+
return;
|
|
23
|
+
for (const match of filtered) {
|
|
24
|
+
const [href] = match;
|
|
25
|
+
links.unshift({ href, index: match.index });
|
|
26
|
+
}
|
|
27
|
+
const newChildren = [];
|
|
28
|
+
for (const { href, index } of links) {
|
|
29
|
+
const postIndex = index + href.length;
|
|
30
|
+
const postValue = child.value.slice(postIndex);
|
|
31
|
+
if (postValue.length > 0) {
|
|
32
|
+
newChildren.unshift({ type: 'text', value: postValue });
|
|
33
|
+
}
|
|
34
|
+
newChildren.unshift({
|
|
35
|
+
type: 'element',
|
|
36
|
+
tagName: 'a',
|
|
37
|
+
properties: { href },
|
|
38
|
+
children: [{ type: 'text', value: href }],
|
|
39
|
+
});
|
|
40
|
+
child = {
|
|
41
|
+
type: 'text',
|
|
42
|
+
value: child.value.slice(0, index),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (child.value.length > 0) {
|
|
46
|
+
newChildren.unshift(child);
|
|
47
|
+
}
|
|
48
|
+
span.children = newChildren;
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
package/dist/components/index.js
CHANGED
package/dist/types/Config.d.ts
CHANGED
package/dist/vite.config.js
CHANGED
|
@@ -5,12 +5,17 @@ import { parsePageSections } from './parsePageSections.js';
|
|
|
5
5
|
import rehypePrettyCode from 'rehype-pretty-code';
|
|
6
6
|
import remarkGfm from 'remark-gfm';
|
|
7
7
|
import { transformerNotationDiff } from '@shikijs/transformers';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { rehypeMetaToProps } from './code-blocks/rehypeMetaToProps.js';
|
|
9
|
+
import { remarkDetype } from './code-blocks/remarkDetype.js';
|
|
10
|
+
import { shikiTransformerAutoLinks } from './code-blocks/shikiTransformerAutoLinks.js';
|
|
10
11
|
const root = process.cwd();
|
|
11
12
|
const prettyCode = [
|
|
12
13
|
rehypePrettyCode,
|
|
13
|
-
{
|
|
14
|
+
{
|
|
15
|
+
theme: 'github-light',
|
|
16
|
+
keepBackground: false,
|
|
17
|
+
transformers: [transformerNotationDiff(), shikiTransformerAutoLinks()],
|
|
18
|
+
},
|
|
14
19
|
];
|
|
15
20
|
const rehypePlugins = [prettyCode, [rehypeMetaToProps]];
|
|
16
21
|
const remarkPlugins = [remarkGfm, remarkDetype];
|
package/index.ts
CHANGED
|
@@ -1,16 +1,8 @@
|
|
|
1
1
|
/**********/
|
|
2
2
|
/* PUBLIC */
|
|
3
3
|
/**********/
|
|
4
|
-
export {
|
|
5
|
-
|
|
6
|
-
Link,
|
|
7
|
-
RepoLink,
|
|
8
|
-
FileAdded,
|
|
9
|
-
FileRemoved,
|
|
10
|
-
ImportMeta,
|
|
11
|
-
Emoji,
|
|
12
|
-
TypescriptOnly,
|
|
13
|
-
} from './components'
|
|
4
|
+
export { CodeBlockTransformer, Link, RepoLink, FileAdded, FileRemoved, ImportMeta, Emoji } from './components'
|
|
5
|
+
export { TypescriptOnly } from './code-blocks/components/CodeSnippets'
|
|
14
6
|
export { MenuToggle } from './Layout'
|
|
15
7
|
export * from './components/Note'
|
|
16
8
|
export * from './icons/index'
|
|
@@ -28,4 +20,4 @@ export { usePageContext } from './renderer/usePageContext'
|
|
|
28
20
|
/************/
|
|
29
21
|
// We provide our own `useMDXComponents()` to enable MDX component injection by setting `providerImportSource` to '@brillout/docpress'.
|
|
30
22
|
// https://mdxjs.com/guides/injecting-components/
|
|
31
|
-
export { useMDXComponents } from './
|
|
23
|
+
export { useMDXComponents } from './code-blocks/hooks/useMDXComponents'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brillout/docpress",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.13",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@brillout/picocolors": "^1.0.10",
|
|
@@ -64,7 +64,8 @@
|
|
|
64
64
|
"@types/mdast": "^4.0.4",
|
|
65
65
|
"@types/node": "^22.5.5",
|
|
66
66
|
"@types/react": "^18.3.8",
|
|
67
|
-
"@types/react-dom": "^18.3.0"
|
|
67
|
+
"@types/react-dom": "^18.3.0",
|
|
68
|
+
"mdast-util-mdx-jsx": "^3.2.0"
|
|
68
69
|
},
|
|
69
70
|
"repository": "https://github.com/brillout/docpress",
|
|
70
71
|
"license": "MIT",
|
package/types/Config.ts
CHANGED
package/vite.config.ts
CHANGED
|
@@ -7,13 +7,18 @@ import { parsePageSections } from './parsePageSections.js'
|
|
|
7
7
|
import rehypePrettyCode from 'rehype-pretty-code'
|
|
8
8
|
import remarkGfm from 'remark-gfm'
|
|
9
9
|
import { transformerNotationDiff } from '@shikijs/transformers'
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { rehypeMetaToProps } from './code-blocks/rehypeMetaToProps.js'
|
|
11
|
+
import { remarkDetype } from './code-blocks/remarkDetype.js'
|
|
12
|
+
import { shikiTransformerAutoLinks } from './code-blocks/shikiTransformerAutoLinks.js'
|
|
12
13
|
|
|
13
14
|
const root = process.cwd()
|
|
14
15
|
const prettyCode = [
|
|
15
16
|
rehypePrettyCode,
|
|
16
|
-
{
|
|
17
|
+
{
|
|
18
|
+
theme: 'github-light',
|
|
19
|
+
keepBackground: false,
|
|
20
|
+
transformers: [transformerNotationDiff(), shikiTransformerAutoLinks()],
|
|
21
|
+
},
|
|
17
22
|
]
|
|
18
23
|
const rehypePlugins: any = [prettyCode, [rehypeMetaToProps]]
|
|
19
24
|
const remarkPlugins = [remarkGfm, remarkDetype]
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
export { useSelectCodeLang };
|
|
2
|
-
import { useState, useEffect, useCallback } from 'react';
|
|
3
|
-
import { assertWarning } from '../../utils/assert';
|
|
4
|
-
const storageKey = 'docpress:code-lang';
|
|
5
|
-
const codeLangDefaultSsr = 'ts';
|
|
6
|
-
const codeLangDefaultClient = 'js';
|
|
7
|
-
function useSelectCodeLang() {
|
|
8
|
-
const [codeLangSelected, setCodeLangSelected] = useState(codeLangDefaultSsr);
|
|
9
|
-
const updateState = () => {
|
|
10
|
-
setCodeLangSelected(getCodeLangStorage());
|
|
11
|
-
};
|
|
12
|
-
const updateStateOnStorageEvent = (event) => {
|
|
13
|
-
if (event.key === storageKey)
|
|
14
|
-
updateState();
|
|
15
|
-
};
|
|
16
|
-
const getCodeLangStorage = () => {
|
|
17
|
-
try {
|
|
18
|
-
return window.localStorage.getItem(storageKey) ?? codeLangDefaultClient;
|
|
19
|
-
}
|
|
20
|
-
catch (error) {
|
|
21
|
-
console.error(error);
|
|
22
|
-
assertWarning(false, 'Error reading from localStorage');
|
|
23
|
-
return codeLangDefaultClient;
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
const selectCodeLang = useCallback((value) => {
|
|
27
|
-
try {
|
|
28
|
-
window.localStorage.setItem(storageKey, value);
|
|
29
|
-
setCodeLangSelected(value);
|
|
30
|
-
window.dispatchEvent(new CustomEvent('code-lang-storage'));
|
|
31
|
-
}
|
|
32
|
-
catch (error) {
|
|
33
|
-
console.error(error);
|
|
34
|
-
assertWarning(false, 'Error setting localStorage');
|
|
35
|
-
}
|
|
36
|
-
}, []);
|
|
37
|
-
useEffect(() => {
|
|
38
|
-
// Initial load from localStorage
|
|
39
|
-
updateState();
|
|
40
|
-
// Update code lang in current tab
|
|
41
|
-
window.addEventListener('code-lang-storage', updateState);
|
|
42
|
-
// Update code lang if changed in another tab
|
|
43
|
-
window.addEventListener('storage', updateStateOnStorageEvent);
|
|
44
|
-
return () => {
|
|
45
|
-
window.removeEventListener('code-lang-storage', updateState);
|
|
46
|
-
window.removeEventListener('storage', updateStateOnStorageEvent);
|
|
47
|
-
};
|
|
48
|
-
}, []);
|
|
49
|
-
return [codeLangSelected, selectCodeLang];
|
|
50
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export { TypescriptOnly };
|
|
2
|
-
export { CodeSnippets };
|
|
3
|
-
import React from 'react';
|
|
4
|
-
import './CodeSnippets.css';
|
|
5
|
-
/** Only show if TypeScript is selected */
|
|
6
|
-
declare function TypescriptOnly({ children }: {
|
|
7
|
-
children: React.ReactNode;
|
|
8
|
-
}): React.JSX.Element;
|
|
9
|
-
declare function CodeSnippets({ children }: {
|
|
10
|
-
children: React.ReactNode;
|
|
11
|
-
}): React.JSX.Element;
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
// Public
|
|
2
|
-
export { TypescriptOnly };
|
|
3
|
-
// Internal
|
|
4
|
-
export { CodeSnippets };
|
|
5
|
-
import React, { useEffect, useRef } from 'react';
|
|
6
|
-
import { useSelectCodeLang } from './CodeSnippets/useSelectCodeLang';
|
|
7
|
-
import './CodeSnippets.css';
|
|
8
|
-
/** Only show if TypeScript is selected */
|
|
9
|
-
function TypescriptOnly({ children }) {
|
|
10
|
-
const [codeLangSelected] = useSelectCodeLang();
|
|
11
|
-
return React.createElement("div", { style: { display: codeLangSelected === 'ts' ? 'block' : 'none' } }, children);
|
|
12
|
-
}
|
|
13
|
-
function CodeSnippets({ children }) {
|
|
14
|
-
const [codeLangSelected, selectCodeLang] = useSelectCodeLang();
|
|
15
|
-
const prevPositionRef = useRef(null);
|
|
16
|
-
// Restores the scroll position of the toggle element after toggling languages.
|
|
17
|
-
useEffect(() => {
|
|
18
|
-
if (!prevPositionRef.current)
|
|
19
|
-
return;
|
|
20
|
-
const { top, el } = prevPositionRef.current;
|
|
21
|
-
const delta = el.getBoundingClientRect().top - top;
|
|
22
|
-
if (delta !== 0) {
|
|
23
|
-
window.scrollBy(0, delta);
|
|
24
|
-
}
|
|
25
|
-
prevPositionRef.current = null;
|
|
26
|
-
}, [codeLangSelected]);
|
|
27
|
-
return (React.createElement("div", { className: "code-snippets" },
|
|
28
|
-
React.createElement("input", { type: "checkbox", name: "code-lang-toggle", className: "code-lang-toggle raised", checked: codeLangSelected === 'ts', onChange: onChange, title: "Toggle language" }),
|
|
29
|
-
children));
|
|
30
|
-
function onChange(e) {
|
|
31
|
-
const element = e.target;
|
|
32
|
-
prevPositionRef.current = { top: element.getBoundingClientRect().top, el: element };
|
|
33
|
-
selectCodeLang(element.checked ? 'ts' : 'js');
|
|
34
|
-
}
|
|
35
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
export { rehypeMetaToProps };
|
|
2
|
-
import type { Root } from 'hast';
|
|
3
|
-
/**
|
|
4
|
-
* Rehype plugin to extract metadata from `<code>` blocks in markdown
|
|
5
|
-
* and attach them as props to the parent `<pre>` element.
|
|
6
|
-
*
|
|
7
|
-
* This allows using those props inside a custom `<Pre>` component.
|
|
8
|
-
*
|
|
9
|
-
* Example:
|
|
10
|
-
* ~~~mdx
|
|
11
|
-
* ```js foo="bar" hide_copy='true'
|
|
12
|
-
* export function add(a, b) {
|
|
13
|
-
* return a + b
|
|
14
|
-
* }
|
|
15
|
-
* ```
|
|
16
|
-
* ~~~
|
|
17
|
-
* These props are then added to the `<pre>` element
|
|
18
|
-
*/
|
|
19
|
-
declare function rehypeMetaToProps(): (tree: Root) => void;
|
|
File without changes
|
|
File without changes
|