src/sn/Grammar.ts
/* ***** BEGIN LICENSE BLOCK *****
Copyright (c) 2019-2024 Famibee (famibee.blog38.fc2.com)
This software is released under the MIT License.
http://opensource.org/licenses/mit-license.php
** ***** END LICENSE BLOCK ***** */
export type HArg = {
':タグ名'? : string;
layer? : string; // レイヤ系
class? : string;
index? : number;
dive? : string;
page? : string;
alpha? : number;
pivot_x?: number;
pivot_y?: number;
rotation? : number;
scale_x?: number;
scale_y?: number;
visible?: boolean;
blendmode? : string;
left? : number;
top? : number;
width? : number;
height? : number;
pl? : number;
pr? : number;
pt? : number;
pb? : number;
rotate? : number;
in_style? : string;
out_style? : string;
ffs? : string;
noffs? : string;
kinsoku_sol? : string;
kinsoku_eol? : string;
kinsoku_dns? : string;
time? : number;
rule? : string;
glsl? : string;
render? : boolean;
pos? : string;
text? : string;
wait? : number;
record? : boolean;
pic? : string;
enabled?: boolean;
hint? : string;
hint_style? : string;
hint_opt? : string;
clickse? : string;
enterse? : string;
leavese? : string;
clicksebuf? : string;
entersebuf? : string;
leavesebuf? : string;
onenter? : string;
onleave? : string;
t? : string;
r? : string;
exp? : string;
char? : string;
sesame? : string;
cast? : string;
val? : string;
flags? : string;
reg? : string;
len? : string;
url? : string;
format? : string;
chain? : string;
path? : string;
fn? : string;
face? : string;
label? : string;
call? : boolean;
global? : boolean;
name? : string;
clear_local_event? : string;
style? : string;
style_hover? : string;
style_clicked? : string;
r_style? : string;
r_style_hover? : string;
r_style_clicked?: string;
r_align? : string;
b_width? : string;
b_height? : string;
b_color? : string;
b_alpha? : number;
b_alpha_isfixed? : boolean;
b_pic? : string;
back_clear? : string;
max_col? : string;
max_row? : string;
bura_col? : string;
chk_overrow? : string;
dx? : number;
dy? : number;
key? : string;
type? : string; // 3Dレイヤで使用
camera_target? : string;
breakout? : Function;
arg? : HArg;
fnc? : (e: Event)=> void;
filter? : string;
matrix? : string;
clear_filter? : boolean;
enable_filter? : boolean;
ease? : string;
canskip? :boolean;
centering? :boolean;
x? : number | string;
y? : number | string;
id? : string;
src? : string;
var_name? : string;
set_fnc? : string;
break_fnc? : string;
swipe? : string;
f2tap? : string;
f2move? : string;
f3tap? : string;
from? : number;
to? : number | string;
place? : number;
add? : string;
del? : string;
buf? : string; // 音系
buf2? : string;
loop? : boolean;
volume? : number;
ret_ms? : number;
end_ms? : number;
join? : boolean;
do_rec? : boolean;
pan? : number;
stop? : boolean;
clear? : boolean;
// デザインモード
':id_dc'? : string;
':id_tag'? : string;
':path'? : string;
':ln'? : number;
':col_s'? : number;
':col_e'? : number;
':idx_tkn'? : number;
':token'? : string;
':redraw'? : boolean;
design_unit?: boolean; // デザインモードでこのマクロへの引数変更とするか(内部をサーチさせない)
// stepin? : boolean; // 拡張機能のみ使用:false指定でステップインしない
// nowarn_unused? : boolean; // 拡張機能のみ使用:未使用警告を出さない
}
export interface ITag { (hArg: HArg): boolean; }
export interface IHTag { [name: string]: ITag; }
export type Script = {
aToken : string[]; // トークン群
len : number; // トークン数
aLNum : number[]; // トークンの行番号
};
export const REG_TAG = /(?<name>[^\s;\]]+)/; // test用にexport
export function tagToken2Name_Args(token: string): [name: string, args: string] {
const e = REG_TAG.exec(token.slice(1, -1));
const g = e?.groups;
if (! g) throw `タグ記述【${token}】異常です(タグ解析)`;
const nm = g.name;
return [nm, token.slice(1 +nm.length, -1)];
}
export function tagToken2Name(token: string): string {
const e = REG_TAG.exec(token.slice(1));
const g = e?.groups;
if (! g) throw `タグ記述【${token}】異常です(タグ解析)`;
return g.name;
}
export function splitAmpersand(token: string): {
name: string;
text: string;
cast: string | undefined;
} { // テスト用にpublic
const equa = token.replaceAll('==', '=').replaceAll('!=', '≠').split('=');
// != を弾けないので中途半端ではある
const cnt_equa = equa.length;
if (cnt_equa < 2 || cnt_equa > 3) throw '「&計算」書式では「=」指定が一つか二つ必要です';
if (equa[1].at(0) === '&') throw '「&計算」書式では「&」指定が不要です';
return {
name: equa[0].replaceAll('=', '==').replaceAll('≠', '!='),
text: equa[1].replaceAll('=', '==').replaceAll('≠', '!='),
cast: ((cnt_equa === 3) ?equa[2].trim() :undefined)
};
}
export class Grammar {
constructor() {this.setEscape('')}
#REG_TOKEN : RegExp;
setEscape(ce: string) {
if (this.#hC2M && (ce in this.#hC2M)) throw '[エスケープ文字] char【'+ ce +'】が登録済みの括弧マクロまたは一文字マクロです';
// 1078 match 16468 step (3.0ms) PCRE2 https://regex101.com/r/gVCcus/1
/*
\\\S |
\n+
| \t+
| \[let_ml \s+ [^\]]+ ]
.+? (?=\[endlet_ml [\]\s])
| \[ (?:
(?=([^"'#;\]]+))\1
| (["'\#]).*?\2
| ;[^\n]* ) *? ]
| ;[^\n]*
| &[^&\n]+&
| &&?[^&;\n\t]+
| ^\*[^\s\[&;\\]+
| [^\n\t\[;\\]+
*/
/*
[^"'#;\]]++
| (["'\#]).*?\1
++ にしたいところだが、jsは未サポートらしい(2022/10/16)
*/
this.#REG_TOKEN = new RegExp(
(ce ?`\\${ce}\\S|` :'')+ // エスケープシーケンス
'\\n+'+ // 改行
'|\\t+'+ // タブ
`|\\[let_ml\\s+[^\\]]+\\]`+
`.+?`+ // [let_ml]〜[endlet_ml]間のテキスト
`(?=\\[endlet_ml[\\]\\s])`+
`|\\[(?:`+
`(?=([^"'#;\\]]+))\\1|`+ // タグ
`(["'#]).*?\\2` +
// . は (?:\\${ ce??'\\' }.|[^\\1]) でなくてよさげ
`|;[^\\n]*)*?]`+
'|;[^\\n]*'+ // コメント
'|&[^&\\n]+&'+ // &表示&
'|&&?[^&;\\n\\t]+'+ // &代入
'|^\\*[^\\s\\[&;\\\\]+'+ // ラベル
`|[^\\n\\t\\[;${ce ?`\\${ce}` :''}]+`, // 本文
'gs');
this.#REG_CANTC2M = new RegExp(`[\\w\\s;[\\]*=&|《》${ce ?`\\${ce}` :''}]`);
this.#REG_TOKEN_NOTXT = new RegExp(`[\\n\\t;\\[*&${ce ?`\\${ce}` :''}]`);
}
// 括弧マクロの定義
bracket2macro(hArg: HArg, hTag: IHTag, scr: Script, start_idx: number) {
const {name, text} = hArg;
if (! name) throw '[bracket2macro] nameは必須です';
if (! text) throw '[bracket2macro] textは必須です';
const op = text.at(0);
if (! op) throw '[bracket2macro] textは必須です';
if (text.length !== 2) throw '[bracket2macro] textは括弧の前後を示す二文字を指定してください';
if (! (name in hTag)) throw `[bracket2macro] 未定義のタグ又はマクロ[${name}]です`;
this.#hC2M ??= {};
const cl = text.charAt(1);
if (op in this.#hC2M) throw '[bracket2macro] text【'+ op +'】が登録済みの括弧マクロまたは一文字マクロです';
if (cl in this.#hC2M) throw '[bracket2macro] text【'+ cl +'】が登録済みの括弧マクロまたは一文字マクロです';
if (this.#REG_CANTC2M.test(op)) throw '[bracket2macro] text【'+ op +'】は括弧マクロに使用できない文字です';
if (this.#REG_CANTC2M.test(cl)) throw '[bracket2macro] text【'+ cl +'】は括弧マクロに使用できない文字です';
this.#hC2M[cl] = '0'; // チェック用ダミー
this.#hC2M[op] = `[${name} text=`;
this.addC2M(`\\${op}[^\\${cl}]*\\${cl}`, `\\${op}\\${cl}`);
this.#replaceScr_C2M(scr, start_idx);
}
// 一文字マクロの定義
char2macro(hArg: HArg, hTag: IHTag, scr: Script, start_idx: number) {
const {char, name} = hArg;
if (! char) throw '[char2macro] charは必須です';
this.#hC2M ??= {};
if (char in this.#hC2M) throw '[char2macro] char【'+ char +'】が登録済みの括弧マクロまたは一文字マクロです';
if (this.#REG_CANTC2M.test(char)) throw '[char2macro] char【'+ char +'】は一文字マクロに使用できない文字です';
if (! name) throw '[char2macro] nameは必須です';
if (! (name in hTag)) throw `[char2macro] 未定義のタグ又はマクロ[${name}]です`;
this.#hC2M[char] = `[${name}]`;
this.addC2M(`\\${char}`, `\\${char}`);
this.#replaceScr_C2M(scr, start_idx);
}
#REG_CANTC2M : RegExp;
#REGC2M = new RegExp('');
#regStrC2M = '';
#regStrC2M4not = '';
addC2M(a: string, b: string) {
this.#regStrC2M += `${a}|`;
this.#regStrC2M4not += `${b}`;
this.#REGC2M = new RegExp(
`(${this.#regStrC2M}[^${this.#regStrC2M4not}]+)`, 'g');
}
resolveScript(txt: string): Script {
const a = txt
.replaceAll(/(\r\n|\r)/g, '\n')
.match(this.#REG_TOKEN)
?.flatMap(tkn=> {
if (! this.testTagLetml(tkn)) return tkn;
const r = /^([^\]]+?])(.*)$/s.exec(tkn);
if (! r) return tkn;
const [, a, b] = r;
return [a, b];
}) ?? [];
const scr = {aToken :a, len :a.length, aLNum :[]};
this.#replaceScr_C2M(scr);
return scr;
}
testTagLetml(tkn: string): boolean {return /^\[let_ml\s/.test(tkn)};
testTagEndLetml(tkn: string): boolean {return /^\[endlet_ml\s*]/.test(tkn)};
analyzToken(token: string): RegExpExecArray | null { // 4LspWs
this.#REG_TOKEN.lastIndex = 0; // /gなので必要
return this.#REG_TOKEN.exec(token);
}
#hC2M : {[char: string]: string};
#REG_TOKEN_NOTXT : RegExp;
#replaceScr_C2M(scr: Script, start_idx = 0): void {
if (! this.#hC2M) return;
for (let i=scr.len- 1; i >= start_idx; --i) {
const token = scr.aToken[i];
if (this.testNoTxt(token.at(0) ?? '\n')) continue;
// 省略時は #REG_TOKEN_NOTXT に引っかかる \n に
const lnum = scr.aLNum[i];
const a = token.match(this.#REGC2M);
if (! a) continue;
let del = 1;
for (let j=a.length -1; j>=0; --j) {
let ch = a[j];
const macro = this.#hC2M[ch.at(0) ?? ' '];
// 省略時は #REG_CANTC2M に引っかかる ' ' に
if (macro) {
ch = macro +((macro.at(-1) === ']')
? ''
: (`'${ch.slice(1, -1)}']`));
// 文字列は半角空白を意識して''で囲むが、いずれ変えたい場合がある?
}
scr.aToken.splice(i, del, ch);
scr.aLNum.splice(i, del, lnum);
del = 0;
}
}
scr.len = scr.aToken.length;
}
testNoTxt(ch: string): boolean {return this.#REG_TOKEN_NOTXT.test(ch)}; //4tst
}