@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
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inputs that the rendered output is pinned against while the pipeline is
|
|
3
|
+
* replaced. They lean on what long-lived documentation accumulates rather than
|
|
4
|
+
* on what Markdown allows.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const plain = {
|
|
8
|
+
headings:
|
|
9
|
+
'# 見出し1\n\n## 見出し 2\n\n### 見出し・3\n\n#### はじめに(概要)',
|
|
10
|
+
paragraph: '段落です。**強調** と *斜体* と `コード` と ~~取り消し~~。',
|
|
11
|
+
cjkSoftBreak: '東京都\n港区',
|
|
12
|
+
list: '- 項目1\n- 項目2\n - 入れ子\n - さらに\n\n1. one\n2. two',
|
|
13
|
+
gfmTable: '| 見出し | 値 |\n| --- | --- |\n| あ | 1 |\n| い | 2 |',
|
|
14
|
+
codeFence: '```sh\necho "hello"\n```\n\n```\n言語指定なし\n```',
|
|
15
|
+
links:
|
|
16
|
+
'[内部](/foo) と [外部](https://example.com) と https://example.com/bare',
|
|
17
|
+
image: '',
|
|
18
|
+
blockquote: '> 引用文\n> 続き',
|
|
19
|
+
rule: '---',
|
|
20
|
+
footnote: '本文[^1]\n\n[^1]: 脚注の中身'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const rawHtml = {
|
|
24
|
+
// Often with Markdown inside, which is what makes it awkward
|
|
25
|
+
details:
|
|
26
|
+
'<details>\n<summary>詳細はこちら</summary>\n\n- リスト項目\n\n| 表 | 見出し |\n| --- | --- |\n| a | b |\n\n</details>',
|
|
27
|
+
// Written in HTML because the cells have to span
|
|
28
|
+
tableSpan:
|
|
29
|
+
'<table>\n<caption>表:01 キャプション</caption>\n<colgroup><col style="width: 15%;"><col style="width: auto;"></colgroup>\n<thead><tr><th colspan="2">見出し</th></tr></thead>\n<tbody><tr><td rowspan="2" style="vertical-align: top;">結合</td><td>値</td></tr><tr><td>値2</td></tr></tbody>\n</table>',
|
|
30
|
+
// Inline style carries the layout of most hand-written HTML
|
|
31
|
+
styledBlock:
|
|
32
|
+
'<div style="background-color: #E8F1FE; padding: 12px 16px; border-radius: 8px; margin: 0 24px;">\n注記のボックス\n</div>',
|
|
33
|
+
styledImg:
|
|
34
|
+
'<img src="/images/a.png" alt="図" style="width: 800px;color-scheme: light;">',
|
|
35
|
+
inlineTags:
|
|
36
|
+
'本文に <strong>太字</strong> と <u>下線</u> と <sup>(※1)</sup> と <small>小</small>。',
|
|
37
|
+
externalAnchor: '<a href="https://example.com" target="_blank">外部リンク</a>'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const malformedHtml = {
|
|
41
|
+
// A closing tag for a void element, written often enough to matter
|
|
42
|
+
closingBr: '一行目<br>二行目</br>三行目</BR>四行目',
|
|
43
|
+
// </td> mistyped as </d>
|
|
44
|
+
mistypedClose: '<table><tbody><tr><td>値</d></tr></tbody></table>',
|
|
45
|
+
unclosedCell: '<table><tbody><tr><td>閉じ忘れ<tr><td>次の行</table>',
|
|
46
|
+
// <ul> wrapping <details> for indentation, with the closing tags crossed
|
|
47
|
+
crossedNesting:
|
|
48
|
+
'<ul>\n<details>\n<summary>まとめ</summary>\n中身\n</details>\n</ul>'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const directives = {
|
|
52
|
+
linkButton:
|
|
53
|
+
'ご案内です。:link-button[サービス一覧]{href=/services} をどうぞ。',
|
|
54
|
+
youtube: '::youtube[動画タイトル]{video=yXdbvBzxeb8 width=560 height=315}',
|
|
55
|
+
card: ':::card\n::card-img{src=/a.png alt=図}\n::card-title[タイトル]\n::card-description[説明]\n:::',
|
|
56
|
+
linkCard:
|
|
57
|
+
':::card{href=/x}\n::card-title[タイトル]\n::card-description[説明]\n::card-footer[2026年8月]\n:::',
|
|
58
|
+
linkCardWithoutTitle:
|
|
59
|
+
':::card{href=/x}\n::card-description[説明]\n::card-footer[2026年8月]\n:::',
|
|
60
|
+
// Cards throughout, so the grid is a list of them.
|
|
61
|
+
grid: '::::grid-cols-3\n:::card\n::card-title[1つ目]\n:::\n:::card\n::card-title[2つ目]\n:::\n::::',
|
|
62
|
+
// Cells: prose beside a figure is a layout, not two of something.
|
|
63
|
+
gridOfCells:
|
|
64
|
+
'::::grid-cols-2\n:::cell\n\n本文1\n:::\n:::cell\n本文2\n:::\n::::',
|
|
65
|
+
// One of each, which is not a list either.
|
|
66
|
+
gridMixed:
|
|
67
|
+
'::::grid-cols-2\n:::card\n::card-title[1つ目]\n:::\n:::cell\n本文\n:::\n::::',
|
|
68
|
+
cell: ':::cell\n::cell-img{src=/a.png alt=図}\n本文\n:::',
|
|
69
|
+
faq: ':::faq\n::faq-q[質問は?]\n::faq-a[回答です。]\n::faq-q[二つ目の質問は?]\n::faq-a[二つ目の回答。]\n:::'
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Text that looks like a directive but is not one. The current attr plugin
|
|
74
|
+
* turns any name into a tag of the same name, so "参照:example" loses its
|
|
75
|
+
* ":example" entirely. Pinned here so the migration is seen to fix it.
|
|
76
|
+
*/
|
|
77
|
+
export const notDirectives = {
|
|
78
|
+
colonWord: '参照:example と note:important を見てください。',
|
|
79
|
+
functionCall: '関数 foo:bar() を呼びます。',
|
|
80
|
+
time: '開始は 10:30 です。',
|
|
81
|
+
ratio: '比率は 16:9 です。',
|
|
82
|
+
emoji: '絵文字 :smile: です。',
|
|
83
|
+
todo: 'TODO: あとで直す',
|
|
84
|
+
xmlAttr: '属性は xml:lang です。',
|
|
85
|
+
chapter: '第1章:はじめに',
|
|
86
|
+
// Shapes that lose text today. Each of them appears in real documentation,
|
|
87
|
+
// and each renders with the part after the colon gone.
|
|
88
|
+
timeFormat: '開始時刻は HH:MM 形式で記載する。',
|
|
89
|
+
scopeName: 'スコープ chat:write を付与する。',
|
|
90
|
+
linkLabel: '[AWSドキュメント:AWS PrivateLink](https://example.com)'
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const headings = {
|
|
94
|
+
japanese:
|
|
95
|
+
'## 概要\n\n## はじめに(概要)\n\n## A/B テスト\n\n## サービス・アプリ',
|
|
96
|
+
duplicated: '## 概要\n\n## 概要\n\n## 概要',
|
|
97
|
+
withImage: '##  会社概要',
|
|
98
|
+
withInlineHtml: '## <b>太字</b> タイトル',
|
|
99
|
+
withCode: '## `code` を含む見出し'
|
|
100
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import sharedDOMPurify from 'dompurify'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { render } from '../src/render'
|
|
4
|
+
|
|
5
|
+
const html = (markdown: string) => render(markdown).html
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The pipeline lets raw HTML through, because 40% of the documents it renders
|
|
9
|
+
* contain some. These are the things it must not let through with it.
|
|
10
|
+
*/
|
|
11
|
+
describe('sanitize', () => {
|
|
12
|
+
describe('script', () => {
|
|
13
|
+
it('should drop a script element', () => {
|
|
14
|
+
expect(html('<script>alert(1)</script>')).not.toContain('alert')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('should drop an event handler', () => {
|
|
18
|
+
const out = html('<img src="/a.png" onerror="alert(1)">')
|
|
19
|
+
expect(out).toContain('src="/a.png"')
|
|
20
|
+
expect(out).not.toContain('onerror')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('should drop a javascript URL', () => {
|
|
24
|
+
expect(html('[click](javascript:alert(1))')).not.toContain('javascript:')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('should drop a javascript URL written as a directive', () => {
|
|
28
|
+
// marked keeps its own URL cleaning to itself, so a renderer writing href
|
|
29
|
+
// has to bring one; without it this href went straight through.
|
|
30
|
+
const out = html(':link-button[click]{href=javascript:alert(1)}')
|
|
31
|
+
expect(out).not.toContain('javascript:')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('should drop a javascript URL in raw HTML', () => {
|
|
35
|
+
expect(html('<a href="javascript:alert(1)">click</a>')).not.toContain(
|
|
36
|
+
'javascript:'
|
|
37
|
+
)
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe('the shared DOMPurify', () => {
|
|
42
|
+
// DOMPurify's default export is one object for the whole page. A hook put
|
|
43
|
+
// on it runs for everyone, and anyone calling removeAllHooks takes it back
|
|
44
|
+
// off again — while sanitize goes on returning a fragment, so nothing says
|
|
45
|
+
// the filters have gone.
|
|
46
|
+
it('should not be given this package hooks', () => {
|
|
47
|
+
html('<div style="position: fixed">x</div>')
|
|
48
|
+
expect(
|
|
49
|
+
sharedDOMPurify.sanitize('<div style="position: fixed">x</div>')
|
|
50
|
+
).toContain('position: fixed')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('should not be able to take this package filters away', () => {
|
|
54
|
+
html('<div style="position: fixed">x</div>')
|
|
55
|
+
sharedDOMPurify.removeAllHooks()
|
|
56
|
+
expect(html('<div style="position: fixed">x</div>')).not.toContain(
|
|
57
|
+
'position: fixed'
|
|
58
|
+
)
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
describe('data URLs', () => {
|
|
63
|
+
// The URL cleaner used to let images through while ALLOWED_URI_REGEXP took
|
|
64
|
+
// them out a step later. Nothing in the content uses one; these say which
|
|
65
|
+
// of the two was meant.
|
|
66
|
+
const PIXEL =
|
|
67
|
+
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg=='
|
|
68
|
+
|
|
69
|
+
it('should drop one written as an image', () => {
|
|
70
|
+
expect(html(``)).not.toContain('data:')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('should drop one written as a directive', () => {
|
|
74
|
+
expect(html(`::card-img{src=${PIXEL} alt=点}`)).not.toContain('data:')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('should drop one written in raw HTML', () => {
|
|
78
|
+
expect(html(`<img src="${PIXEL}" alt="点">`)).not.toContain('data:')
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
describe('style', () => {
|
|
83
|
+
it('should keep the properties the content uses', () => {
|
|
84
|
+
const out = html(
|
|
85
|
+
'<table><tbody><tr><td style="vertical-align: top; white-space: nowrap; width: 15%;">値</td></tr></tbody></table>'
|
|
86
|
+
)
|
|
87
|
+
expect(out).toContain('vertical-align: top')
|
|
88
|
+
expect(out).toContain('white-space: nowrap')
|
|
89
|
+
expect(out).toContain('width: 15%')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('should drop a fixed overlay', () => {
|
|
93
|
+
// A transparent box over the page takes every click aimed at what is
|
|
94
|
+
// underneath it, without a line of script.
|
|
95
|
+
const out = html(
|
|
96
|
+
'<div style="position: fixed; top: 0; left: 0; width: 100%; height: 100%">x</div>'
|
|
97
|
+
)
|
|
98
|
+
expect(out).not.toContain('position: fixed')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('should keep position relative', () => {
|
|
102
|
+
const out = html('<div style="position: relative">x</div>')
|
|
103
|
+
expect(out).toContain('position: relative')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('should drop a url() reference', () => {
|
|
107
|
+
const out = html(
|
|
108
|
+
'<div style="background-color: red; background-image: url(https://evil.example/a.png)">x</div>'
|
|
109
|
+
)
|
|
110
|
+
expect(out).toContain('background-color: red')
|
|
111
|
+
expect(out).not.toContain('url(')
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
describe('frames', () => {
|
|
116
|
+
it('should keep the video the youtube directive embeds', () => {
|
|
117
|
+
const out = html('::youtube[題]{video=abc123}')
|
|
118
|
+
expect(out).toContain('www.youtube-nocookie.com/embed/abc123')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('should drop an iframe pointing anywhere else', () => {
|
|
122
|
+
expect(html('<iframe src="https://evil.example/x"></iframe>')).not.toContain(
|
|
123
|
+
'evil.example'
|
|
124
|
+
)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('should drop an iframe with no source', () => {
|
|
128
|
+
expect(html('<iframe></iframe>')).not.toContain('<iframe')
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
describe('links that leave the page', () => {
|
|
133
|
+
it('should not hand the opener to the page it opens', () => {
|
|
134
|
+
const out = html('<a href="https://example.com" target="_blank">外部</a>')
|
|
135
|
+
expect(out).toContain('rel="noopener noreferrer"')
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('should say that the link opens elsewhere', () => {
|
|
139
|
+
const out = html('[外部](https://example.com)')
|
|
140
|
+
expect(out).toContain('Opens in new tab')
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
describe('what the content needs', () => {
|
|
145
|
+
it('should keep a table that spans cells', () => {
|
|
146
|
+
const out = html(
|
|
147
|
+
'<table><tbody><tr><td colspan="2" rowspan="3">値</td></tr></tbody></table>'
|
|
148
|
+
)
|
|
149
|
+
expect(out).toContain('colspan="2"')
|
|
150
|
+
expect(out).toContain('rowspan="3"')
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('should keep a caption and a column group', () => {
|
|
154
|
+
const out = html(
|
|
155
|
+
'<table><caption>表</caption><colgroup><col style="width: 15%;"></colgroup></table>'
|
|
156
|
+
)
|
|
157
|
+
expect(out).toContain('<caption')
|
|
158
|
+
expect(out).toContain('<col')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('should keep a details block and the markdown inside it', () => {
|
|
162
|
+
const out = html('<details>\n<summary>題</summary>\n\n- 項目\n\n</details>')
|
|
163
|
+
expect(out).toContain('<details>')
|
|
164
|
+
expect(out).toContain('<summary>')
|
|
165
|
+
expect(out).toContain('<li>')
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
})
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { renderToString } from 'react-dom/server'
|
|
3
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { Markdown } from '../src'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The pipeline sanitises and styles through the DOM, so it cannot run where
|
|
9
|
+
* there is no document. What it must not do is throw there: a page that renders
|
|
10
|
+
* on the server has to reach the browser, where the effect fills the markup in.
|
|
11
|
+
*
|
|
12
|
+
* The document is taken away rather than the environment changed, because the
|
|
13
|
+
* component is only ever server-rendered by a caller who has React but no DOM.
|
|
14
|
+
*/
|
|
15
|
+
const withoutDocument = <T,>(body: () => T): T => {
|
|
16
|
+
const saved = Object.getOwnPropertyDescriptor(globalThis, 'document')
|
|
17
|
+
Object.defineProperty(globalThis, 'document', {
|
|
18
|
+
configurable: true,
|
|
19
|
+
get() {
|
|
20
|
+
throw new ReferenceError('document is not defined')
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
try {
|
|
24
|
+
return body()
|
|
25
|
+
} finally {
|
|
26
|
+
if (saved) Object.defineProperty(globalThis, 'document', saved)
|
|
27
|
+
else Reflect.deleteProperty(globalThis, 'document')
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('server rendering', () => {
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
expect(globalThis.document).toBeDefined()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should render without a document', () => {
|
|
37
|
+
expect(() =>
|
|
38
|
+
withoutDocument(() =>
|
|
39
|
+
renderToString(<Markdown>{'# 見出し\n\n本文'}</Markdown>)
|
|
40
|
+
)
|
|
41
|
+
).not.toThrow()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('should render nothing rather than half of something', () => {
|
|
45
|
+
const html = withoutDocument(() =>
|
|
46
|
+
renderToString(<Markdown showToc>{'# 見出し\n\n本文'}</Markdown>)
|
|
47
|
+
)
|
|
48
|
+
expect(html).not.toContain('見出し')
|
|
49
|
+
expect(html).not.toContain('本文')
|
|
50
|
+
})
|
|
51
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// tsconfig.json takes in src alone, because that is what the published types
|
|
2
|
+
// are built from. The tests went unchecked as a result, and one of them spent
|
|
3
|
+
// the migration passing a prop that no longer existed, measuring nothing.
|
|
4
|
+
{
|
|
5
|
+
"extends": "./tsconfig.json",
|
|
6
|
+
"compilerOptions": {
|
|
7
|
+
"noEmit": true,
|
|
8
|
+
"emitDeclarationOnly": false
|
|
9
|
+
},
|
|
10
|
+
"include": ["src", "tests"]
|
|
11
|
+
}
|
|
@@ -24,7 +24,11 @@ export default defineConfig({
|
|
|
24
24
|
},
|
|
25
25
|
test: {
|
|
26
26
|
globals: true,
|
|
27
|
-
|
|
27
|
+
// jsdom rather than happy-dom, which the rest of the repository uses:
|
|
28
|
+
// DOMPurify drops the first top-level element under happy-dom, so <p>段落</p>
|
|
29
|
+
// sanitises to 段落 and a table loses the container it scrolls inside. The
|
|
30
|
+
// same input is left alone under jsdom, and in a browser.
|
|
31
|
+
environment: 'jsdom',
|
|
28
32
|
setupFiles: 'tests/vitest.setup.ts'
|
|
29
33
|
}
|
|
30
34
|
})
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"attr.d.ts","sourceRoot":"","sources":["../../../src/plugins/attr.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,OAAO,CAAA;AAQjC,eAAO,MAAM,UAAU,SACb,MAAM,IAAI,SASnB,CAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"card.d.ts","sourceRoot":"","sources":["../../../src/plugins/card.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,OAAO,CAAA;AAQjC,eAAO,MAAM,UAAU,SACb,MAAM,IAAI,SA6DnB,CAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cell.d.ts","sourceRoot":"","sources":["../../../src/plugins/cell.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,OAAO,CAAA;AAQjC,eAAO,MAAM,UAAU,SACb,MAAM,IAAI,SAqCnB,CAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"faq.d.ts","sourceRoot":"","sources":["../../../src/plugins/faq.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,OAAO,CAAA;AAQjC,eAAO,MAAM,SAAS,SACZ,MAAM,IAAI,SA8BnB,CAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"grid.d.ts","sourceRoot":"","sources":["../../../src/plugins/grid.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,OAAO,CAAA;AAQjC,eAAO,MAAM,UAAU,SACb,MAAM,IAAI,SA4CnB,CAAA"}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { Root } from 'mdast';
|
|
2
|
-
export type HeadingItem = {
|
|
3
|
-
id?: string;
|
|
4
|
-
depth: number;
|
|
5
|
-
value?: string;
|
|
6
|
-
children?: Array<HeadingItem>;
|
|
7
|
-
};
|
|
8
|
-
export declare const headingsPlugin: () => (tree: Root, vFile: {
|
|
9
|
-
data: Record<string, unknown>;
|
|
10
|
-
}) => void;
|
|
11
|
-
//# sourceMappingURL=headings.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"headings.d.ts","sourceRoot":"","sources":["../../../src/plugins/headings.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAW,IAAI,EAAE,MAAM,OAAO,CAAA;AAO1C,MAAM,MAAM,WAAW,GAAG;IACxB,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,KAAK,CAAC,WAAW,CAAC,CAAA;CAC9B,CAAA;AAED,eAAO,MAAM,cAAc,SACjB,MAAM,IAAI,EAAE,OAAO;IAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,SAsB7D,CAAA"}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { Directives } from 'mdast-util-directive';
|
|
2
|
-
import type { Node } from 'unist';
|
|
3
|
-
import type { Heading } from 'mdast';
|
|
4
|
-
export declare const isDirective: (node: Node) => node is Directives;
|
|
5
|
-
export declare const isContainerDirective: (node: Node) => node is Directives;
|
|
6
|
-
export declare const isLeafDirective: (node: Node) => node is Directives;
|
|
7
|
-
export declare const isTextDirective: (node: Node) => node is Directives;
|
|
8
|
-
export declare const isHeading: (node: Node) => node is Heading;
|
|
9
|
-
//# sourceMappingURL=helper.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"helper.d.ts","sourceRoot":"","sources":["../../../src/plugins/helper.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,OAAO,CAAA;AACjC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,OAAO,CAAA;AAEpC,eAAO,MAAM,WAAW,GAAI,MAAM,IAAI,KAAG,IAAI,IAAI,UAMhD,CAAA;AAED,eAAO,MAAM,oBAAoB,GAAI,MAAM,IAAI,KAAG,IAAI,IAAI,UAEzD,CAAA;AAED,eAAO,MAAM,eAAe,GAAI,MAAM,IAAI,KAAG,IAAI,IAAI,UAEpD,CAAA;AAED,eAAO,MAAM,eAAe,GAAI,MAAM,IAAI,KAAG,IAAI,IAAI,UAEpD,CAAA;AAED,eAAO,MAAM,SAAS,GAAI,MAAM,IAAI,KAAG,IAAI,IAAI,OAE9C,CAAA"}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
export { youtubePlugin } from './youtube';
|
|
2
|
-
export { attrPlugin } from './attr';
|
|
3
|
-
export { linkButtonPlugin } from './linkButton';
|
|
4
|
-
export { gridPlugin } from './grid';
|
|
5
|
-
export { cellPlugin } from './cell';
|
|
6
|
-
export { cardPlugin } from './card';
|
|
7
|
-
export { faqPlugin } from './faq';
|
|
8
|
-
export { headingsPlugin, type HeadingItem } from './headings';
|
|
9
|
-
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/plugins/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAA;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AACnC,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AACnC,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AACnC,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AACnC,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACjC,OAAO,EAAE,cAAc,EAAE,KAAK,WAAW,EAAE,MAAM,YAAY,CAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"linkButton.d.ts","sourceRoot":"","sources":["../../../src/plugins/linkButton.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,OAAO,CAAA;AAQjC,eAAO,MAAM,gBAAgB,SACnB,MAAM,IAAI,SAgBnB,CAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"youtube.d.ts","sourceRoot":"","sources":["../../../src/plugins/youtube.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,OAAO,CAAA;AAOjC,eAAO,MAAM,aAAa,SAChB,MAAM,IAAI,SAwCnB,CAAA"}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { Root } from 'mdast'
|
|
2
|
-
import { h } from 'hastscript'
|
|
3
|
-
import { visit } from 'unist-util-visit'
|
|
4
|
-
import type { Directives } from 'mdast-util-directive'
|
|
5
|
-
import { isDirective } from './helper'
|
|
6
|
-
|
|
7
|
-
// This plugin is an example to turn `::node` into divs, passing arbitrary
|
|
8
|
-
// attributes.
|
|
9
|
-
export const attrPlugin = () => {
|
|
10
|
-
return (tree: Root) => {
|
|
11
|
-
visit(tree, isDirective, (node: Directives) => {
|
|
12
|
-
node.data = node.data ?? {}
|
|
13
|
-
const hast = h(node.name, node.attributes || {})
|
|
14
|
-
|
|
15
|
-
node.data.hName = hast.tagName
|
|
16
|
-
node.data.hProperties = hast.properties
|
|
17
|
-
})
|
|
18
|
-
}
|
|
19
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import type { Root } from 'mdast'
|
|
2
|
-
import { h } from 'hastscript'
|
|
3
|
-
import { visit } from 'unist-util-visit'
|
|
4
|
-
import type { Directives } from 'mdast-util-directive'
|
|
5
|
-
import { isDirective } from './helper'
|
|
6
|
-
|
|
7
|
-
// This plugin is to turn `:::card` into divs, passing arbitrary
|
|
8
|
-
// attributes.
|
|
9
|
-
export const cardPlugin = () => {
|
|
10
|
-
return (tree: Root) => {
|
|
11
|
-
visit(tree, isDirective, (node: Directives) => {
|
|
12
|
-
if (node.type !== 'containerDirective') {
|
|
13
|
-
return
|
|
14
|
-
}
|
|
15
|
-
if (node.name !== 'card') {
|
|
16
|
-
return
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
node.data = node.data ?? {}
|
|
20
|
-
|
|
21
|
-
const tagName = 'article'
|
|
22
|
-
|
|
23
|
-
node.data.hName = tagName
|
|
24
|
-
|
|
25
|
-
node.attributes = node.attributes ?? {}
|
|
26
|
-
node.attributes['data-node'] = node.name
|
|
27
|
-
|
|
28
|
-
if (node.attributes.as === 'link') {
|
|
29
|
-
node.attributes.as = undefined
|
|
30
|
-
node.attributes['data-behavior'] = 'link'
|
|
31
|
-
node.attributes['data-href'] = node.attributes.href || ''
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
node.data.hProperties = h(tagName, node.attributes).properties
|
|
35
|
-
|
|
36
|
-
const attributes = node.attributes
|
|
37
|
-
const isLink = attributes['data-behavior'] === 'link'
|
|
38
|
-
|
|
39
|
-
node.children.forEach((child) => {
|
|
40
|
-
if (!isDirective(child)) {
|
|
41
|
-
return
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
child.data = child.data ?? {}
|
|
45
|
-
|
|
46
|
-
let tagName = 'div'
|
|
47
|
-
if (child.name === 'card-img') {
|
|
48
|
-
tagName = 'img'
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
child.data.hName = tagName
|
|
52
|
-
|
|
53
|
-
child.attributes = child.attributes ?? {}
|
|
54
|
-
child.attributes['data-node'] = child.name
|
|
55
|
-
|
|
56
|
-
// The title carries the link, so it needs the href of the card. The
|
|
57
|
-
// footer only needs to know that it belongs to a link card, to render
|
|
58
|
-
// the arrow. A child cannot read its parent once this is a React tree,
|
|
59
|
-
// so the marker is copied down here.
|
|
60
|
-
if (isLink && (child.name === 'card-title' || child.name === 'card-footer')) {
|
|
61
|
-
child.attributes['data-behavior'] = 'link'
|
|
62
|
-
if (child.name === 'card-title') {
|
|
63
|
-
child.attributes['data-href'] = attributes['data-href'] || ''
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
child.data.hProperties = h(tagName, child.attributes).properties
|
|
68
|
-
})
|
|
69
|
-
})
|
|
70
|
-
}
|
|
71
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import type { Root } from 'mdast'
|
|
2
|
-
import { h } from 'hastscript'
|
|
3
|
-
import { visit } from 'unist-util-visit'
|
|
4
|
-
import type { Directives } from 'mdast-util-directive'
|
|
5
|
-
import { isContainerDirective, isDirective } from './helper'
|
|
6
|
-
|
|
7
|
-
// This plugin is to turn `:::cell` into divs, passing arbitrary
|
|
8
|
-
// attributes.
|
|
9
|
-
export const cellPlugin = () => {
|
|
10
|
-
return (tree: Root) => {
|
|
11
|
-
visit(tree, isContainerDirective, (node: Directives) => {
|
|
12
|
-
if (node.name !== 'cell') {
|
|
13
|
-
return
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
node.data = node.data ?? {}
|
|
17
|
-
const tagName = 'div'
|
|
18
|
-
|
|
19
|
-
node.data.hName = tagName
|
|
20
|
-
|
|
21
|
-
node.attributes = node.attributes ?? {}
|
|
22
|
-
node.attributes['data-node'] = node.name
|
|
23
|
-
|
|
24
|
-
node.data.hProperties = h(tagName, node.attributes).properties
|
|
25
|
-
|
|
26
|
-
node.children.forEach((child) => {
|
|
27
|
-
if (!isDirective(child)) {
|
|
28
|
-
return
|
|
29
|
-
}
|
|
30
|
-
if (child.name !== 'cell-img') {
|
|
31
|
-
return
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
child.data = child.data ?? {}
|
|
35
|
-
|
|
36
|
-
const tagName = 'img'
|
|
37
|
-
|
|
38
|
-
child.data.hName = tagName
|
|
39
|
-
|
|
40
|
-
child.attributes = child.attributes ?? {}
|
|
41
|
-
child.attributes['data-node'] = child.name
|
|
42
|
-
|
|
43
|
-
child.data.hProperties = h(tagName, child.attributes).properties
|
|
44
|
-
})
|
|
45
|
-
})
|
|
46
|
-
}
|
|
47
|
-
}
|