フィルターされたNotionページによるMediaWiki

提供:Turgenev's Wiki
2023年11月24日 (金) 19:55時点におけるTurgenev (トーク | 投稿記録)による版 (Notion-MW)

Notionのページから下書きを除去した上でMediaWikiフォーマットに変換してアップロードする」というこのサイトの動作機構に関して説明する。

概要

Notionのページを見出しによって定まる木構造データとみなす。各見出しは無色(デフォルト)である場合と、黄色背景である場合があるとする。この木構造データに対し、「無色の頂点を全て切り落とす」という変換をする。すなわち、自らを包含する見出しが全て黄色であるコンテンツだけを残す。

記事データにこのような変換を施すことで、同じページ内でも場所ごとに公開/非公開を簡便に切り替えられるようにするというのが基本的なアイデアである。

  • 例外的に、最初の見出しより前に書いてあるテキストは、必ず出力させる。つまり記事の冒頭に仮想的な「レベル0」の見出しがあると考えればよい。
  • Notionではブロックを選択してショートカットキー「Ctrl+Shift+H」を押すことで直近に適用されたものと同じ色を適用することができるので、複数種類の色を使い分けているのでなければキー一発で公開/非公開を切り替えられる。

さらに、このコンテンツをMediaWikiで公開することで、変更履歴を誰でも見ることができるようになる。これによりどのタイミングでどの箇所が修正されたかというメタコンテンツを「〇月〇日追記」のような形でコンテンツと混在させる必要がなくなる。

また、その他にも頻繁に使われるメタ情報はMediaWikiのテンプレートとして挿入する。例えば、

  • ある見出しに属する(公開の)内容が完全に空(ほかの見出しすら含まれない)だった場合は、「作業のため一時的に非公開となっている可能性があります。[履歴]もご確認ください。」と表示するUnderConstruction‏‎テンプレートを挿入する。これにより、既に公開した内容を大幅変更のためなどで一時的に非公開にしたい場合は、該当の見出しの直後に同じレベルの無色の見出し(空でもよい)を挿入すればよいということになる。ただし、あまり使われていない。
  • 内容は存在するものの完成度が低く、今後加筆予定であることを表すStubテンプレート。
  • 執筆時点で古くなっている情報を表すOutdatedテンプレート。
  • 他サイトにあまり書かれておらず貴重と思われる情報を表すValuableテンプレート。

データベース部分の設計

notion-mw-db.jpg

このような感じで、「使うNotionページへのリンク」「スクリプトによる変換&更新の対象にするかどうか」(←これは今はもう使われていない)「MediaWikiでのID」「MediaWikiでのタイトル」「MediaWikiでのカテゴリー」「サイトの更新日時(=スクリプトによってMediaWiki側での編集が完了したタイミング)」をデータベースとして保持している。

ページの新規作成時は「MediaWikiでのID」に-1を使おうと思ったが、なぜかここが-1に分かれたrich_textの配列としてデータが返ってくることがあって困ったので-だけにするように変更した。

Notion側でもページの最終更新日時が取れるので、それをこの表の更新日時と比べて、反映されていない更新があったら適用するという実装にしている。(ただし変換スクリプト自体を変更したときなどは全ページ生成し直す)

サイトマップの生成

上記でsandboxタグが入っているもの以外が検索サイトにインデックスしてほしいものたちである。そこで、編集完了後に、表の更新日時を使ってxmlサイトマップをスクリプトから生成する。

変換コードの概説

大まかな流れは以下の通り。

  • 上記データベースの内容をNotion APIから取得する。以下、各行ごとに処理。
  • ページの内容を取得し、黄色い見出しだけ残す変換を行う。Notionのブロックのリストができる
  • notion-to-mdのblocksToMarkdownでマークダウン形式に変換する。
  • いくつか前処理を行う。
  • pandocを用いてMediaWikiに変換する。
  • いくつか後処理(整形)を行う
  • 完成した記事データをMediaWiki APIで投稿する。

その他、ページのリネーム、新規作成時のIDの反映(-を数字に変える)、既存の記事を新規作成しようとした場合のエラーなど、前後に多少の処理がある。

特筆すべき部分のみ以下で説明する。(おおむね処理の進行順)

toggle

detail-summaryタグではなくMediaWikiの標準の折りたたみ形式であるmw-collapsibleタグに変更。notion-to-md側を変えるのではなくpandocで変換した後のmediawikiでdetailsタグを置換している。

