0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

テキストエディタに正規表現でマッチしたものだけ抽出する機能がほしい(Monaco Editorで作る)

Last updated at Posted at 2025-06-02

概要

前回、投稿した記事ではライブラリなどを使わずにHTML&Javascriptで正規表現を使ってデータの加工ができるようにしましたが、今回はMonaco Editorを使って実装しました。
Monaco EditorはVisual Studio Codeにおけるエディタ部分のコアのようです。
なので、検索と置換はMonaco Editorに元々実装されているのでそれを使い、抽出・行を残す・行を消すの部分をプログラミングします。

テスト

まずは、前回の投稿と同じように抽出をします。

regexmonaco01.png

抽出した結果を入力へ移動でエディタに戻します。

入力へ移動でエディタに戻すとUndoで1個前の状態には戻りません。対応としてはクリップボードをクリックして手動で貼り付けて下い。

regexmonaco02.png

次に行を残すをテストします。
まず、cmd+F(Mac), CTRL+F(Windows)で検索ウィンドウを出して、文字列or正規表現を入力します。

regexmonaco11.png

検索した項目全部が選択されるようにoption+ENTER(Mac), ALT+ENTER(Windows)を押下しすると該当項目が全部選択されます。

regexmonaco12.png

行を残すボタンをクリックすると検索でヒットした行が下部に表示されます。

regexmonaco03.png

次に行を消すボタンをクリックすると検索でヒットしなかった行が下部に表示されます。

行を残すでも行を消すでもoption+ENTER or ALT+ENTER 後でないとちゃんと機能しません

regexmonaco04.png

次の画面は\$nを使った置換の例で、2桁-4桁-4桁の電話番号が3桁-3桁-4桁の電話番号に変わったのでそれを変更するものです。まず、検索でヒットしたところがハイライトされます。
下部に表示されているデータは前の残りで今回の操作には関係ありません。

regexmonaco05.png

すべてを置換するボタンを押下すると次のように3桁-3桁-4桁の電話番号に変わっています。

regexmonaco06.png

次の画面は3行で1つのデータの例です。
このデータから製品名と価格と個数だけをcsvとして抽出します。
正規表現は次のようになっています。行を跨いで検索する場合は改行を入れることが必要なようです。

製品名:\s*(\S+).*価格:\s*(\d+).*\n.*\n.*個数:\s*(\d+)

regexmonaco07.png

次の画面はJavascriptを直接書いてデータを下部に出力する例です。
monaco,editor,model,outが変数として使えるようになっています。

regexmonaco08.png

次の画面は右クリックでコンテキストメニューが表示されたものです。

regexmonaco09.png

次の画面はコマンド パレットを表示したもので、いろいろなコマンドとそのショートカットキーなどが確認できます。

regexmonaco10.png

HTML & Javascript

使い方はindex.htmlダブルクリックしてブラウザで表示させるだけです。
Monaco EditorはCDNを使っていますのでインストール等は不要です。
以下のサイトのloader.min.jsを使います。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta http-equiv="Content-Type"
      content="text/html; charset=utf-8">
<style>
    #editor1 { width: 60vw; height: 60vh; border: 1px solid #000000; margin: 10px;}
    #editor2 { width: 35vw; height: 30vh; border: 1px solid #000000;}
    div{
        font-family: monospace;
    }
    input{
        background-color:moccasin;
        font-family: monospace;
    }
    button{
        background-color:lightgreen;
        color: blue;
    }
    .col2{
        padding-left: 10px;
    }
    .output{
        white-space:pre;
    }
    .main{
        display: flex;
    }
</style>
</head>
<body>
<div class="main">
    <div class="col1">
        <div style="text-align: right;">
        <button type=”button” onclick="btn_click('keep');">行を残す</button>
        <button type=”button” onclick="btn_click('delete');">行を消す</button>
        <button type=”button” onclick="btn_click('move');">入力へ移動</button>
        <button type=”button” onclick="btn_click('clipboard');">クリップボード</button>
        </div>
        <div id="editor1"></div>
    </div>
    <div class="col2">
        <div style="border: black solid 1px; font-size: 100%; margin: 5px; padding: 10px;">
            正規表現<br />
            <input type="text" id="regex"  maxlength="100"  size="40" />
            <br /> 抽出<br />
            <input type="text" id="extract" maxlength="100"  size="40" />
            <br /><br />
            <button type=”button” onclick="btn_click('extract');">抽出</button>
            <input type="checkbox" id="caseSensitive" checked/>
            <label for="caseSensitive">Aa区別</label>
        </div>
        <br />
        スクリプト(monaco, editor, model, out)<button type=”button” onclick="btn_click('exec')">実行</button><br />
        <div id="editor2"></div>
        <br />
        <a href="https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Regular_expressions"
            target="_blank">JavaScript正規表現</a>
        <a href="https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Regular_expressions"
            target="_blank">JavaScript正規表現メソッド</a>    <br />
        <a href="https://microsoft.github.io/monaco-editor/"
            target="_blank">Monaco Editor</a>
    </div>
