Skip to content
Go back

Obsidianのルビ記法プラグインを改造する・その2

  • 先日作ったやつのバグ取り
    • visibleRangesが行の中間で範囲を分割した場合、同じ行が複数回処理されてRangeSetBuilder.addが例外を投げることがあったので、処理済みの部分をスキップするようにした
    • 早期離脱したい箇所で早期離脱できないselections.forEachが使われていてもったいなかったので、forループに置き換えた
    • 装飾方法を変更した結果、正規表現に使用しないグループが生まれたので、少しだけ正規表現に手を加えた
    • もう少し扱いやすくなるように、関数単位で置換えられるようにまとめた
  • 結果、以下のようになった
    • 手元のスクリプトだと、y=require("@codemirror/view")V=require("@codemirror/state")u=RubyRegexになっているけど、バージョンによって異なるかも
// グローバルに以下を追記する
var cmLanguage=require("@codemirror/language");

var NovelRubyInvisibleWidget = class extends y.WidgetType { toDOM() { let s = document.createElement("span"); return (s.style.width = "0px"), (s.style.display = "inline-block"), (s.style.overflow = "hidden"), s } ignoreEvent() { return false; } };
// 同名の関数を以下で置き換える
buildDecorations(view) {
    // 外の変数をビルド前の名前にリネームする
    const RangeSetBuilder = V.RangeSetBuilder;
    const RubyRegex = u;
    const syntaxTree = cmLanguage.syntaxTree;
    const Decoration = y.Decoration;

    // ルビ記法に装飾を付ける
    const builder = new RangeSetBuilder();
    const selections = [...view.state.selection.ranges];
    const doc = view.state.doc;
    let latest = 0;  // 処理済となっている最後の行番号
    for (const viewRange of view.visibleRanges) {
        // すべて処理済であれば、全体をスキップする
        const last = doc.lineAt(viewRange.to).number;
        if (last <= latest) continue;

        // 処理済の部分があれば、そこだけスキップする
        let first = doc.lineAt(viewRange.from).number;
        if (first <= latest) first = latest + 1;

        // 処理済でない部分に装飾を付ける
        latest = last;
        for (let i = first; i < last; ++i) {
            const line = doc.line(i);
            matchLoop: for (const match of line.text.matchAll(RubyRegex.RUBY_REGEXP)) {
                const from = match.index + line.from
                const to = from + match[0].length;
                
                // 選択された範囲と重なっていれば、装飾を付けない
                for (const r of selections) {
                    if (r.to >= from && r.from <= to) {
                        continue matchLoop;
                    }
                }

                // コードか数式の範囲と重なっていれば、装飾を付けない
                let inside = false;
                syntaxTree(view.state).iterate({
                    from,
                    to,
                    enter: (node) => {
                        if (inside) return false;
                        if (["code", "math"].some(s => node.name.includes(s))) {
                            if (node.from < to && node.to > from) {
                                inside = true;
                                return false;
                            }
                        }
                        return true;
                    }
                });
                if (inside) continue;

                // 装飾を付ける
                const mid = from + match.groups.body.length;
                if (match.groups.bar != null) {
                    builder.add(from, from + 1, Decoration.replace(new NovelRubyInvisibleWidget()))
                }
                builder.add(from, to, Decoration.mark({ tagName: "ruby" }))
                builder.add(mid, mid + 1, Decoration.replace(new NovelRubyInvisibleWidget()))
                builder.add(mid, to, Decoration.mark({ tagName: "rt" }))
                builder.add(to - 1, to, Decoration.replace(new NovelRubyInvisibleWidget()))
            }
        }
    }
    return builder.finish();
}
// 同名の関数を以下で置き換える
static createRubyRegexp(start, end) {
    return new RegExp(`(?<body>(?:(?<bar>|).+?)|(?:[\\p{sc=Latin}]+?)|(?:[\\p{sc=Hiragana}ー]+?)|(?:[\\p{sc=Katakana}ー]+?)|(?:[\\p{sc=Han}仝々〆〇ヶ]+?))${start}(?:.+?)${end}`, 'gmu');
}
  • syntaxTree()の走査自体がfromtoで制限されているので、syntaxTree().iterateの中で行っているfromtoの範囲判定は冗長かも
  • キャッチされていない例外があると何かあったときにプラグインが停止してしまうので、ちゃんとエラー処理したほうが良いかも