@sakura-ui/sakura-ui 0.5.4 → 0.5.6

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.
Files changed (30) hide show
  1. package/package.json +4 -4
  2. package/packages/core/package.json +1 -1
  3. package/packages/core/src/components/Link.tsx +17 -0
  4. package/packages/core/tests/Link.test.tsx +41 -0
  5. package/packages/forms/.turbo/turbo-build.log +2 -2
  6. package/packages/helper/.turbo/turbo-build.log +1 -1
  7. package/packages/markdown/.turbo/turbo-build.log +5 -5
  8. package/packages/markdown/README.md +17 -1
  9. package/packages/markdown/dist/index.cjs.js +110 -43
  10. package/packages/markdown/dist/index.es.js +862 -744
  11. package/packages/markdown/dist/types/decorate.d.ts.map +1 -1
  12. package/packages/markdown/dist/types/marked/directives/image.d.ts +3 -0
  13. package/packages/markdown/dist/types/marked/directives/image.d.ts.map +1 -0
  14. package/packages/markdown/dist/types/marked/directives/index.d.ts.map +1 -1
  15. package/packages/markdown/dist/types/marked/html.d.ts.map +1 -1
  16. package/packages/markdown/dist/types/marked/registry.d.ts.map +1 -1
  17. package/packages/markdown/dist/types/sanitize.d.ts.map +1 -1
  18. package/packages/markdown/package.json +1 -1
  19. package/packages/markdown/src/decorate.ts +25 -9
  20. package/packages/markdown/src/marked/directives/image.ts +29 -0
  21. package/packages/markdown/src/marked/directives/index.ts +2 -0
  22. package/packages/markdown/src/marked/html.ts +14 -1
  23. package/packages/markdown/src/marked/registry.ts +1 -0
  24. package/packages/markdown/src/marked/renderer.ts +3 -3
  25. package/packages/markdown/src/sanitize.ts +9 -2
  26. package/packages/markdown/tests/Markdown.test.tsx +17 -0
  27. package/packages/markdown/tests/__snapshots__/Markdown.test.tsx.snap +36 -31
  28. package/packages/markdown/tests/image.test.ts +64 -0
  29. package/packages/markdown/tests/sanitize.test.ts +17 -0
  30. package/packages/tailwind-theme-plugin/.turbo/turbo-build.log +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"decorate.d.ts","sourceRoot":"","sources":["../../src/decorate.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AAEH,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,WAAW,EAAE,CAAA;CACzB,CAAA;AAsDD,MAAM,MAAM,eAAe,GAAG;IAC5B,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,EAAE,WAAW,EAAE,CAAA;CACxB,CAAA;AAID,eAAO,MAAM,QAAQ,GACnB,MAAM,gBAAgB,GAAG,OAAO,EAChC,UAAU,QAAQ,EAClB,SAAS,eAAe,KACvB,cAyEF,CAAA"}