さらに、summary部分の文字列の先頭に?が付いていたら折りたたみ状態に変える。これも置換でできる。

  • 完全な仕様としては例えばこんな感じ?(未実装)
    • 先頭が?でない→折りたたまない
    • 先頭が?
      • ?の次が???を削除し、折りたたみ
      • ?の次が!?!を削除し、折りたたまない
      • その他→?だけを削除し、折りたたむ

pandocの実行

"><code>gfm</code>(Github Flavored Markdown)から<code>mediawiki</code>に変換する。<code>&#45;&#45;shift&#45;heading&#45;level&#45;by&#61;1</code>は、[https://www.mediawiki.org/wiki/Help:Formatting/ja Help&#58;書式整形 &#45; MediaWiki]にもある通り、MediaWikiの記事内ではレベル1の見出しは非推奨だからである。

=== 画像 ===

キャプションが未対応([https://github.com/souvikinator/notion-to-md/issues/63 https://github.com/souvikinator/notion-to-md/issues/63])なので←に従ってsetCustomTransformerをする。

[https://zenn.dev/st43/articles/8e2f9c48761e59 Notion API(v0.4.4)で画像を扱う]のようにNotionの内部画像の扱いは面倒なので当サイトではすべて外部ストレージ(もっと言えばMediaWiki内部)にアップロードしている。書式も適宜変換する。

<span id="カテゴリ情報seo情報"></span>
=== カテゴリ情報、seo情報 ===

データベースにあるタイトルとかtagsデータから生成する。seoについては[[MediaWiki|MediaWiki]] も参照。

=== 各種の整形処理 ===

一部コンテンツはnotion&#45;to&#45;mdで生成されるマークダウンそのままだと不都合があるので、専用のコードを入れて変換している。場合によってはnotion&#45;to&#45;md本体側にも変更を入れている(一旦特殊なエスケープシーケンスに変換しておいてpandocに触られるのを回避し、MediaWikiが返ってきてから自前で編集するようにしている箇所もある)。

具体的には以下の通り。

* 特殊文字の処理
** <code>&#42;</code>などがマークダウンにおける特殊文字と解釈されてしまうのを防ぐためエスケープ。また、<code>&#123;&#125;</code>や<code>&#126;</code>などはMediaWiki上の特殊文字であり、これもやはりエスケープする。
* ページ間リンク
** Notionでのページ間リンクをMediaWikiでのページ間リンクに変換するため、先ほどのデータベース表に従って<code>&#91;ページタイトル&#93;(/ページタイトル)</code>という形式に変換する(するとpandocが自動で<code>&#91;&#91;ページタイトル|ページタイトル&#93;&#93;</code>に変換してくれる)

=== テンプレート挿入 ===

「!s」だけの行があったら&#123;&#123;Stub&#125;&#125;テンプレートに変換する、というような実装にしている。

== オリジナルのnotion&#45;to&#45;mdへの変更 ==

上記のような良い感じのMediaWikiに変換する目的と、単純にできたばかりでバグが多いため、色々変えている。

=== 数式関連 ===

以前は変えていたが[https://github.com/souvikinator/notion-to-md/commit/fdd96ca0362663c914cd5fde62baef520dae6763 https://github.com/souvikinator/notion-to-md/commit/fdd96ca0362663c914cd5fde62baef520dae6763]で改善されたため不要

<span id="mdts内"></span>
=== <code>md.ts</code>内 ===

==== 書式 ====

太字、斜体、取り消し線、コード(<code>`</code>)をそれぞれhtmlタグのstrong, em, s, codeに変更。<code>```</code>は変えていない(markdownにおける&lt;pre&gt;の動作が怪しかったため)。

==== ハイパーリンク ====

pandocでの変換においてハイパーリンクの後にスペースなどを開けずに文字が続くとバグることがあるので、他で使わない固定文字列のセパレータを挿入し、変換後に消す。

<span id="codeblock"></span>
==== codeBlock ====

コードにおいて、言語がpythonなどの良く知られたものに設定されていると、pandocによって<strong>syntaxhighlight</strong>タグに変えられてしまう。今回はSyntaxHightlight拡張機能が無効のMediaWikiを使っているので、言語にダミー文字列を付加する(付加ではなくダミー文字列だけにしても変わらないが、情報を落とすのは勿体ない気がしたので)。またpandoc側の問題で<code>visual basic</code>のように言語名にスペースが入っていると<code>visual</code>だけになってしまうようなので、スペースをアンダースコアに置換。

<syntaxhighlight lang="language = "dummy_str_"+language?.replace(" ", "_");//for MediaWiki without SyntaxHightlight extension

">

notion-to-md.ts

特殊文字エスケープ

大きく分けて、notion-to-mdの欠点を補うための弱いエスケープ(\の付加)と、pandocでの変換を回避するための強いエスケープ(数値に変換して他で使わないセパレータで囲う)がある。

解説が面倒なのでコードだけとりあえず。

//`のみcodeに変え、```は```のままという仕様の場合
          if (content.type === "text"){
            if (type == "code"){
              //```の内部では<>は無効なので本来は不要だが、detailsまわりが修正されるまで、<>を最強エスケープの対象としておく。
              //無事修正されたので、対象外に
              //plain_text =  plain_text.replace(/[<>]/g, (c) =>"m_esc_" + c.charCodeAt(0)+"_m")
              //バッククォートはmarkdownの```の中でも有害になる珍しい文字であるが、外側と対処法が異なる。
              //markdownでコード内に`を書くには、両側をそれより多い`で囲うしかなく(\`や&#96;ではそれがそのまま表示されてしまう)、その実装が面倒である。
              //そこで、最強エスケープをかけて、MediaWikiに変換してから元に戻す。ちなみにMediaWikiでは`は特殊文字ではないのでエスケープ文字ではなくベタ書きに戻す。
              plain_text =  plain_text.replace(/`/g, (c) => "m_esc_" + c.charCodeAt(0)+"_m")
              
            }else{//テキストエリア
              //以下は、MediaWikiの特殊文字であるにもかかわらずpandocがそのままべた書きしてしまう文字。最強エスケープが必要。
              //これらはMarkdownの```およびMediaWikiのpreの内側では無害である。(外側では特殊文字であるものとそうでないものがある。)
              plain_text = plain_text.replace(/[=;:#*'{}~\-\[\]]/g, (c) => "m_esc_" + c.charCodeAt(0)+"_m")
              
              //これらは、Markdownのテキストエリアでは特殊文字だが、MediaWikiの特殊文字ではないあるいはpandocがちゃんと扱えるもの。
              //(ただし、"."(ピリオド)のみ、「リンクテキストもURLになっているリンク及びcode内に書かれた(リンクでない)URLがpandocの変換でおかしくなるのを防ぐ」という目的で入れている。)
              //この変換は先ほどの最強エスケープより後でなければいけない。(#が最強エスケープされるのを防ぐため)(もちろん同時でもいい)
              //スラッシュでのエスケープだと`<`でバグったことがあった(おそらくpandocのせい)ので、&#xx;を使用
              plain_text = plain_text.replace(/[`$+&<>\\.]/g, (c) => "&#" + c.charCodeAt(0)+";")

              //2つ以上の連続する空白文字は(そのように表示したい特別な理由があると考えて)&nbsp;に変換。これは&のエスケープより後である必要がある。
              plain_text = plain_text.replace(/ {2,}/g, (c) => c.split('').map(function() { return '&nbsp;'; }).join(''))

              //notion-to-mdがブロック内改行に対応していないので対応。nbspへの変換より後に行う。
              plain_text = plain_text.replace(/\n/g, "  \n")
            }
            //また、"も、mediawikiでの特殊文字ではないが、pandocでは&quot;に変換される。
            //ベタ書きでも表示には影響ないが、span id=""の中身に生のダブルクォーテーションが入ってしまう副作用があるし、その他問題が起きる可能性がある。
            //下記のように最強エスケープすればそのまま書けるが、&quot;を"にしてデータ量を削減するより安全を取ったほうが良さそうである。
            //ちなみに1文字減らすために最強エスケープから&#34;に戻す方法もあるが、&quot;のほうが自動でspan id=""の中身から除去されるようなのでそのほうが良さそう。
            //plain_text = plain_text.replace(/["]/g, (c) => "m_esc_" + c.charCodeAt(0)+"_m")
          }

ブロック内改行

NotionではShift+Enterによりブロック内での改行が可能であるが、notion-to-mdはこれに対応していないので、Markdownの文法に従って行末に半角スペースを2つ追加する。

また、改行を連続して2つ入れると(末尾に空白2つがあっても)Markdownの改段落扱いになり、意図した表示にならないため、避ける。

コードブロックへのキャプション

notion-to-mdでは未対応である。とりあえず独自でつけている。

ページ間リンク

ページ間のメンションであれば、MediaWiki上での記事リンクにする(前述)。そのためのマーカーを付加しておく。

取り消し線&下線の部分の除去

取り消し線と下線が両方指定されているテキストは除去する。これは見出しごとではなく特定の文言だけ非公開にしたい場合のための措置だが、怖いのであまり使っていない。