nadesiko3
Version:
Japanese Programming Language
282 lines (269 loc) • 9.44 kB
text/typescript
/** インデント構文を処理するモジュール */
import { Token, NewEmptyToken } from './nako_types.mjs'
import { NakoIndentError } from '../src/nako_errors.mjs'
import { debugTokens, newToken } from './nako_tools.mjs'
const IS_DEBUG = false
function isSkipWord (t: Token): boolean {
if (t.type === '違えば') { return true }
if (t.type === 'word' && t.value === 'エラー' && t.josi === 'ならば') { return true }
return false
}
/** インラインインデント構文 --- 末尾の":"をインデントを考慮して"ここまで"を挿入 (#1215) */
export function convertInlineIndent (tokens: Token[]): Token[] {
//
// 0:もし、A=0ならば:
// 2: もし、B=0ならば:
// 4: 「A=0,B=0」を表示。
// 2: 違えば:
// 4: 「A=0,B!=0」を表示。
// 5:違えば:
// 6: 「A!=0」を表示。
//
const lines: Token[][] = splitTokens(tokens, 'eol')
const blockIndents: number[] = []
let checkICount = -1
let jsonObjLevel = 0
let jsonArrayLevel = 0
const checkJsonSyntax = (line: Token[]) => {
// JSONのオブジェクトがあるか?
line.forEach((t: Token) => {
if (t.type === '{') { jsonObjLevel++ }
if (t.type === '}') { jsonObjLevel-- }
if (t.type === '[') { jsonArrayLevel++ }
if (t.type === ']') { jsonArrayLevel-- }
})
}
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
// 空行は飛ばす || コメント行だけの行も飛ばす
if (IsEmptyLine(line)) { continue }
const leftToken = GetLeftTokens(line)
// JSONの途中であればブロックの変更は行わない
if (jsonObjLevel > 0 || jsonArrayLevel > 0) {
checkJsonSyntax(line)
continue
}
// インデントの終了を確認する必要があるか?
if (checkICount >= 0) {
const lineICount: number = leftToken.indent
while (checkICount >= lineICount) {
const tFirst: Token = leftToken
// console.log('@@', lineICount, '>>', checkICount, tFirst.type)
if (isSkipWord(tFirst) && (checkICount === lineICount)) { // 「違えば」や「エラーならば」
// 「ここまで」の挿入不要 / ただしネストした際の「違えば」(上記の5の状態なら必要)
} else {
// ここまでを挿入する
lines[i - 1].push(newToken('ここまで', 'ここまで', tFirst))
lines[i - 1].push(newToken('eol', '\n', tFirst))
}
blockIndents.pop()
if (blockIndents.length > 0) {
checkICount = blockIndents[blockIndents.length - 1]
} else {
checkICount = -1
break
}
}
}
// JSONの途中であればブロックの変更は行わない
checkJsonSyntax(line)
if (jsonObjLevel > 0 || jsonArrayLevel > 0) { continue }
// 末尾の「:」をチェック
const tLast: Token = getLastTokenWithoutEOL(line)
if (tLast.type === ':') {
// 末尾の「:」を削除
lines[i] = lines[i].filter(t => t !== tLast)
checkICount = tLast.indent
blockIndents.push(checkICount)
}
}
if (lines.length > 0 && blockIndents.length > 0) {
// トークン情報を得るため、直近のトークンを得る
let t = tokens[0]
for (let i = lines.length - 1; i >= 0; i--) {
const line = lines[i]
if (line.length > 0) {
t = line[line.length - 1]
break
}
}
// ここまでを差し込む
for (let i = 0; i < blockIndents.length; i++) {
lines[lines.length - 1].push(newToken('ここまで', 'ここまで', t))
lines[lines.length - 1].push(newToken('eol', '\n', t))
}
}
const result = joinTokenLines(lines)
if (IS_DEBUG) {
console.log('###', debugTokens(result))
}
return result
}
/** 行ごとに分割していたトークンをくっつける */
export function joinTokenLines (lines: Token[][]): Token[] {
const r: Token[] = []
for (const line of lines) {
for (const t of line) {
r.push(t)
}
}
return r
}
function getLastTokenWithoutEOL (line: Token[]): Token {
const len: number = line.length
if (len === 0) { return NewEmptyToken('?') }
let res: Token = line[len - 1]
if (res.type === 'eol') {
if (len >= 2) { res = line[len - 2] }
}
return res
}
export function splitTokens (tokens: Token[], delimiter: string): Token[][] {
const result: Token[][] = []
let line: Token[] = []
let kakko = 0
for (const t of tokens) {
line.push(t)
if (t.type === '{') {
kakko++
} else if (t.type === '}') {
kakko--
} else if (kakko === 0 && t.type === delimiter) {
result.push(line)
line = []
}
}
if (line.length > 0) {
result.push(line)
}
return result
}
/** トークン行が空かどうか調べる */
function IsEmptyLine (line: Token[]): boolean {
if (line.length === 0) { return true }
for (let j = 0; j < line.length; j++) {
const ty = line[j].type
if (ty === 'eol' || ty === 'line_comment' || ty === 'range_comment') { continue }
return false
}
return true
}
/** コメントを除去した最初のトークンを返す */
function GetLeftTokens (line: Token[]): Token {
for (let i = 0; i < line.length; i++) {
const t = line[i].type
if (t === 'eol' || t === 'line_comment' || t === 'range_comment') { continue }
return line[i]
}
return line[0]
}
// インデント構文のキーワード
const INDENT_MODE_KEYWORDS = ['!インデント構文', '!ここまでだるい', '💡インデント構文', '💡ここまでだるい']
/** インデント構文 --- インデントを見て"ここまで"を自動挿入 (#596) */
export function convertIndentSyntax (tokens: Token[]): Token[] {
// インデント構文の変換が必要か?
if (!useIndentSynax(tokens)) { return tokens }
// 『ここまで』があったらエラーを出す
for (const t of tokens) {
if (t.type === 'ここまで') {
// エラーを出す
throw new NakoIndentError('インデント構文が有効化されているときに『ここまで』を使うことはできません。', t.line, t.file)
}
}
// JSON構文のチェック
let jsonObjLevel = 0
let jsonArrayLevel = 0
const checkJsonSyntax = (line: Token[]) => {
// JSONのオブジェクトがあるか?
line.forEach((t: Token) => {
if (t.type === '{') { jsonObjLevel++ }
if (t.type === '}') { jsonObjLevel-- }
if (t.type === '[') { jsonArrayLevel++ }
if (t.type === ']') { jsonArrayLevel-- }
})
}
// 行ごとにトークンを分割
const blockIndents: number[][] = []
const lines = splitTokens(tokens, 'eol')
let lastI = 0
// 各行を確認する
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
// 空行は飛ばす || コメント行だけの行も飛ばす
if (IsEmptyLine(line)) { continue }
// JSON構文のチェック
if (jsonArrayLevel > 0 || jsonObjLevel > 0) {
checkJsonSyntax(line)
continue
}
const leftToken = GetLeftTokens(line)
const curI: number = leftToken.indent
if (curI === lastI) { continue }
// ブロックの終了?
// 0: 3回
// 2: もし、1 > 1ならば
// 4: 1を表示
// 2: 違えば
// 4: 2を表示
// 0:
// ブロックの終了?
if (lastI >= 0) {
while (lastI > curI) {
const blockIndentTopLast = blockIndents[blockIndents.length - 1][1]
// console.log('@@[', i, ']', lastI, '>', curI, '@', blockIndentTopLast, leftToken.type)
if (isSkipWord(leftToken) && blockIndentTopLast === curI) {
// 「違えば」などなら不要 (ただし、違えばがネストしている場合は必要)
} else {
const t = lines[i - 1][0]
lines[i - 1].push(newToken('ここまで', 'ここまで', t))
lines[i - 1].push(newToken('eol', '\n', t))
}
blockIndents.pop()
if (blockIndents.length > 0) {
lastI = blockIndents[blockIndents.length - 1][0]
} else {
lastI = 0
break
}
}
}
if (jsonArrayLevel > 0 || jsonObjLevel > 0) { continue }
// JSON構文のチェック
checkJsonSyntax(line)
// ブロックの開始?
if (curI > lastI) {
blockIndents.push([curI, lastI])
// console.log('@@@push', curI)
lastI = curI
continue
}
}
// 末尾に「ここまで」を追加する
for (let i = 0; i < blockIndents.length; i++) {
// トークン情報を得るため、直近のトークンを得る
let t = tokens[0]
for (let i = lines.length - 1; i >= 0; i--) {
const line = lines[i]
if (line.length > 0) {
t = line[line.length - 1]
break
}
}
lines[lines.length - 1].push(newToken('ここまで', 'ここまで', t))
lines[lines.length - 1].push(newToken('eol', '\n', t))
}
const result = joinTokenLines(lines)
// console.log('###', debugTokens(result))
return result
}
function useIndentSynax (tokens: Token[]) : boolean {
// インデント構文が必要かチェック (最初の100個をチェック)
for (let i = 0; i < tokens.length; i++) {
if (i > 100) { break }
const t = tokens[i]
if (t.type === 'line_comment' && (INDENT_MODE_KEYWORDS.indexOf(t.value) >= 0)) {
return true
}
}
return false
}