見出し画像

Obsidian で構造的な文章を書くためのいくつかの方法

 Obsidian の Outliner を文章に変えるスクリプトの進化形です。


概要

  • Obsidian の箇条書きを文章に変換する二つのスクリプトをご紹介します。

なぜ作った?

 アウトライナーで文章が書きたいと思って作り始めたスクリプトですが、やっぱりいろいろ制約がありました。
 その中で、「じゃあチェックリストも変換したいな」「埋め込みでコード表示できたらええやん」という感じでどんどん膨らみまして、こうなりました。

前提

 実行には、Obsidian のコミュニティプラグイン Templater が必要です。また、Outliner プラグインの導入も推奨しています。
 実は以前のバージョンで書き忘れていたんですが、実行する際は設定から「タブの使用」をオフにしてください。スペースでインデント幅などを判定しているため、タブを使用していると正しい結果を得られません(ただし正規表現に明るい方は、スペースをタブに書き換えてお使いになれる可能性がございます)。
 また、以下のセクションで用いている水平線は、`---`ではなく`___`です。これは、フロントマターとの区別をつけるためです。

用語定義

 独自の用語定義です。

パッセージとセクション

セクション

 ``で囲まれた、リスト形式の文章のこと。
 より具体的に言うと、`- `から始まるリストを`
`で囲んである一区切りのこと。`___`を基準として、カーソルが処理すべき範囲を判定します。
 今回、チェックリストも処理できるようになりましたが、番号付きリストは処理できません(後日、別のスクリプトをご紹介しようと思っています)。

パッセージ

 セクションを含み、見出しのある文章のこと。
 見出しも変換されます。

 とりあえずパッセージをテンプレートとして登録しておくと、このあとのスクリプトが使いやすくなります。

①SectionLine

 セクションを出力するためのスクリプトです。
 Javascript を実行できるプラグイン Templater にスクリプトを登録します。

<%*
// エディタと現在のカーソルの位置を取得
const editor = this.app.workspace.activeLeaf.view.editor;
const curCur = editor.getCursor();
// カーソルのある位置から、まず先頭位置を捕捉
let str = editor.getRange(editor.offsetToPos(0), curCur);
const preSec = str.match(/(.*___\s).*?$/s);
// セクションの終了位置を捕捉
str = editor.getRange(curCur, editor.offsetToPos(editor.getValue().length)); 
const surSec = str.match(/(.*?)___\s.*$/s);
// オフセットがない場合はこのページのすべてを取得する
let startOffset = 0;
let endOffset = editor.getValue().length;
if(preSec) { startOffset = preSec[1].length; }
if(surSec) { endOffset = editor.posToOffset(curCur)+surSec[1].length; }
// 範囲確定
str = editor.getRange(editor.offsetToPos(startOffset), editor.offsetToPos(endOffset)); 

// 範囲を分割して別途指定→処理
const lines = str.split('\n');
let result = [];
lines.forEach((line, index) => {
    uncheckA = (line.startsWith(`- [ ]`))
    uncheckB = (line.startsWith(`  - [ ]`))||(line.startsWith(`    - [ ]`))
    quote = (line.startsWith(`  - ![[`))||(line.startsWith(`  - [x] ![[`))
    one = line.startsWith(`- `)
    two = line.startsWith(`  - `)
    three = line.startsWith(`    - `)
    if (index === 0) {
        result.push(`# ${line.trim().slice(2).replace(/\[ \] /gm,"")}`);
    } else {
      // チェックされていないものは出力しない(ただし一行目は上記の通り、強制的に見出しにする)
        if (uncheckA){
            result.push(`\n`);
        } else if (uncheckB){
            result.push(``);
        } else if (one){
            result.push(`\n\n## ${line.trim().slice(2)}`);
        } else if (quote){
            result.push(`\n\n${line.replace(/!/gm,"!").trim().slice(2)}\n`);
        } else if (two){
            result.push(`\n ${line.trim().slice(2)}`);
        } else if (three) {
            result.push(`${line.trim().slice(2)}`);
        }
    }
});
finalResult = result.join('');
finalResult = finalResult.replace(/^ \[x\] /gm," ").replace(/\[x\] /gm,"")
.replace(/^ +(?=「)/gm, ""); // チェックボックスと行の先頭の空白を削除
navigator.clipboard.writeText(finalResult);
new Notice("セクションのテキストを変換してコピーしました");
-%>

 上記のセクションで実行すると、クリップボードに処理された文章が格納されます。貼り付けると、以下のようになります。

