UNPKG

nadesiko3

Version:
323 lines (322 loc) 12.1 kB
/** * nako_prepare.js * 字句解析の前の前処理。全角文字を半角文字に変換するのが主な処理。 * ただし、コメントや文字列の中は変換しないように考慮して変換する。 */ class ReplaceHistory { constructor(from, to, index) { this.from = from; this.to = to; this.index = index; } } class ConvertResult { constructor(text, sourcePosition) { this.text = text; this.sourcePosition = sourcePosition; } } /** * 置換後の位置から置換前の位置へマッピングできる文字列 */ export class Replace { constructor(code) { this.history = []; this.code = code; } getText() { return this.code; } replaceAll(from, to) { while (true) { const index = this.getText().indexOf(from); if (index === -1) { break; } if (from.length !== to.length) { this.history.unshift(new ReplaceHistory(from.length, to.length, index)); } this.code = this.code.replace(from, to); } } getSourcePosition(i) { // 少し遅い。パース時間1.4秒に対して0.15秒かかる。iが単調増加することを利用して高速化できるはず。 for (const item of this.history) { if (i >= item.index + item.to) { // 置換範囲より後ろ i += item.from - item.to; } else if (item.index <= i && i < item.index + item.to) { // 置換範囲 // 置換文字列が2文字以上のとき、最後の文字は最後の文字へマップする。それ以外は最初の文字へマップする。 if (item.to >= 2 && i === item.index + item.to - 1) { i = item.index + item.from - 1; } else { i = item.index; } } } return i; } } /** * 字句解析を行う前に全角文字を半角に揃える * [memo] * ただし、文字列部分だけは、そのまま全角で出力するようにする * for https://github.com/kujirahand/nadesiko3/issues/94 */ export class NakoPrepare { /** 唯一のインスタンスを返す */ static getInstance() { if (!NakoPrepare._instance) { NakoPrepare._instance = new NakoPrepare(); } return NakoPrepare._instance; } constructor() { // 単純な変換テーブル this.convertTable = new Map([ // ハイフンへの変換 // 参考) https://hydrocul.github.io/wiki/blog/2014/1101-hyphen-minus-wave-tilde // 0x2d: true, // ASCIIのハイフン [0x2010, '-'], // 別のハイフン [0x2011, '-'], // 改行しないハイフン [0x2013, '-'], // ENダッシュ [0x2014, '-'], // EMダッシュ [0x2015, '-'], // 全角のダッシュ [0x2212, '-'], // 全角のマイナス // チルダの変換 // 0x7e: true, [0x02dc, '~'], // 小さなチルダ [0x02F7, '~'], // Modifier Letter Low Tilde [0x2053, '~'], // Swung Dash - 辞書のみだし [0x223c, '~'], // Tilde Operator: 数学で Similar to [0x301c, '~'], // Wave Dash(一般的な波ダッシュ) [0xFF5E, '~'], // 全角チルダ // スペースの変換 // 参考) http://anti.rosx.net/etc/memo/002_space.html // 0x20: true, [0x2000, ' '], // EN QUAD [0x2002, ' '], // EN SPACE [0x2003, ' '], // EM SPACE [0x2004, ' '], // THREE-PER-EM SPACE [0x2005, ' '], // FOUR-PER-EM SPACE [0x2006, ' '], // SIX-PER-EM SPACE [0x2007, ' '], // FIGURE SPACE [0x2009, ' '], // THIN SPACE [0x200A, ' '], // HAIR SPACE [0x200B, ' '], // ZERO WIDTH SPACE [0x202F, ' '], // NARROW NO-BREAK SPACE [0x205F, ' '], // MEDIUM MATHEMATICAL SPACE [0x3000, ' '], // 全角スペース [0x3164, ' '], // HANGUL FILLER // その他の変換 // [0x09, ' '], // TAB --> SPC [0x203B, '#'], // '※' --- コメント [0x3002, ';'], // 句点 [0x3010, '['], // '【' [0x3011, ']'], // '】' // 読点は「,」に変換する (#877) [0x3001, ','], // 読点 --- JSON記法で「,」と「、」を区別したいので読点は変換しないことに。(#276) [0xFF0C, ','], // 読点 ',' 論文などで利用、ただし句点はドットと被るので変換しない (#735) [0x2715, '*'], // ✕の絵文字 (#1781) @see https://ja.wikipedia.org/wiki/%C3%97#%E7%AC%A6%E5%8F%B7%E4%BD%8D%E7%BD%AE [0x2716, '*'], // ✖の絵文字 (#1183) [0x2717, '*'], // ✗の絵文字 (#1781) [0x2718, '*'], // ✘の絵文字 (#1781) [0x274C, '*'], // ❌の絵文字 (#1781) CROSS MARK [0x2795, '+'], // +の絵文字 (#1183) [0x2796, '-'], // -の絵文字 (#1183) [0x2797, '÷'], // ÷の絵文字 (#1183) [0x1F7F0, '='] //🟰の絵文字(#1781) ]); } // 一文字だけ変換 /** * @param {string} ch */ convert1ch(ch) { if (!ch) { return ''; } const c = ch.codePointAt(0) || 0; // テーブルによる変換 const c2 = this.convertTable.get(c) || ''; if (c2) { return c2; } // ASCIIエリア if (c < 0x7F) { return ch; } // 全角半角単純変換可能 --- '!' - '~' if (c >= 0xFF01 && c <= 0xFF5E) { const c2 = c - 0xFEE0; return String.fromCodePoint(c2); } return ch; } /** convert code */ convert(code) { if (!code) { return []; } const src = new Replace(code); // 改行コードを統一 src.replaceAll('\r\n', '\n'); src.replaceAll('\r', '\n'); let flagStr = false; // 文字列リテラル内かどうか let flagStr2 = false; // 絵文字による文字列リテラル内かどうか let endOfStr = ''; // 文字列リテラルを終了させる記号 const res = []; let left = 0; // 現在処理中の部分文字列の左端の位置 let str = ''; // 文字列リテラルの値 // 一文字ずつ全角を半角に置換する let i = 0; while (i < src.getText().length) { const c = src.getText().charAt(i); const ch2 = src.getText().substr(i, 2); // 文字列のとき if (flagStr) { if (c === endOfStr) { flagStr = false; res.push(new ConvertResult(str + endOfStr, src.getSourcePosition(left))); i++; left = i; continue; } str += c; i++; continue; } // 絵文字制御による文字列のとき if (flagStr2) { if (ch2 === endOfStr) { flagStr2 = false; if (endOfStr === '*/') { endOfStr = '*/'; } // 強制変換 res.push(new ConvertResult(str + endOfStr, src.getSourcePosition(left))); i += 2; left = i; continue; } str += c; i++; continue; } // 文字列判定 if (c === '「') { res.push(new ConvertResult(c, src.getSourcePosition(left))); i++; left = i; flagStr = true; endOfStr = '」'; str = ''; continue; } if (c === '『') { res.push(new ConvertResult(c, src.getSourcePosition(left))); i++; left = i; flagStr = true; endOfStr = '』'; str = ''; continue; } if (c === '“') { res.push(new ConvertResult(c, src.getSourcePosition(left))); i++; left = i; flagStr = true; endOfStr = '”'; str = ''; continue; } // JavaScriptの内部的には文字列はUTF-16で扱われてるので charAt を使う場合 絵文字が2文字扱いになる --- #726 if (ch2 === '🌴' || ch2 === '🌿') { res.push(new ConvertResult(ch2, src.getSourcePosition(left))); i += 2; left = i; flagStr2 = true; endOfStr = ch2; str = ''; continue; } const c1 = this.convert1ch(c); if (c1 === '"' || c1 === '\'') { res.push(new ConvertResult(c1, src.getSourcePosition(left))); i++; left = i; flagStr = true; endOfStr = c; str = ''; continue; } // ラインコメントを飛ばす (#725) if (c1 === '#') { res.push(new ConvertResult(c1, src.getSourcePosition(left))); i++; left = i; flagStr = true; // 本当はコメントだけど便宜上 endOfStr = '\n'; str = ''; continue; } // ラインコメントを飛ばす if (ch2 === '//' || ch2 === '//') { res.push(new ConvertResult('//', src.getSourcePosition(left))); // 強制的に'//'とする i += 2; left = i; flagStr = true; endOfStr = '\n'; str = ''; continue; } // 複数行コメント内を飛ばす if (ch2 === '/*') { res.push(new ConvertResult('/*', src.getSourcePosition(left))); // 強制変換 i += 2; left = i; flagStr2 = true; endOfStr = '*/'; str = ''; continue; } // 複数行コメント内を飛ばす (#731) if (ch2 === '/*') { res.push(new ConvertResult(ch2, src.getSourcePosition(left))); i += 2; left = i; flagStr2 = true; endOfStr = '*/'; str = ''; continue; } // 変換したものを追加 res.push(new ConvertResult(c1, src.getSourcePosition(left))); i++; left = i; } if (flagStr || flagStr2) { res.push(new ConvertResult(str + endOfStr, src.getSourcePosition(left))); } return res; } } /** なでしこのソースコードのモード(!インデント構文など)が設定されているか調べる */ export function checkNakoMode(code, modeNames) { // 先頭の256文字について調べる code = code.substring(0, 256); // 全角半角の揺れを吸収 code = code.replace(/(!|💡)/, '!'); // 範囲コメントを削除 code = code.replace(/\/\*.*?\*\//g, ''); // 毎文調べる const lines = code.split(/[;。\n]/, 30); for (let line of lines) { line = line.replace(/^\s+/, '').replace(/\s+$/, ''); // trim if (modeNames.indexOf(line) >= 0) { return true; } } return false; }