@sakura-ui/sakura-ui 0.5.1 → 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 +2 -2
- 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/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/Pre.tsx +9 -9
- package/packages/core/src/components/Table.tsx +29 -24
- 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 +2 -2
- package/packages/helper/.turbo/turbo-build.log +1 -1
- package/packages/helper/node_modules/.bin/vitest +2 -2
- package/packages/markdown/.turbo/turbo-build.log +4 -4
- package/packages/markdown/README.md +240 -4
- package/packages/markdown/dist/index.cjs.js +71 -137
- package/packages/markdown/dist/index.es.js +2624 -16739
- 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 +2 -2
- 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
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Marked } from 'marked'
|
|
2
|
+
import { decorate, type HeadingItem } from './decorate'
|
|
3
|
+
import { directiveExtension } from './marked'
|
|
4
|
+
import { sanitize } from './sanitize'
|
|
5
|
+
|
|
6
|
+
export type { HeadingItem } from './decorate'
|
|
7
|
+
|
|
8
|
+
export type RenderOptions = {
|
|
9
|
+
/** Moves every heading down by this many levels, as rehype-shift-heading did. */
|
|
10
|
+
shiftHeading?: number
|
|
11
|
+
/** How deep the table of contents goes. Headings below this are still rendered. */
|
|
12
|
+
tocMaxDepth?: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type RenderResult = {
|
|
16
|
+
html: string
|
|
17
|
+
headings: HeadingItem[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const clampLevel = (level: number) => Math.min(6, Math.max(1, level))
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Markdown to HTML, in three steps: marked writes the markup, DOMPurify takes
|
|
24
|
+
* out what must not reach the DOM, and decorate puts the design system on what
|
|
25
|
+
* is left.
|
|
26
|
+
*
|
|
27
|
+
* Styling last is what lets a table written as raw HTML look like one written
|
|
28
|
+
* with pipes, and it is why the heading ids are generated in one place.
|
|
29
|
+
*/
|
|
30
|
+
export const render = (
|
|
31
|
+
markdown: string,
|
|
32
|
+
options: RenderOptions = {}
|
|
33
|
+
): RenderResult => {
|
|
34
|
+
const { shiftHeading = 0, tocMaxDepth = 2 } = options
|
|
35
|
+
|
|
36
|
+
// A fresh Marked per call: marked.use() would reach every other caller.
|
|
37
|
+
const marked = new Marked(
|
|
38
|
+
{ gfm: true, breaks: true },
|
|
39
|
+
directiveExtension({
|
|
40
|
+
// The card title is a heading too, and the shift has to reach it.
|
|
41
|
+
cardHeadingLevel: `h${clampLevel(3 + shiftHeading)}`
|
|
42
|
+
})
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
const fragment = sanitize(marked.parse(markdown, { async: false }) as string)
|
|
46
|
+
const { headings } = decorate(fragment, document, {
|
|
47
|
+
shiftHeading,
|
|
48
|
+
tocMaxDepth
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const container = document.createElement('div')
|
|
52
|
+
container.appendChild(fragment)
|
|
53
|
+
|
|
54
|
+
return { html: container.innerHTML, headings }
|
|
55
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import createDOMPurify, { type DOMPurify } from 'dompurify'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Markdown is allowed to carry raw HTML, so the output is sanitised before it
|
|
5
|
+
* reaches the DOM.
|
|
6
|
+
*
|
|
7
|
+
* The lists below come from measuring what the documents actually contain
|
|
8
|
+
* rather than from what HTML permits: 27 tags, 24 CSS properties, and no
|
|
9
|
+
* script, no event handler, no javascript: or data: URL anywhere.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Tags DOMPurify does not allow by default but the content needs. */
|
|
13
|
+
const ADD_TAGS = ['caption', 'col', 'colgroup', 'small', 'u', 'iframe']
|
|
14
|
+
|
|
15
|
+
/** Attributes beyond the default set. */
|
|
16
|
+
const ADD_ATTR = [
|
|
17
|
+
'target',
|
|
18
|
+
'role',
|
|
19
|
+
'colspan',
|
|
20
|
+
'rowspan',
|
|
21
|
+
'open',
|
|
22
|
+
'allow',
|
|
23
|
+
'allowfullscreen',
|
|
24
|
+
'frameborder',
|
|
25
|
+
'referrerpolicy',
|
|
26
|
+
'loading'
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* iframe is allowed only so that the youtube directive can embed a video, and
|
|
31
|
+
* only from the host that directive writes. Nothing in the content writes an
|
|
32
|
+
* iframe of its own, so the list stays at one host; an embed pointing anywhere
|
|
33
|
+
* else is dropped rather than trusted.
|
|
34
|
+
*/
|
|
35
|
+
const IFRAME_HOSTS = new Set(['www.youtube-nocookie.com'])
|
|
36
|
+
|
|
37
|
+
const isAllowedIframe = (src: string | null): boolean => {
|
|
38
|
+
if (!src) return false
|
|
39
|
+
try {
|
|
40
|
+
return IFRAME_HOSTS.has(new URL(src).host)
|
|
41
|
+
} catch {
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The CSS properties an inline style may set.
|
|
48
|
+
*
|
|
49
|
+
* Letting style through untouched would leave clickjacking on the table: a
|
|
50
|
+
* fixed, transparent box over the page takes every click. None of the content
|
|
51
|
+
* needs position beyond relative, so the list stays closed.
|
|
52
|
+
*/
|
|
53
|
+
const ALLOWED_CSS_PROPERTIES = new Set([
|
|
54
|
+
'background-color',
|
|
55
|
+
'border',
|
|
56
|
+
'border-bottom',
|
|
57
|
+
'border-collapse',
|
|
58
|
+
'border-left',
|
|
59
|
+
'border-radius',
|
|
60
|
+
'border-right',
|
|
61
|
+
'border-top',
|
|
62
|
+
'color',
|
|
63
|
+
'color-scheme',
|
|
64
|
+
'font-size',
|
|
65
|
+
'font-weight',
|
|
66
|
+
'height',
|
|
67
|
+
'line-height',
|
|
68
|
+
'list-style',
|
|
69
|
+
'list-style-type',
|
|
70
|
+
'margin',
|
|
71
|
+
'margin-bottom',
|
|
72
|
+
'margin-left',
|
|
73
|
+
'margin-right',
|
|
74
|
+
'margin-top',
|
|
75
|
+
'max-width',
|
|
76
|
+
'min-width',
|
|
77
|
+
'padding',
|
|
78
|
+
'padding-bottom',
|
|
79
|
+
'padding-left',
|
|
80
|
+
'padding-right',
|
|
81
|
+
'padding-top',
|
|
82
|
+
'text-align',
|
|
83
|
+
'text-indent',
|
|
84
|
+
'text-orientation',
|
|
85
|
+
'vertical-align',
|
|
86
|
+
'white-space',
|
|
87
|
+
'width',
|
|
88
|
+
'writing-mode'
|
|
89
|
+
])
|
|
90
|
+
|
|
91
|
+
/** position is allowed, but only the value that cannot cover the page. */
|
|
92
|
+
const RESTRICTED_VALUES: Record<string, Set<string>> = {
|
|
93
|
+
position: new Set(['relative', 'static'])
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const filterStyle = (style: string): string =>
|
|
97
|
+
style
|
|
98
|
+
.split(';')
|
|
99
|
+
.map((declaration) => declaration.trim())
|
|
100
|
+
.filter(Boolean)
|
|
101
|
+
.filter((declaration) => {
|
|
102
|
+
const [rawProperty, ...rest] = declaration.split(':')
|
|
103
|
+
const property = rawProperty.trim().toLowerCase()
|
|
104
|
+
const value = rest.join(':').trim().toLowerCase()
|
|
105
|
+
// url(), expression() and @import never appear in the content, and each
|
|
106
|
+
// is a way back out of the sanitiser. Asked before the property is looked
|
|
107
|
+
// at, so that a property added to RESTRICTED_VALUES later does not slip
|
|
108
|
+
// past it.
|
|
109
|
+
if (/url\(|expression\(|@import|behavior:/i.test(value)) return false
|
|
110
|
+
if (!ALLOWED_CSS_PROPERTIES.has(property)) {
|
|
111
|
+
return Boolean(RESTRICTED_VALUES[property]?.has(value))
|
|
112
|
+
}
|
|
113
|
+
return true
|
|
114
|
+
})
|
|
115
|
+
.map((declaration) => `${declaration};`)
|
|
116
|
+
.join(' ')
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Whitespace and control characters taken out, so that a scheme spelt with a
|
|
120
|
+
* tab in the middle of it is read the way the browser will read it. Written by
|
|
121
|
+
* code point because the linter refuses a regular expression holding them.
|
|
122
|
+
*/
|
|
123
|
+
const withoutBlanks = (value: string): string =>
|
|
124
|
+
Array.from(value)
|
|
125
|
+
.filter((char) => (char.codePointAt(0) ?? 0) > 0x20)
|
|
126
|
+
.join('')
|
|
127
|
+
.toLowerCase()
|
|
128
|
+
|
|
129
|
+
let instance: DOMPurify | null = null
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* An instance of our own, not the shared default export. A hook added to that
|
|
133
|
+
* one runs for every other caller on the page, and any of them calling
|
|
134
|
+
* removeAllHooks would take the iframe check and the style filter away with it
|
|
135
|
+
* — silently, since sanitize would go on returning a fragment either way.
|
|
136
|
+
*/
|
|
137
|
+
const purifier = (): DOMPurify => {
|
|
138
|
+
if (instance) return instance
|
|
139
|
+
instance = createDOMPurify(window)
|
|
140
|
+
instance.addHook('afterSanitizeAttributes', (node) => {
|
|
141
|
+
const element = node as Element
|
|
142
|
+
if (!element.getAttribute) return
|
|
143
|
+
|
|
144
|
+
if (element.tagName === 'IFRAME') {
|
|
145
|
+
if (!isAllowedIframe(element.getAttribute('src'))) {
|
|
146
|
+
element.remove()
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// DOMPurify allows data: on an img src whatever ALLOWED_URI_REGEXP says,
|
|
152
|
+
// through its own list of tags that may carry one. The content has no use
|
|
153
|
+
// for it, and leaving it would make raw HTML the loose way in while the
|
|
154
|
+
// directives refused the same URL.
|
|
155
|
+
for (const name of ['src', 'href']) {
|
|
156
|
+
const value = element.getAttribute(name)
|
|
157
|
+
if (value && withoutBlanks(value).startsWith('data:')) {
|
|
158
|
+
element.removeAttribute(name)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const style = element.getAttribute('style')
|
|
163
|
+
if (style) {
|
|
164
|
+
const filtered = filterStyle(style)
|
|
165
|
+
if (filtered) {
|
|
166
|
+
element.setAttribute('style', filtered)
|
|
167
|
+
} else {
|
|
168
|
+
element.removeAttribute('style')
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// A link that opens elsewhere must not hand the opener over with it.
|
|
172
|
+
if (element.getAttribute('target') === '_blank') {
|
|
173
|
+
element.setAttribute('rel', 'noopener noreferrer')
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
return instance
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Returns a fragment rather than a string: the caller has to walk the result to
|
|
181
|
+
* put the design system on it, and going back through the parser would only
|
|
182
|
+
* throw away what has just been built.
|
|
183
|
+
*/
|
|
184
|
+
export const sanitize = (html: string): DocumentFragment => {
|
|
185
|
+
return purifier().sanitize(html, {
|
|
186
|
+
ADD_TAGS,
|
|
187
|
+
ADD_ATTR,
|
|
188
|
+
RETURN_DOM_FRAGMENT: true,
|
|
189
|
+
ALLOWED_URI_REGEXP:
|
|
190
|
+
/^(?:(?:https?|mailto):|[^a-z]|[a-z+.-]+(?:[^a-z+.\-:]|$))/i
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export const __testing = { filterStyle, ALLOWED_CSS_PROPERTIES }
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { render, screen, waitFor } from '@testing-library/react'
|
|
4
|
+
|
|
5
|
+
import { Markdown } from '../src'
|
|
6
|
+
import {
|
|
7
|
+
directives,
|
|
8
|
+
headings,
|
|
9
|
+
malformedHtml,
|
|
10
|
+
notDirectives,
|
|
11
|
+
plain,
|
|
12
|
+
rawHtml
|
|
13
|
+
} from './fixtures'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Renders the component and returns the DOM it produced. The component converts
|
|
17
|
+
* inside an effect, so the first paint is empty and the result has to be waited
|
|
18
|
+
* for.
|
|
19
|
+
*/
|
|
20
|
+
const renderMarkdown = async (
|
|
21
|
+
md: string,
|
|
22
|
+
// Typed, so that a prop that no longer exists is a compile error rather than
|
|
23
|
+
// a test that passes while measuring nothing.
|
|
24
|
+
props: Omit<React.ComponentProps<typeof Markdown>, 'children'> = {}
|
|
25
|
+
) => {
|
|
26
|
+
const { container } = render(<Markdown {...props}>{md}</Markdown>)
|
|
27
|
+
await waitFor(() => expect(container.firstElementChild?.innerHTML).not.toBe(''))
|
|
28
|
+
return container.innerHTML
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* These snapshots exist to catch the difference when the pipeline is replaced,
|
|
33
|
+
* not to describe what the output ought to be. Some of them pin behaviour that
|
|
34
|
+
* is wrong today; those say so.
|
|
35
|
+
*/
|
|
36
|
+
describe('Markdown', () => {
|
|
37
|
+
describe('plain markdown', () => {
|
|
38
|
+
for (const [name, md] of Object.entries(plain)) {
|
|
39
|
+
it(`should render ${name}`, async () => {
|
|
40
|
+
expect(await renderMarkdown(md)).toMatchSnapshot()
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('raw html', () => {
|
|
46
|
+
for (const [name, md] of Object.entries(rawHtml)) {
|
|
47
|
+
it(`should render ${name}`, async () => {
|
|
48
|
+
expect(await renderMarkdown(md)).toMatchSnapshot()
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('malformed html', () => {
|
|
54
|
+
for (const [name, md] of Object.entries(malformedHtml)) {
|
|
55
|
+
it(`should recover from ${name}`, async () => {
|
|
56
|
+
expect(await renderMarkdown(md)).toMatchSnapshot()
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe('directives', () => {
|
|
62
|
+
for (const [name, md] of Object.entries(directives)) {
|
|
63
|
+
it(`should render ${name}`, async () => {
|
|
64
|
+
expect(await renderMarkdown(md)).toMatchSnapshot()
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
it('should not dress a card as a link when it has no title', async () => {
|
|
69
|
+
// The link goes on the title, so a card without one has nowhere to put
|
|
70
|
+
// it. It used to keep the hover and the arrow regardless: a card that
|
|
71
|
+
// answered the pointer and then did nothing.
|
|
72
|
+
const html = await renderMarkdown(directives.linkCardWithoutTitle)
|
|
73
|
+
expect(html).not.toContain('<a')
|
|
74
|
+
expect(html).not.toContain('hover:bg-solid-gray-50')
|
|
75
|
+
expect(html).not.toContain('arrow_forward')
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
describe('text that only looks like a directive', () => {
|
|
80
|
+
for (const [name, md] of Object.entries(notDirectives)) {
|
|
81
|
+
it(`should leave ${name} alone`, async () => {
|
|
82
|
+
expect(await renderMarkdown(md)).toMatchSnapshot()
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
describe('headings', () => {
|
|
88
|
+
for (const [name, md] of Object.entries(headings)) {
|
|
89
|
+
it(`should render ${name}`, async () => {
|
|
90
|
+
expect(await renderMarkdown(md)).toMatchSnapshot()
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
describe('table of contents', () => {
|
|
96
|
+
it('should render the table of contents when asked for it', async () => {
|
|
97
|
+
const html = await renderMarkdown(
|
|
98
|
+
'# 章1\n\n## 節1\n\n### 項1\n\n## 節2\n\n# 章2',
|
|
99
|
+
{ showToc: true }
|
|
100
|
+
)
|
|
101
|
+
expect(html).toMatchSnapshot()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('should use the given title', async () => {
|
|
105
|
+
render(
|
|
106
|
+
<Markdown showToc tocTitle="もくじ">
|
|
107
|
+
{'# 章1'}
|
|
108
|
+
</Markdown>
|
|
109
|
+
)
|
|
110
|
+
await waitFor(() => expect(screen.getByText('もくじ')).toBeInTheDocument())
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
describe('heading shift', () => {
|
|
115
|
+
it('should move the headings down by the given amount', async () => {
|
|
116
|
+
const html = await renderMarkdown('# 見出し1\n\n## 見出し2', {
|
|
117
|
+
shiftHeading: 1
|
|
118
|
+
})
|
|
119
|
+
expect(html).toMatchSnapshot()
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
})
|