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

# 概要

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

https://qiita.com/h6591/items/1979a0210ff4c6b3f1db

https://microsoft.github.io/monaco-editor/

# テスト

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

![regexmonaco01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258007/0f7216a3-8765-4aa2-a83e-e278e6594513.png)

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

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


![regexmonaco02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258007/607860a1-2492-498d-ae87-5dca86452e5d.png)

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

![regexmonaco11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258007/73ba2d04-dac2-4b34-8622-410fbcdf761c.png)

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

![regexmonaco12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258007/54025b14-4e0b-4c29-a476-7c14126dc11f.png)

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

![regexmonaco03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258007/ecebf8f1-be70-4b88-9dfb-8dcfb62ffbd0.png)

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

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

![regexmonaco04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258007/e2d073c3-2531-4283-809a-3b0f4dff6093.png)

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

![regexmonaco05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258007/70967659-f780-4420-9fc8-24acf117f1c9.png)

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

![regexmonaco06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258007/a6125746-dfdf-42f6-91fc-f838c0cbfa5a.png)

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

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

![regexmonaco07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258007/8c22856a-5916-4511-9f0e-2dbca569b05c.png)

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

![regexmonaco08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258007/4f3c3da2-b9cb-44f3-8073-d469cb1c4144.png)

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

![regexmonaco09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258007/a5583ed0-814c-4903-833a-e23377ce6de2.png)

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

![regexmonaco10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258007/192843b4-588d-40cc-99b1-3aa7b1d0f624.png)



# HTML & Javascript

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

https://cdnjs.com/libraries/monaco-editor


```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`で与えることができます

https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor.IStandaloneEditorConstructionOptions.html


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

# 終わりに

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