@sakura-ui/sakura-ui 0.5.0 → 0.5.2
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/package.json +5 -5
- package/packages/core/package.json +1 -1
- package/packages/core/src/components/Card.tsx +52 -56
- package/packages/core/src/components/Code.tsx +6 -6
- package/packages/core/src/components/Faq.tsx +30 -26
- package/packages/core/src/components/Heading.tsx +50 -48
- package/packages/core/src/components/Icon.tsx +34 -34
- package/packages/core/src/components/LangSelector.tsx +2 -2
- package/packages/core/src/components/Link.tsx +13 -12
- package/packages/core/src/components/LinkCard.tsx +87 -76
- package/packages/core/src/components/List.tsx +20 -13
- package/packages/core/src/components/OverflowContainer.tsx +9 -9
- package/packages/core/src/components/PopoverMenu.tsx +2 -13
- package/packages/core/src/components/Pre.tsx +9 -9
- package/packages/core/src/components/Table.tsx +30 -25
- package/packages/core/src/components/index.ts +1 -0
- package/packages/core/src/components/styles.ts +42 -0
- package/packages/core/src/index.ts +2 -0
- package/packages/forms/.turbo/turbo-build.log +4 -4
- package/packages/forms/dist/index.cjs.js +13 -21
- package/packages/forms/dist/index.es.js +91 -99
- package/packages/forms/package.json +1 -1
- package/packages/helper/.turbo/turbo-build.log +3 -3
- package/packages/helper/dist/index.cjs.js +6 -14
- package/packages/helper/dist/index.es.js +18 -26
- package/packages/helper/dist/types/styles.d.ts +2 -4
- package/packages/helper/dist/types/styles.d.ts.map +1 -1
- package/packages/helper/node_modules/.bin/vitest +2 -2
- package/packages/helper/package.json +1 -1
- package/packages/helper/src/styles.ts +2 -14
- package/packages/markdown/.turbo/turbo-build.log +4 -4
- package/packages/markdown/README.md +240 -4
- package/packages/markdown/dist/index.cjs.js +71 -145
- package/packages/markdown/dist/index.es.js +2624 -16747
- package/packages/markdown/dist/types/components/Markdown.d.ts +15 -10
- package/packages/markdown/dist/types/components/Markdown.d.ts.map +1 -1
- package/packages/markdown/dist/types/components/index.d.ts +1 -1
- package/packages/markdown/dist/types/components/index.d.ts.map +1 -1
- package/packages/markdown/dist/types/decorate.d.ts +24 -0
- package/packages/markdown/dist/types/decorate.d.ts.map +1 -0
- package/packages/markdown/dist/types/index.d.ts +3 -1
- package/packages/markdown/dist/types/index.d.ts.map +1 -1
- package/packages/markdown/dist/types/marked/attributes.d.ts +31 -0
- package/packages/markdown/dist/types/marked/attributes.d.ts.map +1 -0
- package/packages/markdown/dist/types/marked/directives/card.d.ts +3 -0
- package/packages/markdown/dist/types/marked/directives/card.d.ts.map +1 -0
- package/packages/markdown/dist/types/marked/directives/context.d.ts +36 -0
- package/packages/markdown/dist/types/marked/directives/context.d.ts.map +1 -0
- package/packages/markdown/dist/types/marked/directives/faq.d.ts +11 -0
- package/packages/markdown/dist/types/marked/directives/faq.d.ts.map +1 -0
- package/packages/markdown/dist/types/marked/directives/grid.d.ts +9 -0
- package/packages/markdown/dist/types/marked/directives/grid.d.ts.map +1 -0
- package/packages/markdown/dist/types/marked/directives/index.d.ts +8 -0
- package/packages/markdown/dist/types/marked/directives/index.d.ts.map +1 -0
- package/packages/markdown/dist/types/marked/directives/linkButton.d.ts +6 -0
- package/packages/markdown/dist/types/marked/directives/linkButton.d.ts.map +1 -0
- package/packages/markdown/dist/types/marked/directives/youtube.d.ts +3 -0
- package/packages/markdown/dist/types/marked/directives/youtube.d.ts.map +1 -0
- package/packages/markdown/dist/types/marked/html.d.ts +15 -0
- package/packages/markdown/dist/types/marked/html.d.ts.map +1 -0
- package/packages/markdown/dist/types/marked/index.d.ts +13 -0
- package/packages/markdown/dist/types/marked/index.d.ts.map +1 -0
- package/packages/markdown/dist/types/marked/registry.d.ts +8 -0
- package/packages/markdown/dist/types/marked/registry.d.ts.map +1 -0
- package/packages/markdown/dist/types/marked/renderer.d.ts +16 -0
- package/packages/markdown/dist/types/marked/renderer.d.ts.map +1 -0
- package/packages/markdown/dist/types/marked/tokenizer.d.ts +40 -0
- package/packages/markdown/dist/types/marked/tokenizer.d.ts.map +1 -0
- package/packages/markdown/dist/types/render.d.ts +22 -0
- package/packages/markdown/dist/types/render.d.ts.map +1 -0
- package/packages/markdown/dist/types/sanitize.d.ts +11 -0
- package/packages/markdown/dist/types/sanitize.d.ts.map +1 -0
- package/packages/markdown/node_modules/.bin/marked +55 -0
- package/packages/markdown/package.json +6 -19
- package/packages/markdown/src/components/Markdown.tsx +44 -346
- package/packages/markdown/src/components/index.ts +1 -1
- package/packages/markdown/src/decorate.ts +164 -0
- package/packages/markdown/src/index.ts +3 -1
- package/packages/markdown/src/marked/attributes.ts +88 -0
- package/packages/markdown/src/marked/directives/card.ts +77 -0
- package/packages/markdown/src/marked/directives/context.ts +39 -0
- package/packages/markdown/src/marked/directives/faq.ts +29 -0
- package/packages/markdown/src/marked/directives/grid.ts +50 -0
- package/packages/markdown/src/marked/directives/index.ts +29 -0
- package/packages/markdown/src/marked/directives/linkButton.ts +20 -0
- package/packages/markdown/src/marked/directives/youtube.ts +46 -0
- package/packages/markdown/src/marked/html.ts +55 -0
- package/packages/markdown/src/marked/index.ts +45 -0
- package/packages/markdown/src/marked/registry.ts +34 -0
- package/packages/markdown/src/marked/renderer.ts +52 -0
- package/packages/markdown/src/marked/tokenizer.ts +175 -0
- package/packages/markdown/src/render.ts +55 -0
- package/packages/markdown/src/sanitize.ts +194 -0
- package/packages/markdown/tests/Markdown.test.tsx +122 -0
- package/packages/markdown/tests/__snapshots__/Markdown.test.tsx.snap +451 -0
- package/packages/markdown/tests/anchors.test.ts +108 -0
- package/packages/markdown/tests/fixtures.ts +100 -0
- package/packages/markdown/tests/sanitize.test.ts +168 -0
- package/packages/markdown/tests/server.test.tsx +51 -0
- package/packages/markdown/tests/vitest.setup.ts +4 -0
- package/packages/markdown/tsconfig.test.json +11 -0
- package/packages/markdown/vite.config.ts +5 -1
- package/packages/tailwind-theme-plugin/.turbo/turbo-build.log +1 -1
- package/packages/tailwind-theme-plugin/package.json +1 -1
- package/packages/markdown/dist/types/plugins/attr.d.ts +0 -3
- package/packages/markdown/dist/types/plugins/attr.d.ts.map +0 -1
- package/packages/markdown/dist/types/plugins/card.d.ts +0 -3
- package/packages/markdown/dist/types/plugins/card.d.ts.map +0 -1
- package/packages/markdown/dist/types/plugins/cell.d.ts +0 -3
- package/packages/markdown/dist/types/plugins/cell.d.ts.map +0 -1
- package/packages/markdown/dist/types/plugins/faq.d.ts +0 -3
- package/packages/markdown/dist/types/plugins/faq.d.ts.map +0 -1
- package/packages/markdown/dist/types/plugins/grid.d.ts +0 -3
- package/packages/markdown/dist/types/plugins/grid.d.ts.map +0 -1
- package/packages/markdown/dist/types/plugins/headings.d.ts +0 -11
- package/packages/markdown/dist/types/plugins/headings.d.ts.map +0 -1
- package/packages/markdown/dist/types/plugins/helper.d.ts +0 -9
- package/packages/markdown/dist/types/plugins/helper.d.ts.map +0 -1
- package/packages/markdown/dist/types/plugins/index.d.ts +0 -9
- package/packages/markdown/dist/types/plugins/index.d.ts.map +0 -1
- package/packages/markdown/dist/types/plugins/linkButton.d.ts +0 -3
- package/packages/markdown/dist/types/plugins/linkButton.d.ts.map +0 -1
- package/packages/markdown/dist/types/plugins/youtube.d.ts +0 -3
- package/packages/markdown/dist/types/plugins/youtube.d.ts.map +0 -1
- package/packages/markdown/src/plugins/attr.ts +0 -19
- package/packages/markdown/src/plugins/card.ts +0 -71
- package/packages/markdown/src/plugins/cell.ts +0 -47
- package/packages/markdown/src/plugins/faq.ts +0 -40
- package/packages/markdown/src/plugins/grid.ts +0 -54
- package/packages/markdown/src/plugins/headings.ts +0 -38
- package/packages/markdown/src/plugins/helper.ts +0 -27
- package/packages/markdown/src/plugins/index.ts +0 -8
- package/packages/markdown/src/plugins/linkButton.ts +0 -26
- package/packages/markdown/src/plugins/youtube.ts +0 -49
|
@@ -1,63 +1,13 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import rehypeExternalLinks from 'rehype-external-links'
|
|
12
|
-
import rebypeShiftHeding from 'rehype-shift-heading'
|
|
13
|
-
import rehypeReact from 'rehype-react'
|
|
14
|
-
import rehypeSlug from 'rehype-slug'
|
|
15
|
-
import { cx } from '@sakura-ui/helper'
|
|
16
|
-
import {
|
|
17
|
-
Link,
|
|
18
|
-
H1,
|
|
19
|
-
H2,
|
|
20
|
-
H3,
|
|
21
|
-
H4,
|
|
22
|
-
H5,
|
|
23
|
-
H6,
|
|
24
|
-
Table,
|
|
25
|
-
Caption,
|
|
26
|
-
Thead,
|
|
27
|
-
Tbody,
|
|
28
|
-
Th,
|
|
29
|
-
Tr,
|
|
30
|
-
Td,
|
|
31
|
-
Ul,
|
|
32
|
-
Ol,
|
|
33
|
-
Pre,
|
|
34
|
-
Code,
|
|
35
|
-
Faq,
|
|
36
|
-
Question,
|
|
37
|
-
Answer,
|
|
38
|
-
Button,
|
|
39
|
-
Card,
|
|
40
|
-
CardImg,
|
|
41
|
-
CardHeader,
|
|
42
|
-
CardBody,
|
|
43
|
-
CardFooter,
|
|
44
|
-
LinkCard,
|
|
45
|
-
LinkCardHeader,
|
|
46
|
-
LinkCardFooter,
|
|
47
|
-
type CardHeaderAs,
|
|
48
|
-
OverflowContainer
|
|
49
|
-
} from '@sakura-ui/core'
|
|
50
|
-
import {
|
|
51
|
-
attrPlugin,
|
|
52
|
-
youtubePlugin,
|
|
53
|
-
linkButtonPlugin,
|
|
54
|
-
gridPlugin,
|
|
55
|
-
cellPlugin,
|
|
56
|
-
cardPlugin,
|
|
57
|
-
faqPlugin,
|
|
58
|
-
headingsPlugin,
|
|
59
|
-
type HeadingItem
|
|
60
|
-
} from '../plugins'
|
|
1
|
+
import { H2, Link, Ul } from '@sakura-ui/core'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { type HeadingItem, render, type RenderResult } from '../render'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* What the server, and the first render in the browser, have to show. The
|
|
7
|
+
* pipeline sanitises and styles through the DOM, so it cannot run where there
|
|
8
|
+
* is no document.
|
|
9
|
+
*/
|
|
10
|
+
const NOTHING_YET: RenderResult = { html: '', headings: [] }
|
|
61
11
|
|
|
62
12
|
interface TocProps {
|
|
63
13
|
items: HeadingItem[]
|
|
@@ -80,310 +30,58 @@ export const TableOfContents = ({ items }: TocProps) => {
|
|
|
80
30
|
)
|
|
81
31
|
}
|
|
82
32
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
</Table>
|
|
93
|
-
</OverflowContainer>
|
|
94
|
-
)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
interface DataProps {
|
|
98
|
-
[key: `data-${string}`]: unknown
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
interface AnchorProps extends React.ComponentPropsWithoutRef<'a'>, DataProps {}
|
|
102
|
-
|
|
103
|
-
const Anchor = (props: AnchorProps) => {
|
|
104
|
-
const { children, href, ...restProps } = props
|
|
105
|
-
|
|
106
|
-
if (restProps['data-node'] === 'link-button') {
|
|
107
|
-
return (
|
|
108
|
-
<Button as="a" variant="secondary" href={href}>
|
|
109
|
-
{children}
|
|
110
|
-
</Button>
|
|
111
|
-
)
|
|
33
|
+
export namespace Markdown {
|
|
34
|
+
export interface Props {
|
|
35
|
+
tocTitle?: string
|
|
36
|
+
showToc?: boolean
|
|
37
|
+
/** Moves every heading down by this many levels. */
|
|
38
|
+
shiftHeading?: number
|
|
39
|
+
/** How deep the table of contents goes. Defaults to 2. */
|
|
40
|
+
tocMaxDepth?: number
|
|
41
|
+
children: string
|
|
112
42
|
}
|
|
113
|
-
|
|
114
|
-
return (
|
|
115
|
-
<Link href={href} {...restProps}>
|
|
116
|
-
{children}
|
|
117
|
-
</Link>
|
|
118
|
-
)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
interface ArticleProps
|
|
122
|
-
extends React.ComponentPropsWithoutRef<'article'>,
|
|
123
|
-
DataProps {}
|
|
124
|
-
|
|
125
|
-
const Article = (props: ArticleProps) => {
|
|
126
|
-
const { children, className, ...restProps } = props
|
|
127
|
-
|
|
128
|
-
const node = restProps['data-node']
|
|
129
|
-
|
|
130
|
-
if (node === 'card') {
|
|
131
|
-
// The href now lives on the title, which is what carries the link.
|
|
132
|
-
if (restProps['data-behavior'] === 'link') {
|
|
133
|
-
return <LinkCard className={cx(className)}>{children}</LinkCard>
|
|
134
|
-
}
|
|
135
|
-
return <Card className={cx(className)}>{children}</Card>
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return (
|
|
139
|
-
<article className={className} {...restProps}>
|
|
140
|
-
{children}
|
|
141
|
-
</article>
|
|
142
|
-
)
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
interface DivProps extends React.ComponentPropsWithoutRef<'div'>, DataProps {
|
|
146
|
-
headingLevel?: CardHeaderAs
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const Div = (props: DivProps) => {
|
|
150
|
-
const { children, headingLevel = 'h3', ...restProps } = props
|
|
151
|
-
|
|
152
|
-
const node = restProps['data-node']
|
|
153
|
-
const isLink = restProps['data-behavior'] === 'link'
|
|
154
|
-
|
|
155
|
-
if (node === 'card-title') {
|
|
156
|
-
if (isLink) {
|
|
157
|
-
return (
|
|
158
|
-
<LinkCardHeader
|
|
159
|
-
as={headingLevel}
|
|
160
|
-
href={restProps['data-href'] as string}
|
|
161
|
-
>
|
|
162
|
-
{children}
|
|
163
|
-
</LinkCardHeader>
|
|
164
|
-
)
|
|
165
|
-
}
|
|
166
|
-
return <CardHeader as={headingLevel}>{children}</CardHeader>
|
|
167
|
-
}
|
|
168
|
-
if (node === 'card-description') {
|
|
169
|
-
return <CardBody>{children}</CardBody>
|
|
170
|
-
}
|
|
171
|
-
if (node === 'card-footer') {
|
|
172
|
-
if (isLink) {
|
|
173
|
-
return <LinkCardFooter>{children}</LinkCardFooter>
|
|
174
|
-
}
|
|
175
|
-
return <CardFooter>{children}</CardFooter>
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (restProps['data-behavior'] === 'list') {
|
|
179
|
-
// Set the css class configured for cells to the ul tag
|
|
180
|
-
return (
|
|
181
|
-
<ul className={restProps.className}>
|
|
182
|
-
{Children.map(children, (child) => (
|
|
183
|
-
<li className="sm:grid" key={Math.random()}>
|
|
184
|
-
{child}
|
|
185
|
-
</li>
|
|
186
|
-
))}
|
|
187
|
-
</ul>
|
|
188
|
-
)
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return <div {...restProps}>{children}</div>
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
interface ImgProps extends React.ComponentPropsWithoutRef<'img'>, DataProps {}
|
|
195
|
-
|
|
196
|
-
const Img = (props: ImgProps) => {
|
|
197
|
-
const { children, className, ...restProps } = props
|
|
198
|
-
|
|
199
|
-
if (restProps['data-node'] === 'card-img') {
|
|
200
|
-
return (
|
|
201
|
-
<CardImg
|
|
202
|
-
className={cx('w-full aspect-[352/226]', className)}
|
|
203
|
-
{...restProps}
|
|
204
|
-
/>
|
|
205
|
-
)
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
if (restProps['data-node'] === 'cell-img') {
|
|
209
|
-
// biome-ignore lint/a11y/useAltText: alt is carried by restProps from the markdown image syntax
|
|
210
|
-
return <img className={cx('mb-4', className)} {...restProps} />
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// biome-ignore lint/a11y/useAltText: alt is carried by restProps from the markdown image syntax
|
|
214
|
-
return <img className={className} {...restProps} />
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
interface IframeProps extends React.ComponentPropsWithoutRef<'iframe'> {}
|
|
218
|
-
|
|
219
|
-
const Iframe = (props: IframeProps) => {
|
|
220
|
-
const { className, children, ...restProps } = props
|
|
221
|
-
|
|
222
|
-
const styleMargin = `
|
|
223
|
-
my-8
|
|
224
|
-
`
|
|
225
|
-
|
|
226
|
-
let style = `
|
|
227
|
-
aspect-video
|
|
228
|
-
w-full
|
|
229
|
-
max-w-[470px]
|
|
230
|
-
`
|
|
231
|
-
|
|
232
|
-
if (props.width || props.height) {
|
|
233
|
-
style = `
|
|
234
|
-
aspect-video
|
|
235
|
-
w-full
|
|
236
|
-
lg:aspect-auto
|
|
237
|
-
lg:w-[${props.width}px]
|
|
238
|
-
lg:h-[${props.height}px]
|
|
239
|
-
`
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
return (
|
|
243
|
-
<iframe className={cx(styleMargin, style, className)} {...restProps}>
|
|
244
|
-
{children}
|
|
245
|
-
</iframe>
|
|
246
|
-
)
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const sleep = (time: number) =>
|
|
250
|
-
new Promise((resolve) => setTimeout(resolve, time))
|
|
251
|
-
|
|
252
|
-
type Props = {
|
|
253
|
-
tocTitle?: string
|
|
254
|
-
showToc?: boolean
|
|
255
|
-
shiftHeding?: number
|
|
256
|
-
children: string
|
|
257
43
|
}
|
|
258
44
|
|
|
259
45
|
export const Markdown = ({
|
|
260
46
|
children,
|
|
261
47
|
showToc,
|
|
262
48
|
tocTitle = '目次',
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
(obj: HeadingItem) => obj.depth < 3
|
|
274
|
-
)
|
|
275
|
-
|
|
276
|
-
return headings
|
|
277
|
-
}, [])
|
|
278
|
-
|
|
279
|
-
const markdown2ReactElements = React.useCallback(
|
|
280
|
-
(md: string) => {
|
|
281
|
-
// Card titles sit in the body of the document, so h3 is the natural base.
|
|
282
|
-
// rehype-shift-heading cannot reach them because they are still divs when
|
|
283
|
-
// it runs, so the same shift is applied here.
|
|
284
|
-
const cardHeadingLevel = `h${Math.min(
|
|
285
|
-
6,
|
|
286
|
-
Math.max(1, 3 + shiftHeding)
|
|
287
|
-
)}` as CardHeaderAs
|
|
288
|
-
|
|
289
|
-
const rhypeReactOptions = {
|
|
290
|
-
...production,
|
|
291
|
-
components: {
|
|
292
|
-
a: Anchor,
|
|
293
|
-
article: Article,
|
|
294
|
-
button: Button,
|
|
295
|
-
code: Code,
|
|
296
|
-
dl: Faq,
|
|
297
|
-
dt: Question,
|
|
298
|
-
dd: Answer,
|
|
299
|
-
h1: H1,
|
|
300
|
-
h2: H2,
|
|
301
|
-
h3: H3,
|
|
302
|
-
h4: H4,
|
|
303
|
-
h5: H5,
|
|
304
|
-
h6: H6,
|
|
305
|
-
iframe: Iframe,
|
|
306
|
-
img: Img,
|
|
307
|
-
ul: Ul,
|
|
308
|
-
ol: Ol,
|
|
309
|
-
pre: Pre,
|
|
310
|
-
table: TableContainer,
|
|
311
|
-
caption: Caption,
|
|
312
|
-
thead: Thead,
|
|
313
|
-
tbody: Tbody,
|
|
314
|
-
th: Th,
|
|
315
|
-
tr: Tr,
|
|
316
|
-
td: Td,
|
|
317
|
-
div: (props: DivProps) => (
|
|
318
|
-
<Div {...props} headingLevel={cardHeadingLevel} />
|
|
319
|
-
)
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const elem = unified()
|
|
324
|
-
.use(remarkParse) // md -> mdast (Markdown Abstract Syntax Tree)
|
|
325
|
-
.use(remarkGfm) // mdast -> GFM mdast (GitHub Flavored Markdown Abstract Syntax Tree)
|
|
326
|
-
.use(remarkDirective) // support for directive syntax
|
|
327
|
-
.use(remarkBreaks)
|
|
328
|
-
.use(attrPlugin)
|
|
329
|
-
.use(youtubePlugin)
|
|
330
|
-
.use(linkButtonPlugin)
|
|
331
|
-
.use(cellPlugin)
|
|
332
|
-
.use(cardPlugin)
|
|
333
|
-
.use(faqPlugin)
|
|
334
|
-
.use(gridPlugin)
|
|
335
|
-
.use(remarkRehype, {
|
|
336
|
-
allowDangerousHtml: true
|
|
337
|
-
}) // mdast -> hast (HTML Abstract Syntax Tree)
|
|
338
|
-
.use(rehypeRaw) // hast -> hast
|
|
339
|
-
.use(rehypeExternalLinks, { target: '_blank' }) // hast -> hast
|
|
340
|
-
.use(rebypeShiftHeding, { shift: shiftHeding }) // hast -> hast
|
|
341
|
-
.use(rehypeSlug)
|
|
342
|
-
// biome-ignore lint/suspicious/noExplicitAny: rehype-react's declared option type rejects the component map it accepts at runtime
|
|
343
|
-
.use(rehypeReact, rhypeReactOptions as any) // hast -> React Elements
|
|
344
|
-
.processSync(md).result
|
|
345
|
-
return elem
|
|
346
|
-
},
|
|
347
|
-
[shiftHeding]
|
|
348
|
-
)
|
|
49
|
+
shiftHeading = 0,
|
|
50
|
+
tocMaxDepth = 2
|
|
51
|
+
}: Markdown.Props) => {
|
|
52
|
+
// Converted in an effect, not while rendering. useMemo runs on the server as
|
|
53
|
+
// well, and the pipeline reaches for document, so rendering there threw
|
|
54
|
+
// instead of producing the empty output the caller could recover from. This
|
|
55
|
+
// way the server and the first render in the browser agree, and the markup
|
|
56
|
+
// arrives once the effect has run.
|
|
57
|
+
const [{ html, headings }, setResult] =
|
|
58
|
+
React.useState<RenderResult>(NOTHING_YET)
|
|
349
59
|
|
|
350
60
|
React.useEffect(() => {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
}, [children, markdown2Headings, markdown2ReactElements])
|
|
61
|
+
setResult(render(children, { shiftHeading, tocMaxDepth }))
|
|
62
|
+
}, [children, shiftHeading, tocMaxDepth])
|
|
354
63
|
|
|
355
64
|
React.useEffect(() => {
|
|
356
|
-
//
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
scrollToHash()
|
|
366
|
-
}, [])
|
|
367
|
-
|
|
368
|
-
const style = `
|
|
369
|
-
rounded-3xl
|
|
370
|
-
p-10
|
|
371
|
-
bg-yellow-50
|
|
372
|
-
`
|
|
65
|
+
// The browser looked for the anchor before this markup existed, so the jump
|
|
66
|
+
// is made again once it does.
|
|
67
|
+
if (!html) return
|
|
68
|
+
const id = decodeURI(window.location.hash.slice(1))
|
|
69
|
+
if (!id) return
|
|
70
|
+
document.getElementById(id)?.scrollIntoView()
|
|
71
|
+
}, [html])
|
|
373
72
|
|
|
374
73
|
return (
|
|
375
74
|
<div className="py-8 flex flex-col gap-8">
|
|
376
|
-
{showToc && (
|
|
377
|
-
<nav className=
|
|
378
|
-
<
|
|
379
|
-
{tocTitle}
|
|
380
|
-
</h2>
|
|
75
|
+
{showToc && headings.length > 0 && (
|
|
76
|
+
<nav className="rounded-3xl p-10 bg-yellow-50">
|
|
77
|
+
<H2 className="pt-0">{tocTitle}</H2>
|
|
381
78
|
<Ul>
|
|
382
|
-
<TableOfContents items={
|
|
79
|
+
<TableOfContents items={headings} />
|
|
383
80
|
</Ul>
|
|
384
81
|
</nav>
|
|
385
82
|
)}
|
|
386
|
-
{
|
|
83
|
+
{/* The markup is sanitised in src/sanitize.ts before it gets here. */}
|
|
84
|
+
<div dangerouslySetInnerHTML={{ __html: html }} />
|
|
387
85
|
</div>
|
|
388
86
|
)
|
|
389
87
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { Markdown } from './Markdown'
|
|
1
|
+
export { Markdown, TableOfContents } from './Markdown'
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { styles } from '@sakura-ui/core'
|
|
2
|
+
import Slugger from 'github-slugger'
|
|
3
|
+
import { classNames } from './marked/html'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Applies the look of the design system to the finished document.
|
|
7
|
+
*
|
|
8
|
+
* This runs over the sanitised DOM rather than inside the markdown renderer so
|
|
9
|
+
* that a table written as raw HTML — 56 of the guide's documents do — is styled
|
|
10
|
+
* the same as one written with pipes. The pipeline this replaces had the same
|
|
11
|
+
* property, because rehype-raw turned the raw HTML into real nodes before the
|
|
12
|
+
* tag to component mapping ran.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export type HeadingItem = {
|
|
16
|
+
id?: string
|
|
17
|
+
depth: number
|
|
18
|
+
value?: string
|
|
19
|
+
children?: HeadingItem[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** tag name to the classes the matching component puts on it */
|
|
23
|
+
const CLASS_BY_TAG: Record<string, string> = {
|
|
24
|
+
h1: styles.headingStyle.h1,
|
|
25
|
+
h2: styles.headingStyle.h2,
|
|
26
|
+
h3: styles.headingStyle.h3,
|
|
27
|
+
h4: styles.headingStyle.h4,
|
|
28
|
+
h5: styles.headingStyle.h5,
|
|
29
|
+
h6: styles.headingStyle.h6,
|
|
30
|
+
ul: styles.ulStyle,
|
|
31
|
+
ol: styles.olStyle,
|
|
32
|
+
pre: styles.preStyle,
|
|
33
|
+
code: styles.codeStyle,
|
|
34
|
+
table: styles.tableBorderStyle,
|
|
35
|
+
caption: styles.captionStyle,
|
|
36
|
+
th: classNames(styles.thStyle, styles.tableBorderStyle),
|
|
37
|
+
tr: styles.tableBorderStyle,
|
|
38
|
+
td: classNames(styles.tdStyle, styles.tableBorderStyle),
|
|
39
|
+
a: styles.linkStyle
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Elements a directive rendered are already dressed; leave them be. */
|
|
43
|
+
const isDirectiveOutput = (element: Element) =>
|
|
44
|
+
element.hasAttribute('data-sakura')
|
|
45
|
+
|
|
46
|
+
const addClass = (element: Element, value: string) => {
|
|
47
|
+
const merged = classNames(element.getAttribute('class') ?? '', value)
|
|
48
|
+
if (merged) element.setAttribute('class', merged)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const HEADINGS = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6'])
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Says that the link leaves the page. The glyph is hidden from assistive
|
|
55
|
+
* technology and the words next to it carry the meaning instead, which is what
|
|
56
|
+
* the Icon component does.
|
|
57
|
+
*/
|
|
58
|
+
const appendNewTabIcon = (anchor: Element, document: Document) => {
|
|
59
|
+
if (anchor.querySelector('[data-sakura-new-tab]')) return
|
|
60
|
+
const glyph = document.createElement('span')
|
|
61
|
+
glyph.setAttribute('aria-hidden', 'true')
|
|
62
|
+
glyph.setAttribute('data-sakura-new-tab', 'true')
|
|
63
|
+
glyph.setAttribute(
|
|
64
|
+
'class',
|
|
65
|
+
classNames(styles.iconSizeStyle[16], styles.iconStyle, 'ml-0.5')
|
|
66
|
+
)
|
|
67
|
+
glyph.textContent = 'open_in_new'
|
|
68
|
+
const label = document.createElement('span')
|
|
69
|
+
label.setAttribute('class', 'sr-only')
|
|
70
|
+
label.textContent = 'Opens in new tab'
|
|
71
|
+
anchor.append(glyph, label)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type DecorateOptions = {
|
|
75
|
+
shiftHeading: number
|
|
76
|
+
tocMaxDepth: number
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type DecorateResult = {
|
|
80
|
+
headings: HeadingItem[]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const clampLevel = (level: number) => Math.min(6, Math.max(1, level))
|
|
84
|
+
|
|
85
|
+
export const decorate = (
|
|
86
|
+
root: DocumentFragment | Element,
|
|
87
|
+
document: Document,
|
|
88
|
+
options: DecorateOptions
|
|
89
|
+
): DecorateResult => {
|
|
90
|
+
const { shiftHeading, tocMaxDepth } = options
|
|
91
|
+
const slugger = new Slugger()
|
|
92
|
+
const flat: HeadingItem[] = []
|
|
93
|
+
|
|
94
|
+
// The heading level has to move before the id is read, so that the id belongs
|
|
95
|
+
// to the element that ends up in the document.
|
|
96
|
+
if (shiftHeading !== 0) {
|
|
97
|
+
for (const heading of Array.from(
|
|
98
|
+
root.querySelectorAll('h1,h2,h3,h4,h5,h6')
|
|
99
|
+
)) {
|
|
100
|
+
const depth = Number(heading.tagName[1])
|
|
101
|
+
const moved = document.createElement(
|
|
102
|
+
`h${clampLevel(depth + shiftHeading)}`
|
|
103
|
+
)
|
|
104
|
+
for (const attribute of Array.from(heading.attributes)) {
|
|
105
|
+
moved.setAttribute(attribute.name, attribute.value)
|
|
106
|
+
}
|
|
107
|
+
moved.innerHTML = heading.innerHTML
|
|
108
|
+
heading.replaceWith(moved)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (const element of Array.from(root.querySelectorAll('*'))) {
|
|
113
|
+
const tag = element.tagName.toLowerCase()
|
|
114
|
+
|
|
115
|
+
// One id, generated once, for headings from markdown and from raw HTML
|
|
116
|
+
// alike. The pipeline this replaces generated them in two passes that
|
|
117
|
+
// disagreed whenever a heading held an image, raw HTML or a directive, and
|
|
118
|
+
// the table of contents then linked to anchors that were not there.
|
|
119
|
+
if (HEADINGS.has(tag)) {
|
|
120
|
+
const text = element.textContent ?? ''
|
|
121
|
+
const asked = element.getAttribute('id')
|
|
122
|
+
// An id the author asked for is kept as written, but still handed to the
|
|
123
|
+
// slugger, so that a heading further down does not generate the same one.
|
|
124
|
+
if (asked) slugger.slug(asked)
|
|
125
|
+
const id = asked || slugger.slug(text)
|
|
126
|
+
element.setAttribute('id', id)
|
|
127
|
+
flat.push({ id, depth: Number(tag[1]) - shiftHeading, value: text })
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (isDirectiveOutput(element)) continue
|
|
131
|
+
|
|
132
|
+
if (tag === 'a') {
|
|
133
|
+
const href = element.getAttribute('href') ?? ''
|
|
134
|
+
if (
|
|
135
|
+
href.startsWith('http') ||
|
|
136
|
+
element.getAttribute('target') === '_blank'
|
|
137
|
+
) {
|
|
138
|
+
element.setAttribute('target', '_blank')
|
|
139
|
+
// The content writes target="_blank" 41 times and rel not once, which
|
|
140
|
+
// leaves the opened page holding a handle on this one.
|
|
141
|
+
element.setAttribute('rel', 'noopener noreferrer')
|
|
142
|
+
appendNewTabIcon(element, document)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const cls = CLASS_BY_TAG[tag]
|
|
147
|
+
if (cls) addClass(element, cls)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// A wide table scrolls inside its own box rather than pushing the page.
|
|
151
|
+
for (const table of Array.from(root.querySelectorAll('table'))) {
|
|
152
|
+
const parent = table.parentElement
|
|
153
|
+
if (parent?.dataset?.sakuraTableContainer === 'true') continue
|
|
154
|
+
const container = document.createElement('div')
|
|
155
|
+
container.setAttribute('class', classNames(styles.overflowContainerStyle))
|
|
156
|
+
container.dataset.sakuraTableContainer = 'true'
|
|
157
|
+
table.replaceWith(container)
|
|
158
|
+
container.appendChild(table)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
headings: flat.filter((h) => h.depth <= tocMaxDepth) as HeadingItem[]
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const ATTR_RE =
|
|
2
|
+
/([#.])?([A-Za-z_:][-A-Za-z0-9_.:]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'{}]+)))?/g
|
|
3
|
+
|
|
4
|
+
export type Attrs = Record<string, string>
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Where `{#name}` is kept, apart from anything written as `key=value`.
|
|
8
|
+
*
|
|
9
|
+
* A sigil says something about the element itself and means the same on every
|
|
10
|
+
* directive; an attribute is the directive's own, and each reads the ones it
|
|
11
|
+
* knows. Keeping them in one place would let a directive that wanted an
|
|
12
|
+
* attribute called `id` take the anchor's place, which is how the video on a
|
|
13
|
+
* youtube embed came to be called `id` and stayed that way.
|
|
14
|
+
*
|
|
15
|
+
* A key beginning with `#` cannot be written as `key=value`, since an attribute
|
|
16
|
+
* name has to start with a letter, so there is nowhere for the two to meet.
|
|
17
|
+
*/
|
|
18
|
+
export const ANCHOR = '#id'
|
|
19
|
+
|
|
20
|
+
/** Parses the inside of `{...}`: `key=value key2="値 2" #id .cls` */
|
|
21
|
+
export const parseAttrs = (body: string | null): Attrs => {
|
|
22
|
+
const out: Attrs = {}
|
|
23
|
+
if (!body) return out
|
|
24
|
+
ATTR_RE.lastIndex = 0
|
|
25
|
+
let m = ATTR_RE.exec(body)
|
|
26
|
+
while (m !== null) {
|
|
27
|
+
const [, sigil, key, doubleQuoted, singleQuoted, bare] = m
|
|
28
|
+
const value = doubleQuoted ?? singleQuoted ?? bare
|
|
29
|
+
if (sigil === '#') {
|
|
30
|
+
out[ANCHOR] = key
|
|
31
|
+
} else if (sigil === '.') {
|
|
32
|
+
out.class = out.class ? `${out.class} ${key}` : key
|
|
33
|
+
} else {
|
|
34
|
+
out[key] = value ?? ''
|
|
35
|
+
}
|
|
36
|
+
m = ATTR_RE.exec(body)
|
|
37
|
+
}
|
|
38
|
+
return out
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Reads a delimited run, counting nesting. remark-directive accepts a label
|
|
43
|
+
* like `[a [b] c]`, so matching `\[([^\]]*)\]` would cut it in the wrong place.
|
|
44
|
+
*/
|
|
45
|
+
export const readDelimited = (
|
|
46
|
+
src: string,
|
|
47
|
+
start: number,
|
|
48
|
+
open: string,
|
|
49
|
+
close: string
|
|
50
|
+
): { value: string; end: number } | null => {
|
|
51
|
+
if (src[start] !== open) return null
|
|
52
|
+
let depth = 0
|
|
53
|
+
for (let i = start; i < src.length; i++) {
|
|
54
|
+
const ch = src[i]
|
|
55
|
+
if (ch === '\\') {
|
|
56
|
+
i++
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
if (ch === '\n') return null
|
|
60
|
+
if (ch === open) {
|
|
61
|
+
depth++
|
|
62
|
+
} else if (ch === close) {
|
|
63
|
+
depth--
|
|
64
|
+
if (depth === 0) return { value: src.slice(start + 1, i), end: i + 1 }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** An optional `[label]` followed by an optional `{attrs}`, starting at index i */
|
|
71
|
+
export const readTail = (
|
|
72
|
+
src: string,
|
|
73
|
+
i: number
|
|
74
|
+
): { label: string | null; attrs: Attrs; end: number } => {
|
|
75
|
+
let label: string | null = null
|
|
76
|
+
const labelRun = readDelimited(src, i, '[', ']')
|
|
77
|
+
if (labelRun) {
|
|
78
|
+
label = labelRun.value
|
|
79
|
+
i = labelRun.end
|
|
80
|
+
}
|
|
81
|
+
let attrs: Attrs = {}
|
|
82
|
+
const attrRun = readDelimited(src, i, '{', '}')
|
|
83
|
+
if (attrRun) {
|
|
84
|
+
attrs = parseAttrs(attrRun.value)
|
|
85
|
+
i = attrRun.end
|
|
86
|
+
}
|
|
87
|
+
return { label, attrs, end: i }
|
|
88
|
+
}
|