概要・意図
パーサコンビネータparsimmonを使用します。js以外でも同様のパーサコンビネータが色々あります。数式やプログラミング言語のような再帰を含む言語を表現可能です
パーサやマークアップに興味があれば、文脈自由文法とかBNF記法とか、そのあたりのキーワードで調べると幸せになれるかもしれません
python,pug,hamlのようなインデントベースのブロック構造をパースしたい需要で作ったものです
入出力例・実装
inputの文字列からoutputの配列を生成します
input
@id1
aaa
@id2
ccc
@id2-1
ddd
eee
@id2-1-1
xxx
fff
@id2-2
ggg
hhh
output
[
[
"@id1",
[
[
"aaa"
]
]
],
[
"@id2",
[
[
"ccc"
],
[
"@id2-1",
[
[
"ddd"
],
[
"eee"
],
[
"@id2-1-1",
[
[
"xxx"
]
]
],
[
"fff"
]
]
],
[
"@id2-2",
[
[
"ggg"
],
[
"hhh"
]
]
]
]
]
]
parser.js
import {regex, string, optWhitespace, lazy, newline, whitespace, seq} from 'parsimmon'
function lexeme(p) { return p.skip(optWhitespace) }
const lparen = lexeme(string('{'))
const rparen = lexeme(string('}'))
const elem = lazy('', () => { return block.or(line) })
const id = lexeme(regex(/@[\w-]*/i))
const atom = regex(/[^\{\}\r\n ]+/).skip(regex(/ */))
const line = optWhitespace.then(atom.many()).skip(regex(/\n+/))
const block = optWhitespace.then(seq(id, lparen.then(elem.many()).skip(rparen)))
const root = block.many()
export default {
preserve(string) {
let value = ''
let level = 0
string.split(/\r\n|\r|\n/).forEach((line) => {
const indent = Math.floor(line.match(/^ */)[0].length / 2)
if (level < indent) {
value += "{".repeat(indent - level) + "\n" + line + "\n"
} else if (level > indent) {
value += "}".repeat(level - indent) + "\n" + line + "\n"
} else {
value += line + "\n"
}
level = indent
})
value += "}".repeat(level)
return value
},
parse(string) {
return root.parse(this.preserve(string)).value
}
}
インデントからブロック終端を判別する実装が思いつかなかったので前処理で{}で囲むようにしています
なお、@に特殊な意味を持たせているのは固有の要件からで、単にインデントを分解したいだけならもっとシンプルに書けます
もっとカッコいいやりかたがあれば教えてください