@brillout/docpress 0.15.10-commit-f89d08f → 0.15.10-commit-05c2883
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/CodeSnippets/useSelectCodeLang.ts +1 -2
- package/components/CodeSnippets.css +66 -49
- package/components/CodeSnippets.tsx +26 -83
- package/components/Pre.css +51 -0
- package/components/Pre.tsx +79 -0
- package/components/useMDXComponents.tsx +13 -0
- package/css/button.css +23 -0
- package/css/code.css +3 -21
- package/css/index.css +1 -0
- package/dist/components/CodeSnippets/useSelectCodeLang.js +1 -2
- package/dist/components/CodeSnippets.d.ts +0 -6
- package/dist/components/CodeSnippets.js +17 -55
- package/dist/rehypeMetaToProps.d.ts +19 -0
- package/dist/rehypeMetaToProps.js +62 -0
- package/dist/remarkDetype.d.ts +4 -0
- package/dist/remarkDetype.js +146 -0
- package/dist/resolvePageContext.js +18 -13
- package/dist/vite.config.js +9 -6
- package/index.ts +14 -7
- package/package.json +5 -3
- package/rehypeMetaToProps.ts +69 -0
- package/remarkDetype.ts +172 -0
- package/resolvePageContext.ts +19 -15
- package/vite.config.ts +9 -6
- package/detypePlugin.ts +0 -159
- package/dist/detypePlugin.d.ts +0 -3
- package/dist/detypePlugin.js +0 -139
- package/dist/utils/getMagicString.d.ts +0 -9
- package/dist/utils/getMagicString.js +0 -13
- package/utils/getMagicString.ts +0 -17
|
@@ -13,8 +13,7 @@ function useSelectCodeLang() {
|
|
|
13
13
|
setCodeLangSelected(getCodeLangStorage())
|
|
14
14
|
}
|
|
15
15
|
const updateStateOnStorageEvent = (event: StorageEvent) => {
|
|
16
|
-
if (event.key === storageKey)
|
|
17
|
-
updateState()
|
|
16
|
+
if (event.key === storageKey) updateState()
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
const getCodeLangStorage = () => {
|
|
@@ -1,61 +1,78 @@
|
|
|
1
|
-
/* Wrapper */
|
|
2
|
-
.code-snippet,
|
|
3
1
|
.code-snippets {
|
|
4
2
|
position: relative;
|
|
3
|
+
|
|
5
4
|
&:hover {
|
|
6
|
-
button,
|
|
7
|
-
|
|
5
|
+
.copy-button,
|
|
6
|
+
.code-lang-toggle {
|
|
8
7
|
opacity: 1;
|
|
9
8
|
}
|
|
10
9
|
}
|
|
11
|
-
}
|
|
12
10
|
|
|
13
|
-
/*
|
|
14
|
-
.code-lang-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
11
|
+
/* Hide language toggle for YAML */
|
|
12
|
+
&:has(code[data-language='yaml']) .code-lang-toggle {
|
|
13
|
+
display: none !important;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/* Language toggle styles */
|
|
17
|
+
.code-lang-toggle {
|
|
18
|
+
position: absolute !important;
|
|
19
|
+
top: 10px;
|
|
20
|
+
right: 42px;
|
|
21
|
+
z-index: 3;
|
|
22
|
+
|
|
23
|
+
/* Checkbox appearance reset */
|
|
24
|
+
appearance: none;
|
|
25
|
+
-webkit-appearance: none;
|
|
26
|
+
-moz-appearance: none;
|
|
27
|
+
|
|
28
|
+
margin: 0;
|
|
29
|
+
padding: 0 4px;
|
|
30
|
+
height: 25px;
|
|
31
|
+
display: flex;
|
|
32
|
+
background-color: #f7f7f7;
|
|
33
|
+
opacity: 0;
|
|
34
|
+
transition: opacity 0.5s ease-in-out, background-color 0.4s ease-in-out;
|
|
35
|
+
|
|
36
|
+
&:not(:hover) {
|
|
37
|
+
background-color: #eee;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* Toggle Labels */
|
|
41
|
+
&::before,
|
|
42
|
+
&::after {
|
|
43
|
+
width: 24px;
|
|
44
|
+
display: flex;
|
|
45
|
+
justify-content: center;
|
|
46
|
+
align-items: center;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
&::before {
|
|
50
|
+
content: 'JS';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
&::after {
|
|
54
|
+
content: 'TS';
|
|
55
|
+
border-left: none;
|
|
56
|
+
opacity: 0.3;
|
|
57
|
+
}
|
|
28
58
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
59
|
+
&:checked {
|
|
60
|
+
&::before {
|
|
61
|
+
opacity: 0.3;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
&::after {
|
|
65
|
+
opacity: 1;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* Code block visibility based on toggle */
|
|
71
|
+
&:has(.code-lang-toggle:checked) figure:first-of-type {
|
|
72
|
+
display: none;
|
|
41
73
|
}
|
|
42
|
-
}
|
|
43
74
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
.code-lang-select {
|
|
47
|
-
position: absolute !important;
|
|
48
|
-
height: 25px;
|
|
49
|
-
top: 10px;
|
|
50
|
-
z-index: 3;
|
|
51
|
-
outline: none;
|
|
52
|
-
cursor: pointer;
|
|
53
|
-
border: 1px solid #ccc;
|
|
54
|
-
border-radius: 5px;
|
|
55
|
-
background-color: #f7f7f7;
|
|
56
|
-
opacity: 0;
|
|
57
|
-
transition: opacity 0.8s ease-in-out, background-color 0.4s ease-in-out;
|
|
58
|
-
&:not(:hover) {
|
|
59
|
-
background-color: #eee;
|
|
75
|
+
&:has(.code-lang-toggle:not(:checked)) figure:last-of-type {
|
|
76
|
+
display: none;
|
|
60
77
|
}
|
|
61
78
|
}
|
|
@@ -3,9 +3,8 @@ export { TypescriptOnly }
|
|
|
3
3
|
|
|
4
4
|
// Internal
|
|
5
5
|
export { CodeSnippets }
|
|
6
|
-
export { CodeSnippet }
|
|
7
6
|
|
|
8
|
-
import React, {
|
|
7
|
+
import React, { useEffect, useRef } from 'react'
|
|
9
8
|
import { useSelectCodeLang } from './CodeSnippets/useSelectCodeLang'
|
|
10
9
|
import './CodeSnippets.css'
|
|
11
10
|
|
|
@@ -17,91 +16,35 @@ function TypescriptOnly({ children }: { children: React.ReactNode }) {
|
|
|
17
16
|
|
|
18
17
|
function CodeSnippets({ children }: { children: React.ReactNode }) {
|
|
19
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
|
+
|
|
20
32
|
return (
|
|
21
33
|
<div className="code-snippets">
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
34
|
+
<input
|
|
35
|
+
type="checkbox"
|
|
36
|
+
name="code-lang-toggle"
|
|
37
|
+
className="code-lang-toggle raised"
|
|
38
|
+
checked={codeLangSelected === 'ts'}
|
|
39
|
+
onChange={onChange}
|
|
40
|
+
title="Toggle language"
|
|
41
|
+
/>
|
|
28
42
|
{children}
|
|
29
43
|
</div>
|
|
30
44
|
)
|
|
31
|
-
function onChange(e: React.ChangeEvent<
|
|
32
|
-
|
|
45
|
+
function onChange(e: React.ChangeEvent<HTMLInputElement>) {
|
|
46
|
+
const element = e.target
|
|
47
|
+
prevPositionRef.current = { top: element.getBoundingClientRect().top, el: element }
|
|
48
|
+
selectCodeLang(element.checked ? 'ts' : 'js')
|
|
33
49
|
}
|
|
34
50
|
}
|
|
35
|
-
|
|
36
|
-
function CodeSnippet({
|
|
37
|
-
children,
|
|
38
|
-
codeLang,
|
|
39
|
-
tsOnly = false,
|
|
40
|
-
}: { children: React.ReactNode; codeLang: string; tsOnly: boolean }) {
|
|
41
|
-
const [codeLangSelected] = useSelectCodeLang()
|
|
42
|
-
|
|
43
|
-
const displayStyle = tsOnly ? {} : { display: codeLangSelected === codeLang ? 'block' : 'none' }
|
|
44
|
-
|
|
45
|
-
return (
|
|
46
|
-
<div className="code-snippet" style={{ ...displayStyle }}>
|
|
47
|
-
<CopyButton />
|
|
48
|
-
{children}
|
|
49
|
-
</div>
|
|
50
|
-
)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function CopyButton() {
|
|
54
|
-
const [isSuccess, setIsSuccess] = useState(null as null | boolean)
|
|
55
|
-
const onCopy = (success: boolean) => {
|
|
56
|
-
setIsSuccess(success)
|
|
57
|
-
setTimeout(() => {
|
|
58
|
-
setIsSuccess(null)
|
|
59
|
-
}, 900)
|
|
60
|
-
}
|
|
61
|
-
const tooltip = isSuccess === null ? 'Copy to clipboard' : isSuccess ? 'Copied' : 'Failed'
|
|
62
|
-
const text =
|
|
63
|
-
isSuccess === null ? (
|
|
64
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
|
|
65
|
-
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
|
66
|
-
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
|
67
|
-
</svg>
|
|
68
|
-
) : isSuccess ? (
|
|
69
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke="#28a745" strokeWidth="3">
|
|
70
|
-
<polyline points="20 6 9 17 4 12" />
|
|
71
|
-
</svg>
|
|
72
|
-
) : (
|
|
73
|
-
'❌'
|
|
74
|
-
)
|
|
75
|
-
return (
|
|
76
|
-
<button className="copy-button" aria-label={tooltip} data-label-position="top" type="button" onClick={onClick}>
|
|
77
|
-
{text}
|
|
78
|
-
</button>
|
|
79
|
-
)
|
|
80
|
-
async function onClick(e: React.MouseEvent<HTMLButtonElement>) {
|
|
81
|
-
let success: boolean
|
|
82
|
-
try {
|
|
83
|
-
await copyToClipboard(e)
|
|
84
|
-
success = true
|
|
85
|
-
} catch (error) {
|
|
86
|
-
console.error(error)
|
|
87
|
-
success = false
|
|
88
|
-
}
|
|
89
|
-
onCopy(success)
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async function copyToClipboard(e: React.MouseEvent<HTMLButtonElement>) {
|
|
94
|
-
const figureEl = e.currentTarget.nextElementSibling
|
|
95
|
-
if (figureEl?.tagName === 'FIGURE') {
|
|
96
|
-
let text = figureEl.textContent ?? ''
|
|
97
|
-
text = removeTrailingWhitespaces(text)
|
|
98
|
-
await navigator.clipboard.writeText(text)
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function removeTrailingWhitespaces(text: string) {
|
|
103
|
-
return text
|
|
104
|
-
.split('\n')
|
|
105
|
-
.map((line) => line.trimEnd())
|
|
106
|
-
.join('\n')
|
|
107
|
-
}
|
|
@@ -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,79 @@
|
|
|
1
|
+
export { Pre }
|
|
2
|
+
|
|
3
|
+
import React, { ComponentPropsWithoutRef, useState } from 'react'
|
|
4
|
+
/* Importing it here chokes the tests. I don't know why.
|
|
5
|
+
import './Pre.css'
|
|
6
|
+
//*/
|
|
7
|
+
|
|
8
|
+
function Pre({ children, ...props }: ComponentPropsWithoutRef<'pre'> & { 'hide-menu'?: string }) {
|
|
9
|
+
return (
|
|
10
|
+
<pre {...props}>
|
|
11
|
+
{!props['hide-menu'] && <CopyButton />}
|
|
12
|
+
{children}
|
|
13
|
+
</pre>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function CopyButton() {
|
|
18
|
+
const [isSuccess, setIsSuccess] = useState(null as null | boolean)
|
|
19
|
+
const onCopy = (success: boolean) => {
|
|
20
|
+
setIsSuccess(success)
|
|
21
|
+
setTimeout(() => {
|
|
22
|
+
setIsSuccess(null)
|
|
23
|
+
}, 900)
|
|
24
|
+
}
|
|
25
|
+
const tooltip = isSuccess === null ? 'Copy to clipboard' : isSuccess ? 'Copied' : 'Failed'
|
|
26
|
+
const icon =
|
|
27
|
+
isSuccess === null ? (
|
|
28
|
+
// Copy icon
|
|
29
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
|
|
30
|
+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
|
31
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
|
32
|
+
</svg>
|
|
33
|
+
) : isSuccess ? (
|
|
34
|
+
// Green checkmark
|
|
35
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke="#28a745" strokeWidth="3">
|
|
36
|
+
<polyline points="20 6 9 17 4 12" />
|
|
37
|
+
</svg>
|
|
38
|
+
) : (
|
|
39
|
+
'❌'
|
|
40
|
+
)
|
|
41
|
+
return (
|
|
42
|
+
<button
|
|
43
|
+
className="copy-button raised"
|
|
44
|
+
aria-label={tooltip}
|
|
45
|
+
data-label-position="top"
|
|
46
|
+
type="button"
|
|
47
|
+
onClick={onClick}
|
|
48
|
+
>
|
|
49
|
+
{icon}
|
|
50
|
+
</button>
|
|
51
|
+
)
|
|
52
|
+
async function onClick(e: React.MouseEvent<HTMLButtonElement>) {
|
|
53
|
+
let success: boolean
|
|
54
|
+
try {
|
|
55
|
+
await copyToClipboard(e)
|
|
56
|
+
success = true
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error(error)
|
|
59
|
+
success = false
|
|
60
|
+
}
|
|
61
|
+
onCopy(success)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function copyToClipboard(e: React.MouseEvent<HTMLButtonElement>) {
|
|
66
|
+
const codeEl = e.currentTarget.nextElementSibling
|
|
67
|
+
if (codeEl?.tagName === 'CODE') {
|
|
68
|
+
let text = codeEl.textContent ?? ''
|
|
69
|
+
text = removeTrailingWhitespaces(text)
|
|
70
|
+
await navigator.clipboard.writeText(text)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function removeTrailingWhitespaces(text: string) {
|
|
75
|
+
return text
|
|
76
|
+
.split('\n')
|
|
77
|
+
.map((line) => line.trimEnd())
|
|
78
|
+
.join('\n')
|
|
79
|
+
}
|
|
@@ -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 './Pre.js'
|
|
6
|
+
import { CodeSnippets } from './CodeSnippets.js'
|
|
7
|
+
|
|
8
|
+
const useMDXComponents: UseMdxComponents = () => {
|
|
9
|
+
return {
|
|
10
|
+
CodeSnippets,
|
|
11
|
+
pre: (props) => <Pre {...props} />,
|
|
12
|
+
}
|
|
13
|
+
}
|
package/css/button.css
CHANGED
|
@@ -5,3 +5,26 @@ button,
|
|
|
5
5
|
border-radius: 5px;
|
|
6
6
|
cursor: pointer;
|
|
7
7
|
}
|
|
8
|
+
|
|
9
|
+
/* Raised button */
|
|
10
|
+
.raised {
|
|
11
|
+
cursor: pointer;
|
|
12
|
+
border-radius: 5px;
|
|
13
|
+
border-style: solid;
|
|
14
|
+
border-width: 1px 2px 2px 1px;
|
|
15
|
+
border-color: hsl(0, 0%, 75%) hsl(0, 0%, 72%) hsl(0, 0%, 72%) hsl(0, 0%, 75%);
|
|
16
|
+
|
|
17
|
+
&:hover {
|
|
18
|
+
border-color: hsl(0, 0%, 72%) hsl(0, 0%, 66%) hsl(0, 0%, 66%) hsl(0, 0%, 72%);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
&:active {
|
|
22
|
+
border-width: 2px 1px 1px 2px;
|
|
23
|
+
border-color: hsl(0, 0%, 66%) hsl(0, 0%, 72%) hsl(0, 0%, 72%) hsl(0, 0%, 66%);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
&:disabled {
|
|
27
|
+
border-width: 1px;
|
|
28
|
+
border-color: hsl(0, 0%, 72%);
|
|
29
|
+
}
|
|
30
|
+
}
|
package/css/code.css
CHANGED
|
@@ -2,29 +2,11 @@
|
|
|
2
2
|
@import './code/block.css';
|
|
3
3
|
@import './code/diff.css';
|
|
4
4
|
|
|
5
|
-
code
|
|
6
|
-
border-radius: 4px;
|
|
7
|
-
}
|
|
8
|
-
pre {
|
|
9
|
-
background: none !important;
|
|
10
|
-
}
|
|
5
|
+
/* For code blocks, see Pre.css instead */
|
|
11
6
|
|
|
12
|
-
/* Inline */
|
|
7
|
+
/* Inline <code> */
|
|
13
8
|
code {
|
|
9
|
+
border-radius: 4px;
|
|
14
10
|
background: rgba(0, 0, 0, 0.063137255);
|
|
15
11
|
font-size: 1.1em;
|
|
16
12
|
}
|
|
17
|
-
|
|
18
|
-
/* Block */
|
|
19
|
-
pre > code {
|
|
20
|
-
/*
|
|
21
|
-
background-color: #f4f4f4;
|
|
22
|
-
0.043137255 = 1 - (#f4 / #ff)
|
|
23
|
-
*/
|
|
24
|
-
background: rgba(0, 0, 0, 0.043137255);
|
|
25
|
-
font-size: 1em;
|
|
26
|
-
}
|
|
27
|
-
/* Workaround for shiki regression */
|
|
28
|
-
pre > code:not([data-language]) {
|
|
29
|
-
padding: 16px !important;
|
|
30
|
-
}
|
package/css/index.css
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export { TypescriptOnly };
|
|
2
2
|
export { CodeSnippets };
|
|
3
|
-
export { CodeSnippet };
|
|
4
3
|
import React from 'react';
|
|
5
4
|
import './CodeSnippets.css';
|
|
6
5
|
/** Only show if TypeScript is selected */
|
|
@@ -10,8 +9,3 @@ declare function TypescriptOnly({ children }: {
|
|
|
10
9
|
declare function CodeSnippets({ children }: {
|
|
11
10
|
children: React.ReactNode;
|
|
12
11
|
}): React.JSX.Element;
|
|
13
|
-
declare function CodeSnippet({ children, codeLang, tsOnly, }: {
|
|
14
|
-
children: React.ReactNode;
|
|
15
|
-
codeLang: string;
|
|
16
|
-
tsOnly: boolean;
|
|
17
|
-
}): React.JSX.Element;
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
export { TypescriptOnly };
|
|
3
3
|
// Internal
|
|
4
4
|
export { CodeSnippets };
|
|
5
|
-
|
|
6
|
-
import React, { useState } from 'react';
|
|
5
|
+
import React, { useEffect, useRef } from 'react';
|
|
7
6
|
import { useSelectCodeLang } from './CodeSnippets/useSelectCodeLang';
|
|
8
7
|
import './CodeSnippets.css';
|
|
9
8
|
/** Only show if TypeScript is selected */
|
|
@@ -13,61 +12,24 @@ function TypescriptOnly({ children }) {
|
|
|
13
12
|
}
|
|
14
13
|
function CodeSnippets({ children }) {
|
|
15
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]);
|
|
16
27
|
return (React.createElement("div", { className: "code-snippets" },
|
|
17
|
-
React.createElement("
|
|
18
|
-
React.createElement("select", { className: "code-lang-select", onChange: onChange, value: codeLangSelected },
|
|
19
|
-
React.createElement("option", { value: "js" }, "JavaScript"),
|
|
20
|
-
React.createElement("option", { value: "ts" }, "TypeScript"))),
|
|
28
|
+
React.createElement("input", { type: "checkbox", name: "code-lang-toggle", className: "code-lang-toggle raised", checked: codeLangSelected === 'ts', onChange: onChange, title: "Toggle language" }),
|
|
21
29
|
children));
|
|
22
30
|
function onChange(e) {
|
|
23
|
-
|
|
31
|
+
const element = e.target;
|
|
32
|
+
prevPositionRef.current = { top: element.getBoundingClientRect().top, el: element };
|
|
33
|
+
selectCodeLang(element.checked ? 'ts' : 'js');
|
|
24
34
|
}
|
|
25
35
|
}
|
|
26
|
-
function CodeSnippet({ children, codeLang, tsOnly = false, }) {
|
|
27
|
-
const [codeLangSelected] = useSelectCodeLang();
|
|
28
|
-
const displayStyle = tsOnly ? {} : { display: codeLangSelected === codeLang ? 'block' : 'none' };
|
|
29
|
-
return (React.createElement("div", { className: "code-snippet", style: { ...displayStyle } },
|
|
30
|
-
React.createElement(CopyButton, null),
|
|
31
|
-
children));
|
|
32
|
-
}
|
|
33
|
-
function CopyButton() {
|
|
34
|
-
const [isSuccess, setIsSuccess] = useState(null);
|
|
35
|
-
const onCopy = (success) => {
|
|
36
|
-
setIsSuccess(success);
|
|
37
|
-
setTimeout(() => {
|
|
38
|
-
setIsSuccess(null);
|
|
39
|
-
}, 900);
|
|
40
|
-
};
|
|
41
|
-
const tooltip = isSuccess === null ? 'Copy to clipboard' : isSuccess ? 'Copied' : 'Failed';
|
|
42
|
-
const text = isSuccess === null ? (React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: "2" },
|
|
43
|
-
React.createElement("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2", ry: "2" }),
|
|
44
|
-
React.createElement("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" }))) : isSuccess ? (React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", stroke: "#28a745", strokeWidth: "3" },
|
|
45
|
-
React.createElement("polyline", { points: "20 6 9 17 4 12" }))) : ('❌');
|
|
46
|
-
return (React.createElement("button", { className: "copy-button", "aria-label": tooltip, "data-label-position": "top", type: "button", onClick: onClick }, text));
|
|
47
|
-
async function onClick(e) {
|
|
48
|
-
let success;
|
|
49
|
-
try {
|
|
50
|
-
await copyToClipboard(e);
|
|
51
|
-
success = true;
|
|
52
|
-
}
|
|
53
|
-
catch (error) {
|
|
54
|
-
console.error(error);
|
|
55
|
-
success = false;
|
|
56
|
-
}
|
|
57
|
-
onCopy(success);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
async function copyToClipboard(e) {
|
|
61
|
-
const figureEl = e.currentTarget.nextElementSibling;
|
|
62
|
-
if (figureEl?.tagName === 'FIGURE') {
|
|
63
|
-
let text = figureEl.textContent ?? '';
|
|
64
|
-
text = removeTrailingWhitespaces(text);
|
|
65
|
-
await navigator.clipboard.writeText(text);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
function removeTrailingWhitespaces(text) {
|
|
69
|
-
return text
|
|
70
|
-
.split('\n')
|
|
71
|
-
.map((line) => line.trimEnd())
|
|
72
|
-
.join('\n');
|
|
73
|
-
}
|
|
@@ -0,0 +1,19 @@
|
|
|
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;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export { rehypeMetaToProps };
|
|
2
|
+
import { visit } from 'unist-util-visit';
|
|
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
|
+
function rehypeMetaToProps() {
|
|
20
|
+
return (tree) => {
|
|
21
|
+
visit(tree, 'element', (node, _index, parent) => {
|
|
22
|
+
if (node.tagName === 'code' && parent?.type === 'element' && parent.tagName === 'pre') {
|
|
23
|
+
const props = parseMetaString(node.data?.meta);
|
|
24
|
+
parent.properties ??= {};
|
|
25
|
+
parent.properties = { ...parent.properties, ...props };
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Minimal parser for a metadata string into key-value pairs.
|
|
32
|
+
*
|
|
33
|
+
* Supports simple patterns: key or key="value".
|
|
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.
|
|
37
|
+
*
|
|
38
|
+
* Example:
|
|
39
|
+
* parseMetaString('foo fooBar="value"')
|
|
40
|
+
* => { foo: 'true', foo_bar: 'value' }
|
|
41
|
+
*
|
|
42
|
+
* @param metaString - The input metadata string.
|
|
43
|
+
* @returns A plain object of parsed key-value pairs.
|
|
44
|
+
*/
|
|
45
|
+
function parseMetaString(metaString) {
|
|
46
|
+
if (!metaString)
|
|
47
|
+
return {};
|
|
48
|
+
const props = {};
|
|
49
|
+
const keyValuePairRE = /([a-zA-Z_-]+)(?:="([^"]*)")?(?=\s|$)/g;
|
|
50
|
+
for (const match of metaString.matchAll(keyValuePairRE)) {
|
|
51
|
+
let [_, key, value] = match;
|
|
52
|
+
props[kebabCase(key)] = value || 'true';
|
|
53
|
+
}
|
|
54
|
+
return props;
|
|
55
|
+
}
|
|
56
|
+
// Simple function to convert a camelCase or PascalCase string to kebab-case.
|
|
57
|
+
function kebabCase(str) {
|
|
58
|
+
return str
|
|
59
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
60
|
+
.replace('_', '-')
|
|
61
|
+
.toLowerCase();
|
|
62
|
+
}
|