Markdown は広く使用されている軽量マークアップ言語で、人々が読みやすく書きやすいプレーンテキスト形式で文書を書くことを可能にします。また、xLogが主に使用する記事形式でもあり、この記事ではxLog Flavored Markdownを例に、Markdown 文書を優雅に解析する方法を説明します。
アーキテクチャ#
解析プロセスは次のようなアーキテクチャで表現できます:
重要な概念:
- unified:構文ツリーとプラグインを使用してコンテンツを解析、検査、変換、シリアル化するライブラリ
- remark:unified のエコシステムプロジェクトの 1 つで、プラグイン駆動の Markdown 処理ライブラリ
- rehype:unified のエコシステムプロジェクトの 1 つで、プラグイン駆動の HTML 処理ライブラリ
- mdast:remark が使用する Markdown を表現するための抽象構文木の仕様
- hast:rehype が使用する HTML を表現するための抽象構文木の仕様
簡単に言えば、Markdown 文書を unified エコシステムのパーサーに渡して、unified が認識できる構文ツリーに解析し、その後一連の unified エコシステムのプラグインを通じて必要なコンテンツに変換し、さらに一連の unified エコシステムのツールライブラリを使用して必要な形式で出力します。以下では、解析、変換、出力の 3 つのステップについてそれぞれ説明します。
解析 Parse#
入力が Markdown、HTML、またはプレーンテキストであっても、それを操作可能な形式に解析する必要があります。この形式は構文ツリーと呼ばれます。仕様(例えば mdast)は、このような構文ツリーの外観を定義します。プロセッサ(例えば mdast の remark)は、それらを作成する責任があります。
最も簡単なステップで、解析するのは Markdown なので、ここではremark-parseを使用して Markdown 文書を mdast 形式の構文ツリーにコンパイルする必要があります。
xLog Flavored Markdownの中の対応する部分は次の通りです。
const processor = unified().use(remarkParse)
const file = new VFile(content)
const mdastTree = processor.parse(file)
変換 Transform#
ここが魔法が起こる場所です。ユーザーはプラグインを組み合わせ、それらが実行される順序を決定します。プラグインはこの段階で挿入され、取得した形式を変換および検査します。
このステップは最も重要で、Markdown から HTML への変換だけでなく、コンパイルプロセス中に挿入したい非標準の構文糖、HTML をクリーンアップして XSS を防ぐ、構文ハイライトを追加する、カスタムコンポーネントを埋め込むなどが含まれます。
unified のプラグインは非常に多く、更新も比較的迅速で、基本的なニーズはほぼ満たされます。特定のニーズに応じて変換スクリプトを自分で作成することも容易です。
特に remark-rehype というプラグインがあり、mdast 構文ツリーを hast 構文ツリーに変換します。そのため、これを使用する前に Markdown を処理する remark プラグインを使用し、その後に HTML を処理する rehype プラグインを使用する必要があります。
xLog Flavored Markdownでは非常に多くの変換プラグインが追加されています。
const processor = unified()
.use(remarkParse)
.use(remarkGithubAlerts)
.use(remarkBreaks)
.use(remarkFrontmatter, ["yaml"])
.use(remarkGfm, {
singleTilde: false,
})
.use(remarkDirective)
.use(remarkDirectiveRehype)
.use(remarkCalloutDirectives)
.use(remarkYoutube)
.use(remarkMath, {
singleDollarTextMath: false,
})
.use(remarkPangu)
.use(emoji)
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeRaw)
.use(rehypeIpfs)
.use(rehypeSlug)
.use(rehypeAutolinkHeadings, {
behavior: "append",
properties: {
className: "xlog-anchor",
ariaHidden: true,
tabIndex: -1,
},
content(node) {
return [
{
type: "text",
value: "#",
},
]
},
})
.use(rehypeSanitize, strictMode ? undefined : sanitizeScheme)
.use(rehypeTable)
.use(rehypeExternalLink)
.use(rehypeMermaid)
.use(rehypeWrapCode)
.use(rehypeInferDescriptionMeta)
.use(rehypeEmbed, {
transformers,
})
.use(rehypeRemoveH1)
.use(rehypePrism, {
ignoreMissing: true,
showLineNumbers: true,
})
.use(rehypeKatex, {
strict: false,
})
.use(rehypeMention)
const hastTree = pipeline.runSync(mdastTree, file)
以下は一部のプラグインの紹介です。
- remarkGithubAlerts:GitHub スタイルの Alerts 構文を追加します。デモ
- remarkBreaks:新しい段落として認識されるために空行を必要としなくなります。
- remarkFrontmatter:前置内容(YAML、TOML など)をサポートします。
- remarkGfm:非標準の GitHub が元の Markdown 構文に拡張した一連の構文をサポートします(実際にはこの一連の構文は非常に広く使用されており、事実上の標準となっています)。
- remarkDirective remarkDirectiveRehyp:非標準の Markdown汎用指令提案をサポートします。
- remarkMath rehypeKatex:複雑な数式をサポートします。デモ
- rehypeRaw:Markdown に埋め込まれたカスタム HTML をサポートします。
- rehypeIpfs:画像、音声、動画に
ipfs://
プロトコルのアドレスをサポートするカスタムプラグイン。 - rehypeSlug:タイトルに ID を追加します。
- rehypeAutolinkHeadings:タイトルに自身を指すリンクを追加します。
- rehypeSanitize:HTML をクリーンアップし、XSS 攻撃を防ぐために HTML の安全性を確保します。
- rehypeExternalLink:外部リンクに
target="_blank"
とrel="noopener noreferrer"
を追加するカスタムプラグイン。 - rehypeMermaid:図や表作成ツールMermaidをレンダリングするカスタムプラグイン。この記事のアーキテクチャ図は Mermaid でレンダリングされています。
- rehypeInferDescriptionMeta:文書の説明を自動生成するために使用されます。
- rehypeEmbed:リンクに基づいて YouTube、Twitter、GitHub などのカードを自動的に埋め込むカスタムプラグイン。
- rehypeRemoveH1:h1 を h2 に変換するカスタムプラグイン。
- rehypePrism:構文ハイライトをサポートします。
- rehypeMention:他の xLog ユーザーを @DIYgod のようにメンションすることをサポートするカスタムプラグイン。
出力 Stringify#
最後のステップは、(調整された)形式を Markdown、HTML、またはプレーンテキストに変換することです(入力形式とは異なる可能性があります!)。
unified のツールライブラリも多く、さまざまな形式を出力できます。
例えば、xLog は記事の右側に自動生成された目次を表示する必要があり、プレーンテキストを出力して推定読書時間を計算し、AI 要約を生成し、RSS 用に HTML を生成し、ページにレンダリングするために React Element を生成し、記事の画像と説明を抽出して記事カードを表示する必要があるため、mdast-util-toc、hast-util-to-text、hast-util-to-html、hast-util-to-jsx-runtime、unist-util-visit などのツールをそれぞれ使用しています。
xLog Flavored Markdownの中の対応する部分は次の通りです。
{
toToc: () =>
mdastTree &&
toc(mdastTree, {
tight: true,
ordered: true,
}),
toHTML: () => hastTree && toHtml(hastTree),
toElement: () =>
hastTree &&
toJsxRuntime(hastTree, {
Fragment,
components: {
// @ts-expect-error
img: AdvancedImage,
mention: Mention,
mermaid: Mermaid,
// @ts-expect-error
audio: APlayer,
// @ts-expect-error
video: DPlayer,
tweet: Tweet,
"github-repo": GithubRepo,
"xlog-post": XLogPost,
// @ts-expect-error
style: Style,
},
ignoreInvalidStyle: true,
jsx,
jsxs,
passNode: true,
}),
toMetadata: () => {
let metadata = {
frontMatter: undefined,
images: [],
audio: undefined,
excerpt: undefined,
} as {
frontMatter?: Record<string, any>
images: string[]
audio?: string
excerpt?: string
}
metadata.excerpt = file.data.meta?.description || undefined
if (mdastTree) {
visit(mdastTree, (node, index, parent) => {
if (node.type === "yaml") {
metadata.frontMatter = jsYaml.load(node.value) as Record<
string,
any
>
}
})
}
if (hastTree) {
visit(hastTree, (node, index, parent) => {
if (node.type === "element") {
if (
node.tagName === "img" &&
typeof node.properties.src === "string"
) {
metadata.images.push(node.properties.src)
}
if (node.tagName === "audio") {
if (typeof node.properties.cover === "string") {
metadata.images.push(node.properties.cover)
}
if (!metadata.audio && typeof node.properties.src === "string") {
metadata.audio = node.properties.src
}
}
}
})
}
return metadata
},
}
これにより、元の Markdown 文書から必要なさまざまな形式の出力を優雅に得ることができました。
さらに、解析された unified 構文ツリーを利用して、左右同期スクロールとリアルタイムプレビューが可能な Markdown エディタを作成することもできます。xLog の二段 Markdown エディタを参考にできます(コード)。次回の機会にまたお話ししましょう。