新ふらっとブログ立ち上げました
今後の展開を考え、これまでの議会活動等のまとめをこのDocusaurusの仕組みに移行しています。
+新ふらっとブログ立ち上げました
今後の展開を考え、これまでの議会活動等のまとめをこのDocusaurusの仕組みに移行しています。
ふらっとブログもこちらに新しく立ち上げることにしました。
議員活動や、その裏で活用している技術について、誰かの役に立つかもしれないことを中心にブログにしていきます😁
Docusaurusの注意書きや警告文のタイトルを見出しにして、目次にも乗せる方法
Docusaurus🦖
+Docusaurusの注意書きや警告文のタイトルを見出しにして、目次にも乗せる方法
Docusaurus🦖
1か月弱使いましたがこのDocusaurus(ドキュサウルス)は数あるCMSの中でも秀逸です。
文書作成と管理が容易で、拡張の自由度も非常に高く、完全なオープンソース。
議員活動に重要な「資料を作成しまとめて公開するツール」として現状の最適解と感じます。
Facebookが母体なのでいろいろと気になるところですが、Reactを初めとして有益なソフトウェアを完全なオープンソースとして提供してくれていることは純粋にありがたいと感じます。
-Admonitionのタイトルが見出しにならない
+Admonitionのタイトルが見出しにならない
さてDocusaurusにはAdmonition(注意書きや警告文)を容易にMarkdownで書く方法が用意されています。
たとえばinfoなら、次のようにMarkdownで書けば、
:::info[infoの例]
ここに文章を書く
:::
次のように本文中に見出しを書く方法もありますが
:::info
#### テスト
~文章~
:::
見た目がイマイチになります。
-テスト
~文章~
テスト
~文章~
ほかのユーザーからの要望も上がっており、私も少し不便に感じていたので、次の仕様になるようカスタマイズしましたのでその方法を解説します。
-カスタマイズ後はどうなるか
+カスタマイズ後はどうなるか
後述のカスタマイズをすると、Admonitionのタイトル部に(通常の見出しMarkdownと同様に)#を冒頭に2個以上入れる ことで見出しになります。またTOCにも反映されます。#を2個以上としているのは、H1をAdmonitionには使わないはずのため。#を入れない場合は見出しにならず、TOCにも反映されません。
-タイトル冒頭に#を入れた場合
+タイトル冒頭に#を入れた場合
:::info[#### 見出しになりTOCに反映されるタイトルの例]
#が4つ分のためH4見出しになります。TOCにも反映されます。
:::
↓
見出しになり、TOCに反映されるタイトルの例
#が4つ分のためH4見出しになります。TOCにも反映されます。
このブログでもTOCに表示されています。
なおマウスカーソルを乗せた際にハッシュリンク(#)が表示されるようにするにはCSSの設定が必要です。
タイトル冒頭に#を入れない場合
+タイトル冒頭に#を入れない場合
:::info[見出しにならずTOCに反映されないタイトルの例]
#がないため見出しにならず、TOCにも反映されません。
:::
↓
見出しにならず、TOCに反映されないタイトルの例
#がないため見出しになりません。
タイトルにHTMLを入れることも可能
+タイトルにHTMLを入れることも可能
なおタイトル部にHTMLを入れることもできます。TOCにも反映されます。
テスト123テスト下線
タイトル部のmarkdownは次の通りです。
#### テスト<sup>123</sup>テスト<u>下線</u>
カスタマイズに利用した機能
+カスタマイズに利用した機能
カスタマイズは次の機能を活用しました。
-RemarkとRehypeについて
+RemarkとRehypeについて
RemarkとRehypeは、MarkdownをHTMLに変換するプロセスにおいて、AST(抽象構文木・Abstract Syntax Tree)に作用するプラグインです。なおASTを操作するオープンソースのエコシステムの中にはもう1つRetextというプラグインもありますが、Docusaurusには実装されていないようです。
| ........................ 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でこれらのプラグインを利用するためにはdocusaurus.config.jsonに設定が必要です。このページによると次の4種類の設定値にてプラグインを登録できます。
デフォルトプラグイン適用前 | デフォルトプラグイン適用後 | |
---|---|---|
Remark Markdown形式 | beforeDefaultRemarkPlugins | remarkPlugins |
Rehype HTML形式 | beforeDefaultRehypePlugins | rehypePlugins |
MarkdownからHTMLへの変換処理のところで、Docusaurusは自前のプラグイン(デフォルトプラグイン)を使い「見出しにidをつける」「ASTからTOCを作成する」などの処理を行っています。そのため今回のように「Amonitionのタイトルを読んでTOCに反映する」ためには、デフォルトプラグイン適用前と適用後の両方のタイミングでの処理が必要になります。
-Swizzlingについて
+Swizzlingについて
Swizzlingはこちらに説明があるとおりの機能で、簡単に言うとReactのコンポーネントをカスタマイズできる機能です。
Swizzlingの設定をすると、Docusaurusがデフォルトのコンポーネントの代わりに自動的にカスタマイズしたコンポーネントを使用します。
今回は、デフォルトのAdmonitionにないID属性を持たせるためAdmonitionコンポーネントをカスタマイズしました。Swizzlingの設定をすることにより、デフォルトのAdmonitionの代わりにこのカスタムコンポーネントが使われるようにします。
-動作原理
+動作原理
TOCは「ASTに含まれているheading要素を単純に配列に入れている」だけですが、この処理はカスタマイズで上書きできません。そこで、カスタマイズできる処理だけでAdmonitionのタイトルをTOCに反映する方法として次を思いつき、実装しました。
- docusaurusのデフォルトプラグインがTOCの処理を行うより前に、Admonitionのタイトル部を見出しとして新規作成し、Admonition要素の直前に追加する @@ -85,34 +85,34 @@
- デフォルトプラグインの処理が終了したら作成した見出しは不要になるので削除する。その際、削除する見出しと同じタイトルをもつAdmonition要素を探し、idを与える
- AdmonitionコンポーネントでidをHTMLタグに付与する
実装
-docusaurus.config.json
+実装
+docusaurus.config.json
まずdocusaurus.config.jsonにimportとplugin設定を記入します(ハイライト部)。
これでDocusaurusデフォルトプラグイン適用の前後にそれぞれ自作のRemark/Rehypeプラグインが実行されることになります。
blogなどを入れている場合は、そのプロパティにも記載します。
import admonitionTitleToHeadingBeforeTOC from './src/remark/admonition-title-to-heading-before-toc.js';
import admonitionTitleToHeadingAfterTOC from './src/rehype/admonition-title-to-heading-after-toc.js';
export default {
// ...
presets: [
[
'classic',
/** @type {import('@docusaurus/preset-classic').Options} */
({
docs: {
// ...
beforeDefaultRemarkPlugins: [admonitionTitleToHeadingBeforeTOC],
rehypePlugins: [admonitionTitleToHeadingAfterTOC],
},
blog: {
// ...
beforeDefaultRemarkPlugins: [admonitionTitleToHeadingBeforeTOC],
rehypePlugins: [admonitionTitleToHeadingAfterTOC],
},
// ...
}),
]],
// ...
}
Remark/Rehypeプラグイン
+Remark/Rehypeプラグイン
次にプラグインを実装します。
docusaurusのsrcディレクトリ下にrehypeとremarkというディレクトリを作り、次のファイル名と内容で2つのプラグインを作ります。
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;
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;
参考までに、Remarkのプラグインから見るとAdmonitionのASTはたとえば次のようになっています。
{
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
次にSwizzlingです。
Docusaurusのsrc/themeディレクトリにAdmonitionというディレクトリを作り、次の一例のファイルを格納します。
なおここに説明がある通りSwizzlingにはEjectingとWrappingの方法があります。
WrappingではAdmonitionの内部までカスタマイズできないため「タイトル部分にidプロパティを付ける」といったことができません。そのためEjectingを使います。
Ejectingを使う場合はバージョンアップによってデフォルトのコンポーネントと挙動が変わってくる可能性があるのでアップグレードの際は注意が必要です。
Ejecting
+Ejecting
npm run swizzle @docusaurus/theme-classic Admonition -- --eject
Docusaurus V3.1では次のようなメッセージが出ますので、YESを選びます。
? Do you really want to swizzle this unsafe internal component? » - Use arrow-keys. Return to submit.
NO: cancel and stay safe
> YES: I know what I am doing!
Ejectingをすると、実質的にnode_modulesの@docusaurus/theme-classic/lib/themeにあるコンポーネントがsrc/themeディレクトリにコピーされます。
あとはコピーされたコンポーネントをいじるだけです。
-コードの変更
+コードの変更
Layoutファイルを次のように変更するのみです。
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;
// titleにHTML等が含まれている場合は文字列ではなく配列になる
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;
}
// スクロール位置調整のcss
const classNames = clsx("anchor", "title", headingStyles.anchorWithStickyNavbar);
// depthに応じて見出しタグをレンダー
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等の再起動が必要です。
-以上です。
以上です。
合気公園の整備進捗状況
小平市上水南町二丁目の佐川幸義邸跡地に新設される合気公園の整備状況を見てきましたので、写真と動画で現在の様子をお知らせします。
+合気公園の整備進捗状況
小平市上水南町二丁目の佐川幸義邸跡地に新設される合気公園の整備状況を見てきましたので、写真と動画で現在の様子をお知らせします。
順調に進んでいるようですね😊
-これまでの経緯
+これまでの経緯
合気公園に関してこれまでの経緯はこちら↓にまとめています。
-市民による公園整備案
+市民による公園整備案
旧佐川邸の公園化を考える会が市に提出した公園計画案はこちら↓(ランドスケープデザイナー・鈴木綾氏による)
-市の整備イメージ案
+市の整備イメージ案
上記の提案を受けて市が令和5年5月に提示した整備イメージ案がこちら↓
この整備イメージに基づいて整備が進んでいます。
-写真
+写真
次の写真と動画は令和6年1月30日(火)に撮影したものです。
-お知らせ
+お知らせ
-野外卓・顕彰碑・背なしベンチ
+野外卓・顕彰碑・背なしベンチ
養生されている?道場跡の部分が見えます。
-パーゴラ・顕彰碑
+パーゴラ・顕彰碑
-メッシュフェンス・ブロック
+メッシュフェンス・ブロック
-水飲み場・東側出入口
+水飲み場・東側出入口
園名碑が少し写っています。トラックが止まっていて撮影できませんでした。
-水飲み場・東側出入口
+水飲み場・東側出入口
-庭園付近
+庭園付近
-西側出入口への道・目隠しフェンス
+西側出入口への道・目隠しフェンス
-南東の角
+南東の角
道路が拡幅されていることが分かります。
-庭園
+庭園
このあたりは今後手が入るのかもですね。
-売却予定地
+売却予定地
奥の砂が盛ってあるところが売却予定地です。
-拡幅された東側道路
+拡幅された東側道路
-道路拡幅は売却予定地前まで
+道路拡幅は売却予定地前まで
-佐川氏が四股を踏んでいたとされる石
+佐川氏が四股を踏んでいたとされる石
右下の平らな石が、佐川幸義氏が四股を踏んでいたとされる石です。
-西側出入口
+西側出入口
案内板はまだ設置されていません。
-西側園路と目隠しフェンス
+西側園路と目隠しフェンス
-西側の工事看板・目隠しフェンス上部
+西側の工事看板・目隠しフェンス上部
目隠しフェンス上部は人の乗り越えられないような突起が出ています。
-西側園路の舗装
+西側園路の舗装
-動画
+動画
以上です。完成が楽しみですね。
以上です。完成が楽しみですね。
小平市立合気公園・開園セレモニー
小平市上水南町二丁目の佐川幸義邸跡地に新設された小平市立合気公園がついに開園しました!
+小平市立合気公園・開園セレモニー
小平市上水南町二丁目の佐川幸義邸跡地に新設された小平市立合気公園がついに開園しました!
令和6年3月23日(土)午前10時から現地で行われた開園セレモニーに参加しましたので、写真と動画で様子をお知らせします。
-これまでの経緯
+これまでの経緯
合気公園に関してこれまでの経緯はこちら↓にまとめています。
- 旧佐川邸の公園化を考える会・これまでの経緯(2)
- 請願第5号 旧佐川邸の土地を売らないことも含め、市民の意見を聞きながら公園整備を行うことについて
- (令和6年1月31日の様子)小平市上水南町二丁目の佐川幸義邸跡地に新設される合気公園の整備進捗状況を見てきました
セレモニーの様子
+セレモニーの様子
次のような次第で、約30分間行われました。
- 開会の辞(水と緑と公園課長) @@ -53,19 +53,19 @@
↑市長、市議会議長、木村氏、岡江氏により、顕彰碑の除幕が行われました。
式次第(小平市)
+式次第(小平市)
-そのほかの写真
+そのほかの写真
次の写真と動画はセレモニー直後と3月28日(木)に撮影したものです。
-出入口
+出入口
正面出入口
北東側出入口
園名碑
+園名碑
道場に使われていた屋根瓦(鬼瓦)の一部が埋め込まれています。道場関係の方によるデザインです。
顕彰碑
+顕彰碑
@@ -79,33 +79,33 @@ 円満に滞り無く動じて居るのである-
その調和が合氣なのである
日本庭園
+日本庭園
早くも子どもたちが石に上って遊んでいました😍
道場跡地横にあるベンチ
+道場跡地横にあるベンチ
緑色に舗装された部分が道場跡地です。
テーブル
+テーブル
-パーゴラとベンチ
+パーゴラとベンチ
-水飲み
+水飲み
-南西出入口への通路
+南西出入口への通路
-看板
+看板
-売却予定地
+売却予定地
佐川幸義氏のご遺族から土地約1,300㎡と現金約3,000万円をご遺贈いただき、さらにふるさと納税で小平市として過去最多の約1,600万円ものご寄附をいただきました。ありがとうございました。それでもなお整備費用として約2千万円が不足しているという理由から、小林洋子市政である小平市は1区画を売却するとしました。
路線価は約21万円/㎡ですので、土地の評価額は約2.7億円となります。合計で優に3億円もの寄附をいただいたことになります。また市民と市が理想的な形での協働ができたり、ふるさと納税で歴代記録を更新している画期的な事業だったわけですから、最後は市に残りの整備費用を出してもらいたかったところです。残念ですがこの1区画は令和6年度中に売却予定です。
-動画
+動画
Xに投稿した動画をご覧ください。
令和6年3月23日(土)午前10時から現地で行われた開園セレモニーに参加しましたので、写真と動画で様子をお知らせします。
-これまでの経緯
+これまでの経緯
合気公園に関してこれまでの経緯はこちら↓にまとめています。
- 旧佐川邸の公園化を考える会・これまでの経緯(2)
- 請願第5号 旧佐川邸の土地を売らないことも含め、市民の意見を聞きながら公園整備を行うことについて
- (令和6年1月31日の様子)小平市上水南町二丁目の佐川幸義邸跡地に新設される合気公園の整備進捗状況を見てきました
セレモニーの様子
+セレモニーの様子
次のような次第で、約30分間行われました。
- 開会の辞(水と緑と公園課長) @@ -49,19 +49,19 @@
- docusaurusのデフォルトプラグインがTOCの処理を行うより前に、Admonitionのタイトル部を見出しとして新規作成し、Admonition要素の直前に追加する @@ -270,31 +270,31 @@
- デフォルトプラグインの処理が終了したら作成した見出しは不要になるので削除する。その際、削除する見出しと同じタイトルをもつAdmonition要素を探し、idを与える
- AdmonitionコンポーネントでidをHTMLタグに付与する
- 旧佐川邸の公園化を考える会・これまでの経緯(2)
- 請願第5号 旧佐川邸の土地を売らないことも含め、市民の意見を聞きながら公園整備を行うことについて
- (令和6年1月31日の様子)小平市上水南町二丁目の佐川幸義邸跡地に新設される合気公園の整備進捗状況を見てきました
- 開会の辞(水と緑と公園課長) @@ -50,19 +50,19 @@
- docusaurusのデフォルトプラグインがTOCの処理を行うより前に、Admonitionのタイトル部を見出しとして新規作成し、Admonition要素の直前に追加する @@ -263,31 +263,31 @@
- デフォルトプラグインの処理が終了したら作成した見出しは不要になるので削除する。その際、削除する見出しと同じタイトルをもつAdmonition要素を探し、idを与える
- AdmonitionコンポーネントでidをHTMLタグに付与する
↑市長、市議会議長、木村氏、岡江氏により、顕彰碑の除幕が行われました。
式次第(小平市)
+式次第(小平市)
-そのほかの写真
+そのほかの写真
次の写真と動画はセレモニー直後と3月28日(木)に撮影したものです。
-出入口
+出入口
正面出入口
北東側出入口
園名碑
+園名碑
道場に使われていた屋根瓦(鬼瓦)の一部が埋め込まれています。道場関係の方によるデザインです。
顕彰碑
+顕彰碑
@@ -75,28 +75,28 @@ 円満に滞り無く動じて居るのである-
その調和が合氣なのである
日本庭園
+日本庭園
早くも子どもたちが石に上って遊んでいました😍
道場跡地横にあるベンチ
+道場跡地横にあるベンチ
緑色に舗装された部分が道場跡地です。
テーブル
+テーブル
-パーゴラとベンチ
+パーゴラとベンチ
-水飲み
+水飲み
-南西出入口への通路
+南西出入口への通路
-看板
+看板
-売却予定地
+売却予定地
佐川幸義氏のご遺族から土地約1,300㎡と現金約3,000万円をご遺贈いただき、さらにふるさと納税で小平市として過去最多の約1,600万円ものご寄附をいただきました。ありがとうございました。それでもなお整備費用として約2千万円が不足しているという理由から、小林洋子市政である小平市は1区画を売却するとしました。
路線価は約21万円/㎡ですので、土地の評価額は約2.7億円となります。合計で優に3億円もの寄附をいただいたことになります。また市民と市が理想的な形での協働ができたり、ふるさと納税で歴代記録を更新している画期的な事業だったわけですから、最後は市に残りの整備費用を出してもらいたかったところです。残念ですがこの1区画は令和6年度中に売却予定です。
-動画
+動画
Xに投稿した動画をご覧ください。
@@ -122,65 +122,65 @@順調に進んでいるようですね😊
-これまでの経緯
+これまでの経緯
合気公園に関してこれまでの経緯はこちら↓にまとめています。
-市民による公園整備案
+市民による公園整備案
旧佐川邸の公園化を考える会が市に提出した公園計画案はこちら↓(ランドスケープデザイナー・鈴木綾氏による)
-市の整備イメージ案
+市の整備イメージ案
上記の提案を受けて市が令和5年5月に提示した整備イメージ案がこちら↓
この整備イメージに基づいて整備が進んでいます。
-写真
+写真
次の写真と動画は令和6年1月30日(火)に撮影したものです。
-お知らせ
+お知らせ
-野外卓・顕彰碑・背なしベンチ
+野外卓・顕彰碑・背なしベンチ
養生されている?道場跡の部分が見えます。
-パーゴラ・顕彰碑
- -メッシュフェンス・ブロック
+パーゴラ・顕彰碑
+ +メッシュフェンス・ブロック
-水飲み場・東側出入口
+水飲み場・東側出入口
園名碑が少し写っています。トラックが止まっていて撮影できませんでした。
-水飲み場・東側出入口
+水飲み場・東側出入口
-庭園付近
+庭園付近
-西側出入口への道・目隠しフェンス
+西側出入口への道・目隠しフェンス
-南東の角
+南東の角
道路が拡幅されていることが分かります。
-庭園
+庭園
このあたりは今後手が入るのかもですね。
-売却予定地
+売却予定地
奥の砂が盛ってあるところが売却予定地です。
-拡幅された東側道路
+拡幅された東側道路
-道路拡幅は売却予定地前まで
+道路拡幅は売却予定地前まで
-佐川氏が四股を踏んでいたとされる石
+佐川氏が四股を踏んでいたとされる石
右下の平らな石が、佐川幸義氏が四股を踏んでいたとされる石です。
-西側出入口
+西側出入口
案内板はまだ設置されていません。
-西側園路と目隠しフェンス
+西側園路と目隠しフェンス
-西側の工事看板・目隠しフェンス上部
+西側の工事看板・目隠しフェンス上部
目隠しフェンス上部は人の乗り越えられないような突起が出ています。
-西側園路の舗装
+西側園路の舗装
-動画
+動画
1か月弱使いましたがこのDocusaurus(ドキュサウルス)は数あるCMSの中でも秀逸です。
文書作成と管理が容易で、拡張の自由度も非常に高く、完全なオープンソース。
議員活動に重要な「資料を作成しまとめて公開するツール」として現状の最適解と感じます。
Facebookが母体なのでいろいろと気になるところですが、Reactを初めとして有益なソフトウェアを完全なオープンソースとして提供してくれていることは純粋にありがたいと感じます。
-Admonitionのタイトルが見出しにならない
+Admonitionのタイトルが見出しにならない
さてDocusaurusにはAdmonition(注意書きや警告文)を容易にMarkdownで書く方法が用意されています。
たとえばinfoなら、次のようにMarkdownで書けば、
:::info[infoの例]
ここに文章を書く
:::
次のように本文中に見出しを書く方法もありますが
:::info
#### テスト
~文章~
:::
見た目がイマイチになります。
-テスト
~文章~
テスト
~文章~
ほかのユーザーからの要望も上がっており、私も少し不便に感じていたので、次の仕様になるようカスタマイズしましたのでその方法を解説します。
-カスタマイズ後はどうなるか
+カスタマイズ後はどうなるか
後述のカスタマイズをすると、Admonitionのタイトル部に(通常の見出しMarkdownと同様に)#を冒頭に2個以上入れる ことで見出しになります。またTOCにも反映されます。#を2個以上としているのは、H1をAdmonitionには使わないはずのため。#を入れない場合は見出しにならず、TOCにも反映されません。
-タイトル冒頭に#を入れた場合
+タイトル冒頭に#を入れた場合
:::info[#### 見出しになりTOCに反映されるタイトルの例]
#が4つ分のためH4見出しになります。TOCにも反映されます。
:::
↓
見出しになり、TOCに反映されるタイトルの例
#が4つ分のためH4見出しになります。TOCにも反映されます。
このブログでもTOCに表示されています。
なおマウスカーソルを乗せた際にハッシュリンク(#)が表示されるようにするにはCSSの設定が必要です。
タイトル冒頭に#を入れない場合
+タイトル冒頭に#を入れない場合
:::info[見出しにならずTOCに反映されないタイトルの例]
#がないため見出しにならず、TOCにも反映されません。
:::
↓
見出しにならず、TOCに反映されないタイトルの例
#がないため見出しになりません。
タイトルにHTMLを入れることも可能
+タイトルにHTMLを入れることも可能
なおタイトル部にHTMLを入れることもできます。TOCにも反映されます。
テスト123テスト下線
タイトル部のmarkdownは次の通りです。
#### テスト<sup>123</sup>テスト<u>下線</u>
カスタマイズに利用した機能
+カスタマイズに利用した機能
カスタマイズは次の機能を活用しました。
-RemarkとRehypeについて
+RemarkとRehypeについて
RemarkとRehypeは、MarkdownをHTMLに変換するプロセスにおいて、AST(抽象構文木・Abstract Syntax Tree)に作用するプラグインです。なおASTを操作するオープンソースのエコシステムの中にはもう1つRetextというプラグインもありますが、Docusaurusには実装されていないようです。
| ........................ 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でこれらのプラグインを利用するためにはdocusaurus.config.jsonに設定が必要です。このページによると次の4種類の設定値にてプラグインを登録できます。
デフォルトプラグイン適用前 | デフォルトプラグイン適用後 | |
---|---|---|
Remark Markdown形式 | beforeDefaultRemarkPlugins | remarkPlugins |
Rehype HTML形式 | beforeDefaultRehypePlugins | rehypePlugins |
MarkdownからHTMLへの変換処理のところで、Docusaurusは自前のプラグイン(デフォルトプラグイン)を使い「見出しにidをつける」「ASTからTOCを作成する」などの処理を行っています。そのため今回のように「Amonitionのタイトルを読んでTOCに反映する」ためには、デフォルトプラグイン適用前と適用後の両方のタイミングでの処理が必要になります。
-Swizzlingについて
+Swizzlingについて
Swizzlingはこちらに説明があるとおりの機能で、簡単に言うとReactのコンポーネントをカスタマイズできる機能です。
Swizzlingの設定をすると、Docusaurusがデフォルトのコンポーネントの代わりに自動的にカスタマイズしたコンポーネントを使用します。
今回は、デフォルトのAdmonitionにないID属性を持たせるためAdmonitionコンポーネントをカスタマイズしました。Swizzlingの設定をすることにより、デフォルトのAdmonitionの代わりにこのカスタムコンポーネントが使われるようにします。
-動作原理
+動作原理
TOCは「ASTに含まれているheading要素を単純に配列に入れている」だけですが、この処理はカスタマイズで上書きできません。そこで、カスタマイズできる処理だけでAdmonitionのタイトルをTOCに反映する方法として次を思いつき、実装しました。
実装
-docusaurus.config.json
+実装
+docusaurus.config.json
まずdocusaurus.config.jsonにimportとplugin設定を記入します(ハイライト部)。
これでDocusaurusデフォルトプラグイン適用の前後にそれぞれ自作のRemark/Rehypeプラグインが実行されることになります。
blogなどを入れている場合は、そのプロパティにも記載します。
import admonitionTitleToHeadingBeforeTOC from './src/remark/admonition-title-to-heading-before-toc.js';
import admonitionTitleToHeadingAfterTOC from './src/rehype/admonition-title-to-heading-after-toc.js';
export default {
// ...
presets: [
[
'classic',
/** @type {import('@docusaurus/preset-classic').Options} */
({
docs: {
// ...
beforeDefaultRemarkPlugins: [admonitionTitleToHeadingBeforeTOC],
rehypePlugins: [admonitionTitleToHeadingAfterTOC],
},
blog: {
// ...
beforeDefaultRemarkPlugins: [admonitionTitleToHeadingBeforeTOC],
rehypePlugins: [admonitionTitleToHeadingAfterTOC],
},
// ...
}),
]],
// ...
}
Remark/Rehypeプラグイン
+Remark/Rehypeプラグイン
次にプラグインを実装します。
docusaurusのsrcディレクトリ下にrehypeとremarkというディレクトリを作り、次のファイル名と内容で2つのプラグインを作ります。
-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;
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;
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;
参考までに、Remarkのプラグインから見るとAdmonitionのASTはたとえば次のようになっています。
{
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
次にSwizzlingです。
Docusaurusのsrc/themeディレクトリにAdmonitionというディレクトリを作り、次の一例のファイルを格納します。
なおここに説明がある通りSwizzlingにはEjectingとWrappingの方法があります。
WrappingではAdmonitionの内部までカスタマイズできないため「タイトル部分にidプロパティを付ける」といったことができません。そのためEjectingを使います。
Ejectingを使う場合はバージョンアップによってデフォルトのコンポーネントと挙動が変わってくる可能性があるのでアップグレードの際は注意が必要です。
Ejecting
+Ejecting
npm run swizzle @docusaurus/theme-classic Admonition -- --eject
Docusaurus V3.1では次のようなメッセージが出ますので、YESを選びます。
? Do you really want to swizzle this unsafe internal component? » - Use arrow-keys. Return to submit.
NO: cancel and stay safe
> YES: I know what I am doing!
Ejectingをすると、実質的にnode_modulesの@docusaurus/theme-classic/lib/themeにあるコンポーネントがsrc/themeディレクトリにコピーされます。
あとはコピーされたコンポーネントをいじるだけです。
-コードの変更
+コードの変更
Layoutファイルを次のように変更するのみです。
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;
// titleにHTML等が含まれている場合は文字列ではなく配列になる
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;
}
// スクロール位置調整のcss
const classNames = clsx("anchor", "title", headingStyles.anchorWithStickyNavbar);
// depthに応じて見出しタグをレンダー
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等の再起動が必要です。
diff --git a/build/blog/index.html b/build/blog/index.html index f36c9282..6fe22cb9 100644 --- a/build/blog/index.html +++ b/build/blog/index.html @@ -2,7 +2,7 @@ - +小平市立合気公園・開園セレモニー
小平市上水南町二丁目の佐川幸義邸跡地に新設された小平市立合気公園がついに開園しました!
-令和6年3月23日(土)午前10時から現地で行われた開園セレモニーに参加しましたので、写真と動画で様子をお知らせします。
合気公園の整備進捗状況
小平市上水南町二丁目の佐川幸義邸跡地に新設される合気公園の整備状況を見てきましたので、写真と動画で現在の様子をお知らせします。
-順調に進んでいるようですね😊
Docusaurusの注意書きや警告文のタイトルを見出しにして、目次にも乗せる方法
Docusaurus🦖
+小平市立合気公園・開園セレモニー
小平市上水南町二丁目の佐川幸義邸跡地に新設された小平市立合気公園がついに開園しました!
+令和6年3月23日(土)午前10時から現地で行われた開園セレモニーに参加しましたので、写真と動画で様子をお知らせします。
合気公園の整備進捗状況
小平市上水南町二丁目の佐川幸義邸跡地に新設される合気公園の整備状況を見てきましたので、写真と動画で現在の様子をお知らせします。
+順調に進んでいるようですね😊
Docusaurusの注意書きや警告文のタイトルを見出しにして、目次にも乗せる方法
Docusaurus🦖
1か月弱使いましたがこのDocusaurus(ドキュサウルス)は数あるCMSの中でも秀逸です。
文書作成と管理が容易で、拡張の自由度も非常に高く、完全なオープンソース。
-議員活動に重要な「資料を作成しまとめて公開するツール」として現状の最適解と感じます。
新ふらっとブログ立ち上げました
今後の展開を考え、これまでの議会活動等のまとめをこのDocusaurusの仕組みに移行しています。
+議員活動に重要な「資料を作成しまとめて公開するツール」として現状の最適解と感じます。
新ふらっとブログ立ち上げました
今後の展開を考え、これまでの議会活動等のまとめをこのDocusaurusの仕組みに移行しています。
ふらっとブログもこちらに新しく立ち上げることにしました。
議員活動や、その裏で活用している技術について、誰かの役に立つかもしれないことを中心にブログにしていきます😁
令和6年3月23日(土)午前10時から現地で行われた開園セレモニーに参加しましたので、写真と動画で様子をお知らせします。
-これまでの経緯
+これまでの経緯
合気公園に関してこれまでの経緯はこちら↓にまとめています。
セレモニーの様子
+セレモニーの様子
次のような次第で、約30分間行われました。
↑市長、市議会議長、木村氏、岡江氏により、顕彰碑の除幕が行われました。
式次第(小平市)
+式次第(小平市)
-そのほかの写真
+そのほかの写真
次の写真と動画はセレモニー直後と3月28日(木)に撮影したものです。
-出入口
+出入口
正面出入口
北東側出入口
園名碑
+園名碑
道場に使われていた屋根瓦(鬼瓦)の一部が埋め込まれています。道場関係の方によるデザインです。
顕彰碑
+顕彰碑
@@ -76,28 +76,28 @@ 円満に滞り無く動じて居るのである-
その調和が合氣なのである
日本庭園
+日本庭園
早くも子どもたちが石に上って遊んでいました😍
道場跡地横にあるベンチ
+道場跡地横にあるベンチ
緑色に舗装された部分が道場跡地です。
テーブル
+テーブル
-パーゴラとベンチ
+パーゴラとベンチ
-水飲み
+水飲み
-南西出入口への通路
+南西出入口への通路
-看板
+看板
-売却予定地
+売却予定地
佐川幸義氏のご遺族から土地約1,300㎡と現金約3,000万円をご遺贈いただき、さらにふるさと納税で小平市として過去最多の約1,600万円ものご寄附をいただきました。ありがとうございました。それでもなお整備費用として約2千万円が不足しているという理由から、小林洋子市政である小平市は1区画を売却するとしました。
路線価は約21万円/㎡ですので、土地の評価額は約2.7億円となります。合計で優に3億円もの寄附をいただいたことになります。また市民と市が理想的な形での協働ができたり、ふるさと納税で歴代記録を更新している画期的な事業だったわけですから、最後は市に残りの整備費用を出してもらいたかったところです。残念ですがこの1区画は令和6年度中に売却予定です。
-動画
+動画
Xに投稿した動画をご覧ください。
@@ -119,65 +119,65 @@順調に進んでいるようですね😊
-これまでの経緯
+これまでの経緯
合気公園に関してこれまでの経緯はこちら↓にまとめています。
-市民による公園整備案
+市民による公園整備案
旧佐川邸の公園化を考える会が市に提出した公園計画案はこちら↓(ランドスケープデザイナー・鈴木綾氏による)
-市の整備イメージ案
+市の整備イメージ案
上記の提案を受けて市が令和5年5月に提示した整備イメージ案がこちら↓
この整備イメージに基づいて整備が進んでいます。
-写真
+写真
次の写真と動画は令和6年1月30日(火)に撮影したものです。
-お知らせ
+お知らせ
-野外卓・顕彰碑・背なしベンチ
+野外卓・顕彰碑・背なしベンチ
養生されている?道場跡の部分が見えます。
-パーゴラ・顕彰碑
- -メッシュフェンス・ブロック
+パーゴラ・顕彰碑
+ +メッシュフェンス・ブロック
-水飲み場・東側出入口
+水飲み場・東側出入口
園名碑が少し写っています。トラックが止まっていて撮影できませんでした。
-水飲み場・東側出入口
+水飲み場・東側出入口
-庭園付近
+庭園付近
-西側出入口への道・目隠しフェンス
+西側出入口への道・目隠しフェンス
-南東の角
+南東の角
道路が拡幅されていることが分かります。
-庭園
+庭園
このあたりは今後手が入るのかもですね。
-売却予定地
+売却予定地
奥の砂が盛ってあるところが売却予定地です。
-拡幅された東側道路
+拡幅された東側道路
-道路拡幅は売却予定地前まで
+道路拡幅は売却予定地前まで
-佐川氏が四股を踏んでいたとされる石
+佐川氏が四股を踏んでいたとされる石
右下の平らな石が、佐川幸義氏が四股を踏んでいたとされる石です。
-西側出入口
+西側出入口
案内板はまだ設置されていません。
-西側園路と目隠しフェンス
+西側園路と目隠しフェンス
-西側の工事看板・目隠しフェンス上部
+西側の工事看板・目隠しフェンス上部
目隠しフェンス上部は人の乗り越えられないような突起が出ています。
-西側園路の舗装
+西側園路の舗装
-動画
+動画
1か月弱使いましたがこのDocusaurus(ドキュサウルス)は数あるCMSの中でも秀逸です。
文書作成と管理が容易で、拡張の自由度も非常に高く、完全なオープンソース。
議員活動に重要な「資料を作成しまとめて公開するツール」として現状の最適解と感じます。
Facebookが母体なのでいろいろと気になるところですが、Reactを初めとして有益なソフトウェアを完全なオープンソースとして提供してくれていることは純粋にありがたいと感じます。
-Admonitionのタイトルが見出しにならない
+Admonitionのタイトルが見出しにならない
さてDocusaurusにはAdmonition(注意書きや警告文)を容易にMarkdownで書く方法が用意されています。
たとえばinfoなら、次のようにMarkdownで書けば、
:::info[infoの例]
ここに文章を書く
:::
次のように本文中に見出しを書く方法もありますが
:::info
#### テスト
~文章~
:::
見た目がイマイチになります。
-テスト
~文章~
テスト
~文章~
ほかのユーザーからの要望も上がっており、私も少し不便に感じていたので、次の仕様になるようカスタマイズしましたのでその方法を解説します。
-カスタマイズ後はどうなるか
+カスタマイズ後はどうなるか
後述のカスタマイズをすると、Admonitionのタイトル部に(通常の見出しMarkdownと同様に)#を冒頭に2個以上入れる ことで見出しになります。またTOCにも反映されます。#を2個以上としているのは、H1をAdmonitionには使わないはずのため。#を入れない場合は見出しにならず、TOCにも反映されません。
-タイトル冒頭に#を入れた場合
+タイトル冒頭に#を入れた場合
:::info[#### 見出しになりTOCに反映されるタイトルの例]
#が4つ分のためH4見出しになります。TOCにも反映されます。
:::
↓
見出しになり、TOCに反映されるタイトルの例
#が4つ分のためH4見出しになります。TOCにも反映されます。
このブログでもTOCに表示されています。
なおマウスカーソルを乗せた際にハッシュリンク(#)が表示されるようにするにはCSSの設定が必要です。
タイトル冒頭に#を入れない場合
+タイトル冒頭に#を入れない場合
:::info[見出しにならずTOCに反映されないタイトルの例]
#がないため見出しにならず、TOCにも反映されません。
:::
↓
見出しにならず、TOCに反映されないタイトルの例
#がないため見出しになりません。
タイトルにHTMLを入れることも可能
+タイトルにHTMLを入れることも可能
なおタイトル部にHTMLを入れることもできます。TOCにも反映されます。
テスト123テスト下線
タイトル部のmarkdownは次の通りです。
#### テスト<sup>123</sup>テスト<u>下線</u>
カスタマイズに利用した機能
+カスタマイズに利用した機能
カスタマイズは次の機能を活用しました。
-RemarkとRehypeについて
+RemarkとRehypeについて
RemarkとRehypeは、MarkdownをHTMLに変換するプロセスにおいて、AST(抽象構文木・Abstract Syntax Tree)に作用するプラグインです。なおASTを操作するオープンソースのエコシステムの中にはもう1つRetextというプラグインもありますが、Docusaurusには実装されていないようです。
| ........................ 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でこれらのプラグインを利用するためにはdocusaurus.config.jsonに設定が必要です。このページによると次の4種類の設定値にてプラグインを登録できます。
デフォルトプラグイン適用前 | デフォルトプラグイン適用後 | |
---|---|---|
Remark Markdown形式 | beforeDefaultRemarkPlugins | remarkPlugins |
Rehype HTML形式 | beforeDefaultRehypePlugins | rehypePlugins |
MarkdownからHTMLへの変換処理のところで、Docusaurusは自前のプラグイン(デフォルトプラグイン)を使い「見出しにidをつける」「ASTからTOCを作成する」などの処理を行っています。そのため今回のように「Amonitionのタイトルを読んでTOCに反映する」ためには、デフォルトプラグイン適用前と適用後の両方のタイミングでの処理が必要になります。
-Swizzlingについて
+Swizzlingについて
Swizzlingはこちらに説明があるとおりの機能で、簡単に言うとReactのコンポーネントをカスタマイズできる機能です。
Swizzlingの設定をすると、Docusaurusがデフォルトのコンポーネントの代わりに自動的にカスタマイズしたコンポーネントを使用します。
今回は、デフォルトのAdmonitionにないID属性を持たせるためAdmonitionコンポーネントをカスタマイズしました。Swizzlingの設定をすることにより、デフォルトのAdmonitionの代わりにこのカスタムコンポーネントが使われるようにします。
-動作原理
+動作原理
TOCは「ASTに含まれているheading要素を単純に配列に入れている」だけですが、この処理はカスタマイズで上書きできません。そこで、カスタマイズできる処理だけでAdmonitionのタイトルをTOCに反映する方法として次を思いつき、実装しました。
実装
-docusaurus.config.json
+実装
+docusaurus.config.json
まずdocusaurus.config.jsonにimportとplugin設定を記入します(ハイライト部)。
これでDocusaurusデフォルトプラグイン適用の前後にそれぞれ自作のRemark/Rehypeプラグインが実行されることになります。
blogなどを入れている場合は、そのプロパティにも記載します。
import admonitionTitleToHeadingBeforeTOC from './src/remark/admonition-title-to-heading-before-toc.js';
import admonitionTitleToHeadingAfterTOC from './src/rehype/admonition-title-to-heading-after-toc.js';
export default {
// ...
presets: [
[
'classic',
/** @type {import('@docusaurus/preset-classic').Options} */
({
docs: {
// ...
beforeDefaultRemarkPlugins: [admonitionTitleToHeadingBeforeTOC],
rehypePlugins: [admonitionTitleToHeadingAfterTOC],
},
blog: {
// ...
beforeDefaultRemarkPlugins: [admonitionTitleToHeadingBeforeTOC],
rehypePlugins: [admonitionTitleToHeadingAfterTOC],
},
// ...
}),
]],
// ...
}
Remark/Rehypeプラグイン
+Remark/Rehypeプラグイン
次にプラグインを実装します。
docusaurusのsrcディレクトリ下にrehypeとremarkというディレクトリを作り、次のファイル名と内容で2つのプラグインを作ります。
-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;
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;
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;
参考までに、Remarkのプラグインから見るとAdmonitionのASTはたとえば次のようになっています。
{
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
次にSwizzlingです。
Docusaurusのsrc/themeディレクトリにAdmonitionというディレクトリを作り、次の一例のファイルを格納します。
なおここに説明がある通りSwizzlingにはEjectingとWrappingの方法があります。
WrappingではAdmonitionの内部までカスタマイズできないため「タイトル部分にidプロパティを付ける」といったことができません。そのためEjectingを使います。
Ejectingを使う場合はバージョンアップによってデフォルトのコンポーネントと挙動が変わってくる可能性があるのでアップグレードの際は注意が必要です。
Ejecting
+Ejecting
npm run swizzle @docusaurus/theme-classic Admonition -- --eject
Docusaurus V3.1では次のようなメッセージが出ますので、YESを選びます。
? Do you really want to swizzle this unsafe internal component? » - Use arrow-keys. Return to submit.
NO: cancel and stay safe
> YES: I know what I am doing!
Ejectingをすると、実質的にnode_modulesの@docusaurus/theme-classic/lib/themeにあるコンポーネントがsrc/themeディレクトリにコピーされます。
あとはコピーされたコンポーネントをいじるだけです。
-コードの変更
+コードの変更
Layoutファイルを次のように変更するのみです。
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;
// titleにHTML等が含まれている場合は文字列ではなく配列になる
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;
}
// スクロール位置調整のcss
const classNames = clsx("anchor", "title", headingStyles.anchorWithStickyNavbar);
// depthに応じて見出しタグをレンダー
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等の再起動が必要です。
diff --git a/build/blog/tags/docusaurus/index.html b/build/blog/tags/docusaurus/index.html index abcc0473..d43f01ff 100644 --- a/build/blog/tags/docusaurus/index.html +++ b/build/blog/tags/docusaurus/index.html @@ -2,7 +2,7 @@ - +「docusaurus」タグの記事が2件件あります
全てのタグを見るDocusaurusの注意書きや警告文のタイトルを見出しにして、目次にも乗せる方法
Docusaurus🦖
+「docusaurus」タグの記事が2件件あります
全てのタグを見るDocusaurusの注意書きや警告文のタイトルを見出しにして、目次にも乗せる方法
Docusaurus🦖
1か月弱使いましたがこのDocusaurus(ドキュサウルス)は数あるCMSの中でも秀逸です。
文書作成と管理が容易で、拡張の自由度も非常に高く、完全なオープンソース。
-議員活動に重要な「資料を作成しまとめて公開するツール」として現状の最適解と感じます。
新ふらっとブログ立ち上げました
今後の展開を考え、これまでの議会活動等のまとめをこのDocusaurusの仕組みに移行しています。
+議員活動に重要な「資料を作成しまとめて公開するツール」として現状の最適解と感じます。
新ふらっとブログ立ち上げました
今後の展開を考え、これまでの議会活動等のまとめをこのDocusaurusの仕組みに移行しています。
ふらっとブログもこちらに新しく立ち上げることにしました。
議員活動や、その裏で活用している技術について、誰かの役に立つかもしれないことを中心にブログにしていきます😁
「v3.1」タグの記事が1件件あります
全てのタグを見るDocusaurusの注意書きや警告文のタイトルを見出しにして、目次にも乗せる方法
Docusaurus🦖
+「v3.1」タグの記事が1件件あります
全てのタグを見るDocusaurusの注意書きや警告文のタイトルを見出しにして、目次にも乗せる方法
Docusaurus🦖
1か月弱使いましたがこのDocusaurus(ドキュサウルス)は数あるCMSの中でも秀逸です。
文書作成と管理が容易で、拡張の自由度も非常に高く、完全なオープンソース。
議員活動に重要な「資料を作成しまとめて公開するツール」として現状の最適解と感じます。
「お知らせ」タグの記事が1件件あります
全てのタグを見る新ふらっとブログ立ち上げました
今後の展開を考え、これまでの議会活動等のまとめをこのDocusaurusの仕組みに移行しています。
+「お知らせ」タグの記事が1件件あります
全てのタグを見る新ふらっとブログ立ち上げました
今後の展開を考え、これまでの議会活動等のまとめをこのDocusaurusの仕組みに移行しています。
ふらっとブログもこちらに新しく立ち上げることにしました。
議員活動や、その裏で活用している技術について、誰かの役に立つかもしれないことを中心にブログにしていきます😁
「まちづくり」タグの記事が2件件あります
全てのタグを見る小平市立合気公園・開園セレモニー
小平市上水南町二丁目の佐川幸義邸跡地に新設された小平市立合気公園がついに開園しました!
-令和6年3月23日(土)午前10時から現地で行われた開園セレモニーに参加しましたので、写真と動画で様子をお知らせします。
合気公園の整備進捗状況
小平市上水南町二丁目の佐川幸義邸跡地に新設される合気公園の整備状況を見てきましたので、写真と動画で現在の様子をお知らせします。
+「まちづくり」タグの記事が2件件あります
全てのタグを見る小平市立合気公園・開園セレモニー
小平市上水南町二丁目の佐川幸義邸跡地に新設された小平市立合気公園がついに開園しました!
+令和6年3月23日(土)午前10時から現地で行われた開園セレモニーに参加しましたので、写真と動画で様子をお知らせします。
合気公園の整備進捗状況
小平市上水南町二丁目の佐川幸義邸跡地に新設される合気公園の整備状況を見てきましたので、写真と動画で現在の様子をお知らせします。
順調に進んでいるようですね😊
「佐川幸義氏」タグの記事が2件件あります
全てのタグを見る小平市立合気公園・開園セレモニー
小平市上水南町二丁目の佐川幸義邸跡地に新設された小平市立合気公園がついに開園しました!
-令和6年3月23日(土)午前10時から現地で行われた開園セレモニーに参加しましたので、写真と動画で様子をお知らせします。
合気公園の整備進捗状況
小平市上水南町二丁目の佐川幸義邸跡地に新設される合気公園の整備状況を見てきましたので、写真と動画で現在の様子をお知らせします。
+「佐川幸義氏」タグの記事が2件件あります
全てのタグを見る小平市立合気公園・開園セレモニー
小平市上水南町二丁目の佐川幸義邸跡地に新設された小平市立合気公園がついに開園しました!
+令和6年3月23日(土)午前10時から現地で行われた開園セレモニーに参加しましたので、写真と動画で様子をお知らせします。
合気公園の整備進捗状況
小平市上水南町二丁目の佐川幸義邸跡地に新設される合気公園の整備状況を見てきましたので、写真と動画で現在の様子をお知らせします。
順調に進んでいるようですね😊
「公園」タグの記事が2件件あります
全てのタグを見る小平市立合気公園・開園セレモニー
小平市上水南町二丁目の佐川幸義邸跡地に新設された小平市立合気公園がついに開園しました!
-令和6年3月23日(土)午前10時から現地で行われた開園セレモニーに参加しましたので、写真と動画で様子をお知らせします。
合気公園の整備進捗状況
小平市上水南町二丁目の佐川幸義邸跡地に新設される合気公園の整備状況を見てきましたので、写真と動画で現在の様子をお知らせします。
+「公園」タグの記事が2件件あります
全てのタグを見る小平市立合気公園・開園セレモニー
小平市上水南町二丁目の佐川幸義邸跡地に新設された小平市立合気公園がついに開園しました!
+令和6年3月23日(土)午前10時から現地で行われた開園セレモニーに参加しましたので、写真と動画で様子をお知らせします。
合気公園の整備進捗状況
小平市上水南町二丁目の佐川幸義邸跡地に新設される合気公園の整備状況を見てきましたので、写真と動画で現在の様子をお知らせします。
順調に進んでいるようですね😊
「合気公園」タグの記事が2件件あります
全てのタグを見る小平市立合気公園・開園セレモニー
小平市上水南町二丁目の佐川幸義邸跡地に新設された小平市立合気公園がついに開園しました!
-令和6年3月23日(土)午前10時から現地で行われた開園セレモニーに参加しましたので、写真と動画で様子をお知らせします。
合気公園の整備進捗状況
小平市上水南町二丁目の佐川幸義邸跡地に新設される合気公園の整備状況を見てきましたので、写真と動画で現在の様子をお知らせします。
+「合気公園」タグの記事が2件件あります
全てのタグを見る小平市立合気公園・開園セレモニー
小平市上水南町二丁目の佐川幸義邸跡地に新設された小平市立合気公園がついに開園しました!
+令和6年3月23日(土)午前10時から現地で行われた開園セレモニーに参加しましたので、写真と動画で様子をお知らせします。
合気公園の整備進捗状況
小平市上水南町二丁目の佐川幸義邸跡地に新設される合気公園の整備状況を見てきましたので、写真と動画で現在の様子をお知らせします。
順調に進んでいるようですね😊
「技術」タグの記事が1件件あります
全てのタグを見るDocusaurusの注意書きや警告文のタイトルを見出しにして、目次にも乗せる方法
Docusaurus🦖
+「技術」タグの記事が1件件あります
全てのタグを見るDocusaurusの注意書きや警告文のタイトルを見出しにして、目次にも乗せる方法
Docusaurus🦖
1か月弱使いましたがこのDocusaurus(ドキュサウルス)は数あるCMSの中でも秀逸です。
文書作成と管理が容易で、拡張の自由度も非常に高く、完全なオープンソース。
議員活動に重要な「資料を作成しまとめて公開するツール」として現状の最適解と感じます。