1
+ {"version":3,"file":"decorate.d.ts","sourceRoot":"","sources":["../../src/decorate.ts"],"names":[],"mappings":"AAKA;;;;;GAKG;AAEH,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,WAAW,EAAE,CAAA;CACzB,CAAA;AAoED,MAAM,MAAM,eAAe,GAAG;IAC5B,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,EAAE,WAAW,EAAE,CAAA;CACxB,CAAA;AAID,eAAO,MAAM,QAAQ,GACnB,MAAM,gBAAgB,GAAG,OAAO,EAChC,UAAU,QAAQ,EAClB,SAAS,eAAe,KACvB,cA0EF,CAAA"}
@@ -0,0 +1,3 @@
1
+ import type { DirectiveRenderer } from './context';
2
+ export declare const imageRenderers: Record<string, DirectiveRenderer>;
3
+ //# sourceMappingURL=image.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../../../../src/marked/directives/image.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAA;AAmBlD,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAQ5D,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/marked/directives/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAA;AAclD;;;GAGG;AACH,eAAO,MAAM,YAAY,GAAI,MAAM,MAAM,KAAG,iBAAiB,GAAG,SACN,CAAA;AAE1D,YAAY,EACV,gBAAgB,EAChB,iBAAiB,EACjB,MAAM,EACN,eAAe,EAChB,MAAM,WAAW,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/marked/directives/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAA;AAgBlD;;;GAGG;AACH,eAAO,MAAM,YAAY,GAAI,MAAM,MAAM,KAAG,iBAAiB,GAAG,SACN,CAAA;AAE1D,YAAY,EACV,gBAAgB,EAChB,iBAAiB,EACjB,MAAM,EACN,eAAe,EAChB,MAAM,WAAW,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../../src/marked/html.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,UAAU,GAAI,OAAO,OAAO,KAAG,MACe,CAAA;AAgB3D;;;GAGG;AACH,eAAO,MAAM,QAAQ,GAAI,MAAM,MAAM,GAAG,SAAS,KAAG,MAAM,GAAG,SAS5D,CAAA;AAED,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,IAAI,CAAA;AAE3D,2DAA2D;AAC3D,eAAO,MAAM,WAAW,GAAI,OAAO,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,KAAG,MAIlD,CAAA;AAEb;;;GAGG;AACH,eAAO,MAAM,UAAU,GAAI,GAAG,QAAQ,CAAC,MAAM,GAAG,SAAS,GAAG,KAAK,CAAC,EAAE,KAAG,MACE,CAAA"}
1
+ {"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../../src/marked/html.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,UAAU,GAAI,OAAO,OAAO,KAAG,MACe,CAAA;AAgB3D;;;GAGG;AACH,eAAO,MAAM,QAAQ,GAAI,MAAM,MAAM,GAAG,SAAS,KAAG,MAAM,GAAG,SAS5D,CAAA;AAED,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,IAAI,CAAA;AAS3D,2DAA2D;AAC3D,eAAO,MAAM,WAAW,GAAI,OAAO,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,KAAG,MAUlD,CAAA;AAEb;;;GAGG;AACH,eAAO,MAAM,UAAU,GAAI,GAAG,QAAQ,CAAC,MAAM,GAAG,SAAS,GAAG,KAAK,CAAC,EAAE,KAAG,MACE,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../src/marked/registry.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,SAAS,cAAc,CAAA;AACpC,eAAO,MAAM,IAAI,SAAS,CAAA;AAC1B,eAAO,MAAM,IAAI,SAAS,CAAA;AAE1B,MAAM,MAAM,aAAa,GAAG,OAAO,SAAS,GAAG,OAAO,IAAI,GAAG,OAAO,IAAI,CAAA;AAsBxE,+EAA+E;AAC/E,eAAO,MAAM,OAAO,QAAiC,CAAA;AAErD,eAAO,MAAM,OAAO,GAAI,MAAM,MAAM,EAAE,MAAM,aAAa,KAAG,OACmB,CAAA"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../src/marked/registry.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,SAAS,cAAc,CAAA;AACpC,eAAO,MAAM,IAAI,SAAS,CAAA;AAC1B,eAAO,MAAM,IAAI,SAAS,CAAA;AAE1B,MAAM,MAAM,aAAa,GAAG,OAAO,SAAS,GAAG,OAAO,IAAI,GAAG,OAAO,IAAI,CAAA;AAuBxE,+EAA+E;AAC/E,eAAO,MAAM,OAAO,QAAiC,CAAA;AAErD,eAAO,MAAM,OAAO,GAAI,MAAM,MAAM,EAAE,MAAM,aAAa,KAAG,OACmB,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../../src/sanitize.ts"],"names":[],"mappings":"AAkLA;;;;GAIG;AACH,eAAO,MAAM,QAAQ,GAAI,MAAM,MAAM,KAAG,gBAQvC,CAAA;AAED,eAAO,MAAM,SAAS;yBAlGM,MAAM,KAAG,MAAM;;CAkGqB,CAAA"}
1
+ {"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../../src/sanitize.ts"],"names":[],"mappings":"AAyLA;;;;GAIG;AACH,eAAO,MAAM,QAAQ,GAAI,MAAM,MAAM,KAAG,gBAQvC,CAAA;AAED,eAAO,MAAM,SAAS;yBAzGM,MAAM,KAAG,MAAM;;CAyGqB,CAAA"}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sakura-ui/markdown",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "",
5
5
  "keywords": [
6
6
  "markdown"
@@ -1,4 +1,5 @@
1
1
  import { styles } from '@sakura-ui/core'
2
+ import { treefy } from '@sakura-ui/helper'
2
3
  import Slugger from 'github-slugger'
3
4
  import { classNames } from './marked/html'
4
5
 
@@ -38,7 +39,21 @@ const CLASS_BY_TAG: Record<string, string> = {
38
39
 
39
40
  /** Elements a directive rendered are already dressed; leave them be. */
40
41
  const isDirectiveOutput = (element: Element) =>
41
- element.hasAttribute('data-sakura')
42
+ element.hasAttribute('data-styled')
43
+
44
+ /**
45
+ * A link that opens elsewhere must not hand the opener to the page it opens.
46
+ * Merged with what the document asked for rather than replacing it, so a
47
+ * `rel="nofollow"` does not take `noopener` away with it.
48
+ */
49
+ const externalRel = (rel: string | null): string =>
50
+ Array.from(
51
+ new Set([
52
+ 'noopener',
53
+ 'noreferrer',
54
+ ...(rel ?? '').split(/\s+/).filter(Boolean)
55
+ ])
56
+ ).join(' ')
42
57
 
43
58
  const addClass = (element: Element, value: string) => {
44
59
  const merged = classNames(element.getAttribute('class') ?? '', value)
@@ -53,10 +68,10 @@ const HEADINGS = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6'])
53
68
  * the Icon component does.
54
69
  */
55
70
  const appendNewTabIcon = (anchor: Element, document: Document) => {
56
- if (anchor.querySelector('[data-sakura-new-tab]')) return
71
+ if (anchor.querySelector('[data-new-tab]')) return
57
72
  const glyph = document.createElement('span')
58
73
  glyph.setAttribute('aria-hidden', 'true')
59
- glyph.setAttribute('data-sakura-new-tab', 'true')
74
+ glyph.setAttribute('data-new-tab', 'true')
60
75
  glyph.setAttribute(
61
76
  'class',
62
77
  classNames(styles.iconSizeStyle[16], styles.iconStyle, 'ml-0.5')
@@ -131,9 +146,7 @@ export const decorate = (
131
146
  element.getAttribute('target') === '_blank'
132
147
  ) {
133
148
  element.setAttribute('target', '_blank')
134
- // The content writes target="_blank" 41 times and rel not once, which
135
- // leaves the opened page holding a handle on this one.
136
- element.setAttribute('rel', 'noopener noreferrer')
149
+ element.setAttribute('rel', externalRel(element.getAttribute('rel')))
137
150
  appendNewTabIcon(element, document)
138
151
  }
139
152
  }
@@ -145,15 +158,18 @@ export const decorate = (
145
158
  // A wide table scrolls inside its own box rather than pushing the page.
146
159
  for (const table of Array.from(root.querySelectorAll('table'))) {
147
160
  const parent = table.parentElement
148
- if (parent?.dataset?.sakuraTableContainer === 'true') continue
161
+ if (parent?.dataset?.tableContainer === 'true') continue
149
162
  const container = document.createElement('div')
150
163
  container.setAttribute('class', classNames(styles.overflowContainerStyle))
151
- container.dataset.sakuraTableContainer = 'true'
164
+ container.dataset.tableContainer = 'true'
152
165
  table.replaceWith(container)
153
166
  container.appendChild(table)
154
167
  }
155
168
 
169
+ // Nested, so that the table of contents can show a heading under the one it
170
+ // belongs to. Cut to depth before nesting: filtering the tree afterwards
171
+ // would only reach the headings sitting at its root.
156
172
  return {
157
- headings: flat.filter((h) => h.depth <= tocMaxDepth) as HeadingItem[]
173
+ headings: treefy(flat.filter((h) => h.depth <= tocMaxDepth))
158
174
  }
159
175
  }
@@ -0,0 +1,29 @@
1
+ import { classNames, cleanUrl } from '../html'
2
+ import type { DirectiveRenderer } from './context'
3
+
4
+ /**
5
+ * `width` and `height` are pixels. `max-width` goes with a width so that the
6
+ * image shrinks inside its column on a narrow screen rather than overflowing
7
+ * it; with a height alone there is nothing to overflow, since the width
8
+ * follows the proportions.
9
+ */
10
+ const sizeStyle = (
11
+ width: string | undefined,
12
+ height: string | undefined
13
+ ): string | undefined => {
14
+ const rules: string[] = []
15
+ if (width) rules.push(`width:${width}px`)
16
+ if (height) rules.push(`height:${height}px`)
17
+ if (width) rules.push('max-width:100%')
18
+ return rules.length > 0 ? rules.join(';') : undefined
19
+ }
20
+
21
+ export const imageRenderers: Record<string, DirectiveRenderer> = {
22
+ img: ({ attrs, root, nl }) =>
23
+ `<img${root({
24
+ class: classNames(attrs.class),
25
+ src: cleanUrl(attrs.src),
26
+ alt: attrs.alt ?? '',
27
+ style: sizeStyle(attrs.width, attrs.height)
28
+ })}>${nl}`
29
+ }
@@ -2,6 +2,7 @@ import { GRID_RE } from '../registry'
2
2
  import { cardRenderers } from './card'
3
3
  import type { DirectiveRenderer } from './context'
4
4
  import { faqRenderers } from './faq'
5
+ import { imageRenderers } from './image'
5
6
  import { cellRenderers, grid } from './grid'
6
7
  import { linkButtonRenderers } from './linkButton'
7
8
  import { youtubeRenderers } from './youtube'
@@ -10,6 +11,7 @@ const BY_NAME: Record<string, DirectiveRenderer> = {
10
11
  ...cardRenderers,
11
12
  ...cellRenderers,
12
13
  ...faqRenderers,
14
+ ...imageRenderers,
13
15
  ...youtubeRenderers,
14
16
  ...linkButtonRenderers
15
17
  }
@@ -40,10 +40,23 @@ export const cleanUrl = (href: string | undefined): string | undefined => {
40
40
 
41
41
  export type AttrValue = string | boolean | undefined | null
42
42
 
43
+ /**
44
+ * Attributes where an empty value says something, and so is written out. On an
45
+ * image an empty alt marks it as decoration; leaving the attribute off instead
46
+ * makes a screen reader read the file name.
47
+ */
48
+ const KEEP_WHEN_EMPTY = new Set(['alt'])
49
+
43
50
  /** `{a: 'x', b: true, c: undefined}` becomes ` a="x" b` */
44
51
  export const attrsToHtml = (attrs: Record<string, AttrValue>): string =>
45
52
  Object.entries(attrs)
46
- .filter(([, v]) => v !== undefined && v !== null && v !== false && v !== '')
53
+ .filter(
54
+ ([k, v]) =>
55
+ v !== undefined &&
56
+ v !== null &&
57
+ v !== false &&
58
+ (v !== '' || KEEP_WHEN_EMPTY.has(k))
59
+ )
47
60
  .map(([k, v]) => (v === true ? ` ${k}` : ` ${k}="${escapeHtml(v)}"`))
48
61
  .join('')
49
62
 
@@ -20,6 +20,7 @@ const STATIC: Record<string, DirectiveKind[]> = {
20
20
  'faq-a': [LEAF, CONTAINER],
21
21
  cell: [CONTAINER],
22
22
  'cell-img': [LEAF],
23
+ img: [LEAF],
23
24
  youtube: [LEAF, CONTAINER],
24
25
  'link-button': [TEXT]
25
26
  }
@@ -24,10 +24,10 @@ export function directiveRenderer(
24
24
  const { name, kind, attrs, tokens } = token
25
25
  const nl = kind === TEXT ? '' : '\n'
26
26
 
27
- // Marks what the directive dressed itself, so that the pass which styles the
28
- // rest of the document leaves it alone.
27
+ // Marks the element as dressed, so that the pass which styles the rest of
28
+ // the document leaves it alone. The value names the directive it came from.
29
29
  const own: DirectiveContext['own'] = (values) =>
30
- attrsToHtml({ ...values, 'data-sakura': name })
30
+ attrsToHtml({ ...values, 'data-styled': name })
31
31
 
32
32
  const context: DirectiveContext = {
33
33
  token,
@@ -168,9 +168,16 @@ const purifier = (): DOMPurify => {
168
168
  element.removeAttribute('style')
169
169
  }
170
170
  }
171
- // A link that opens elsewhere must not hand the opener over with it.
171
+ // A link that opens elsewhere must not hand the opener over with it. What
172
+ // the document asked for is kept alongside rather than replaced.
172
173
  if (element.getAttribute('target') === '_blank') {
173
- element.setAttribute('rel', 'noopener noreferrer')
174
+ const asked = (element.getAttribute('rel') ?? '').split(/\s+/)
175
+ element.setAttribute(
176
+ 'rel',
177
+ Array.from(
178
+ new Set(['noopener', 'noreferrer', ...asked.filter(Boolean)])
179
+ ).join(' ')
180
+ )
174
181
  }
175
182
  })
176
183
  return instance
@@ -101,6 +101,23 @@ describe('Markdown', () => {
101
101
  expect(html).toMatchSnapshot()
102
102
  })
103
103
 
104
+ // The nesting is read from the DOM rather than left to the snapshot, so a
105
+ // return to one flat list says which shape was lost.
106
+ it('should put a heading under the one it belongs to', async () => {
107
+ render(<Markdown showToc>{'# 章1\n\n## 節1\n\n## 節2\n\n# 章2'}</Markdown>)
108
+ const toc = await screen.findByRole('navigation')
109
+
110
+ const chapters = toc.querySelectorAll(':scope > ul > li')
111
+ expect(
112
+ Array.from(chapters, (li) => li.querySelector('a')?.textContent)
113
+ ).toEqual(['章1', '章2'])
114
+
115
+ const sections = chapters[0].querySelectorAll(':scope > ul > li')
116
+ expect(
117
+ Array.from(sections, (li) => li.querySelector('a')?.textContent)
118
+ ).toEqual(['節1', '節2'])
119
+ })
120
+
104
121
  it('should use the given title', async () => {
105
122
  render(
106
123
  <Markdown showToc tocTitle="もくじ">
@@ -1,78 +1,78 @@
1
1
  // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
3
  exports[`Markdown > directives > should render card 1`] = `
4
- "<div class="py-8 flex flex-col gap-8"><div class="flex flex-col gap-8"><div class="border border-solid border-solid-gray-500 rounded-2xl sm:rounded-3xl text-solid-gray-900 overflow-hidden" data-sakura="card"><img class="object-cover mb-2 w-full aspect-[352/226]" src="/a.png" alt="図" data-sakura="card-img">
5
- <h3 class="m-0 text-base leading-[2rem] font-medium first:pt-4 pt-2 last:pb-4 px-6" data-sakura="card-title" id="タイトル">タイトル</h3>
6
- <div class="text-base-sm leading-[1.85rem] first:pt-4 pt-2 last:pb-4 px-6" data-sakura="card-description">説明</div>
4
+ "<div class="py-8 flex flex-col gap-8"><div class="flex flex-col gap-8"><div class="border border-solid border-solid-gray-500 rounded-2xl sm:rounded-3xl text-solid-gray-900 overflow-hidden" data-styled="card"><img class="object-cover mb-2 w-full aspect-[352/226]" src="/a.png" alt="図" data-styled="card-img">
5
+ <h3 class="m-0 text-base leading-[2rem] font-medium first:pt-4 pt-2 last:pb-4 px-6" data-styled="card-title" id="タイトル">タイトル</h3>
6
+ <div class="text-base-sm leading-[1.85rem] first:pt-4 pt-2 last:pb-4 px-6" data-styled="card-description">説明</div>
7
7
  </div>
8
8
  </div></div>"
9
9
  `;
10
10
 
11
11
  exports[`Markdown > directives > should render cell 1`] = `
12
- "<div class="py-8 flex flex-col gap-8"><div class="flex flex-col gap-8"><div data-sakura="cell"><img class="mb-4" src="/a.png" alt="図" data-sakura="cell-img">
12
+ "<div class="py-8 flex flex-col gap-8"><div class="flex flex-col gap-8"><div data-styled="cell"><img class="mb-4" src="/a.png" alt="図" data-styled="cell-img">
13
13
  <p>本文</p>
14
14
  </div>
15
15
  </div></div>"
16
16
  `;
17
17
 
18
18
  exports[`Markdown > directives > should render faq 1`] = `
19
- "<div class="py-8 flex flex-col gap-8"><div class="flex flex-col gap-8"><dl class="flex flex-col gap-8" data-sakura="faq"><dt class="flex flex-row gap-8 mt-8 text-h-med-m sm:text-h-med" data-sakura="faq-q"><span aria-hidden="true">Q</span><span>質問は?</span></dt>
20
- <dd class="flex flex-row items-start gap-8" data-sakura="faq-a"><span class="text-h-med-m sm:text-h-med !leading-none" aria-hidden="true">A</span><span>回答です。</span></dd>
21
- <dt class="flex flex-row gap-8 mt-8 text-h-med-m sm:text-h-med" data-sakura="faq-q"><span aria-hidden="true">Q</span><span>二つ目の質問は?</span></dt>
22
- <dd class="flex flex-row items-start gap-8" data-sakura="faq-a"><span class="text-h-med-m sm:text-h-med !leading-none" aria-hidden="true">A</span><span>二つ目の回答。</span></dd>
19
+ "<div class="py-8 flex flex-col gap-8"><div class="flex flex-col gap-8"><dl class="flex flex-col gap-8" data-styled="faq"><dt class="flex flex-row gap-8 mt-8 text-h-med-m sm:text-h-med" data-styled="faq-q"><span aria-hidden="true">Q</span><span>質問は?</span></dt>
20
+ <dd class="flex flex-row items-start gap-8" data-styled="faq-a"><span class="text-h-med-m sm:text-h-med !leading-none" aria-hidden="true">A</span><span>回答です。</span></dd>
21
+ <dt class="flex flex-row gap-8 mt-8 text-h-med-m sm:text-h-med" data-styled="faq-q"><span aria-hidden="true">Q</span><span>二つ目の質問は?</span></dt>
22
+ <dd class="flex flex-row items-start gap-8" data-styled="faq-a"><span class="text-h-med-m sm:text-h-med !leading-none" aria-hidden="true">A</span><span>二つ目の回答。</span></dd>
23
23
  </dl>
24
24
  </div></div>"
25
25
  `;
26
26
 
27
27
  exports[`Markdown > directives > should render grid 1`] = `
28
- "<div class="py-8 flex flex-col gap-8"><div class="flex flex-col gap-8"><ul class="flex flex-col md:grid md:grid-cols-3 gap-8" data-sakura="grid-cols-3"><li class="sm:grid"><div class="border border-solid border-solid-gray-500 rounded-2xl sm:rounded-3xl text-solid-gray-900 overflow-hidden" data-sakura="card"><h3 class="m-0 text-base leading-[2rem] font-medium first:pt-4 pt-2 last:pb-4 px-6" data-sakura="card-title" id="1つ目">1つ目</h3>
28
+ "<div class="py-8 flex flex-col gap-8"><div class="flex flex-col gap-8"><ul class="flex flex-col md:grid md:grid-cols-3 gap-8" data-styled="grid-cols-3"><li class="sm:grid"><div class="border border-solid border-solid-gray-500 rounded-2xl sm:rounded-3xl text-solid-gray-900 overflow-hidden" data-styled="card"><h3 class="m-0 text-base leading-[2rem] font-medium first:pt-4 pt-2 last:pb-4 px-6" data-styled="card-title" id="1つ目">1つ目</h3>
29
29
  </div></li>
30
- <li class="sm:grid"><div class="border border-solid border-solid-gray-500 rounded-2xl sm:rounded-3xl text-solid-gray-900 overflow-hidden" data-sakura="card"><h3 class="m-0 text-base leading-[2rem] font-medium first:pt-4 pt-2 last:pb-4 px-6" data-sakura="card-title" id="2つ目">2つ目</h3>
30
+ <li class="sm:grid"><div class="border border-solid border-solid-gray-500 rounded-2xl sm:rounded-3xl text-solid-gray-900 overflow-hidden" data-styled="card"><h3 class="m-0 text-base leading-[2rem] font-medium first:pt-4 pt-2 last:pb-4 px-6" data-styled="card-title" id="2つ目">2つ目</h3>
31
31
  </div></li>
32
32
  </ul>
33
33
  </div></div>"
34
34
  `;
35
35
 
36
36
  exports[`Markdown > directives > should render gridMixed 1`] = `
37
- "<div class="py-8 flex flex-col gap-8"><div class="flex flex-col gap-8"><div class="flex flex-col md:grid md:grid-cols-2 gap-8" data-sakura="grid-cols-2"><div class="border border-solid border-solid-gray-500 rounded-2xl sm:rounded-3xl text-solid-gray-900 overflow-hidden" data-sakura="card"><h3 class="m-0 text-base leading-[2rem] font-medium first:pt-4 pt-2 last:pb-4 px-6" data-sakura="card-title" id="1つ目">1つ目</h3>
37
+ "<div class="py-8 flex flex-col gap-8"><div class="flex flex-col gap-8"><div class="flex flex-col md:grid md:grid-cols-2 gap-8" data-styled="grid-cols-2"><div class="border border-solid border-solid-gray-500 rounded-2xl sm:rounded-3xl text-solid-gray-900 overflow-hidden" data-styled="card"><h3 class="m-0 text-base leading-[2rem] font-medium first:pt-4 pt-2 last:pb-4 px-6" data-styled="card-title" id="1つ目">1つ目</h3>
38
38
  </div>
39
- <div data-sakura="cell"><p>本文</p>
39
+ <div data-styled="cell"><p>本文</p>
40
40
  </div>
41
41
  </div>
42
42
  </div></div>"
43
43
  `;
44
44
 
45
45
  exports[`Markdown > directives > should render gridOfCells 1`] = `
46
- "<div class="py-8 flex flex-col gap-8"><div class="flex flex-col gap-8"><div class="flex flex-col md:grid md:grid-cols-2 gap-8" data-sakura="grid-cols-2"><div data-sakura="cell"><p><img src="/a.png" alt="alt"><br>本文1</p>
46
+ "<div class="py-8 flex flex-col gap-8"><div class="flex flex-col gap-8"><div class="flex flex-col md:grid md:grid-cols-2 gap-8" data-styled="grid-cols-2"><div data-styled="cell"><p><img src="/a.png" alt="alt"><br>本文1</p>
47
47
  </div>
48
- <div data-sakura="cell"><p>本文2</p>
48
+ <div data-styled="cell"><p>本文2</p>
49
49
  </div>
50
50
  </div>
51
51
  </div></div>"
52
52
  `;
53
53
 
54
54
  exports[`Markdown > directives > should render linkButton 1`] = `
55
- "<div class="py-8 flex flex-col gap-8"><div class="flex flex-col gap-8"><p>ご案内です。<a class="inline-block text-button text-center cursor-pointer select-none touch-manipulation whitespace-nowrap outline-offset-2 border border-solid border-blue-900 hover:border-blue-1000 active:border-blue-1200 disabled:border-solid-gray-500 disabled:cursor-not-allowed text-blue-900 hover:text-blue-1000 active:text-blue-1200 bg-transparent hover:bg-blue-200 active:bg-blue-300 disabled:bg-transparent disabled:text-solid-gray-500 hover:underline hover:underline-offset-[calc(3/16*1rem)] focus-visible:outline-4 focus-visible:outline-black focus-visible:ring-yellow-300 focus-visible:outline-offset-[calc(2/16*1rem)] focus-visible:ring-[calc(2/16*1rem)] focus-visible:rounded-sm p-4 text-button leading-snug rounded-lg" href="/services" data-sakura="link-button">サービス一覧</a> をどうぞ。</p>
55
+ "<div class="py-8 flex flex-col gap-8"><div class="flex flex-col gap-8"><p>ご案内です。<a class="inline-block text-button text-center cursor-pointer select-none touch-manipulation whitespace-nowrap outline-offset-2 border border-solid border-blue-900 hover:border-blue-1000 active:border-blue-1200 disabled:border-solid-gray-500 disabled:cursor-not-allowed text-blue-900 hover:text-blue-1000 active:text-blue-1200 bg-transparent hover:bg-blue-200 active:bg-blue-300 disabled:bg-transparent disabled:text-solid-gray-500 hover:underline hover:underline-offset-[calc(3/16*1rem)] focus-visible:outline-4 focus-visible:outline-black focus-visible:ring-yellow-300 focus-visible:outline-offset-[calc(2/16*1rem)] focus-visible:ring-[calc(2/16*1rem)] focus-visible:rounded-sm p-4 text-button leading-snug rounded-lg" href="/services" data-styled="link-button">サービス一覧</a> をどうぞ。</p>
56
56
  </div></div>"
57
57
  `;
58
58
 
59
59
  exports[`Markdown > directives > should render linkCard 1`] = `
60
- "<div class="py-8 flex flex-col gap-8"><div class="flex flex-col gap-8"><div class="border border-solid border-solid-gray-500 rounded-2xl sm:rounded-3xl text-solid-gray-900 overflow-hidden relative z-0 group hover:outline hover:outline-2 hover:outline-black hover:bg-solid-gray-50 has-[a:focus-visible]:outline has-[a:focus-visible]:outline-4 has-[a:focus-visible]:outline-black has-[a:focus-visible]:outline-offset-[calc(2/16*1rem)] has-[a:focus-visible]:ring-[calc(2/16*1rem)] has-[a:focus-visible]:ring-yellow-300" data-sakura="card"><h3 class="m-0 text-base leading-[2rem] font-medium first:pt-4 pt-2 last:pb-4 px-6 decoration-blue-900 underline underline-offset-[calc(3/16*1rem)] group-hover:text-blue-900 group-hover:decoration-[calc(3/16*1rem)]" data-sakura="card-title" id="タイトル"><a class="after:content-[''] after:absolute after:inset-0 after:z-10 focus-visible:outline-none" href="/x" data-sakura="card-title">タイトル</a></h3>
61
- <div class="text-base-sm leading-[1.85rem] first:pt-4 pt-2 last:pb-4 px-6" data-sakura="card-description">説明</div>
62
- <div class="sticky top-full text-base-sm leading-[1.85rem] first:pt-4 pt-2 last:pb-4 px-6 flex justify-between items-center" data-sakura="card-footer"><span>2026年8月</span><span class="inline-flex items-center justify-center w-6 h-6 text-blue-1000 border border-blue-1000 rounded-full group-hover:bg-blue-1000 group-hover:text-white"><span aria-hidden="true" class="!text-[16px] font-icon font-light inline-block leading-none align-middle whitespace-nowrap">arrow_forward</span><span class="sr-only"></span></span></div>
60
+ "<div class="py-8 flex flex-col gap-8"><div class="flex flex-col gap-8"><div class="border border-solid border-solid-gray-500 rounded-2xl sm:rounded-3xl text-solid-gray-900 overflow-hidden relative z-0 group hover:outline hover:outline-2 hover:outline-black hover:bg-solid-gray-50 has-[a:focus-visible]:outline has-[a:focus-visible]:outline-4 has-[a:focus-visible]:outline-black has-[a:focus-visible]:outline-offset-[calc(2/16*1rem)] has-[a:focus-visible]:ring-[calc(2/16*1rem)] has-[a:focus-visible]:ring-yellow-300" data-styled="card"><h3 class="m-0 text-base leading-[2rem] font-medium first:pt-4 pt-2 last:pb-4 px-6 decoration-blue-900 underline underline-offset-[calc(3/16*1rem)] group-hover:text-blue-900 group-hover:decoration-[calc(3/16*1rem)]" data-styled="card-title" id="タイトル"><a class="after:content-[''] after:absolute after:inset-0 after:z-10 focus-visible:outline-none" href="/x" data-styled="card-title">タイトル</a></h3>
61
+ <div class="text-base-sm leading-[1.85rem] first:pt-4 pt-2 last:pb-4 px-6" data-styled="card-description">説明</div>
62
+ <div class="sticky top-full text-base-sm leading-[1.85rem] first:pt-4 pt-2 last:pb-4 px-6 flex justify-between items-center" data-styled="card-footer"><span>2026年8月</span><span class="inline-flex items-center justify-center w-6 h-6 text-blue-1000 border border-blue-1000 rounded-full group-hover:bg-blue-1000 group-hover:text-white"><span aria-hidden="true" class="!text-[16px] font-icon font-light inline-block leading-none align-middle whitespace-nowrap">arrow_forward</span><span class="sr-only"></span></span></div>
63
63
  </div>
64
64
  </div></div>"
65
65
  `;
66
66
 
67
67
  exports[`Markdown > directives > should render linkCardWithoutTitle 1`] = `
68
- "<div class="py-8 flex flex-col gap-8"><div class="flex flex-col gap-8"><div class="border border-solid border-solid-gray-500 rounded-2xl sm:rounded-3xl text-solid-gray-900 overflow-hidden" data-sakura="card"><div class="text-base-sm leading-[1.85rem] first:pt-4 pt-2 last:pb-4 px-6" data-sakura="card-description">説明</div>
69
- <div class="sticky top-full text-base-sm leading-[1.85rem] first:pt-4 pt-2 last:pb-4 px-6" data-sakura="card-footer">2026年8月</div>
68
+ "<div class="py-8 flex flex-col gap-8"><div class="flex flex-col gap-8"><div class="border border-solid border-solid-gray-500 rounded-2xl sm:rounded-3xl text-solid-gray-900 overflow-hidden" data-styled="card"><div class="text-base-sm leading-[1.85rem] first:pt-4 pt-2 last:pb-4 px-6" data-styled="card-description">説明</div>
69
+ <div class="sticky top-full text-base-sm leading-[1.85rem] first:pt-4 pt-2 last:pb-4 px-6" data-styled="card-footer">2026年8月</div>
70
70
  </div>
71
71
  </div></div>"
72
72
  `;
73
73
 
74
74
  exports[`Markdown > directives > should render youtube 1`] = `
75
- "<div class="py-8 flex flex-col gap-8"><div class="flex flex-col gap-8"><iframe title="動画タイトル" src="https://www.youtube-nocookie.com/embed/yXdbvBzxeb8" class="aspect-video w-full max-w-[470px]" style="width:560px;" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy" data-sakura="youtube"></iframe>
75
+ "<div class="py-8 flex flex-col gap-8"><div class="flex flex-col gap-8"><iframe title="動画タイトル" src="https://www.youtube-nocookie.com/embed/yXdbvBzxeb8" class="aspect-video w-full max-w-[470px]" style="width:560px;" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy" data-styled="youtube"></iframe>
76
76
  </div></div>"
77
77
  `;
78
78
 
@@ -126,9 +126,9 @@ exports[`Markdown > malformed html > should recover from crossedNesting 1`] = `
126
126
  </ul></div></div>"
127
127
  `;
128
128
 
129
- exports[`Markdown > malformed html > should recover from mistypedClose 1`] = `"<div class="py-8 flex flex-col gap-8"><div class="flex flex-col gap-8"><div class="w-full overflow-x-auto whitespace-nowrap print:overflow-visible print:whitespace-normal" data-sakura-table-container="true"><table class="border border-collapse border-solid-gray-420"><tbody><tr class="border border-collapse border-solid-gray-420"><td class="p-2 text-label border border-collapse border-solid-gray-420">値</td></tr></tbody></table></div></div></div>"`;
129
+ exports[`Markdown > malformed html > should recover from mistypedClose 1`] = `"<div class="py-8 flex flex-col gap-8"><div class="flex flex-col gap-8"><div class="w-full overflow-x-auto whitespace-nowrap print:overflow-visible print:whitespace-normal" data-table-container="true"><table class="border border-collapse border-solid-gray-420"><tbody><tr class="border border-collapse border-solid-gray-420"><td class="p-2 text-label border border-collapse border-solid-gray-420">値</td></tr></tbody></table></div></div></div>"`;
130
130
 
131
- exports[`Markdown > malformed html > should recover from unclosedCell 1`] = `"<div class="py-8 flex flex-col gap-8"><div class="flex flex-col gap-8"><div class="w-full overflow-x-auto whitespace-nowrap print:overflow-visible print:whitespace-normal" data-sakura-table-container="true"><table class="border border-collapse border-solid-gray-420"><tbody><tr class="border border-collapse border-solid-gray-420"><td class="p-2 text-label border border-collapse border-solid-gray-420">閉じ忘れ</td></tr><tr class="border border-collapse border-solid-gray-420"><td class="p-2 text-label border border-collapse border-solid-gray-420">次の行</td></tr></tbody></table></div></div></div>"`;
131
+ exports[`Markdown > malformed html > should recover from unclosedCell 1`] = `"<div class="py-8 flex flex-col gap-8"><div class="flex flex-col gap-8"><div class="w-full overflow-x-auto whitespace-nowrap print:overflow-visible print:whitespace-normal" data-table-container="true"><table class="border border-collapse border-solid-gray-420"><tbody><tr class="border border-collapse border-solid-gray-420"><td class="p-2 text-label border border-collapse border-solid-gray-420">閉じ忘れ</td></tr><tr class="border border-collapse border-solid-gray-420"><td class="p-2 text-label border border-collapse border-solid-gray-420">次の行</td></tr></tbody></table></div></div></div>"`;
132
132
 
133
133
  exports[`Markdown > plain markdown > should render blockquote 1`] = `
134
134
  "<div class="py-8 flex flex-col gap-8"><div class="flex flex-col gap-8"><blockquote>
@@ -156,7 +156,7 @@ exports[`Markdown > plain markdown > should render footnote 1`] = `
156
156
  `;
157
157
 
158
158
  exports[`Markdown > plain markdown > should render gfmTable 1`] = `
159
- "<div class="py-8 flex flex-col gap-8"><div class="flex flex-col gap-8"><div class="w-full overflow-x-auto whitespace-nowrap print:overflow-visible print:whitespace-normal" data-sakura-table-container="true"><table class="border border-collapse border-solid-gray-420">
159
+ "<div class="py-8 flex flex-col gap-8"><div class="flex flex-col gap-8"><div class="w-full overflow-x-auto whitespace-nowrap print:overflow-visible print:whitespace-normal" data-table-container="true"><table class="border border-collapse border-solid-gray-420">
160
160
  <thead>
161
161
  <tr class="border border-collapse border-solid-gray-420">
162
162
  <th class="p-2 bg-[#f8f8fb] text-label text-left border border-collapse border-solid-gray-420">見出し</th>
@@ -189,7 +189,7 @@ exports[`Markdown > plain markdown > should render image 1`] = `
189
189
  `;
190
190
 
191
191
  exports[`Markdown > plain markdown > should render links 1`] = `
192
- "<div class="py-8 flex flex-col gap-8"><div class="flex flex-col gap-8"><p><a href="/foo" class="cursor-pointer text-blue-1000 active:text-orange-700 visited:text-magenta-900 focus-visible:outline-4 focus-visible:outline-black focus-visible:ring-yellow-300 focus-visible:outline-offset-[calc(2/16*1rem)] focus-visible:ring-[calc(2/16*1rem)] focus-visible:bg-yellow-300 focus-visible:rounded-sm underline underline-offset-[calc(3/16*1rem)] hover:decoration-[calc(3/16*1rem)] disabled:border-solid-gray-500 [overflow-wrap:anywhere]">内部</a> と <a href="https://example.com" target="_blank" rel="noopener noreferrer" class="cursor-pointer text-blue-1000 active:text-orange-700 visited:text-magenta-900 focus-visible:outline-4 focus-visible:outline-black focus-visible:ring-yellow-300 focus-visible:outline-offset-[calc(2/16*1rem)] focus-visible:ring-[calc(2/16*1rem)] focus-visible:bg-yellow-300 focus-visible:rounded-sm underline underline-offset-[calc(3/16*1rem)] hover:decoration-[calc(3/16*1rem)] disabled:border-solid-gray-500 [overflow-wrap:anywhere]">外部<span aria-hidden="true" data-sakura-new-tab="true" class="!text-[16px] font-icon font-light inline-block leading-none align-middle whitespace-nowrap ml-0.5">open_in_new</span><span class="sr-only">Opens in new tab</span></a> と <a href="https://example.com/bare" target="_blank" rel="noopener noreferrer" class="cursor-pointer text-blue-1000 active:text-orange-700 visited:text-magenta-900 focus-visible:outline-4 focus-visible:outline-black focus-visible:ring-yellow-300 focus-visible:outline-offset-[calc(2/16*1rem)] focus-visible:ring-[calc(2/16*1rem)] focus-visible:bg-yellow-300 focus-visible:rounded-sm underline underline-offset-[calc(3/16*1rem)] hover:decoration-[calc(3/16*1rem)] disabled:border-solid-gray-500 [overflow-wrap:anywhere]">https://example.com/bare<span aria-hidden="true" data-sakura-new-tab="true" class="!text-[16px] font-icon font-light inline-block leading-none align-middle whitespace-nowrap ml-0.5">open_in_new</span><span class="sr-only">Opens in new tab</span></a></p>
192
+ "<div class="py-8 flex flex-col gap-8"><div class="flex flex-col gap-8"><p><a href="/foo" class="cursor-pointer text-blue-1000 active:text-orange-700 visited:text-magenta-900 focus-visible:outline-4 focus-visible:outline-black focus-visible:ring-yellow-300 focus-visible:outline-offset-[calc(2/16*1rem)] focus-visible:ring-[calc(2/16*1rem)] focus-visible:bg-yellow-300 focus-visible:rounded-sm underline underline-offset-[calc(3/16*1rem)] hover:decoration-[calc(3/16*1rem)] disabled:border-solid-gray-500 [overflow-wrap:anywhere]">内部</a> と <a href="https://example.com" target="_blank" rel="noopener noreferrer" class="cursor-pointer text-blue-1000 active:text-orange-700 visited:text-magenta-900 focus-visible:outline-4 focus-visible:outline-black focus-visible:ring-yellow-300 focus-visible:outline-offset-[calc(2/16*1rem)] focus-visible:ring-[calc(2/16*1rem)] focus-visible:bg-yellow-300 focus-visible:rounded-sm underline underline-offset-[calc(3/16*1rem)] hover:decoration-[calc(3/16*1rem)] disabled:border-solid-gray-500 [overflow-wrap:anywhere]">外部<span aria-hidden="true" data-new-tab="true" class="!text-[16px] font-icon font-light inline-block leading-none align-middle whitespace-nowrap ml-0.5">open_in_new</span><span class="sr-only">Opens in new tab</span></a> と <a href="https://example.com/bare" target="_blank" rel="noopener noreferrer" class="cursor-pointer text-blue-1000 active:text-orange-700 visited:text-magenta-900 focus-visible:outline-4 focus-visible:outline-black focus-visible:ring-yellow-300 focus-visible:outline-offset-[calc(2/16*1rem)] focus-visible:ring-[calc(2/16*1rem)] focus-visible:bg-yellow-300 focus-visible:rounded-sm underline underline-offset-[calc(3/16*1rem)] hover:decoration-[calc(3/16*1rem)] disabled:border-solid-gray-500 [overflow-wrap:anywhere]">https://example.com/bare<span aria-hidden="true" data-new-tab="true" class="!text-[16px] font-icon font-light inline-block leading-none align-middle whitespace-nowrap ml-0.5">open_in_new</span><span class="sr-only">Opens in new tab</span></a></p>
193
193
  </div></div>"
194
194
  `;
195
195
 
@@ -226,7 +226,7 @@ exports[`Markdown > raw html > should render details 1`] = `
226
226
  <summary>詳細はこちら</summary><ul class="list-disc [&amp;_&amp;]:list-circle [&amp;_&amp;_&amp;]:list-square pl-8">
227
227
  <li>リスト項目</li>
228
228
  </ul>
229
- <div class="w-full overflow-x-auto whitespace-nowrap print:overflow-visible print:whitespace-normal" data-sakura-table-container="true"><table class="border border-collapse border-solid-gray-420">
229
+ <div class="w-full overflow-x-auto whitespace-nowrap print:overflow-visible print:whitespace-normal" data-table-container="true"><table class="border border-collapse border-solid-gray-420">
230
230
  <thead>
231
231
  <tr class="border border-collapse border-solid-gray-420">
232
232
  <th class="p-2 bg-[#f8f8fb] text-label text-left border border-collapse border-solid-gray-420">表</th>
@@ -242,7 +242,7 @@ exports[`Markdown > raw html > should render details 1`] = `
242
242
  `;
243
243
 
244
244
  exports[`Markdown > raw html > should render externalAnchor 1`] = `
245
- "<div class="py-8 flex flex-col gap-8"><div class="flex flex-col gap-8"><p><a href="https://example.com" target="_blank" rel="noopener noreferrer" class="cursor-pointer text-blue-1000 active:text-orange-700 visited:text-magenta-900 focus-visible:outline-4 focus-visible:outline-black focus-visible:ring-yellow-300 focus-visible:outline-offset-[calc(2/16*1rem)] focus-visible:ring-[calc(2/16*1rem)] focus-visible:bg-yellow-300 focus-visible:rounded-sm underline underline-offset-[calc(3/16*1rem)] hover:decoration-[calc(3/16*1rem)] disabled:border-solid-gray-500 [overflow-wrap:anywhere]">外部リンク<span aria-hidden="true" data-sakura-new-tab="true" class="!text-[16px] font-icon font-light inline-block leading-none align-middle whitespace-nowrap ml-0.5">open_in_new</span><span class="sr-only">Opens in new tab</span></a></p>
245
+ "<div class="py-8 flex flex-col gap-8"><div class="flex flex-col gap-8"><p><a href="https://example.com" target="_blank" rel="noopener noreferrer" class="cursor-pointer text-blue-1000 active:text-orange-700 visited:text-magenta-900 focus-visible:outline-4 focus-visible:outline-black focus-visible:ring-yellow-300 focus-visible:outline-offset-[calc(2/16*1rem)] focus-visible:ring-[calc(2/16*1rem)] focus-visible:bg-yellow-300 focus-visible:rounded-sm underline underline-offset-[calc(3/16*1rem)] hover:decoration-[calc(3/16*1rem)] disabled:border-solid-gray-500 [overflow-wrap:anywhere]">外部リンク<span aria-hidden="true" data-new-tab="true" class="!text-[16px] font-icon font-light inline-block leading-none align-middle whitespace-nowrap ml-0.5">open_in_new</span><span class="sr-only">Opens in new tab</span></a></p>
246
246
  </div></div>"
247
247
  `;
248
248
 
@@ -260,7 +260,7 @@ exports[`Markdown > raw html > should render styledBlock 1`] = `
260
260
  exports[`Markdown > raw html > should render styledImg 1`] = `"<div class="py-8 flex flex-col gap-8"><div class="flex flex-col gap-8"><img src="/images/a.png" alt="図" style="width: 800px; color-scheme: light;"></div></div>"`;
261
261
 
262
262
  exports[`Markdown > raw html > should render tableSpan 1`] = `
263
- "<div class="py-8 flex flex-col gap-8"><div class="flex flex-col gap-8"><div class="w-full overflow-x-auto whitespace-nowrap print:overflow-visible print:whitespace-normal" data-sakura-table-container="true"><table class="border border-collapse border-solid-gray-420">
263
+ "<div class="py-8 flex flex-col gap-8"><div class="flex flex-col gap-8"><div class="w-full overflow-x-auto whitespace-nowrap print:overflow-visible print:whitespace-normal" data-table-container="true"><table class="border border-collapse border-solid-gray-420">
264
264
  <caption class="text-left">表:01 キャプション</caption>
265
265
  <colgroup><col style="width: 15%;"><col style="width: auto;"></colgroup>
266
266
  <thead><tr class="border border-collapse border-solid-gray-420"><th colspan="2" class="p-2 bg-[#f8f8fb] text-label text-left border border-collapse border-solid-gray-420">見出し</th></tr></thead>
@@ -301,7 +301,12 @@ exports[`Markdown > table of contents > should render the table of contents when
301
301
 
302
302
  disabled:border-solid-gray-500
303
303
  [overflow-wrap:anywhere]
304
- " href="#章1">章1</a></li><li><a class="
304
+ " href="#章1">章1</a><ul class="
305
+ list-disc
306
+ [&amp;_&amp;]:list-circle
307
+ [&amp;_&amp;_&amp;]:list-square
308
+ pl-8
309
+ "><li><a class="
305
310
  cursor-pointer
306
311
  text-blue-1000
307
312
  active:text-orange-700
@@ -355,7 +360,7 @@ exports[`Markdown > table of contents > should render the table of contents when
355
360
 
356
361
  disabled:border-solid-gray-500
357
362
  [overflow-wrap:anywhere]
358
- " href="#節2">節2</a></li><li><a class="
363
+ " href="#節2">節2</a></li></ul></li><li><a class="
359
364
  cursor-pointer
360
365
  text-blue-1000
361
366
  active:text-orange-700
@@ -411,7 +416,7 @@ exports[`Markdown > text that only looks like a directive > should leave functio
411
416
  `;
412
417
 
413
418
  exports[`Markdown > text that only looks like a directive > should leave linkLabel alone 1`] = `
414
- "<div class="py-8 flex flex-col gap-8"><div class="flex flex-col gap-8"><p><a href="https://example.com" target="_blank" rel="noopener noreferrer" class="cursor-pointer text-blue-1000 active:text-orange-700 visited:text-magenta-900 focus-visible:outline-4 focus-visible:outline-black focus-visible:ring-yellow-300 focus-visible:outline-offset-[calc(2/16*1rem)] focus-visible:ring-[calc(2/16*1rem)] focus-visible:bg-yellow-300 focus-visible:rounded-sm underline underline-offset-[calc(3/16*1rem)] hover:decoration-[calc(3/16*1rem)] disabled:border-solid-gray-500 [overflow-wrap:anywhere]">AWSドキュメント:AWS PrivateLink<span aria-hidden="true" data-sakura-new-tab="true" class="!text-[16px] font-icon font-light inline-block leading-none align-middle whitespace-nowrap ml-0.5">open_in_new</span><span class="sr-only">Opens in new tab</span></a></p>
419
+ "<div class="py-8 flex flex-col gap-8"><div class="flex flex-col gap-8"><p><a href="https://example.com" target="_blank" rel="noopener noreferrer" class="cursor-pointer text-blue-1000 active:text-orange-700 visited:text-magenta-900 focus-visible:outline-4 focus-visible:outline-black focus-visible:ring-yellow-300 focus-visible:outline-offset-[calc(2/16*1rem)] focus-visible:ring-[calc(2/16*1rem)] focus-visible:bg-yellow-300 focus-visible:rounded-sm underline underline-offset-[calc(3/16*1rem)] hover:decoration-[calc(3/16*1rem)] disabled:border-solid-gray-500 [overflow-wrap:anywhere]">AWSドキュメント:AWS PrivateLink<span aria-hidden="true" data-new-tab="true" class="!text-[16px] font-icon font-light inline-block leading-none align-middle whitespace-nowrap ml-0.5">open_in_new</span><span class="sr-only">Opens in new tab</span></a></p>
415
420
  </div></div>"
416
421
  `;
417
422
 
@@ -0,0 +1,64 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { render } from '../src/render'
3
+
4
+ const html = (markdown: string) => render(markdown).html
5
+ const style = (markdown: string) => /style="([^"]*)"/.exec(html(markdown))?.[1]
6
+
7
+ /**
8
+ * `::img` is the Markdown image plus a size. A document that needs one reaches
9
+ * for raw HTML and an inline style otherwise.
10
+ */
11
+ describe('img', () => {
12
+ it('should render the source and the alternative text', () => {
13
+ const out = html('::img{src=/a.png alt="A field at dawn"}')
14
+ expect(out).toContain('src="/a.png"')
15
+ expect(out).toContain('alt="A field at dawn"')
16
+ })
17
+
18
+ it('should take no size', () => {
19
+ expect(style('::img{src=/a.png alt=図}')).toBeUndefined()
20
+ })
21
+
22
+ it('should draw at the width asked for', () => {
23
+ expect(style('::img{src=/a.png alt=図 width=520}')).toBe(
24
+ 'width:520px; max-width:100%;'
25
+ )
26
+ })
27
+
28
+ it('should keep the image inside a narrow column', () => {
29
+ // Without max-width a fixed width overflows on a phone.
30
+ expect(style('::img{src=/a.png alt=図 width=1200}')).toContain(
31
+ 'max-width:100%'
32
+ )
33
+ })
34
+
35
+ it('should draw at the height asked for', () => {
36
+ expect(style('::img{src=/a.png alt=図 height=300}')).toBe('height:300px;')
37
+ })
38
+
39
+ it('should take both', () => {
40
+ expect(style('::img{src=/a.png alt=図 width=520 height=300}')).toBe(
41
+ 'width:520px; height:300px; max-width:100%;'
42
+ )
43
+ })
44
+
45
+ it('should survive the sanitiser', () => {
46
+ // width and max-width are on the CSS allowlist; a property that is not
47
+ // would be dropped here rather than in the renderer.
48
+ expect(html('::img{src=/a.png alt=図 width=520}')).toContain('520px')
49
+ })
50
+
51
+ it('should refuse a javascript URL', () => {
52
+ expect(html('::img{src=javascript:alert(1) alt=図}')).not.toContain(
53
+ 'javascript:'
54
+ )
55
+ })
56
+
57
+ it('should answer to {#name}', () => {
58
+ expect(html('::img{#figure-1 src=/a.png alt=図}')).toContain('id="figure-1"')
59
+ })
60
+
61
+ it('should be empty alt when none is given', () => {
62
+ expect(html('::img{src=/a.png}')).toContain('alt=""')
63
+ })
64
+ })
@@ -135,6 +135,23 @@ describe('sanitize', () => {
135
135
  expect(out).toContain('rel="noopener noreferrer"')
136
136
  })
137
137
 
138
+ it('should keep a rel the document asked for as well', () => {
139
+ const out = html(
140
+ '<a href="https://example.com" target="_blank" rel="nofollow">外部</a>'
141
+ )
142
+ const rel = /rel="([^"]*)"/.exec(out)?.[1].split(' ')
143
+ expect(rel).toContain('noopener')
144
+ expect(rel).toContain('noreferrer')
145
+ expect(rel).toContain('nofollow')
146
+ })
147
+
148
+ it('should not repeat a rel the document already asked for', () => {
149
+ const out = html(
150
+ '<a href="https://example.com" target="_blank" rel="noopener">外部</a>'
151
+ )
152
+ expect(out).toContain('rel="noopener noreferrer"')
153
+ })
154
+
138
155
  it('should say that the link opens elsewhere', () => {
139
156
  const out = html('[外部](https://example.com)')
140
157
  expect(out).toContain('Opens in new tab')
@@ -9,4 +9,4 @@ rendering chunks...
9
9
  computing gzip size...
10
10
  dist/index.es.js 160.05 kB │ gzip: 10.15 kB
11
11
  dist/index.cjs.js 88.17 kB │ gzip: 8.80 kB
12
- ✓ built in 621ms
12
+ ✓ built in 631ms