List 形式で書いて、表形式で表示したい (単語リスト)
やりたいこと
■ 動機
読書メモ用に単語リストを記録したい。
出力は表形式が見やすいが、入力は箇条書きのように手軽に書きたい。
■ Markdown 上
md
:::list-to-table-kv[単語,意味]
- apple
- りんご
- banana
- バナナ
- 黄色い
- peach
- 桃
:::
■ 出てほしい HTML (を Markdown で表現したもの )
md
| 単語 | 意味 |
|--------|-------------------|
| apple | りんご |
| banana | バナナ<br/>黄色い |
| peach | 桃 |
※
セル内が複数行になったときの可読性が良くないのも、
このケースで Table 記法を使いたくない理由のひとつ
■ 表示例
単語 | 意味 |
---|---|
apple | りんご |
banana | バナナ 黄色い |
peach | 桃 |
実装
remark plugin の実装
※ 試行錯誤した辺りに雑にコメント記載
src/plugins/remark-list-to-table-kv.ts
import { Plugin } from 'unified'
import { visit } from 'unist-util-visit'
import { h } from 'hastscript'
import { Node } from 'unist'
import { ContainerDirective } from 'mdast-util-directive'
interface ListItemNode extends Node {
type: 'listItem'
children: Node[]
}
export const remarkListToTableKv: Plugin = () => {
return (tree) => {
visit(tree, 'containerDirective', (node: ContainerDirective, index, parent) => {
// ↑
// Markdown 側で :::list-to-table-kv[単語,意味] のフォーマットが違うと、ここに入ってこない
if (node.name !== 'list-to-table-kv' || !parent || index == null) return
let headers = ['Key', 'Value']
if (node.children[0].data?.directiveLabel == true) {
headers = node.children[0].children[0].value.split(',') // ← [単語,意味]は attribute に入ってきそうに思われるが、実際はなぜか1段落目にいる
}
const rows: [string, string[]][] = []
for (const item of node.children) {
if (item.type !== 'list') continue
for (const li of item.children) {
const listItem = li as ListItemNode
const [termNode, ...rest] = listItem.children
const key = getText(termNode)
const values: string[] = []
for (const child of listItem.children.slice(1)) {
if (child.type === 'list') {
for (const v of child.children) { // ← 1個深い。children[0].children[0]
const para = v.children[0]
values.push(getText(para))
}
}
}
rows.push([key, values])
}
}
// console.log(rows)
const tableNode = {
type: 'table',
align: [null, null], // ← ここをカラにすると <td> が出ない
children: [
{
type: 'tableRow',
children: headers.map((h) => ({
type: 'tableCell',
children: [{ type: 'text', value: h }],
})),
},
...rows.map(([key, values]) => ({
type: 'tableRow',
children: [
{ type: 'tableCell', children: [{ type: 'text', value: key }] },
{
type: 'tableCell',
children: interleaveBreaks(values),
},
],
})),
],
}
parent.children.splice(index, 1, tableNode)
})
}
}
// Utility Functions
function getText(node: Node | undefined): string {
if (!node || !('children' in node)) return ''
const children = (node as any).children
return children.map((c: any) => ('value' in c ? c.value : '')).join('')
}
function escapeHtml(str: string): string {
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
}
function interleaveBreaks(values: string[]): Node[] {
const result: Node[] = []
values.forEach((val, i) => {
result.push({ type: 'text', value: val })
if (i < values.length - 1) result.push({ type: 'break' }) // 改行ノード
})
return result
}
export default remarkListToTableKv;
docusaurus.config.ts からの読み込み
docusaurus.config.ts
import remarkListToTableKv from './src/plugins/remark-list-to-table-kv';
...
const config: Config = {
...
presets: [
[
'classic',
{
docs: {
routeBasePath: '/',
sidebarPath: './sidebars.ts',
showLastUpdateTime: true,
remarkPlugins: [
remarkListToTableKv, // ← ★ ここに足す
],