1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

javascript/parsimmonでインデントで構造化された文書のパーサを書く

Posted at

概要・意図

パーサコンビネータ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
  }
}

インデントからブロック終端を判別する実装が思いつかなかったので前処理で{}で囲むようにしています
なお、@に特殊な意味を持たせているのは固有の要件からで、単にインデントを分解したいだけならもっとシンプルに書けます

もっとカッコいいやりかたがあれば教えてください

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?