UNPKG

bloodyroots

Version:

Recursive descent parser

300 lines (242 loc) 14 kB
inspect_orig = require('util').inspect inspect = (x) -> inspect_orig(x, false, null) deep_equal = require('deep-equal') chai = require('chai') assert = chai.assert expect = chai.expect should = chai.should { Parser } = require('../lib/bloodyroots') String.prototype.repeat = (num) -> new Array( num + 1 ).join( this ); describe 'Parser', -> describe 'unit testing', -> describe 'at_least_one', -> class TestParser extends Parser @define_production('Document', @at_least_one(@re('abc'))) p = new TestParser() describe 'with none', -> r = p.parse('dabc') it 'should not parse', -> assert not r? describe 'with one', -> r = p.parse('abcdef') r_correct = `{ pos: 0, length: 3, type: 'seq', seq: [ { pos: 0, length: 3, type: 're', match: 'abc', groups: [ 'abc' ] } ] }` it 'should parse', -> assert deep_equal(r, r_correct) describe 'with two', -> r = p.parse('abcabcdef') r_correct = `{ pos: 0, length: 6, type: 'seq', seq: [ { pos: 0, length: 3, type: 're', match: 'abc', groups: [ 'abc' ] }, { pos: 3, length: 3, type: 're', match: 'abc', groups: [ 'abc' ] } ] }` it 'should parse', -> assert deep_equal(r, r_correct) describe 'zero_or_more', -> class TestParser extends Parser @define_production('Document', @zero_or_more(@re('abc'))) p = new TestParser() describe 'with none', -> r = p.parse('dabc') r_correct = `{ pos: 0, length: 0, type: 'seq', seq: [ ] }` it 'should parse', -> assert deep_equal(r, r_correct) describe 'with one', -> r = p.parse('abcdef') r_correct = `{ pos: 0, length: 3, type: 'seq', seq: [ { pos: 0, length: 3, type: 're', match: 'abc', groups: [ 'abc' ] } ] }` it 'should parse', -> assert deep_equal(r, r_correct) describe 'with two', -> r = p.parse('abcabcdef') r_correct = `{ pos: 0, length: 6, type: 'seq', seq: [ { pos: 0, length: 3, type: 're', match: 'abc', groups: [ 'abc' ] }, { pos: 3, length: 3, type: 're', match: 'abc', groups: [ 'abc' ] } ] }` it 'should parse', -> assert deep_equal(r, r_correct) describe 'zero_or_one', -> class TestParser extends Parser @define_production('Document', @zero_or_one(@re('abc'))) p = new TestParser() describe 'with none', -> r = p.parse('dabc') r_correct = `{ pos: 0, length: 0, type: 'seq', seq: [ ] }` it 'should match zero times', -> assert deep_equal(r, r_correct) describe 'with one', -> r = p.parse('abcdef') r_correct = `{ pos: 0, length: 3, type: 'seq', seq: [ { pos: 0, length: 3, type: 're', match: 'abc', groups: [ 'abc' ] } ] }` it 'should match one time', -> assert deep_equal(r, r_correct) describe 'with two', -> r = p.parse('abcabcdef') r_correct = `{ pos: 0, length: 3, type: 'seq', seq: [ { pos: 0, length: 3, type: 're', match: 'abc', groups: [ 'abc' ] } ] }` it 'should match one time', -> assert deep_equal(r, r_correct) describe 'alternation', -> describe 'parsing abcde with (abc|ab)cde', -> # suffix describe 'with suffix', -> class TestParser extends Parser @define_production('Document', @alternation( [ @re('abc'), @re('ab') ], @re('cde'))) p = new TestParser() r = p.parse('abcde') it 'should parse', -> assert r? and r.length == 5 describe 'without suffix', -> class TestParser extends Parser @define_production('Document', @seq(@alternation(@re('abc'), @re('ab')), @re('cde'))) p = new TestParser() r = p.parse('abcde') it 'should not parse', -> assert not r? describe 'range', -> describe 'parsing abc{m}abc{k}def with abc{i,j}def', -> for i in [0..6] do (i) -> for j in [0..6].concat(undefined) do (j) -> for k in [0..1] do (k) -> # non-greedy no-suffix class TestParser extends Parser @define_production('Document', @seq(@range(@re('abc'), i, j, false), @re('abc'.repeat(k) + 'def'))) p = new TestParser() for m in [0..5] do (m) -> describe 'with non-greedy no-suffix m=%d k=%d i=%d j=%d'.sprintf(m,k,i,j), -> r = p.parse('abc'.repeat(m) + 'def') if i + k == m and (not j? or i <= j) it 'should succeed', -> assert r? else it 'should fail', -> assert not r? # greedy no-suffix class TestParser extends Parser @define_production('Document', @seq(@range(@re('abc'), i, j, true), @re('abc'.repeat(k) + 'def'))) p = new TestParser() for m in [0..5] do (m) -> describe 'with greedy no-suffix m=%d k=%d i=%d j=%d'.sprintf(m,k,i,j), -> r = p.parse('abc'.repeat(m) + 'def') if k == 0 if i <= m and (not j? or j >= m) it 'should succeed', -> assert r? else it 'should fail', -> assert not r? else if j? and j + k == m and i <= j it 'should succeed', -> assert r? else it 'should fail', -> assert not r? # non-greedy suffix class TestParser extends Parser @define_production('Document', @range(@re('abc'), i, j, false, @re('abc'.repeat(k) + 'def'))) p = new TestParser() for m in [0..5] do (m) -> describe 'with non-greedy suffix m=%d k=%d i=%d j=%d'.sprintf(m,k,i,j), -> r = p.parse('abc'.repeat(m) + 'def') if i + k <= m and (not j? or j + k >= m) it 'should succeed', -> assert r? else it 'should fail', -> assert not r? # greedy suffix class TestParser extends Parser @define_production('Document', @range(@re('abc'), i, j, true, @re('abc'.repeat(k) + 'def'))) p = new TestParser() for m in [0..5] do (m) -> describe 'with greedy suffix m=%d k=%d i=%d j=%d'.sprintf(m,k,i,j), -> r = p.parse('abc'.repeat(m) + 'def') if i + k <= m and (not j? or j + k >= m) it 'should succeed', -> assert r? else it 'should fail', -> assert not r? # distinguishes between greedy suffix and non-greedy suffix describe 'parsing ab{k}babab with ([ab]{2}){i,j}bab', -> for i in [0..6] do (i) -> for j in [0..6].concat(undefined) do (j) -> # non-greedy suffix class TestParser extends Parser @define_production('Document', @range(@re('[ab]{2}'), i, j, false, @re('bab'))) p = new TestParser() for k in [0..5] do (k) -> describe 'with non-greedy suffix {%d,%d} on ab{%d}babab'.sprintf(i,j,k), -> r = p.parse('ab'.repeat(k) + 'babab') if not j? or (i <= j and j >= k) if i <= k it 'should have length %d'.sprintf(3+2*k), -> assert r.length == 3+2*k else if i == k + 1 it 'should have length %d'.sprintf(5+2*k), -> assert r.length == 5+2*k else it 'should fail', -> assert not r? else it 'should fail', -> assert not r? describe 'with BBCode', -> element = (elt) -> { type: 'element', tag: elt.seq[0].groups[1], arg: elt.seq[0].groups[2], contents: elt.seq[1], } seq2array = (elt) -> elt.seq text = (elt) -> { type: 'text', text: elt.match, } class BBCodeParser extends Parser @define_production('Document', @transform(seq2array, @zero_or_more( @alternation( @v('Element'), @v('TagOrText'))))) @define_production('Element', @transform(element, @seq( @re('\\[([A-Za-z]*)(?:=([^\\]]*))?\\]', 'opentag'), @transform(seq2array, @zero_or_more( @alternation( @v('Element'), @v('NotSpecificCloseTagOrText', @backref('opentag[1]'))))), @var_re('\\[/\\=opentag[1]\\]')))) @define_production('NotSpecificCloseTagOrText', @transform(text, @var_re('\\[/(?!\\=arg[0]\\])[^\\]]*\\]|\\[(?:[^/][^\\]]*)?\\]|[^\\[]+'))) @define_production('TagOrText', @transform(text, @re('\\[[^\\]]*\\]|[^\\[]+'))) b = new BBCodeParser() describe 'basic test case', -> r = b.parse('[zoo][/zoo][/i][q][b=1]text[w][a]text2[/a][/b]') r_correct = `[ { type: 'element', tag: 'zoo', arg: undefined, contents: [] }, { type: 'text', text: '[/i]' }, { type: 'text', text: '[q]' }, { type: 'element', tag: 'b', arg: '1', contents: [ { type: 'text', text: 'text' }, { type: 'text', text: '[w]' }, { type: 'element', tag: 'a', arg: undefined, contents: [ { type: 'text', text: 'text2' } ] } ] } ]` it 'should output DOM matching model', -> assert deep_equal(r, r_correct) describe 'newline', -> r = b.parse('[zoo]abc\ndef[/zoo]') r_correct = `[ { type: 'element', tag: 'zoo', arg: undefined, contents: [ { type: 'text', text: 'abc\ndef' } ] } ]` it 'should be accepted as text', -> assert deep_equal(r, r_correct)