このセクションの場合、
セクションの内部のみが変換されます。

仕様

 リストと、チェックリストのうちチェックされたものが出力されます。一段落目でチェックがされていない場合は空行が挿入されます。
 例によって全角空白字下げが入りますが、行頭に鍵括弧がある場合は字下げされません。このあたりは日本語長文段落の暗黙のルールに従った処理を行っています。

新機能

 二段目のみ埋め込みに対応しました。
 一段目はタイトル、三段目はキャプション的な役割を持たせているので、段落の頭になる二段目のみを対象としています。
 使い方ですが、全角の「!」マークに続いて`[[` `]]` で囲んだリンクを入力してください。このとき、!マークは半角ではありません。半角にすると、アウトライナーの状態で埋め込みがダイレクトに表示されちゃって読みづらいので、あえて全角にしています。
 埋め込みはチェックボックスにも対応しておりまして、チェックを入れれば埋め込み、入れなければ無視できるようになっています。

②HeadLine

 パッセージを出力するためのスクリプトです。

<%*
// エディタと現在のカーソルの位置を取得
const editor = this.app.workspace.activeLeaf.view.editor;
const curCur = editor.getCursor();
// カーソルのある位置から、まず先頭位置を捕捉
let str = editor.getRange(editor.offsetToPos(0), curCur);
const preSec = str.match(/(.*##\s).*?$/s);
// セクションの終了位置を捕捉
str = editor.getRange(curCur, editor.offsetToPos(editor.getValue().length)); 
const surSec = str.match(/(.*?)___\s.*$/s);
// オフセットがない場合はこのページのすべてを取得する
let startOffset = 0;
let endOffset = editor.getValue().length;
if(preSec) { startOffset = preSec[1].length; }
if(surSec) { endOffset = editor.posToOffset(curCur)+surSec[1].length; }
// 範囲確定
str = editor.getRange(editor.offsetToPos(startOffset), editor.offsetToPos(endOffset)); 

// 以降はSectionOutに近い処理方法です。
const lines = str.split('\n');
let result = [];
lines.forEach((line, index) => {
    uncheckA = (line.startsWith(`- [ ]`))
    uncheckB = (line.startsWith(`  - [ ]`))
    quote = (line.startsWith(`- ![[`))||(line.startsWith(`- [x] ![[`))
    Head = line.startsWith(`## `)
    one = line.startsWith(`- `)
    two = line.startsWith(`  - `)
    if (index === 0) {
        result.push(`# ${line.replace(/\[ \] /gm,"")}`);
    } else {
      // チェックされていないものは出力しない(ただし一行目は上記の通り、強制的に見出しにする)
        if (uncheckA){
            result.push(`\n`);
        } else if (uncheckB){
            result.push(``);
        } else if (Head){
            result.push(`\n\n# ${line.trim().slice(2)}`);
        } else if (quote){
            result.push(`\n\n${line.replace(/!/gm,"!").trim().slice(2)}\n`);
        } else if (one){
            result.push(`\n ${line.trim().slice(2)}`);
        } else if (two) {
            result.push(`${line.trim().slice(2)}`);
        }
    }
});
finalResult = result.join('');
finalResult = finalResult.replace(/^ \[x\] /gm," ").replace(/\[x\] /gm,"")
.replace(/^ +(?=「)/gm, ""); // チェックボックスと行の先頭の空白を削除
navigator.clipboard.writeText(finalResult);
new Notice("パッセージのテキストを変換してコピーしました");
-%>

実行すると……

これが
こうなります

 細かい違いですが、SectionLine とは異なる出力になっていることがわかると思います。

仕様

  • 見出しは H1 に変換されます。

  • 一段目がそのまま段落になり字下げされ、二段目は改行なしでそれに続く形になります。

  • SectionLine 同様、埋め込みやチェックボックスに対応済みです。仕様は同一。

  • 見出しは H2 をデフォルト設定としています。適宜、書き換えるなどしてください。

まとめ

 雑なコードですが、チェックリストも処理できるようになると色々はかどりますね。かなりガラパゴス化してきたし、これからも更新していくと思うので、ちょっと note だと情報発信が難しくなってきている気もしますが。
 不具合報告はコメントに書いていただければ、できる範囲ならできる時間にChatGPTくんを酷使して対応します。
 ちなみに、この記事を書く大本のきっかけになった masaki さんのこちらのプラグインもかなり進化しているので、ぜひお試しを。

この記事が気に入ったらサポートをしてみませんか?