</div>
<hr />
<div id="output" name="output" class="output"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.52.2/min/vs/loader.min.js"></script>
<script>
var g_editor1 = null;
var g_model1 = null;
var g_editor2 = null;
const regex = document.getElementById('regex');
const extract = document.getElementById('extract');
const output = document.getElementById('output');
const lineNumber = document.getElementById('lineNumber');
const exec = document.getElementById('exec');
const check_caseSensitive = document.getElementById('caseSensitive');
const btn_func = {
    "extract": btn_extract, //抽出
    "keep": btn_keep,       //行を残す
    "delete": btn_delete,   //行を消す
    "move": btn_move,       //入力へ移動
    "clipboard": btn_clipboard, //クリップボード
    "exec": btn_exec,       //スクリプト実行
}
function btn_click(kind) {
    try {
        btn_func[kind]();
    } catch (error) {
        output.innerText = error;
    }
}
function btn_extract() {
    let repl_value = extract.value;
    if (extract.value == "")
        repl_value = "$0"    //抽出に入力がないときは"$0"
    //$nを${match.matches[n]}に変更する nは1桁のみ対応
    const str = repl_value.replaceAll(/\$(\d)/g,"\${match.matches[$1]}");
    const matches = g_model1.findMatches( //matches ===> FindMatch[] = [{range: Range, matches:[]}]
                regex.value,  //正規表現の内容
                true,   //searchOnlyEditableRange:
                true,   //正規表現
                check_caseSensitive.checked,  //大文字小文字を区別
                null,   //wordSeparators
                true    //キャプチャグループを取得する
                );      //Optional limitResultCount: number
    let outline = "";
    let oldnum = 0;
    let outlines = [];
    matches.forEach(match => {
        newnum = match.range.startLineNumber;
        if (newnum != oldnum) {
            oldnum = newnum;
            if (outline != ""){
                outlines.push(outline);
                outline = "";
            }
        }
        //${match.matches[n]}を実際の値に置き換えてoutlineに追加
        outline += eval("`"+str+"`");
    });
    if (outline != "")
        outlines.push(outline);
    set_output(outlines);
}
function btn_keep(keep=true) {
    let oldnum = 0;
    let outlines = [];
    // すべての選択範囲を取得
    const selections = g_editor1.getSelections();
    selections.forEach(selection => {
        let newnum = selection.startLineNumber;
        if (newnum != oldnum) {
            if (keep) { //行を残すのとき
                //newnumで指定した行のテキストを取得
                const outline = g_model1.getLineContent(newnum);
                outlines.push(outline);
            } else { //行を消すのとき
                for(let i = oldnum+1; i < newnum; i++) {
                    const outline = g_model1.getLineContent(i);
                    outlines.push(outline);
                }
            }
            oldnum = newnum;
        }
    });
    if (!keep) { //行を消すのとき
        const lastnum = g_model1.getLineCount();
        for(let i = oldnum+1; i < lastnum; i++) {
            const outline = g_model1.getLineContent(i);
            outlines.push(outline);
        }
    }
    set_output(outlines);
}
function btn_delete() {
    btn_keep(false);
}
function btn_move() {
    g_editor1.setValue(output.innerText);
}
function btn_clipboard() {
    navigator.clipboard.writeText(output.innerText)
}
function btn_exec() {
    let func = eval("(monaco, editor, model, out) => {\n"+g_editor2.getValue()+"\n}");
    func(monaco, g_editor1, g_model1, output);
}
function set_output(outlines) {
    output.innerText = outlines.join("\n");
}
function create_editor1(){
    require(['vs/editor/editor.main'], function () {
        const editor = document.getElementById('editor1');
        const options = {
            lineNumbers: "on", //"on" | "off" | "relative" | "interval"
            lineNumbersMinChars: 3,  //行数がオーバーしても表示される
            minimap: {
                enabled: false  //右側に表示されるmapを非表示
            },
            language: ''
        };
        g_editor1 = monaco.editor.create(editor, options);
        g_model1 = g_editor1.getModel();
    });
}
function create_editor2(){
    require(['vs/editor/editor.main'], function () {
        const editor = document.getElementById('editor2');
        const options = {
            minimap: {enabled: false},
            lineNumbersMinChars: 2,
            language: 'javascript'
        };
        g_editor2 = monaco.editor.create(editor, options);
    });
}
function init() {
    require.config({ paths:
        { 'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.52.2/min/vs' },
         'vs/nls': {availableLanguages: {'*': 'ja'}}
    });
    create_editor1();
    create_editor2();
    window.addEventListener('resize', () => {
        //ブラウザのリサイズ対応
        //エディタのレイアウトを再計算
        g_editor1.layout();
        g_editor2.layout();
    });
}
init();
</script>
</body>
</html>
  • 抽出には\$0,\$1~\$9がキャプチャとして使えるようにしていますが、これはMonaco Editorと合わせるためで、実際は変数match.matches[n]に対応します

  • Monaco Editorを生成してg_editor1, g_editor2に代入していますが、これは非同期で行われるため、create_editor1(); create_editor2();の後すぐにg_editor1, g_editor2を参照してはダメです。 nullままですから。生成直後に何かしたいのであればrequire内でg_editor1, g_editor2への代入直後を実行してください

  • 'vs/nls': {availableLanguages: {'*': 'ja'}}はコンテキストメニューなどを日本語にする設定

  • エディタ生成時のoptionは下記を参照してください

    • 例えば、初期のテキストはvalueで与えることができます

その他、詳細はソースとそのコメントを見て下さい。

終わりに

Monaco Editorはdiff画面も表示できて、テキストも文字の色や行の背景色なども個々に変えることが出来るなど、機能が豊富すぎて使えきれないと思いました。
UI設計は苦手なので、どうしたらもっと使いやすくなるのかは今のところ思案中です。
データ加工のためにプログラムを書かずに正規表現だけでしたいことが出来るようになったのは良かったと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?