Docusaurus🦖
1ヵ月弱使いましたがこのDocusaurus(ドキュサウルス)は数あるCMSの中でも秀逸です。
文書作成と管理が容易で、拡張の自由度も非常に高く、完全なオープンソース。
議員活動に重要な「資料を作成しまとめて公開するツール」として現状の最適解と感じます。
-
facebookが母体なので色々と気になるところですが、Reactを初めとして有益なソフトウェアを完全なオープンソースとして提供してくれていることは純粋にありがたいと感じます。
+
Facebookが母体なのでいろいろと気になるところですが、Reactを初めとして有益なソフトウェアを完全なオープンソースとして提供してくれていることは純粋にありがたいと感じます。
Admonitionのタイトルが見出しにならない
-
さてDocusaurusにはAdmonition(注意書きや警告文)を容易にmarkdownで書く方法が用意されています。
-
例えばinfoなら、次のようにmarkdownで書けば、
+
さてDocusaurusにはAdmonition(注意書きや警告文)を容易にMarkdownで書く方法が用意されています。
+
たとえばinfoなら、次のようにMarkdownで書けば、
Admonitionの記法(mdもしくはmdxに記載)
:::info[infoの例]
ここに文章を書く
:::
次のようにHTMLで表示されます。
@@ -40,19 +40,19 @@
ほかのユーザーからの要望も上がっており、私も少し不便に感じていたので、次の仕様になるようカスタマイズしましたのでその方法を解説します。
カスタマイズ後はどうなるか
-
後述のカスタマイズをすると、Admonitionのタイトル部に(通常の見出しmarkdownと同様に)#を冒頭に2個以上入れる ことで見出しになります。またTOCにも反映されます。#を2個以上としているのは、H1をAdmonitionには使わないはずのため。また#を入れない場合は見出しにならず、TOCにも反映されません。
+
後述のカスタマイズをすると、Admonitionのタイトル部に(通常の見出しMarkdownと同様に)#を冒頭に2個以上入れる ことで見出しになります。またTOCにも反映されます。#を2個以上としているのは、H1をAdmonitionには使わないはずのため。#を入れない場合は見出しにならず、TOCにも反映されません。
タイトル冒頭に#を入れた場合
Admonitionの記法(mdもしくはmdxに記載)
:::info[#### 見出しになりTOCに反映されるタイトルの例]
#が4つ分のためH4見出しになります。TOCにも反映されます。
:::
↓
-
#が4つ分のためH4見出しになります。TOCにも反映されます。
+
#が4つ分のためH4見出しになります。TOCにも反映されます。
このブログでもTOCに表示されています。
なおマウスカーソルを乗せた際にハッシュリンク(#)が表示されるようにするにはCSSの設定が必要です。
タイトル冒頭に#を入れない場合
Admonitionの記法(mdもしくはmdxに記載)
:::info[見出しにならずTOCに反映されないタイトルの例]
#がないため見出しにならず、TOCにも反映されません。
:::
↓
-
+
タイトルにHTMLを入れることも可能
なおタイトル部にHTMLを入れることもできます。TOCにも反映されます。
@@ -64,25 +64,25 @@
Swizzling
-
RemarkとRehypeは、markdownをHTMLに変換するプロセスにおいて、AST(抽象構文木・Abstract Syntax Tree)に作用するプラグインです。なおASTを操作するオープンソースのエコシステムの中にはもう一つRetextというプラグインもありますが、Docusaurusには実装されていないようです。
+
RemarkとRehypeは、MarkdownをHTMLに変換するプロセスにおいて、AST(抽象構文木・Abstract Syntax Tree)に作用するプラグインです。なおASTを操作するオープンソースのエコシステムの中にはもう1つRetextというプラグインもありますが、Docusaurusには実装されていないようです。
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を操作できますが、データ構造が違うため、目的に応じて選択することになるのかなと思います。
+
RemarkはMarkdown形式で、RehypeはHTML形式でASTを扱います。どちらも同じようにASTを操作できますが、データ構造が違うため、目的に応じて選択することになるのかなと思います。
こちらのサイトなどが詳しいです。
Docusaurusにおけるプラグインの実行タイミング
Docusaurusでこれらのプラグインを利用するためにはdocusaurus.config.jsonに設定が必要です。このページによると次の4種類の設定値にてプラグインを登録できます。
| デフォルトプラグイン適用前 | デフォルトプラグイン適用後 |
---|
Remark Markdown形式 | beforeDefaultRemarkPlugins | remarkPlugins |
Rehype HTML形式 | beforeDefaultRehypePlugins | rehypePlugins |
-
markdownからHTMLへの変換処理のところで、Docusaurusは自前のプラグイン(デフォルトプラグイン)を使い「見出しにidをつける」「ASTからTOCを作成する」などの処理を行っています。そのため今回のように「Amonitionのタイトルを読んでTOCに反映する」ためには、デフォルトプラグイン適用前と適用後の両方のタイミングでの処理が必要になります。
+
MarkdownからHTMLへの変換処理のところで、Docusaurusは自前のプラグイン(デフォルトプラグイン)を使い「見出しにidをつける」「ASTからTOCを作成する」などの処理を行っています。そのため今回のように「Amonitionのタイトルを読んでTOCに反映する」ためには、デフォルトプラグイン適用前と適用後の両方のタイミングでの処理が必要になります。
Swizzlingについて
-
Swizzlingはこちらに説明がある通りの機能で、簡単に言うとReactのコンポーネントをカスタマイズできる機能です。
-
Swizzlingの設定をすると、Docusaurusがデフォルトのコンポーネントの代わりに自動的にカスタマイズしたコンポーネントを使用するようになります。
+
Swizzlingはこちらに説明があるとおりの機能で、簡単に言うとReactのコンポーネントをカスタマイズできる機能です。
+
Swizzlingの設定をすると、Docusaurusがデフォルトのコンポーネントの代わりに自動的にカスタマイズしたコンポーネントを使用します。
今回は、デフォルトのAdmonitionにないID属性を持たせるためAdmonitionコンポーネントをカスタマイズしました。Swizzlingの設定をすることにより、デフォルトのAdmonitionの代わりにこのカスタムコンポーネントが使われるようにします。
動作原理
TOCは「ASTに含まれているheading要素を単純に配列に入れている」だけですが、この処理はカスタマイズで上書きできません。そこで、カスタマイズできる処理だけでAdmonitionのタイトルをTOCに反映する方法として次を思いつき、実装しました。
- docusaurusのデフォルトプラグインがTOCの処理を行うより前に、Admonitionのタイトル部を見出しとして新規作成し、Admonition要素の直前に追加する
- docusaurusのデフォルトプラグインがTOCの処理を行い、Admonitionのタイトル部がTOCに入る。見出しにはidが付与される
-- デフォルトプラグインの処理が終了したら作成した見出しは不要になるので削除する。その際、削除する見出しと同じタイトルを持つAdmonition要素を探し、idを与える
+- デフォルトプラグインの処理が終了したら作成した見出しは不要になるので削除する。その際、削除する見出しと同じタイトルをもつAdmonition要素を探し、idを与える
- AdmonitionコンポーネントでidをHTMLタグに付与する
@@ -96,7 +96,7 @@
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') {
const beginningText = node.children[0].children[0].value;
if(/^##/.test(beginningText)) {
newBeginningText = beginningText.replace(/^#+/, '').trim();
let titleNodes = [...node.children[0].children];
const newTitleBeginningNode = {
...titleNodes[0],
value: newBeginningText,
}
const newTitleNodes = [ ...titleNodes ];
newTitleNodes[0] = newTitleBeginningNode;
parent.children.splice(index, 0, {
type: 'heading',
depth: (beginningText.match(/^##+/) || [''])[0].length,
children: newTitleNodes,
});
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) {
hId = node.properties.id;
hContent = node.children ? node.children[0].value :
node.children[0].children[0] ? node.children[0].children[0].value : '';
for (let i = index + 1; i < index + 4 && i < parent.children.length; i++) {
if(parent.children[i] && parent.children[i].tagName === 'admonition') {
const admonitionNode = parent.children[i];
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()) {
admonitionNode.properties.id = hId;
parent.children.splice(index, 1);
}
}
}
}
});
};
return transformer;
};
export default plugin;
-
参考までに、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] },
...
],
...
}
+
参考までに、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
次にSwizzlingです。
Docusaurusのsrc/themeディレクトリにAdmonitionというディレクトリを作り、次の一例のファイルを格納します。
@@ -113,6 +113,6 @@
Layoutファイルを次のように変更するのみです。
src/theme/Admonition/Layout/index.js
import React from 'react';
import clsx from 'clsx';
import {ThemeClassNames} from '@docusaurus/theme-common';
import styles from './styles.module.css';
import headingStyles from '@docusaurus/theme-classic/lib/theme/Heading/styles.module.css';
function AdmonitionContainer({type, className, children}) {
return (
<div
className={clsx(
ThemeClassNames.common.admonition,
ThemeClassNames.common.admonitionType(type),
styles.admonition,
className,
)}>
{children}
</div>
);
}
function AdmonitionHeading({icon, title, id}) {
let depth = 0;
let trimmedTitle = title;
if(typeof title === "string") {
depth = title.match ? (title.toString().match(/^#+/) || [''])[0].length : 0;
trimmedTitle = depth > 0 ? title.replace(/^#+/, '').trim() : title;
} else if (typeof title[0] === "string") {
depth = title[0].match ? (title[0].match(/^#+/) || [''])[0].length : 0;
trimmedTitle = depth > 0 ? [title[0].replace(/^#+/, '').trim(), ...title.slice(1)] : title;
}
const classNames = clsx("anchor", "title", headingStyles.anchorWithStickyNavbar);
return (
<div className={styles.admonitionHeading}>
<span className={styles.admonitionIcon}>{icon}</span>
{(() => {
if (depth == 3) {
return(
<h3
id={id}
className={classNames}
>
{trimmedTitle}
</h3>
)
} else if (depth == 4) {
return(
<h4
id={id}
className={classNames}
>
{trimmedTitle}
</h4>
)
} else if (depth == 5) {
return(
<h5
id={id}
className={classNames}
>
{trimmedTitle}
</h5>
)
} else if (depth == 6) {
return(
<h6
id={id}
className={classNames}
>
{trimmedTitle}
</h6>
)
} else {
return(
<>
{trimmedTitle}
</>
)
}
})()}
</div>
);
}
function AdmonitionContent({children}) {
return children ? (
<div className={styles.admonitionContent}>{children}</div>
) : null;
}
export default function AdmonitionLayout(props) {
const {type, icon, title, children, className, id} = props;
return (
<AdmonitionContainer type={type} className={className}>
<AdmonitionHeading title={title} icon={icon} id={id} />
<AdmonitionContent>{children}</AdmonitionContent>
</AdmonitionContainer>
);
}
上記を設定後、npm start等の再起動が必要です。
-
以上です。