概要
前回、投稿した記事ではライブラリなどを使わずにHTML&Javascriptで正規表現を使ってデータの加工ができるようにしましたが、今回はMonaco Editorを使って実装しました。
Monaco EditorはVisual Studio Codeにおけるエディタ部分のコアのようです。
なので、検索と置換はMonaco Editorに元々実装されているのでそれを使い、抽出・行を残す・行を消すの部分をプログラミングします。
テスト
まずは、前回の投稿と同じように抽出をします。
抽出した結果を入力へ移動でエディタに戻します。
入力へ移動でエディタに戻すとUndoで1個前の状態には戻りません。対応としてはクリップボードをクリックして手動で貼り付けて下い。
次に行を残すをテストします。
まず、cmd+F(Mac), CTRL+F(Windows)で検索ウィンドウを出して、文字列or正規表現を入力します。
検索した項目全部が選択されるようにoption+ENTER(Mac), ALT+ENTER(Windows)を押下しすると該当項目が全部選択されます。
行を残すボタンをクリックすると検索でヒットした行が下部に表示されます。
次に行を消すボタンをクリックすると検索でヒットしなかった行が下部に表示されます。
行を残すでも行を消すでもoption+ENTER or ALT+ENTER 後でないとちゃんと機能しません
次の画面は\$nを使った置換の例で、2桁-4桁-4桁の電話番号が3桁-3桁-4桁の電話番号に変わったのでそれを変更するものです。まず、検索でヒットしたところがハイライトされます。
下部に表示されているデータは前の残りで今回の操作には関係ありません。
すべてを置換するボタンを押下すると次のように3桁-3桁-4桁の電話番号に変わっています。
次の画面は3行で1つのデータの例です。
このデータから製品名と価格と個数だけをcsvとして抽出します。
正規表現は次のようになっています。行を跨いで検索する場合は改行を入れることが必要なようです。
製品名:\s*(\S+).*価格:\s*(\d+).*\n.*\n.*個数:\s*(\d+)
次の画面はJavascriptを直接書いてデータを下部に出力する例です。
monaco,editor,model,outが変数として使えるようになっています。
次の画面は右クリックでコンテキストメニューが表示されたものです。
次の画面はコマンド パレットを表示したもので、いろいろなコマンドとそのショートカットキーなどが確認できます。
HTML & Javascript
使い方はindex.html
ダブルクリックしてブラウザで表示させるだけです。
Monaco EditorはCDNを使っていますのでインストール等は不要です。
以下のサイトのloader.min.js
を使います。
<!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設計は苦手なので、どうしたらもっと使いやすくなるのかは今のところ思案中です。
データ加工のためにプログラムを書かずに正規表現だけでしたいことが出来るようになったのは良かったと思います。