その2の続きです。 今まではパーサーが letter, zeroOrMore しか用意していなかったので、 HelloWrold! などという中途半端な文字列をパースする例で説明していた。 今回は普通に Hello, World! 文字列をパースできるように、 one, and および seq パーサーを追加します。
$ deno -version
deno 1.44.1
parser.ts に one パーサーを追加します。 ほとんど letter パーサーと同じですが、 letter がアルファベットの任意の1文字にマッチするパーサーに対して、 この one パーサーは指定した1文字にマッチします。
const one = <T>(toSomething: ToSomething<T>, moji: Moji): Parser<T>=> {
const re = /[a-zA-Z]/
const p = (ms: Moji[]): HtmlWriter<T> => {
if( ms.length<1 ){
return ngHtmlWriter(ms)
} else {
const m = head(ms)
if( m.c == moji.c ){
return okHtmlWriter(tail(ms), [toSomething(m)])
} else {
return ngHtmlWriter(ms)
}
}
}
return p
}
たとえば、コンマ1文字にマッチする commaParser の例:
const comma: Moji = { c: ',' }
const commaParser = one<HtmlBlock>(toJustHtmlBlock, comma)
次に and パーサーを用意します。
const and = <T>(p1: Parser<T>, p2: Parser<T>): Parser<T>=> {
return (ms: Moji[]): HtmlWriter<T> => {
const w1 = p1(ms)
if( w1.r.ok ){
const w2 = p2(w1.ms)
if( w2.r.ok ) {
return okHtmlWriter(w2.ms, w1.r.xs.concat(w2.r.xs))
} else {
return ngHtmlWriter(w1.ms)
}
} else {
return ngHtmlWriter(w1.ms)
}
}
}
たとえば、コンマが2回連続する文字列にマッチするパーサーであれば、次のようにコードします。
const p = and<HtmlBlock>( commaParser, commaParser )
この段階で Hello, World! 文字列をパースするパーサーを書いてみます。
const word = zeroOrMore( letter<HtmlBlock>(toJustHtmlBlock) )
const comma = one<HtmlBlock>( toJustHtmlBlock, {c: ','} )
const space = one<HtmlBlock>( toJustHtmlBlock, {c: ' '} )
const exclamation = one<HtmlBlock>( toJustHtmlBlock, {c: '!'} )
const p = and( and( word, comma ), space )
この p パーサーで Hello, World! 文字列をパースすると次のようになります。
$ deno run --check main.ts
{
ms: [
{ c: "W" },
{ c: "o" },
{ c: "r" },
{ c: "l" },
{ c: "d" },
{ c: "!" }
],
r: {
ok: true,
xs: [
{ m: { c: "H" } },
{ m: { c: "e" } },
{ m: { c: "l" } },
{ m: { c: "l" } },
{ m: { c: "o" } },
{ m: { c: "," } },
{ m: { c: " " } }
]
},
parse: [Function: parse]
}
r.xs にパースできた Moji[] が出力されています。 Hello と コンマとスペースまでパースできていることがわかります。 このまま最後までパースできるパーサーを書くとこうなります。
const p = and( and( and( and( word, comma ), space ), word), exclamation)
これで機能するにはするのですが、とても読みづらい上に もし変更が入ったら、もう書きかえる気にもなりません。
そこで seq パーサーを導入します。 もし seq パーサーがあれば、このパーサーは次のように書くことができます。
const p = seq( [word, comma, space, word, exclamation] )
const seq = <T>(parsers: Parser<T>[]): Parser<T> => {
if( parsers.length<1 ){
return (ms: Moji[]): HtmlWriter<T> => {
return okHtmlWriter<T>(ms, [])
}
} else if( parsers.length==1 ){
return parsers[0]
} else {
const initValue = parsers[0]
return parsers.slice(1).reduce( (acc, p)=> {
return and(acc, p)
}, initValue )
}
}
与えられたパーサーが0個の場合と1個の場合は特別に対処しています。 2個以上のパーサーがきた場合は、 and を利用してパーサーを組み立てます。
ここまでで parser.ts に one, and, seq のパーサーを追加したので、これをエクスポートに追記します。
// parser.ts
export type { Moji, Parser, ParseResult, HtmlWriter, ToSomething }
export { toMojiList, parseResult, htmlWriter, letter, one, and, seq, zeroOrMore }
// main.ts
import { Moji, toMojiList } from "./moji.ts"
import { Parser, ParseResult, HtmlWriter, ToSomething, parseResult, htmlWriter, letter, one, seq, zeroOrMore, and } from "./parser.ts"
type JustHtmlBlock = { m: Moji }
type NothingHtmlBlock = {}
type HtmlBlock = JustHtmlBlock | NothingHtmlBlock
const text = "Hello, World!"
const toJustHtmlBlock: ToSomething<HtmlBlock> = (m: Moji): HtmlBlock => {
return { m: m }
}
const word = zeroOrMore( letter<HtmlBlock>(toJustHtmlBlock) )
const comma = one<HtmlBlock>( toJustHtmlBlock, {c: ','} )
const space = one<HtmlBlock>( toJustHtmlBlock, {c: ' '} )
const exclamation = one<HtmlBlock>( toJustHtmlBlock, {c: '!'} )
//const p = and( and( word, comma ), space )
//const p = and( and( and( and( word, comma ), space ), word), exclamation)
const p = seq<HtmlBlock>( [word, comma, space, word, exclamation] )
const mojiList = toMojiList(text)
const initParseResult = parseResult<HtmlBlock>(true, [])
const result = htmlWriter(mojiList, initParseResult).parse( p )
console.log(result)
実行してみます。
$ deno run --check main.ts
Check file:///home/moca/1021-parser/parser-ts/step2/index.ts
{
ms: [],
r: {
ok: true,
xs: [
{ m: { c: "H" } },
{ m: { c: "e" } },
{ m: { c: "l" } },
{ m: { c: "l" } },
{ m: { c: "o" } },
{ m: { c: "," } },
{ m: { c: " " } },
{ m: { c: "W" } },
{ m: { c: "o" } },
{ m: { c: "r" } },
{ m: { c: "l" } },
{ m: { c: "d" } },
{ m: { c: "!" } }
]
},
parse: [Function: parse]
}
意図通り最後までパースできました。