skynovel
Version:
webgl novelgame framework
362 lines (314 loc) • 11 kB
text/typescript
/* ***** BEGIN LICENSE BLOCK *****
Copyright (c) 2018-2020 Famibee (famibee.blog38.fc2.com)
This software is released under the MIT License.
http://opensource.org/licenses/mit-license.php
** ***** END LICENSE BLOCK ***** */
import P = require('parsimmon');
import {int} from './CmnLib';
import {IPropParser, IVariable} from './CmnInterface';
interface IFncCalc { (a: any[]): any;}
interface IHFncCalc { [key: string]: IFncCalc; }
export class PropParser implements IPropParser {
private parser: any = null;
constructor(private readonly val: IVariable) {
function ope(a: (string | RegExp)[]) {
const ps: any = [];
for (const v of a) ps.push(
((v instanceof RegExp)
? P.regex(v as RegExp)
: P.string(v as string))
.trim(P.optWhitespace)
);
return P.alt.apply(null, ps);
}
function PREFIX(operatorsParser: any, nextParser: any) {
const parser: any = P.lazy(()=> {
return P.seq(operatorsParser, parser).or(nextParser);
});
return parser;
}
// right. (e.g. 1^2^3 is 1^(2^3) not (1^2)^3)
function BINARY_RIGHT(operatorsParser: any, nextParser: any) {
let parser = P.lazy(
()=> nextParser.chain(
(next: any)=> P.seq(
operatorsParser,
P.of(next),
parser
).or(P.of(next))
)
);
return parser;
}
// left. (e.g. 1-2-3 is (1-2)-3 not 1-(2-3))
function BINARY_LEFT(operatorsParser: any, nextParser: any) {
return P.seqMap(
nextParser,
P.seq(operatorsParser, nextParser).many(),
(first, rest)=> {
return rest.reduce((acc, ch)=> {
return [ch[0], acc, ch[1]];
}, first);
}
);
}
const Num = P.alt(
P.alt(
P.regex(/-?(0|[1-9][0-9]*)\.[0-9]+/),
P.regex(/0x[0-9a-fA-F]+/)
).map(Number),
P.alt(
P.regex(/-?(0|[1-9][0-9]*)/)
).map(n=> int(n))
)
.map(str=> ['!num!', str])
.desc('number');
const NullLiteral = P.string('null')
.map(()=> ['!str!', null]);
const BooleanLiteral = P.regex(/(true|false)/)
.map(b=> ['!bool!', b === 'true'])
.desc('boolean');
const StringLiteral = P
.regex(/("|'|#).*?\1/)
.map(b=> ['!str!', b.slice(1, -1)])
.desc('string');
const REG_BRACKETS = /\[[^\]]+\]/g;
const VarLiteral = P
.regex(/-?(?:(?:tmp|sys|save|mp):)?[^\s!-\/:-@[-^`{-~]+(?:\.[^\s!-\/:-@[-^`{-~]+|\[[^\]]+\])*(?:@str)?/)
.map(b=> {
//console.log(' 👺 VarLiteral:0 b:%O:', b);
const s = String(b).replace(REG_BRACKETS, v=>
'.'+ this.parse(v.slice(1, -1))
);
if (s.charAt(0) === '-') { // 変数頭に「-」
const val = this.val.getVal(s.slice(1));
if (val == null || String(val) === 'null') throw Error('(PropParser)数値以外に-符号がついています');
return ['!num!', -Number(val)];
}
const val = this.val.getVal(s);
//console.log(' 👹 s:%O: val:%O:', s, val);
if (val == null) return ['!str!', val]; // undefined も
if (typeof val === 'boolean') return ['!bool!', val];
return (Object.prototype.toString.call(val) === '[object String]')
? ['!str!', String(val)]
: ['!num!', Number(val)];
})
.desc('string');
const Basic = P.lazy(()=> P
.string('(').then(this.parser).skip(P.string(')'))
.or(Num)
.or(NullLiteral)
.or(BooleanLiteral)
.or(StringLiteral)
.or(VarLiteral)
);
const table = [
// 優先順位:19(メンバーへのアクセス、計算値によるメンバーへのアクセス)
// a.b a[b]
{type: PREFIX, ops: ope([/[A-Za-z_][A-Za-z0-9_]*(?=\()/])},
// ++ -- // 優先順位:17(後置インクリメント・デクリメント)
{type: PREFIX, ops: ope([/(!(?!=)|~)/])}, // 優先順位:16
// {type: PREFIX, ops: ope([/(!(?!=)|++|--)/])},
// 「n!」階乗演算子は優先順位がよく判らないし、使わない・ミスも考え無いほうが
// // 優先順位:16(前置インクリメント・デクリメント)
{type: BINARY_RIGHT, ops: ope(['**'])},
{type: BINARY_LEFT, ops: ope(['*', '/', '¥', '%'])},
{type: BINARY_LEFT, ops: ope(['+', '-'])},
{type: BINARY_LEFT, ops: ope([/(>>>|<<|>>)/])},
{type: BINARY_LEFT, ops: ope([/(<=|<|>=|>)/])},
{type: BINARY_LEFT, ops: ope([/(===|!==|==|!=)/])},
{type: BINARY_LEFT, ops: ope([/&(?!&)/])},
{type: BINARY_LEFT, ops: ope(['^'])},
{type: BINARY_LEFT, ops: ope([/\|(?!\|)/])},
{type: BINARY_LEFT, ops: ope(['&&'])},
{type: BINARY_LEFT, ops: ope(['||'])},
{type: BINARY_RIGHT, ops: ope([':'])},
{type: BINARY_RIGHT, ops: ope(['?'])},
];
const tableParser = table.reduce(
(acc, level)=> level.type(level.ops, acc),
Basic
);
this.parser = tableParser.trim(P.optWhitespace);
}
parse(s: string): any {
//console.log("🌱 Parsimmon'%s'", s);
const p = this.parser.parse(s);
if (! p.status) throw Error('(PropParser)文法エラー【'+ s +'】');
const a = p.value;
if (a[0] === '!str!') return this.procEmbedVar(a[1]);
return this.calc(a);
}
private calc(a: any[]): object {
//console.log('🌷 calc%O', a);
const elm = a.shift();
if (elm instanceof Array) return this.calc(elm);
const fnc = this.hFnc[elm];
return (fnc) ?fnc(a) :Object(null);
}
private hFnc: IHFncCalc = {
'!num!': a=> a.shift(),
'!str!': a=> this.procEmbedVar(a.shift()),
'!bool!':a=> a.shift(),
// 論理 NOT
'!': a=> {
const b = a.shift();
return (b[0] === '!bool!')
? ! Boolean( b[1] )
: ! (String(this.calc(b)) === 'true');
},
// チルダ演算子(ビット反転)
'~': a=> ~ Number(this.calc(a.shift())),
// 乗算、除算、剰余
'**': a=> Number(this.calc(a.shift())) **
Number(this.calc(a.shift())),
'*': a=> Number(this.calc(a.shift())) *
Number(this.calc(a.shift())),
'/': a=> Number(this.calc(a.shift())) /
Number(this.calc(a.shift())),
'¥': a=> Math.floor( this.hFnc['/'](a) ),
'%': a=> Number(this.calc(a.shift())) %
Number(this.calc(a.shift())),
// 加算、減算、文字列の連結
'+': a=> {
const b = this.calc(a.shift());
const c = this.calc(a.shift());
if (Object.prototype.toString.call(b) === '[object String]'
|| Object.prototype.toString.call(c) === '[object String]') {
return String(b) + String(c);
}
return Number(b) + Number(c);
},
'-': a=> Number(this.calc(a.shift())) -
Number(this.calc(a.shift())),
// 関数
'int': a=> int(this.fncSub_ChkNum(a.shift())),
'parseInt': a=> int(this.hFnc['Number'](a)),
'Number': a=> {
const b = this.calc(a.shift());
if (Object.prototype.toString.call(b) !== '[object String]') return Number(b);
return this.fncSub_ChkNum(this.parser.parse(String(b)).value);
},
'ceil': a=> Math.ceil( this.fncSub_ChkNum(a.shift()) ),
'floor': a=> Math.floor( this.fncSub_ChkNum(a.shift()) ),
'round': a=> Math.round( this.fncSub_ChkNum(a.shift()) ),
// ビットシフト
'<<': a=> Number(this.calc(a.shift())) <<
Number(this.calc(a.shift())),
'>>': a=> Number(this.calc(a.shift())) >>
Number(this.calc(a.shift())),
'>>>': a=> Number(this.calc(a.shift())) >>>
Number(this.calc(a.shift())),
// 等値、非等値、厳密等価、厳密非等価
'<': a=> Number(this.calc(a.shift())) <
Number(this.calc(a.shift())),
'<=': a=> Number(this.calc(a.shift())) <=
Number(this.calc(a.shift())),
'>': a=> Number(this.calc(a.shift())) >
Number(this.calc(a.shift())),
'>=': a=> Number(this.calc(a.shift())) >=
Number(this.calc(a.shift())),
// 小なり、以下、大なり、以上
'==': a=> {
const b = this.calc(a.shift());
const c = this.calc(a.shift());
if ((b == null) && (c == null) && (!b || !c)) return (b == c);
// 一・二項目は undefined も適合。
// 三項目での falseは、""か 0か falseか undefinedか nullかも
// ここでは undefined == null でよい。(===では区別する)
return String(b) === String(c);
},
'!=': a=> ! this.hFnc['=='](a),
'===': a=> {
const b = this.calc(a.shift());
const c = this.calc(a.shift());
if (Object.prototype.toString.call(b) !=
Object.prototype.toString.call(c)) return false;
return String(b) === String(c);
},
'!==': a=> ! this.hFnc['==='](a),
// ビット演算子
'&': a=> Number(this.calc(a.shift())) &
Number(this.calc(a.shift())),
'^': a=> Number(this.calc(a.shift())) ^
Number(this.calc(a.shift())),
'|': a=> Number(this.calc(a.shift())) |
Number(this.calc(a.shift())),
// 論理 AND,OR
'&&': a=> (String(this.calc(a.shift())) === 'true') &&
(String(this.calc(a.shift())) === 'true'),
'||': a=> (String(this.calc(a.shift())) === 'true') ||
(String(this.calc(a.shift())) === 'true'),
// 条件
'?': a=> {
const b = a.shift();
let cond = false;
if (b[0] === '!bool!') {
cond = Boolean( b[1] );
}
else {
const cond2 = String( this.calc(b) );
cond = (cond2 !== 'true' && cond2 !== 'false')
? (int(cond2) !== 0)
: (cond2 === 'true');
}
const elm2 = a.shift();
if (elm2[0] !== ':') throw Error('(PropParser)三項演算子の文法エラーです。: が見つかりません');
return this.calc(elm2[cond ?1 :2]);
},
':': ()=> { throw Error('(PropParser)三項演算子の文法エラーです。? が見つかりません') },
}
private fncSub_ChkNum(v: any[]): number {
const b = this.calc(v);
if (Object.prototype.toString.call(b) !== '[object Number]') throw Error('(PropParser)引数【'+ b +'】が数値ではありません');
return Number(b);
}
private readonly REG_EMBEDVAR
= /(\$((tmp|sys|save|mp):)?[^\s!--\/:-@[-^`{-~]+|\#\{[^\}]+})/g;
private procEmbedVar(b: object): any {
if (b == null) return b; // undefined も
return String(b).replace(this.REG_EMBEDVAR, v=> {
return (v.charAt(0) === '$')
? this.val.getVal(v.slice(1))
: this.parse(v.slice(2, -1));
});
}
getValAmpersand = (val: string)=> (val.charAt(0) === '&')
? String(this.parse(val.slice(1)))
: val;
private static readonly REG_VAL
= /^((?<scope>\w+?):)?(?<name>[^\s :@]+)(?<at>\@str)?$/;
// 522 match 18413 step(~10ms) https://regex101.com/r/tmCKuE/1
// →これは改良しようがない。いい意味で改善の余地なし
static getValName(arg_name: string): {[name: string]: string} | undefined {
const e = this.REG_VAL.exec(arg_name.trim());
const g = e?.groups;
if (! g) return undefined;
return {
scope : g.scope || 'tmp',
//name : (g.name || '')
// .replace(REG_VALN_B2D, getValName_B2D)
name : PropParser.getValName_B2D(g.name),
at : g.at ?? '',
};
}
private static getValName_B2D(str: string): string {
let i = 0, e = 0;
while (true) {
i = str.indexOf('["');
if (i < 0) {
i = str.indexOf("['");
if (i < 0) break;
e = str.indexOf("']", i+2);
}
else {
e = str.indexOf('"]', i+2);
}
if (e < 0) break;
str = str.slice(0, i) +'.'+ str.slice(i+2, e)
+ str.slice(e+2);
i = e-2; // -3+1
}
return str;
}
}