Closed6
Remix+CloudflareでWebサイトを作る 13(MarkdownをHTMLに変換してスタイルを付与して表示・GitHub形式/改行/生HTML/シンタックスハイライトに対応)
【2024-03-03】Markdownパーサーのライブラリを学ぶ
Remark
- 曰く「Markdown processor」
- テキストをパース(解析)し変換・操作することができるJavaScriptライブラリ(e.g. Markdown)
- 様々なライブラリと組み合わせて目的の形式のテキストに変換可能
-
Rehype と一緒に使うことで Markdown を HTML に変換できる
- Marked.jsもあるが、Remarkは抽象構文木(AST)に変換することができるためより柔軟に構文を改造できる
Unified
- テキストや構文木や AST と AST を変換するための JavaScriptライブラリ
- Remarkを含む多くのプラグインを提供しており、これらのプラグインを組み合わせてテキスト処理パイプラインを構築することが可能
- さまざまなテキスト形式や処理ツールを統一的なAPIで扱うことができるように設計されているので、テキスト処理パイプラインをカスタマイズして拡張することが可能
remarkのドキュメントを見ると「HTMLとか別のフォーマットに変換するなら、remark()じゃなくてunified()で初期化したほうがいい。そのほうがpluginが色々使えるから」と記載されていました。一方で、remark()で初期化してremark-rehypeでHTML変換するサンプルをたくさん見かけるので、動きに大差はないのかもしれませんが
npmの巨大なエコシステム、unifiedについて調べました より
remarkのドキュメントに書かれてあるが機能ごとにライブラリを分けているため数がめちゃくちゃに多い...。
基本知識
テキストを変換する流れと基本知識は以下の記事に丸投げ
参考
記事
- Remark で広げる Markdown の世界
- unified を使う前準備
- npmの巨大なエコシステム、unifiedについて調べました
- unified, remark, rehypeでマークダウンをHTMLに変換する - ハウテレビジョンブログ
リポジトリ
【2024-03-03】Markdownを変換してWebに表示してみる
はじめの一歩
なんだかいろいろな書き方ありそうだし拡張のしようがいくらでもありそうでExampleコード見ても頭こんがらがるのでまずは書いてみるか〜と思ってたらとても良い記事を見つけた。
import rehypeStringify from "rehype-stringify";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import { unified } from "unified";
export const markdownToHtml = async (markdown: string) => {
const vfile = await unified()
.use(remarkParse) // Markdown → mdast(Markdown文字列ASTにしたもの)
.use(remarkRehype) // mdast → hast(HTML文字列をASTにしたもの)
.use(rehypeStringify) // hast → HTML
.process(markdown);
// remark-htmlを用いると一気に以下のように書ける
// const vfile = await remark().use(remarkHtml).process(markdown);
return vfile.toString();
};
const markdown = `
# Head 1
## Head 2
### Head 3
普通の文字
**太字**
|key|value|
|---|---|
|キー|バリュー|
- li 1
- li 2
- li 3
`;
export async function loader({ params, context }: LoaderFunctionArgs) {
const html = await markdownToHtml(markdown);
console.log(html); // 適当にloaderで表示してみる
return json({ html });
}
export default function Layout() {
const { html } = useLoaderData<typeof loader>();
return (
<div dangerouslySetInnerHTML={{ __html: html }} />
)
}
結果
loaderの出力結果
VFile {
cwd: '<コードがあるディレクトリの絶対パス>',
data: {},
history: [],
messages: [],
value: '<h1>Head 1</h1>\n' +
'<h2>Head 2</h2>\n' +
'<h3>Head 3</h3>\n' +
'<p><strong>strong</strong></p>\n' +
'<ul>\n' +
'<li>li 1</li>\n' +
'<li>li 2</li>\n' +
'<li>li 3</li>\n' +
'</ul>'
}
画面
基本的なMarkdownは反映されてそう。
テーブルと改行は反映されないので今後対応。
VFile
とは
- Virtualized File の略で、ファイルを仮想化して扱えるようにしたもの
- Unified では VFile を用いてデータをやりとりする
- ファイルのパス情報を抽象化して管理している
【2022-03-03】リンク・改行・テーブル等に対応する
色々導入
-
remark-html
- 1つ前にも書いてあるがこれによって一気通貫で変換を行う
-
unified
、remark
、mdast
、hast
、rehype
とか調べてたけどなんも考えなくて良くなったな...
-
remark-gfm
- GFM(github flavored markdown)のこと
- GitHubのマークダウンに変換することでリンクに対応
- これによって
こういう打ち消し線やテーブルにも対応できるようになった
-
remark-breaks
- 改行することで
<br />
タグを挿入できるようになった
- 改行することで
コード
変換
import { remark } from "remark";
import remarkBreaks from "remark-breaks";
import remarkGfm from "remark-gfm";
import remarkHtml from "remark-html";
export const markdownToHtml = async (markdown: string) => {
const vfile = await remark()
.use(remarkGfm)
.use(remarkBreaks)
.use(remarkHtml)
.process(markdown);
return vfile.toString();
};
Markdown
const markdown = `
# Head 1
## Head 2
### Head 3
普通の文字
**太字**
ここと
ここは改行しておいて欲しい
[Google](https://www.google.co.jp/)のリンクです。
~~打ち消し線~~に対応した
|key|value|
|---|---|
|キー|バリュー|
- li 1
- li 2
- li 3
`;
結果
次はCSSを書いてスタイルを付与する。
【2024-03-03】CSSを適用
Sass導入
npm install -D sass
CSS書く
色はJoy UIの Theme colors - Joy UI から取ってきた。
markdown.scss
.markdown {
h1 {
padding-bottom: 4px;
margin-bottom: 16px;
font-size: 24px;
font-weight: bold;
border-bottom: solid 2px var(--joy-palette-neutral-500, #636b74);
}
h2 {
padding-bottom: 4px;
margin: 16px 0;
font-size: 22px;
font-weight: bold;
border-bottom: solid 1px var(--joy-palette-neutral-500, #636b74);
}
h3 {
margin: 16px 0;
font-size: 18px;
font-weight: bold;
}
p {
margin: 6px 0;
}
a {
color: var(--joy-palette-primary-500, #0b6bcb);
transition: opacity 0.1s linear;
&:hover {
color: var(--joy-palette-primary-600, #185ea5);
}
}
table {
border-collapse: collapse;
th,
td {
border: solid 1px black;
}
th {
font-weight: bold;
color: var(--joy-palette-neutral-800, #171a1c);
background: var(--joy-palette-neutral-100, #f0f4f8);
padding: 10px;
}
td {
padding: 4px 10px;
}
}
}
適用
import styles from "~/styles/markdown.scss";
export const links = () => [{ rel: "stylesheet", href: styles }];
// loaderとかは変わっていないので省略
export default function Layout() {
const { html } = useLoaderData<typeof loader>();
return (
<div className="markdown" dangerouslySetInnerHTML={{ __html: html }} />
)
}
結果
ビルドエラー
エラー内容
Error [RollupError]: "default" is not exported by "app/styles/markdown.scss", imported by "app/routes/articles.$id.tsx".
修正
- import styles from "~/styles/markdown.scss";
+ import "~/styles/markdown.scss";
- export const links = () => [{ rel: "stylesheet", href: styles }];
+ export const links = () => [
+ { rel: "stylesheet", href: "~/styles/markdown.scss" },
+ ];
次したいこと
- YoutubeとTwitterなどの埋め込み対応
- 独自記法を作成(例:Zennの
:::message
みたいなやつ)
remark-html
を使わない形式に戻す
【2024-03-03】Markdownに直接HTMLを書いても有効にするためにrehype-raw
を導入したい。
が、remark-html
を使っているとうまくいかなかった。
というのも、unifiedでuseを記載するときにはその順番が重要になってくるのでremark-html
を使って一気に変換を行っていると間に処理を挟むことができず自由度が減って不都合だった。
import rehypeRaw from "rehype-raw";
import rehypeStringify from "rehype-stringify";
import remarkBreaks from "remark-breaks";
import remarkGfm from "remark-gfm";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import { unified } from "unified";
/**
* 適用順序
* 1. remarkParseでMarkdownをmdastに変換
* 2. remark-xxx のプラグインを適用
* 3. remarkRehypeでmdastをhastに変換
* 4. rehype-xxx のプラグインを適用
* 5. hastをReactElementに変換
*/
export const markdownToHtml = async (markdown: string) => {
const vfile = await unified()
.use(remarkParse, { fragment: true }) // Markdown → mdast(Markdown文字列ASTにしたもの)
.use(remarkGfm) // GitHubのMarkdown記法を使えるようにする
.use(remarkBreaks) // 改行を<br>にする
.use(remarkRehype, {
allowDangerousHtml: true, // rehype-rawのために直接記載されたタグを許可する
})
.use(remarkRehype) // mdast → hast(HTML文字列をASTにしたもの)
.use(rehypeRaw) // Markdown内でHTMLを使えるようにする
.use(rehypeStringify) // hast → HTML
.process(markdown);
return vfile.toString();
};
【2024-03-03】シンタックスハイライトをきれいにしよう
ここを参考に。
実装
$ npm i rehype-pretty-code
// シンタックスハイライト部分を追記
const markdown = `
\`\`\`sh
$ npm i hoge
\`\`\`
\`\`\`ts
export const markdownToHtml = async (markdown: string) => {
const vfile = await unified()
.use(remarkParse, { fragment: true }) // Markdown → mdast(Markdown文字列ASTにしたもの)
.use(remarkGfm) // GitHubのMarkdown記法を使えるようにする
.use(remarkBreaks) // 改行を<br>にする
.use(remarkRehype, {
allowDangerousHtml: true, // rehype-rawのために直接記載されたタグを許可する
})
.use(remarkRehype) // mdast → hast(HTML文字列をASTにしたもの)
.use(rehypeRaw) // Markdown内でHTMLを使えるようにする
.use(rehypeStringify) // hast → HTML
.process(markdown);
return vfile.toString();
};
\`\`\`
`;
+ import rehypePrettyCode from "rehype-pretty-code";
import rehypeRaw from "rehype-raw";
import rehypeStringify from "rehype-stringify";
import remarkBreaks from "remark-breaks";
import remarkGfm from "remark-gfm";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import { unified } from "unified";
/**
* 適用順序
* 1. remarkParseでMarkdownをmdastに変換
* 2. remark-xxx のプラグインを適用
* 3. remarkRehypeでmdastをhastに変換
* 4. rehype-xxx のプラグインを適用
* 5. hastをReactElementに変換
*/
export const markdownToHtml = async (markdown: string) => {
const vfile = await unified()
.use(remarkParse, { fragment: true }) // Markdown → mdast(Markdown文字列ASTにしたもの)
.use(remarkGfm) // GitHubのMarkdown記法を使えるようにする
.use(remarkBreaks) // 改行を<br>にする
.use(remarkRehype, {
allowDangerousHtml: true, // rehype-rawのために直接記載されたタグを許可する
})
.use(remarkRehype) // mdast → hast(HTML文字列をASTにしたもの)
.use(rehypeRaw) // Markdown内でHTMLを使えるようにする
+ .use(rehypePrettyCode, {
+ theme: "one-dark-pro",
+ keepBackground: true, // テーマの背景色をそのまま使用
+ }) // シンタックスハイライト
.use(rehypeStringify) // hast → HTML
.process(markdown);
return vfile.toString();
};
その他のテーマは以下を参照
markdown.scss
.markdown {
...
+ figure {
+ margin: 0;
+ pre {
+ border-radius: 4px;
+ font-size: 0.9rem;
+ padding: 12px;
+ }
+ }
}
結果
Before
After
このスクラップは2024/03/03にクローズされました