From 8f764806d980ca964330d3151c93fd7229e21e17 Mon Sep 17 00:00:00 2001 From: 安竹洋平 <61961825+yasutakeyohei@users.noreply.github.com> Date: Wed, 31 Jan 2024 00:42:45 +0900 Subject: build --- build/blog/rss.xml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'build/blog/rss.xml') diff --git a/build/blog/rss.xml b/build/blog/rss.xml index d128f782..6a58eac1 100644 --- a/build/blog/rss.xml +++ b/build/blog/rss.xml @@ -65,6 +65,7 @@
markdownからHTMLへ変換処理の流れ
| ........................ process ........................... |
| .......... parse ... | ... run ... | ... stringify ..........|

+--------+ +----------+
Input ->- | Parser | ->- Syntax Tree ->- | Compiler | ->- Output
+--------+ | +----------+
X
|
+--------------+
| Transformers |
+--------------+

上図(Unified Overviewより)にTransformersとあるところがRemark/Rehypeの動作するところ。

Remarkはmarkdown形式で、RehypeはHTML形式でASTを扱います。どちらも同じようにASTを操作できますが、データ構造が違うため、目的に応じて選択することになるのかなと思います。

+

こちらのサイトなどが詳しいです。

Docusaurusにおけるプラグインの実行タイミング

Docusaurusでこれらのプラグインを利用するためにはdocusaurus.config.jsonに設定が必要です。このページによると次の4種類の設定値にてプラグインを登録できます。

デフォルトプラグイン適用前デフォルトプラグイン適用後
Remark
Markdown形式
beforeDefaultRemarkPluginsremarkPlugins
Rehype
HTML形式
beforeDefaultRehypePluginsrehypePlugins
@@ -73,7 +74,7 @@

Swizzlingはこちらに説明がある通りの機能で、簡単に言うとReactのコンポーネントをカスタマイズできる機能です。

Swizzlingの設定をすると、Docusaurusがデフォルトのコンポーネントの代わりに自動的にカスタマイズしたコンポーネントを使用するようになります。

今回は、デフォルトのAdmonitionにないID属性を持たせるためAdmonitionコンポーネントをカスタマイズしました。Swizzlingの設定をすることにより、デフォルトのAdmonitionの代わりにこのカスタムコンポーネントが使われるようにします。

-

動作原理

+

動作原理

