@brillout/docpress 0.15.10 → 0.15.11-commit-e615832
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 +3 -0
- package/autoScrollNav.ts +3 -3
- package/code-blocks/components/CodeSnippets.css +74 -0
- package/code-blocks/components/CodeSnippets.tsx +51 -0
- package/code-blocks/components/Pre.css +51 -0
- package/code-blocks/components/Pre.tsx +70 -0
- package/code-blocks/hooks/useMDXComponents.tsx +13 -0
- package/code-blocks/hooks/useSelectCodeLang.ts +74 -0
- package/code-blocks/rehypeMetaToProps.ts +69 -0
- package/code-blocks/remarkDetype.ts +192 -0
- package/code-blocks/shikiTransformerAutoLinks.ts +61 -0
- package/css/button.css +23 -0
- package/css/code.css +3 -21
- package/css/tooltip.css +10 -2
- package/dist/+config.js +1 -1
- package/dist/NavItemComponent.js +38 -46
- package/dist/code-blocks/rehypeMetaToProps.d.ts +35 -0
- package/dist/code-blocks/rehypeMetaToProps.js +62 -0
- package/dist/code-blocks/remarkDetype.d.ts +4 -0
- package/dist/code-blocks/remarkDetype.js +164 -0
- package/dist/code-blocks/shikiTransformerAutoLinks.d.ts +8 -0
- package/dist/code-blocks/shikiTransformerAutoLinks.js +51 -0
- package/dist/components/CodeBlockTransformer.js +2 -3
- package/dist/components/Comment.js +1 -2
- package/dist/components/FileRemoved.js +4 -6
- package/dist/components/HorizontalLine.js +1 -2
- package/dist/components/ImportMeta.js +2 -3
- package/dist/components/Link.js +34 -50
- package/dist/components/Note.js +17 -29
- package/dist/components/P.js +1 -12
- package/dist/components/RepoLink.js +7 -9
- package/dist/determineNavItemsColumnLayout.js +48 -63
- package/dist/parseMarkdownMini.js +5 -17
- package/dist/parsePageSections.js +41 -82
- package/dist/renderer/usePageContext.js +6 -7
- package/dist/resolvePageContext.js +103 -110
- package/dist/utils/Emoji/Emoji.js +13 -21
- package/dist/utils/assert.js +14 -16
- package/dist/utils/cls.js +1 -1
- package/dist/utils/determineSectionUrlHash.js +5 -5
- package/dist/utils/filter.js +2 -2
- package/dist/utils/getGlobalObject.js +3 -3
- package/dist/vite.config.js +17 -7
- package/index.ts +16 -14
- package/package.json +7 -2
- package/resolvePageContext.ts +19 -15
- package/tsconfig.json +2 -1
- package/vite.config.ts +14 -4
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
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
.code-snippets {
|
|
2
|
+
position: relative;
|
|
3
|
+
|
|
4
|
+
&:hover {
|
|
5
|
+
.copy-button,
|
|
6
|
+
.code-lang-toggle {
|
|
7
|
+
opacity: 1;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/* Language toggle styles */
|
|
12
|
+
.code-lang-toggle {
|
|
13
|
+
position: absolute !important;
|
|
14
|
+
top: 10px;
|
|
15
|
+
right: 42px;
|
|
16
|
+
z-index: 3;
|
|
17
|
+
|
|
18
|
+
/* Checkbox appearance reset */
|
|
19
|
+
appearance: none;
|
|
20
|
+
-webkit-appearance: none;
|
|
21
|
+
-moz-appearance: none;
|
|
22
|
+
|
|
23
|
+
margin: 0;
|
|
24
|
+
padding: 0 4px;
|
|
25
|
+
height: 25px;
|
|
26
|
+
width: 59px;
|
|
27
|
+
display: flex;
|
|
28
|
+
background-color: #f7f7f7;
|
|
29
|
+
opacity: 0;
|
|
30
|
+
transition: opacity 0.5s ease-in-out, background-color 0.4s ease-in-out;
|
|
31
|
+
|
|
32
|
+
&:not(:hover) {
|
|
33
|
+
background-color: #eee;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* Toggle Labels */
|
|
37
|
+
&::before,
|
|
38
|
+
&::after {
|
|
39
|
+
width: 24px;
|
|
40
|
+
display: flex;
|
|
41
|
+
justify-content: center;
|
|
42
|
+
align-items: center;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
&::before {
|
|
46
|
+
content: 'JS';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
&::after {
|
|
50
|
+
content: 'TS';
|
|
51
|
+
border-left: none;
|
|
52
|
+
opacity: 0.3;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
&:checked {
|
|
56
|
+
&::before {
|
|
57
|
+
opacity: 0.3;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
&::after {
|
|
61
|
+
opacity: 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* Code block visibility based on toggle */
|
|
67
|
+
&:has(.code-lang-toggle:checked) figure:first-of-type {
|
|
68
|
+
display: none;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
&:has(.code-lang-toggle:not(:checked)) figure:last-of-type {
|
|
72
|
+
display: none;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Public
|
|
2
|
+
export { TypescriptOnly }
|
|
3
|
+
|
|
4
|
+
// Internal
|
|
5
|
+
export { CodeSnippets }
|
|
6
|
+
|
|
7
|
+
import React, { useEffect, useRef } from 'react'
|
|
8
|
+
import { useSelectCodeLang } from '../hooks/useSelectCodeLang'
|
|
9
|
+
import './CodeSnippets.css'
|
|
10
|
+
|
|
11
|
+
/** Only show if TypeScript is selected */
|
|
12
|
+
function TypescriptOnly({ children }: { children: React.ReactNode }) {
|
|
13
|
+
const [codeLangSelected] = useSelectCodeLang()
|
|
14
|
+
return <div style={{ display: codeLangSelected === 'ts' ? 'block' : 'none' }}>{children}</div>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function CodeSnippets({ children, hideToggle = false }: { children: React.ReactNode; hideToggle: boolean }) {
|
|
18
|
+
const [codeLangSelected, selectCodeLang] = useSelectCodeLang()
|
|
19
|
+
const prevPositionRef = useRef<null | { top: number; el: Element }>(null)
|
|
20
|
+
|
|
21
|
+
// Restores the scroll position of the toggle element after toggling languages.
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!prevPositionRef.current) return
|
|
24
|
+
const { top, el } = prevPositionRef.current
|
|
25
|
+
const delta = el.getBoundingClientRect().top - top
|
|
26
|
+
if (delta !== 0) {
|
|
27
|
+
window.scrollBy(0, delta)
|
|
28
|
+
}
|
|
29
|
+
prevPositionRef.current = null
|
|
30
|
+
}, [codeLangSelected])
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className="code-snippets">
|
|
34
|
+
<input
|
|
35
|
+
type="checkbox"
|
|
36
|
+
name="code-lang-toggle"
|
|
37
|
+
className="code-lang-toggle raised"
|
|
38
|
+
style={{ display: hideToggle ? 'none' : undefined }}
|
|
39
|
+
checked={codeLangSelected === 'ts'}
|
|
40
|
+
onChange={onChange}
|
|
41
|
+
title="Toggle language"
|
|
42
|
+
/>
|
|
43
|
+
{children}
|
|
44
|
+
</div>
|
|
45
|
+
)
|
|
46
|
+
function onChange(e: React.ChangeEvent<HTMLInputElement>) {
|
|
47
|
+
const element = e.target
|
|
48
|
+
prevPositionRef.current = { top: element.getBoundingClientRect().top, el: element }
|
|
49
|
+
selectCodeLang(element.checked ? 'ts' : 'js')
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
pre > code {
|
|
2
|
+
/*
|
|
3
|
+
background-color: #f4f4f4;
|
|
4
|
+
0.043137255 = 1 - (#f4 / #ff)
|
|
5
|
+
*/
|
|
6
|
+
background: rgba(0, 0, 0, 0.043137255);
|
|
7
|
+
font-size: 1em;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/* Copy button */
|
|
11
|
+
pre {
|
|
12
|
+
&:has(.copy-button) {
|
|
13
|
+
position: relative;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
&:hover {
|
|
17
|
+
.copy-button {
|
|
18
|
+
opacity: 1;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.copy-button {
|
|
23
|
+
position: absolute !important;
|
|
24
|
+
top: 10px;
|
|
25
|
+
right: 10px;
|
|
26
|
+
z-index: 3;
|
|
27
|
+
margin: 0;
|
|
28
|
+
height: 25px;
|
|
29
|
+
width: 30px;
|
|
30
|
+
background-color: #f7f7f7;
|
|
31
|
+
opacity: 0;
|
|
32
|
+
transition: opacity 0.5s ease-in-out, background-color 0.4s ease-in-out;
|
|
33
|
+
|
|
34
|
+
&:not(:hover) {
|
|
35
|
+
background-color: #eee;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
& svg {
|
|
39
|
+
width: 100%;
|
|
40
|
+
height: 100%;
|
|
41
|
+
fill: none;
|
|
42
|
+
stroke-linecap: round;
|
|
43
|
+
stroke-linejoin: round;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* Workaround for shiki regression */
|
|
49
|
+
pre > code:not([data-language]) {
|
|
50
|
+
padding: 16px !important;
|
|
51
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export { Pre }
|
|
2
|
+
|
|
3
|
+
import React, { ComponentPropsWithoutRef, useState } from 'react'
|
|
4
|
+
import './Pre.css'
|
|
5
|
+
|
|
6
|
+
function Pre({ children, ...props }: ComponentPropsWithoutRef<'pre'> & { 'hide-menu'?: string }) {
|
|
7
|
+
return (
|
|
8
|
+
<pre {...props}>
|
|
9
|
+
{!props['hide-menu'] && <CopyButton />}
|
|
10
|
+
{children}
|
|
11
|
+
</pre>
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function CopyButton() {
|
|
16
|
+
const [isSuccess, setIsSuccess] = useState(null as null | boolean)
|
|
17
|
+
const onCopy = (success: boolean) => {
|
|
18
|
+
setIsSuccess(success)
|
|
19
|
+
setTimeout(() => {
|
|
20
|
+
setIsSuccess(null)
|
|
21
|
+
}, 900)
|
|
22
|
+
}
|
|
23
|
+
const tooltip = isSuccess === null ? 'Copy to clipboard' : isSuccess ? 'Copied' : 'Failed'
|
|
24
|
+
const icon =
|
|
25
|
+
isSuccess === null ? (
|
|
26
|
+
// Copy icon
|
|
27
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke="black" strokeWidth="2">
|
|
28
|
+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
|
29
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
|
30
|
+
</svg>
|
|
31
|
+
) : isSuccess ? (
|
|
32
|
+
// Green checkmark
|
|
33
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke="#28a745" strokeWidth="3">
|
|
34
|
+
<polyline points="20 6 9 17 4 12" />
|
|
35
|
+
</svg>
|
|
36
|
+
) : (
|
|
37
|
+
'❌'
|
|
38
|
+
)
|
|
39
|
+
return (
|
|
40
|
+
<button
|
|
41
|
+
className="copy-button raised"
|
|
42
|
+
aria-label={tooltip}
|
|
43
|
+
data-label-position="top"
|
|
44
|
+
type="button"
|
|
45
|
+
onClick={onClick}
|
|
46
|
+
>
|
|
47
|
+
{icon}
|
|
48
|
+
</button>
|
|
49
|
+
)
|
|
50
|
+
async function onClick(e: React.MouseEvent<HTMLButtonElement>) {
|
|
51
|
+
let success: boolean
|
|
52
|
+
const preEl = e.currentTarget.parentElement!
|
|
53
|
+
let text = preEl.textContent || ''
|
|
54
|
+
text = removeTrailingWhitespaces(text)
|
|
55
|
+
try {
|
|
56
|
+
await navigator.clipboard.writeText(text)
|
|
57
|
+
success = true
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error(error)
|
|
60
|
+
success = false
|
|
61
|
+
}
|
|
62
|
+
onCopy(success)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function removeTrailingWhitespaces(text: string) {
|
|
66
|
+
return text
|
|
67
|
+
.split('\n')
|
|
68
|
+
.map((line) => line.trimEnd())
|
|
69
|
+
.join('\n')
|
|
70
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { useMDXComponents }
|
|
2
|
+
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import type { UseMdxComponents } from '@mdx-js/mdx'
|
|
5
|
+
import { Pre } from '../components/Pre.js'
|
|
6
|
+
import { CodeSnippets } from '../components/CodeSnippets.js'
|
|
7
|
+
|
|
8
|
+
const useMDXComponents: UseMdxComponents = () => {
|
|
9
|
+
return {
|
|
10
|
+
CodeSnippets,
|
|
11
|
+
pre: (props) => <Pre {...props} />,
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export { useSelectCodeLang }
|
|
2
|
+
export { initializeJsToggle_SSR }
|
|
3
|
+
|
|
4
|
+
import { useState, useEffect, useCallback } from 'react'
|
|
5
|
+
import { assertWarning } from '../../utils/assert'
|
|
6
|
+
|
|
7
|
+
const storageKey = 'docpress:code-lang'
|
|
8
|
+
const codeLangDefaultSsr = 'ts'
|
|
9
|
+
const codeLangDefaultClient = 'js'
|
|
10
|
+
|
|
11
|
+
function useSelectCodeLang() {
|
|
12
|
+
const [codeLangSelected, setCodeLangSelected] = useState(codeLangDefaultSsr)
|
|
13
|
+
const updateState = () => {
|
|
14
|
+
setCodeLangSelected(getCodeLangStorage())
|
|
15
|
+
}
|
|
16
|
+
const updateStateOnStorageEvent = (event: StorageEvent) => {
|
|
17
|
+
if (event.key === storageKey) updateState()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const getCodeLangStorage = () => {
|
|
21
|
+
try {
|
|
22
|
+
return window.localStorage.getItem(storageKey) ?? codeLangDefaultClient
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error(error)
|
|
25
|
+
assertWarning(false, 'Error reading from localStorage')
|
|
26
|
+
return codeLangDefaultClient
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const selectCodeLang = useCallback((value: string) => {
|
|
31
|
+
try {
|
|
32
|
+
window.localStorage.setItem(storageKey, value)
|
|
33
|
+
setCodeLangSelected(value)
|
|
34
|
+
window.dispatchEvent(new CustomEvent('code-lang-storage'))
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error(error)
|
|
37
|
+
assertWarning(false, 'Error setting localStorage')
|
|
38
|
+
}
|
|
39
|
+
}, [])
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
// Initial load from localStorage
|
|
43
|
+
updateState()
|
|
44
|
+
// Update code lang in current tab
|
|
45
|
+
window.addEventListener('code-lang-storage', updateState)
|
|
46
|
+
// Update code lang if changed in another tab
|
|
47
|
+
window.addEventListener('storage', updateStateOnStorageEvent)
|
|
48
|
+
return () => {
|
|
49
|
+
window.removeEventListener('code-lang-storage', updateState)
|
|
50
|
+
window.removeEventListener('storage', updateStateOnStorageEvent)
|
|
51
|
+
}
|
|
52
|
+
}, [])
|
|
53
|
+
|
|
54
|
+
return [codeLangSelected, selectCodeLang] as const
|
|
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
|
+
|
|
70
|
+
declare global {
|
|
71
|
+
interface WindowEventMap {
|
|
72
|
+
'code-lang-storage': CustomEvent
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export { rehypeMetaToProps, parseMetaString }
|
|
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]+))?(?=\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,192 @@
|
|
|
1
|
+
export { remarkDetype }
|
|
2
|
+
|
|
3
|
+
import type { Root, Parent, Code } from 'mdast'
|
|
4
|
+
import type { MdxJsxFlowElement } from 'mdast-util-mdx-jsx'
|
|
5
|
+
import type { VFile } from '@mdx-js/mdx/internal-create-format-aware-processors'
|
|
6
|
+
import { visit } from 'unist-util-visit'
|
|
7
|
+
import { assertUsage } from '../utils/assert.js'
|
|
8
|
+
import { parseMetaString } from './rehypeMetaToProps.js'
|
|
9
|
+
import pc from '@brillout/picocolors'
|
|
10
|
+
import module from 'node:module'
|
|
11
|
+
// Cannot use `import { transform } from 'detype'` as it results in errors,
|
|
12
|
+
// and the package has no default export. Using `module.createRequire` instead.
|
|
13
|
+
const { transform: detype } = module.createRequire(import.meta.url)('detype') as typeof import('detype')
|
|
14
|
+
|
|
15
|
+
const prettierOptions: NonNullable<Parameters<typeof detype>[2]>['prettierOptions'] = {
|
|
16
|
+
semi: false,
|
|
17
|
+
singleQuote: true,
|
|
18
|
+
trailingComma: 'none',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type CodeNode = {
|
|
22
|
+
codeBlock: Code
|
|
23
|
+
index: number
|
|
24
|
+
parent: Parent
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function remarkDetype() {
|
|
28
|
+
return async function transformer(tree: Root, file: VFile) {
|
|
29
|
+
const code_nodes: CodeNode[] = []
|
|
30
|
+
|
|
31
|
+
visit(tree, 'code', (node, index, parent) => {
|
|
32
|
+
if (!parent || typeof index === 'undefined') return
|
|
33
|
+
// Skip if `node.lang` is not ts, tsx, vue, or yaml
|
|
34
|
+
if (!['ts', 'tsx', 'vue', 'yaml'].includes(node.lang || '')) return
|
|
35
|
+
|
|
36
|
+
// Skip if 'ts-only' meta is present
|
|
37
|
+
if (node.meta?.includes('ts-only')) return
|
|
38
|
+
|
|
39
|
+
// Collect this node for post-processing
|
|
40
|
+
code_nodes.push({ codeBlock: node, index, parent })
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
for (const node of code_nodes.reverse()) {
|
|
44
|
+
if (node.codeBlock.lang === 'yaml') {
|
|
45
|
+
transformYaml(node)
|
|
46
|
+
} else {
|
|
47
|
+
await transformTsToJs(node, file)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function transformYaml(node: CodeNode) {
|
|
54
|
+
const { codeBlock, index, parent } = node
|
|
55
|
+
const codeBlockContentJs = replaceFileNameSuffixes(codeBlock.value)
|
|
56
|
+
|
|
57
|
+
// Skip wrapping if the YAML code block hasn't changed
|
|
58
|
+
if (codeBlockContentJs === codeBlock.value) return
|
|
59
|
+
|
|
60
|
+
const { position, ...rest } = codeBlock
|
|
61
|
+
|
|
62
|
+
// Create a new code node for the JS version based on the modified YAML
|
|
63
|
+
const yamlJsCode: Code = {
|
|
64
|
+
...rest,
|
|
65
|
+
value: codeBlockContentJs,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Wrap both the original YAML and `yamlJsCode` with <CodeSnippets>
|
|
69
|
+
const yamlContainer: MdxJsxFlowElement = {
|
|
70
|
+
type: 'mdxJsxFlowElement',
|
|
71
|
+
name: 'CodeSnippets',
|
|
72
|
+
children: [yamlJsCode, codeBlock],
|
|
73
|
+
attributes: [
|
|
74
|
+
{
|
|
75
|
+
name: 'hideToggle',
|
|
76
|
+
type: 'mdxJsxAttribute',
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
}
|
|
80
|
+
parent.children.splice(index, 1, yamlContainer)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function transformTsToJs(node: CodeNode, file: VFile) {
|
|
84
|
+
const { codeBlock, index, parent } = node
|
|
85
|
+
const maxWidth = Number(parseMetaString(codeBlock.meta)['max-width'])
|
|
86
|
+
let codeBlockReplacedJs = replaceFileNameSuffixes(codeBlock.value)
|
|
87
|
+
let codeBlockContentJs = ''
|
|
88
|
+
|
|
89
|
+
// Remove TypeScript from the TS/TSX/Vue code node
|
|
90
|
+
try {
|
|
91
|
+
codeBlockContentJs = await detype(codeBlockReplacedJs, `some-dummy-filename.${codeBlock.lang}`, {
|
|
92
|
+
customizeBabelConfig(config) {
|
|
93
|
+
// Add `onlyRemoveTypeImports: true` to the internal `@babel/preset-typescript` config
|
|
94
|
+
// https://github.com/cyco130/detype/blob/46ec867e9efd31d31a312a215ca169bd6bff4726/src/transform.ts#L206
|
|
95
|
+
assertUsage(config.presets && config.presets.length === 1, 'Unexpected Babel config presets')
|
|
96
|
+
config.presets = [[config.presets[0], { onlyRemoveTypeImports: true }]]
|
|
97
|
+
},
|
|
98
|
+
removeTsComments: true,
|
|
99
|
+
prettierOptions: {
|
|
100
|
+
...prettierOptions,
|
|
101
|
+
printWidth: maxWidth ? maxWidth : 99,
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
} catch (error) {
|
|
105
|
+
// Log errors and return original content instead of throwing
|
|
106
|
+
console.error(pc.red((error as SyntaxError).message))
|
|
107
|
+
console.error(
|
|
108
|
+
[
|
|
109
|
+
`Failed to transform the code block in: ${pc.bold(pc.blue(file.path))}.`,
|
|
110
|
+
"This likely happened due to invalid TypeScript syntax (see detype's error message above). You can either:",
|
|
111
|
+
'- Fix the code block syntax',
|
|
112
|
+
'- Set the code block language to js instead of ts',
|
|
113
|
+
'- Use custom meta or comments https://github.com/brillout/docpress#detype',
|
|
114
|
+
].join('\n') + '\n',
|
|
115
|
+
)
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Clean up both JS and TS code contents: correct diff comments (for js only) and apply custom magic comment replacements
|
|
120
|
+
codeBlockContentJs = cleanUpCode(codeBlockContentJs.trimEnd(), true)
|
|
121
|
+
codeBlock.value = cleanUpCode(codeBlock.value)
|
|
122
|
+
|
|
123
|
+
// No wrapping needed if JS and TS code are still identical
|
|
124
|
+
if (codeBlockContentJs === codeBlock.value) return
|
|
125
|
+
|
|
126
|
+
const { position, lang, ...rest } = codeBlock
|
|
127
|
+
const attributes: MdxJsxFlowElement['attributes'] = []
|
|
128
|
+
|
|
129
|
+
const jsCode: Code = {
|
|
130
|
+
...rest,
|
|
131
|
+
// The jsCode lang should be js|jsx|vue
|
|
132
|
+
lang: lang!.replace('t', 'j'),
|
|
133
|
+
value: codeBlockContentJs,
|
|
134
|
+
}
|
|
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
|
+
|
|
144
|
+
// Wrap both the original `codeBlock` and `jsCode` with <CodeSnippets>
|
|
145
|
+
const container: MdxJsxFlowElement = {
|
|
146
|
+
type: 'mdxJsxFlowElement',
|
|
147
|
+
name: 'CodeSnippets',
|
|
148
|
+
children: [jsCode, codeBlock],
|
|
149
|
+
attributes,
|
|
150
|
+
}
|
|
151
|
+
parent.children.splice(index, 1, container)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Replace all '.ts' extensions with '.js'
|
|
155
|
+
function replaceFileNameSuffixes(codeBlockValue: string) {
|
|
156
|
+
return codeBlockValue.replaceAll('.ts', '.js')
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function cleanUpCode(code: string, isJsCode: boolean = false) {
|
|
160
|
+
if (isJsCode) {
|
|
161
|
+
code = correctCodeDiffComments(code)
|
|
162
|
+
}
|
|
163
|
+
return processMagicComments(code)
|
|
164
|
+
}
|
|
165
|
+
function processMagicComments(code: string) {
|
|
166
|
+
// @detype-replace DummyLayout Layout
|
|
167
|
+
const renameCommentRE = /^\/\/\s@detype-replace\s([^ ]+) ([^ ]+)\n/gm
|
|
168
|
+
const matches = Array.from(code.matchAll(renameCommentRE))
|
|
169
|
+
|
|
170
|
+
if (matches.length) {
|
|
171
|
+
for (let i = matches.length - 1; i >= 0; i--) {
|
|
172
|
+
const match = matches[i]
|
|
173
|
+
const [fullMatch, renameFrom, renameTo] = match
|
|
174
|
+
code = code.split(fullMatch).join('').replaceAll(renameFrom, renameTo)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
code = code.replaceAll('// @detype-uncomment ', '')
|
|
178
|
+
|
|
179
|
+
return code
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Correct code diff comments that detype() unexpectedly reformatted (using Prettier and Babel internally)
|
|
183
|
+
* after removing TypeScript.
|
|
184
|
+
* See https://github.com/brillout/docpress/pull/47#issuecomment-3263953899
|
|
185
|
+
* @param code Transformed Javascript code.
|
|
186
|
+
* @returns The corrected code.
|
|
187
|
+
*/
|
|
188
|
+
function correctCodeDiffComments(code: string) {
|
|
189
|
+
// Expected to match the code diff comments: `// [!code ++]` and `// [!code --]` started with newline and optional spaces
|
|
190
|
+
const codeDiffRE = /\n\s*\/\/\s\[!code.+\]/g
|
|
191
|
+
return code.replaceAll(codeDiffRE, (codeDiff) => codeDiff.trimStart())
|
|
192
|
+
}
|
|
@@ -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
|
+
}
|