Markdownでリッチなコードブロックを実現する「Expressive Code」
![サムネイル](/_astro/thumbnail.sxQZpu1l_1OPvXw.png)
QiitaなどのサイトではMarkdown構文が独自に拡張されており、たとえばコードブロックにファイル名を表示したりできます。しかし、通常のMarkdownでは、そのようなことはできません。
Expressive Codeを使うと、ファイル名を表示したり、特定の行を強調表示したりなど、MarkdownまたはMDXを使ってWebでリッチなコードブロックを表現できます。
実際にこのブログではExpressive Codeを使っており、次のコードブロックのように美しく機能的な表現ができます。
1const foo = () => {2 // コード中の「テキスト」という文字列を強調3 console.log("テキスト");4 console.log("テキスト");5 // この行だけ強調6};7
8foo();
この記事では、そんなExpressive Codeの使い方を説明します。
Expressive Codeとは
Expressive Codeは、Webでソースコードを美しく表示するためのエンジンです。VS Codeと同じ正確なエンジンでシンタックスハイライトを処理しており、またテキストマーカーや差分表示、フレームなどの機能を備えています。
クライアントサイドのフレームワークに依存せず、高パフォーマンスで軽量なことが特徴です。
また、コードブロックの右上にはコピーボタンが表示され、クリックするとコードをクリップボードにコピーできます。
前提条件
Expressive Codeは、Markdownパーサーのremarkと、静的サイトジェネレーターのAstroに対応しています。
コア部分のパッケージは独立して提供されており、自分で他のフレームワークに対応させることもできます。しかし、通常はremarkかAstroを使うことになるでしょう。
インストール方法
remarkで使う場合とAstroで使う場合に分けて説明します。
remarkの場合
Expressive Codeをremarkで使う場合は、プラグインとしてインストールします。
npm install remark-expressive-code
他のプラグインと同じように利用できます。第2引数としてオプションを指定できます。詳細は公式リポジトリーを確認してください。
※コード例は公式リポジトリーより
1import { unified } from 'unified'2import remarkParse from 'remark-parse'3import remarkDirective from 'remark-directive'4import remarkExpressiveCode from 'remark-expressive-code'5import remarkRehype from 'remark-rehype'6import rehypeRaw from 'rehype-raw'7import rehypeStringify from 'rehype-stringify'8
9const markdownExample = `10# Hello world11
12\`\`\`js title="hello-world.js" ins={1}13console.log('Hello world')14\`\`\`15`16
17main()18
19async function main() {20 const file = await unified()21 .use(remarkParse)22 .use(remarkDirective)23 // Here we add the plugin to the remark pipeline24 // (you can also pass options as a second argument)25 .use(remarkExpressiveCode)26 // As remark-expressive-code generates HTML nodes,27 // we need to pass `allowDangerousHtml: true`28 // to prevent remark-rehype from dropping them29 .use(remarkRehype, { allowDangerousHtml: true })30 // We also need `rehype-raw` to prevent HTML tags31 // inside the output from being escaped32 .use(rehypeRaw)33 .use(rehypeStringify)34 .process(markdownExample)35
36 console.log(String(file))37}
Astroの場合
Astroの場合は、インテグレーションが用意されています。次のコマンドを実行するだけで、自動的に使えるようになります。
npx astro add astro-expressive-code
設定
Expressive Codeでは、コードブロックの配色(テーマ)などをカスタマイズできます。また、閲覧しているユーザーの環境がライトモードかダークモードかに応じた自動切り替えもできます。
詳細は公式ドキュメントを確認してください。
使い方
Expressive Codeは、通常のMarkdownのコードブロック構文に加え、いくつかの独自の構文をサポートしています。
タイトル
コードブロックにタイトルを表示するには、次のように書きます。
1```javascript title="foo.js"2const foo = () => {3 console.log("foo");4};5```
すると、次のように表示されます。
1const foo = () => {2 console.log("foo");3};
行の強調
コードの特定の行を強調表示するには、mark={}
で行番号を指定します。
1```javascript title="foo.js" mark={2}2const foo = () => {3 console.log("foo");4 alert("foo");5};6```
1const foo = () => {2 console.log("foo");3 alert("foo");4};
たとえば、2〜3行目を強調表示したい場合は、mark={2-3}
とします。
1```javascript title="foo.js" mark={2-3}2const foo = () => {3 console.log("foo");4 alert("foo");5};6```
1const foo = () => {2 console.log("foo");3 alert("foo");4};
複数の行を強調表示したい場合は、mark={1,4}
のようにカンマ区切りで指定します。
1```javascript title="foo.js" mark={1,4}2const foo = () => {3 console.log("foo");4 alert("foo");5};6```
1const foo = () => {2 console.log("foo");3 alert("foo");4};
行の挿入
コードの特定の行を挿入したような差分表示をするには、ins={}
で行番号を指定します。
1```javascript title="foo.js" ins={3}2const foo = () => {3 console.log("foo");4 alert("foo");5};6```
1const foo = () => {2 console.log("foo");3 alert("foo");4};
複数行にまたがって挿入したい場合や、複数箇所に挿入したい場合の指定方法は、行の強調と同じです。
行の削除
コードの特定の行を削除したような差分表示をするには、del={}
で行番号を指定します。
1```javascript title="foo.js" del={3}2const foo = () => {3 console.log("foo");4 alert("foo");5};6```
1const foo = () => {2 console.log("foo");3 alert("foo");4};
複数行にまたがって削除したい場合や、複数箇所に削除したい場合の指定方法は、行の強調や行の挿入と同じです。
ラベル付きマーカー
コードの特定の行を強調表示し、そこにラベルを付与するにはmark={"ラベル":行番号}
で行番号を指定します。また、del
やins
でも同様にできます。
1```javascript title="foo.js" mark={"1":2}2const foo = () => {3 console.log("foo");4 alert("foo");5};6```
1const foo = () => {2 console.log("foo");3 alert("foo");4};
テキストマーカー
コード中の特定の文字列を強調表示するには、"テキスト"
のようにダブルクォーテーションで囲みます。
1```javascript title="foo.js" mark={5} ins={8} "テキスト"2const foo = () => {3 // コード中の「テキスト」という文字列を強調4 console.log("テキスト");5 console.log("テキスト");6 // この行だけ強調7};8```
1const foo = () => {2 // コード中の「テキスト」という文字列を強調3 console.log("テキスト");4 console.log("テキスト");5 // この行だけ強調6};
また、正規表現も利用できます。正規表現ではキャプチャーされた範囲のみが強調表示されるので、キャプチャーしたくない場合は(?:)
でグループ化します。
1```markdown /(?:hoge)+/2- foo3- hoge4- bar5- hogehoge6```
1- foo2- hoge3- bar4- hogehoge
フレームの変更
デフォルトではエディター風のフレームが表示されます。
コードの言語として次のいずれかを指定すると、ターミナルウィンドウ風のフレームが表示されます。
bash
shellscript
shell
sh
zsh
1```bash title="terminal"2echo "foo"3```
echo "foo"
フレームの設定を上書きしたい場合は、frame=
で指定します。frame
オプションにはcode
、terminal
、none
、auto
のいずれかを指定できます。デフォルトではauto
です。
1```javascript title="foo.js" frame="terminal"2const foo = () => {3 console.log("foo");4};5```
1const foo = () => {2 console.log("foo");3};
ワードラップ(行の折り返し)
行を折り返すには、wrap
にtrue
を指定します。false
を指定すると折り返しを無効にできます。true
やfalse
を省略した場合はtrue
とみなされます。
1```javascript wrap2const hoge = () => {3 console.log("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.");4}5```
1const hoge = () => {2 console.log("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.");3}
折り返したときに折り返し部分のインデントを無効化するには、preserveIndent
にfalse
を指定します。true
やfalse
を省略した場合はtrue
とみなされます。
1```javascript wrap preserveIndent=false2const hoge = () => {3 console.log("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.");4}5```
1const hoge = () => {2 console.log("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.");3}
行番号の表示
Expressive Codeで行番号を表示するには、次のようにプラグインをインストールします。
npm i @expressive-code/plugin-line-numbers
Astroの場合は、プラグインとしてpluginLineNumbers()
を追加します。
1import { defineConfig } from "astro/config";2import astroExpressiveCode from "astro-expressive-code";3import { pluginLineNumbers } from "@expressive-code/plugin-line-numbers";4
5export default defineConfig({6 integrations: [7 astroExpressiveCode({8 plugins: [9 pluginLineNumbers()10 ]11 })12 ]13});
コードブロックごとに個別に行番号のオン・オフを切り替えたい場合は、showLineNumbers
にtrue
またはfalse
を指定します。true
やfalse
を省略した場合はtrue
とみなされます。
1```javascript title="foo.js" showLineNumbers=false2const foo = () => {3 console.log("foo");4};5```
const foo = () => { console.log("foo");};
また、行番号の開始番号を変更したい場合は、startLineNumber=
に開始番号を指定します。
1```javascript title="foo.js" startLineNumber=102const foo = () => {3 console.log("foo");4};5```
10const foo = () => {11 console.log("foo");12};
特定の言語の場合にのみ行番号を表示したい場合や、逆に特定の言語の場合にのみ行番号を非表示にしたい場合は、設定で挙動を変更できます。たとえば、コンソール系の言語では行番号を表示しない場合は、次のようにします。
1import { defineConfig } from "astro/config";2import astroExpressiveCode from "astro-expressive-code";3import { pluginLineNumbers } from "@expressive-code/plugin-line-numbers";4
5export default defineConfig({6 integrations: [7 astroExpressiveCode({8 plugins: [9 pluginLineNumbers()10 ],11 defaultProps: {12 overridesByLang: {13 "shell,sh,bash,powershell": {14 showLineNumbers: false15 }16 }17 }18 })19 ]20});
古い内容(一応残しているだけなので無視して大丈夫)
Expressive Codeには、記事執筆時点では行番号を表示するオプションが存在しません。Issueは上がっていますが、現時点では実装方法を検討中のようです。
そこで、Expressive Codeが内部で使用しているShikiに上がっていたIssueを参考に、次のようなCSSを追加すると、行番号を表示できます。
ただし、行の強調表示(差分表示を含む)との非互換性があるため、オススメしません。
1.expressive-code pre > code {2 counter-reset: step !important;3 counter-increment: step 0 !important;4}5
6
7.expressive-code pre > code .ec-line::before {8 content: counter(step);9 counter-increment: step;10 width: 1rem;11 margin-right: 1rem;12 display: inline-block;13 text-align: right;14 color: rgba(115, 138, 148, 0.4);15}
Tips
ターミナルウィンドウをmacOS風にする
Expressive Codeのターミナルウィンドウ風フレームの左上には、3つのボタンのような飾りがついています。これらのボタンは、デフォルトでは灰色です。
![Expressive Codeのターミナルウィンドウ風フレームのスクリーンショット](/_astro/image.Dh5pQ0Iq_205Rz3.webp)
これをmacOSのウィンドウ風の配色にするには、次のようなCSSを追加します。
1.expressive-code .frame.is-terminal .header::before {2 background-image: linear-gradient(to right, #c95b5b 30%, 30%, #e0b054 70%, 70%, #62b162);3 opacity: 0.8 !important;4}
![macOS風に配色を変更したターミナルウィンドウ風フレームのスクリーンショット](/_astro/image-1.BQjAztBZ_9zrKL.webp)
コピーボタンのテキストを変更する
Expressive Codeのコピーボタンのテキストは、デフォルトでは英語表記になっています。Astroでこれを日本語にしたい場合は、次のようにします。
1import { defineConfig } from "astro/config";2import astroExpressiveCode, { pluginFramesTexts } from "astro-expressive-code";3import { pluginLineNumbers } from "@expressive-code/plugin-line-numbers";4
5pluginFramesTexts.overrideTexts("ja", {6 copyButtonTooltip: "クリップボードにコピーする",7 copyButtonCopied: "コピーしました!",8});9
10// https://astro.build/config11export default defineConfig({12 integrations: [13 astroExpressiveCode({14 themes: ["dark-plus", "light-plus"],15 defaultLocale: "ja"16 })17 ]18});
まとめ
MarkdownやMDXを使って、Webでリッチなコードブロックを表現できるExpressive Codeを紹介しました。
Expressive Codeは、Astroの公式ドキュメント(が使っているStarlight)でも利用されているようです。
通常のMarkdownではできない、高度で美しいコードブロックを表現できるので、ぜひ使ってみてください。