TOCは「ASTに含まれているheading要素を単純に配列に入れている」だけですが、この処理はカスタマイズで上書きできません。そこで、カスタマイズできる処理だけでAdmonitionのタイトルをTOCに反映する方法として次を思いつき、実装しました。

  1. docusaurusのデフォルトプラグインがTOCの処理を行うより前に、Admonitionのタイトル部を見出しとして新規作成し、Admonition要素の直前に追加する
  2. @@ -90,7 +91,7 @@

    Remark/Rehypeプラグイン

    次にプラグインを実装します。

    docusaurusのsrcディレクトリ下にrehypeとremarkというディレクトリを作り、次のファイル名と内容で2つのプラグインを作ります。

    -
    src/rehype/admonition-title-to-heading-before-toc.js
    import {visit} from 'unist-util-visit';

    const plugin = (options) => {
    const transformer = async (ast) => {
    let newBeginningText = "";
    const visitor = ((node, index, parent) => {
    if (node.type === 'containerDirective') {
    // :::infoなどに続くタイトル冒頭Text部(冒頭#を含む(もしくは含まない)部分)を取得(:::info ##** )
    // (タイトル全体にはHTML等が含まれる可能性があるため冒頭Text部だけ操作する、残りはシャロ―コピー)
    const beginningText = node.children[0].children[0].value;

    // タイトル冒頭Text部に#が2つ以上連続しているとき
    if(/^##/.test(beginningText)) {
    // タイトル冒頭部から#とそれに続く空白を削除
    newBeginningText = beginningText.replace(/^#+/, '').trim();

    // タイトル部冒頭だけ更新し、残りはシャロ―コピー
    // まずタイトル部全体をシャロ―コピー
    let titleNodes = [...node.children[0].children];

    // 冒頭要素のvalueを更新(ほかはシャロ―コピー)
    const newTitleBeginningNode = {
    ...titleNodes[0],
    value: newBeginningText,
    }

    // タイトルノードの冒頭要素だけ更新(ほかはシャロ―コピー)
    const newTitleNodes = [ ...titleNodes ];
    newTitleNodes[0] = newTitleBeginningNode;

    // visitしているcontainerDirectiveの前にheadingノードを追加
    parent.children.splice(index, 0, {
    type: 'heading',
    depth: (beginningText.match(/^##+/) || [''])[0].length, // #の連続数がheadingの深さ
    children: newTitleNodes,
    });
    // 次に検索するのはindexを2つ分飛ばしたノード
    return index + 2;
    }
    }
    });

    visit(ast, 'containerDirective', visitor);

    };
    return transformer;
    };

    export default plugin;
    +
    src/rehype/admonition-title-to-heading-before-toc.js
    import {visit} from 'unist-util-visit';

    const plugin = (options) => {
    const transformer = async (ast) => {
    let newBeginningText = "";
    const visitor = ((node, index, parent) => {
    if (node.type === 'containerDirective') {
    // :::infoなどに続くタイトル冒頭Text部(冒頭#を含む(もしくは含まない)部分)を取得(:::info ##** )
    // (タイトル全体にはHTML等が含まれる可能性があるため冒頭Text部だけ操作する、残りはシャロ―コピー)
    const beginningText = node.children[0].children[0].value;

    // タイトル冒頭Text部に#が2つ以上連続しているとき
    if(/^##/.test(beginningText)) {
    // タイトル冒頭部から#とそれに続く空白を削除
    newBeginningText = beginningText.replace(/^#+/, '').trim();

    // タイトル部冒頭だけ更新し、残りはシャロ―コピー
    // まずタイトル部全体をシャロ―コピー
    let titleNodes = [...node.children[0].children];

    // 冒頭要素のvalueを更新(ほかはシャロ―コピー)
    const newTitleBeginningNode = {
    ...titleNodes[0],
    value: newBeginningText,
    }

    // タイトルノードの冒頭要素だけ更新(ほかはシャロ―コピー)
    const newTitleNodes = [ ...titleNodes ];
    newTitleNodes[0] = newTitleBeginningNode;

    // visitしているcontainerDirectiveの前にheadingノードを追加
    parent.children.splice(index, 0, {
    type: 'heading',
    depth: (beginningText.match(/^##+/) || [''])[0].length, // #の連続数がheadingの深さ
    children: newTitleNodes,
    });
    // 次に検索するのはindexを2つ分飛ばしたノード
    return index + 2;
    }
    }
    });

    visit(ast, 'containerDirective', visitor);

    };
    return transformer;
    };

    export default plugin;
    src/rehype/admonition-title-to-heading-after-toc.js
    import {visit} from 'unist-util-visit';

    const plugin = (options) => {
    const transformer = async (ast) => {
    let hId = null;
    let hContent = null;
    visit(ast, 'element', (node, index, parent) => {
    if (/^h[2-6]$/.test(node.tagName) && node.properties && node.properties.id) {
    // H要素(h2~h6)を見つけた場合

    // IDとタイトルの冒頭Text部を取得する
    hId = node.properties.id;
    hContent = node.children ? node.children[0].value :
    node.children[0].children[0] ? node.children[0].children[0].value : '';

    // 続くAdmonitionを探す(docはH要素とadmonitionが連続しているが
    // blogではなぜか改行要素{ type:'text', value:'\n' }が間に入っているので念のため隣接3要素を探す
    for (let i = index + 1; i < index + 4 && i < parent.children.length; i++) {
    if(parent.children[i] && parent.children[i].tagName === 'admonition') {
    // admonition(div)を見つけた場合
    const admonitionNode = parent.children[i];

    // admonitionタイトルの冒頭Text部分を取得(properties.titleもしくはchildren[0].children[0].value)
    const admonitionNodeTitle = admonitionNode.properties.title ? admonitionNode.properties.title :
    admonitionNode.children[0] && admonitionNode.children[0].children[0] ? admonitionNode.children[0].children[0].value : '';

    if(/^##/.test(admonitionNodeTitle) && admonitionNodeTitle.replace(/^#+/, '').trim() === hContent.trim()) {
    // #で始まっていて、タイトル冒頭部が同じ場合
    // divのidをHタグのidに設定
    admonitionNode.properties.id = hId;
    // H要素を削除
    parent.children.splice(index, 1);
    }
    }
    }
    }
    });
    };
    return transformer;
    };

    export default plugin;
    Admonitionのツリー構造

    参考までに、Remarkのプラグインから見るとAdmonitionのASTは例えば次のようになっています。

    Admonitionのツリー構造(一例)
    {
    type: 'containerDirective',
    name: 'info',
    attributes: {},
    children: [
    {
    type: 'paragraph',
    data: { directiveLabel: true },
    children: [
    {
    type: 'text',
    value: '#### info title もしHTML等が入ると(ここにaタグを入れると)',
    position: [Object]
    },
    {
    type: 'mdxJsxTextElement',
    name: 'a',
    attributes: [],
    position: [Object],
    data: [Object],
    children: [Array]
    },
    { type: 'text', value: 'このようにタイトル部が別々の要素として配列に入っている。', position: [Object] }
    ],
    position: {
    start: { line: 1347, column: 8, offset: 34053 },
    end: { line: 1347, column: 55, offset: 34100 }
    }
    },
    { type: 'paragraph', children: [Array], position: [Object] },
    ...
    ],
    ...
    }

    Swizzling

    -- cgit v1.2.3-54-g00ecf