From 22d568333353eed6851543e0c64e091e398910fc Mon Sep 17 00:00:00 2001 From: Mart Roosmaa Date: Sat, 19 Aug 2017 20:47:58 +0200 Subject: [PATCH 1/7] Update grammar docstrings to follow BNF in SVG spec. --- lib/src/grammar.dart | 161 +++++++++++++++++++++++++---------------- lib/src/parser.dart | 2 +- test/grammar_test.dart | 2 +- 3 files changed, 101 insertions(+), 64 deletions(-) diff --git a/lib/src/grammar.dart b/lib/src/grammar.dart index ac6c6a2..564b573 100644 --- a/lib/src/grammar.dart +++ b/lib/src/grammar.dart @@ -1,125 +1,162 @@ library svg.src.grammar; -import 'package:petitparser/petitparser.dart'; +import 'package:petitparser/petitparser.dart' as pp; /// An implementation of the the [WC3 SVG Path Grammar] /// (http://www.w3.org/TR/SVG/paths.html) for Dart. -class SvgGrammarDefinition extends GrammarDefinition { +/// +/// The grammar follows the BNF grammar provided in the +/// [SVG specifications](https://www.w3.org/TR/SVG/paths.html#PathDataBNF). +class SvgGrammarDefinition extends pp.GrammarDefinition { const SvgGrammarDefinition(); @override start() => svgPath(); - /// wsp* moveto-drawto-command-groups? wsp* + /// svg-path: + /// wsp* moveto-drawto-command-groups? wsp* svgPath() => - whitespace().star() & + wsp().star() & moveToDrawToCommandGroups().optional() & - whitespace().star(); + wsp().star(); - /// moveto-drawto-command-group - /// | moveto-drawto-command-group wsp* moveto-drawto-command-groups + /// moveto-drawto-command-groups: + /// moveto-drawto-command-group + /// | moveto-drawto-command-group wsp* moveto-drawto-command-groups moveToDrawToCommandGroups() => (moveToDrawToCommandGroup() & - whitespace().star() & + wsp().star() & ref(moveToDrawToCommandGroups)) | moveToDrawToCommandGroup(); - /// moveto wsp* drawto-commands? + /// moveto-drawto-command-group: + /// moveto wsp* drawto-commands? moveToDrawToCommandGroup() => - moveTo() & commaWhitespace().optional() & drawToCommands(); + moveTo() & commaWsp().optional() & drawToCommands(); - /// drawto-command - /// | drawto-command wsp* drawto-commands + /// drawto-commands: + /// drawto-command + /// | drawto-command wsp* drawto-commands drawToCommands() => - (drawToCommand() & whitespace().star() & ref(drawToCommands)) + (drawToCommand() & wsp().star() & ref(drawToCommands)) | drawToCommand(); - /// closepath - /// | lineto - /// | horizontal-lineto - /// | vertical-lineto - /// | curveto - /// | smooth-curveto - /// | quadratic-bezier-curveto - /// | smooth-quadratic-bezier-curveto - /// | elliptical-arc + /// drawto-command: + /// closepath + /// | lineto + /// | horizontal-lineto + /// | vertical-lineto + /// | curveto + /// | smooth-curveto + /// | quadratic-bezier-curveto + /// | smooth-quadratic-bezier-curveto + /// | elliptical-arc drawToCommand() => closePath() | lineTo(); - /// ( "M" | "m" ) wsp* moveto-argument-sequence - moveTo() => pattern('Mm') & whitespace().star() & moveToArgumentSequence(); + /// moveto: + /// ( "M" | "m" ) wsp* moveto-argument-sequence + moveTo() => pp.pattern('Mm') & wsp().star() & moveToArgumentSequence(); - /// coordinate-pair - /// | coordinate-pair comma-wsp? lineto-argument-sequence + /// moveto-argument-sequence: + /// coordinate-pair + /// | coordinate-pair comma-wsp? lineto-argument-sequence moveToArgumentSequence() => (coordinatePair() & - commaWhitespace().optional() & + commaWsp().optional() & lineToArgumentSequence()) | coordinatePair(); - /// ( "Z" | "z" ) - closePath() => pattern('Zz'); + /// closepath: + /// ("Z" | "z") + closePath() => pp.pattern('Zz'); - /// ( "L" | "l" ) wsp* lineto-argument-sequence - lineTo() => pattern('Ll') & whitespace().star() & lineToArgumentSequence(); + /// lineto: + /// ( "L" | "l" ) wsp* lineto-argument-sequence + lineTo() => pp.pattern('Ll') & wsp().star() & lineToArgumentSequence(); - /// coordinate-pair - /// | coordinate-pair comma-wsp? lineto-argument-sequence + /// lineto-argument-sequence: + /// coordinate-pair + /// | coordinate-pair comma-wsp? lineto-argument-sequence lineToArgumentSequence() => (coordinatePair() & - commaWhitespace().optional() & + commaWsp().optional() & ref(lineToArgumentSequence)) | coordinatePair(); - /// coordinate comma-wsp? coordinate + /// coordinate-pair: + /// coordinate comma-wsp? coordinate coordinatePair() => - coordinate() & commaWhitespace().optional() & coordinate(); + coordinate() & commaWsp().optional() & coordinate(); - /// number + /// coordinate: + /// number coordinate() => number(); - /// integer-constant - /// | floating-point-constant + /// nonnegative-number: + /// integer-constant + /// | floating-point-constant nonNegativeNumber() => floatingPointConstant() | integerConstant(); - /// sign? integer-constant - /// | sign? floating-point-constant + /// number: + /// sign? integer-constant + /// | sign? floating-point-constant number() => sign().optional() & floatingPointConstant() | sign().optional() & integerConstant(); - /// "0" | 1 - flag() => pattern('01'); + /// flag: + /// "0" | "1" + flag() => pp.pattern('01'); - /// (wsp+ comma? wsp*) | (comma wsp*) - commaWhitespace() => - (comma() & whitespace().star()) - | (whitespace().plus() & comma().optional() & whitespace().star()); + /// comma-wsp: + /// (wsp+ comma? wsp*) | (comma wsp*) + commaWsp() => + (comma() & wsp().star()) + | (wsp().plus() & comma().optional() & wsp().star()); - /// "," - comma() => char(','); + /// comma: + /// "," + comma() => pp.char(','); - /// digit-sequence + /// integer-constant: + /// digit-sequence integerConstant() => digitSequence(); - /// fractional-constant exponent? - /// | digit-sequence exponent + /// floating-point-constant: + /// fractional-constant exponent? + /// | digit-sequence exponent floatingPointConstant() => fractionalConstant() & exponent().optional() | digitSequence() & exponent(); - /// digit-sequence? "." digit-sequence - /// | digit-sequence "." + /// fractional-constant: + /// digit-sequence? "." digit-sequence + /// | digit-sequence "." fractionalConstant() => - (digitSequence().optional() & char('.') & digitSequence()) - | (digitSequence() & char('.')); + (digitSequence().optional() & pp.char('.') & digitSequence()) + | (digitSequence() & pp.char('.')); - /// ( "e" | "E" ) sign? digit-sequence - exponent() => pattern('eE') & sign().optional() & digitSequence(); + /// exponent: + /// ( "e" | "E" ) sign? digit-sequence + exponent() => pp.pattern('eE') & sign().optional() & digitSequence(); - /// "+" | "-" - sign() => pattern('+-'); + /// sign: + /// "+" | "-" + sign() => pp.pattern('+-'); - /// digit - /// | digit digit-sequence + /// digit-sequence: + /// digit + /// | digit digit-sequence digitSequence() => digit().plus().flatten(); + + /// digit: + /// "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" + digit() => pp.digit(); + + /// wsp: + /// (#x20 | #x9 | #xD | #xA) + wsp() => + // Petiteparser whitespace parser is slightly more lenient + // than the SVG spec, but that should be all right. + pp.whitespace(); } diff --git a/lib/src/parser.dart b/lib/src/parser.dart index 62efabb..7004109 100644 --- a/lib/src/parser.dart +++ b/lib/src/parser.dart @@ -74,7 +74,7 @@ class SvgParserDefinition extends SvgGrammarDefinition { }); @override - commaWhitespace() => super.commaWhitespace().map((_) => null); + commaWsp() => super.commaWsp().map((_) => null); @override integerConstant() => super.integerConstant().flatten().map(int.parse); diff --git a/test/grammar_test.dart b/test/grammar_test.dart index ed62c32..41c9464 100644 --- a/test/grammar_test.dart +++ b/test/grammar_test.dart @@ -84,7 +84,7 @@ void main() { }); group('Comma and whitespace', () { - final parseCommaWsp = definition.commaWhitespace().parse; + final parseCommaWsp = definition.commaWsp().parse; test('can parse simple case', () { expect(parseCommaWsp(' , ').value, [[' '], ',', [' ']]); From 4877bd3e4f763d06556afcff8ad6cb2e6bb05742 Mon Sep 17 00:00:00 2001 From: Mart Roosmaa Date: Sat, 19 Aug 2017 20:59:11 +0200 Subject: [PATCH 2/7] Test very compact forms from spec --- test/grammar_test.dart | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/test/grammar_test.dart b/test/grammar_test.dart index 41c9464..6344194 100644 --- a/test/grammar_test.dart +++ b/test/grammar_test.dart @@ -197,6 +197,34 @@ void main() { ] ]); }); + + test('can parse subtraction compact', () { + expect( + parseMove('M 100-200').value, + [ + 'M', + [' '], + [ + [null, '100'], + null, + ['-', '200'] + ] + ]); + }); + + test('can parse float compact', () { + expect( + parseMove('M 0.6.5').value, + [ + 'M', + [' '], + [ + [null, [['0', '.', '6'], null]], + null, + [null, [[null, '.', '5'], null]] + ] + ]); + }); }); group('Draw to', () { From 520fe1c0b92d406aab4cb6e5505ab44d94cd95f9 Mon Sep 17 00:00:00 2001 From: Mart Roosmaa Date: Sat, 19 Aug 2017 23:10:08 +0200 Subject: [PATCH 3/7] Add grammar for missing SVG path commands --- lib/src/grammar.dart | 131 +++++++++++++++ test/grammar_test.dart | 374 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 505 insertions(+) diff --git a/lib/src/grammar.dart b/lib/src/grammar.dart index 564b573..5a59d7b 100644 --- a/lib/src/grammar.dart +++ b/lib/src/grammar.dart @@ -83,6 +83,137 @@ class SvgGrammarDefinition extends pp.GrammarDefinition { ref(lineToArgumentSequence)) | coordinatePair(); + /// horizontal-lineto: + /// ( "H" | "h" ) wsp* horizontal-lineto-argument-sequence + horizontalLineTo() => + pp.pattern('Hh') & wsp().star() & horizontalLineToArgumentSequence(); + + /// horizontal-lineto-argument-sequence: + /// coordinate + /// | coordinate comma-wsp? horizontal-lineto-argument-sequence + horizontalLineToArgumentSequence() => + (coordinate() & + commaWsp().optional() & + ref(horizontalLineToArgumentSequence)) + | coordinate(); + + /// vertical-lineto: + /// ( "V" | "v" ) wsp* vertical-lineto-argument-sequence + verticalLineTo() => + pp.pattern('Vv') & wsp().star() & verticalLineToArgumentSequence(); + + /// vertical-lineto-argument-sequence: + /// coordinate + /// | coordinate comma-wsp? vertical-lineto-argument-sequence + verticalLineToArgumentSequence() => + (coordinate() & + commaWsp().optional() & + ref(verticalLineToArgumentSequence)) + | coordinate(); + + /// curveto: + /// ( "C" | "c" ) wsp* curveto-argument-sequence + curveTo() => + pp.pattern('Cc') & wsp().star() & curveToArgumentSequence(); + + /// curveto-argument-sequence: + /// curveto-argument + /// | curveto-argument comma-wsp? curveto-argument-sequence + curveToArgumentSequence() => + (curveToArgument() & + commaWsp().optional() & + ref(curveToArgumentSequence)) + | curveToArgument(); + + /// curveto-argument: + /// coordinate-pair comma-wsp? coordinate-pair comma-wsp? coordinate-pair + curveToArgument() => + coordinatePair() & commaWsp().optional() & + coordinatePair() & commaWsp().optional() & + coordinatePair(); + + /// smooth-curveto: + /// ( "S" | "s" ) wsp* smooth-curveto-argument-sequence + smoothCurveTo() => + pp.pattern('Ss') & wsp().star() & smoothCurveToArgumentSequence(); + + /// smooth-curveto-argument-sequence: + /// smooth-curveto-argument + /// | smooth-curveto-argument comma-wsp? smooth-curveto-argument-sequence + smoothCurveToArgumentSequence() => + (smoothCurvetoArgument() & + commaWsp().optional() & + ref(smoothCurveToArgumentSequence)) + | smoothCurvetoArgument(); + + /// smooth-curveto-argument: + /// coordinate-pair comma-wsp? coordinate-pair + smoothCurvetoArgument() => + coordinatePair() & commaWsp().optional() & + coordinatePair(); + + /// quadratic-bezier-curveto: + /// ( "Q" | "q" ) wsp* quadratic-bezier-curveto-argument-sequence + quadraticBezierCurveTo() => + pp.pattern('Qq') & wsp().star() & + quadraticBezierCurveToArgumentSequence(); + + /// quadratic-bezier-curveto-argument-sequence: + /// quadratic-bezier-curveto-argument + /// | quadratic-bezier-curveto-argument comma-wsp? + /// quadratic-bezier-curveto-argument-sequence + quadraticBezierCurveToArgumentSequence() => + (quadraticBezierCurveToArgument() & + commaWsp().optional() & + ref(quadraticBezierCurveToArgumentSequence)) + | quadraticBezierCurveToArgument(); + + /// quadratic-bezier-curveto-argument: + /// coordinate-pair comma-wsp? coordinate-pair + quadraticBezierCurveToArgument() => + coordinatePair() & commaWsp().optional() & + coordinatePair(); + + /// smooth-quadratic-bezier-curveto: + /// ( "T" | "t" ) wsp* smooth-quadratic-bezier-curveto-argument-sequence + smoothQuadraticBezierCurveTo() => + pp.pattern('Tt') & wsp().star() & + smoothQuadraticBezierCurveToArgumentSequence(); + + /// smooth-quadratic-bezier-curveto-argument-sequence: + /// coordinate-pair + /// | coordinate-pair comma-wsp? smooth-quadratic-bezier-curveto-argument-sequence + smoothQuadraticBezierCurveToArgumentSequence() => + (coordinatePair() & + commaWsp().optional() & + ref(smoothQuadraticBezierCurveToArgumentSequence)) + | coordinatePair(); + + /// elliptical-arc: + /// ( "A" | "a" ) wsp* elliptical-arc-argument-sequence + ellipticalArc() => + pp.pattern('Aa') & wsp().star() & ellipticalArcArgumentSequence(); + + /// elliptical-arc-argument-sequence: + /// elliptical-arc-argument + /// | elliptical-arc-argument comma-wsp? elliptical-arc-argument-sequence + ellipticalArcArgumentSequence() => + (ellipticalArcArgument() & + commaWsp().optional() & + ref(ellipticalArcArgumentSequence)) + | ellipticalArcArgument(); + + /// elliptical-arc-argument: + /// nonnegative-number comma-wsp? nonnegative-number comma-wsp? + /// number comma-wsp flag comma-wsp? flag comma-wsp? coordinate-pair + ellipticalArcArgument() => + nonNegativeNumber() & commaWsp().optional() & + nonNegativeNumber() & commaWsp().optional() & + number() & commaWsp() & + flag() & commaWsp().optional() & + flag() & commaWsp().optional() & + coordinatePair(); + /// coordinate-pair: /// coordinate comma-wsp? coordinate coordinatePair() => diff --git a/test/grammar_test.dart b/test/grammar_test.dart index 6344194..0093991 100644 --- a/test/grammar_test.dart +++ b/test/grammar_test.dart @@ -227,6 +227,380 @@ void main() { }); }); + group('Horizontal line to', () { + final parseHorizontalLine = definition.build( + start: definition.horizontalLineTo) + .parse; + + test('can parse a single line', () { + expect( + parseHorizontalLine('H5').value, + ['H', [], [null, '5']]); + }); + + test('can parse a single line with fractional values', () { + expect(parseHorizontalLine('h1.2').value, [ + 'h', [], [null, [['1', '.', '2'], null]] + ]); + }); + + test('can parse multiple line values', () { + expect(parseHorizontalLine('h1,2').value, [ + 'h', + [], + [ + [null, '1'], + [',', []], + [null, '2'] + ] + ]); + }); + }); + + group('Vertical line to', () { + final parseVerticalLine = definition.build( + start: definition.verticalLineTo) + .parse; + + test('can parse a single line', () { + expect( + parseVerticalLine('V5').value, + ['V', [], [null, '5']]); + }); + + test('can parse a single line with fractional values', () { + expect(parseVerticalLine('v1.2').value, [ + 'v', [], [null, [['1', '.', '2'], null]] + ]); + }); + + test('can parse multiple line values', () { + expect(parseVerticalLine('v1,2').value, [ + 'v', + [], + [ + [null, '1'], + [',', []], + [null, '2'] + ] + ]); + }); + }); + + group('Curve to', () { + final parseCurve = definition.build( + start: definition.curveTo) + .parse; + + test('can parse a single curve', () { + expect(parseCurve('C101,102 251,103 252,201').value, [ + 'C', + [], + [ + [null, '101'], + [',', []], + [null, '102'], + [[' '], null, []], + [[null, '251'], [',', []], [null, '103']], + [[' '], null, []], + [[null, '252'], [',', []], [null, '201']] + ], + ]); + }); + + test('can parse a single curve with fractional values', () { + expect(parseCurve('c 1.01,1.02 2.51,1.03 2.52,2.01').value, [ + 'c', + [' '], + [ + [null, [['1', '.', '01'], null]], + [',', []], + [null, [['1', '.', '02'], null]], + [[' '], null, []], + [ + [null, [['2', '.', '51'], null]], + [',', []], + [null, [['1', '.', '03'], null]] + ], + [[' '], null, []], + [ + [null, [['2', '.', '52'], null]], + [',', []], + [null, [['2', '.', '01'], null]] + ] + ] + ]); + }); + + test('can parse a multiple curve values', () { + expect(parseCurve('C100,200 500,100 300,400 101,102 251,103 252,201').value, [ + 'C', + [], + [ + [null, '100'], + [',', []], + [null, '200'], + [[' '], null, []], + [[null, '500'], [',', []], [null, '100']], + [[' '], null, []], + [[null, '300'], [',', []], [null, '400']], + [[' '], null, []], + [ + [null, '101'], + [',', []], + [null, '102'], + [[' '], null, []], + [[null, '251'], [',', []], [null, '103']], + [[' '], null, []], + [[null, '252'], [',', []], [null, '201']] + ] + ] + ]); + }); + }); + + group('Smooth curve to', () { + final parseSmoothCurve = definition.build( + start: definition.smoothCurveTo) + .parse; + + test('can parse a single curve', () { + expect(parseSmoothCurve('S400,300 400,200').value, [ + 'S', + [], + [ + [null, '400'], + [',', []], + [null, '300'], + [[' '], null, []], + [[null, '400'], [',', []], [null, '200']] + ] + ]); + }); + + test('can parse a single curve with fractional values', () { + expect(parseSmoothCurve('s40.1,30.20 40.999,200').value, [ + 's', + [], + [ + [null, [['40', '.', '1'], null]], + [',', []], + [null, [['30', '.', '20'], null]], + [[' '], null, []], + [[null, [['40', '.', '999'], null]], [',', []], [null, '200']] + ] + ]); + }); + + test('can parse a multiple curve values', () { + expect(parseSmoothCurve('S400,300 400,200 600,200 600,100').value, [ + 'S', + [], + [ + [null, '400'], + [',', []], + [null, '300'], + [[' '], null, []], + [[null, '400'], [',', []], [null, '200']], + [[' '], null, []], + [ + [null, '600'], + [',', []], + [null, '200'], + [[' '], null, []], + [[null, '600'], [',', []], [null, '100']] + ] + ] + ]); + }); + }); + + group('Quadratic bezier curve to', () { + final parseQuadraticBezierCurve = definition.build( + start: definition.quadraticBezierCurveTo) + .parse; + + test('can parse a single curve', () { + expect(parseQuadraticBezierCurve('Q400,50 600,300').value, [ + 'Q', + [], + [ + [null, '400'], + [',', []], + [null, '50'], + [[' '], null, []], + [[null, '600'], [',', []], [null, '300']] + ] + ]); + }); + + test('can parse a single curve with fractional values', () { + expect(parseQuadraticBezierCurve('q400.30,50.1 60.0,30.01').value, [ + 'q', + [], + [ + [null, [['400', '.', '30'], null]], + [',', []], + [null, [['50', '.', '1'], null]], + [[' '], null, []], + [ + [null, [['60', '.', '0'], null]], + [',', []], + [null, [['30', '.', '01'], null]] + ] + ] + ]); + }); + + test('can parse a multiple curve values', () { + expect(parseQuadraticBezierCurve('Q400,50 600,300 300,20 500,100').value, [ + 'Q', + [], + [ + [null, '400'], + [',', []], + [null, '50'], + [[' '], null, []], + [[null, '600'], [',', []], [null, '300']], + [[' '], null, []], + [ + [null, '300'], + [',', []], + [null, '20'], + [[' '], null, []], + [[null, '500'], [',', []], [null, '100']] + ] + ] + ]); + }); + }); + + group('Smooth quadratic bezier curve to', () { + final parseSmoothQuadraticBezierCurve = definition.build( + start: definition.smoothQuadraticBezierCurveTo) + .parse; + + test('can parse a single curve', () { + expect(parseSmoothQuadraticBezierCurve('T400,300').value, [ + 'T', + [], + [[null, '400'], + [',', []], + [null, '300']] + ]); + }); + + test('can parse a single curve with fractional values', () { + expect(parseSmoothQuadraticBezierCurve('t40.2,30.4').value, [ + 't', + [], + [ + [null, [['40', '.', '2'], null]], + [',', []], + [null, [['30', '.', '4'], null]] + ] + ]); + }); + + test('can parse a multiple curve values', () { + expect(parseSmoothQuadraticBezierCurve('t400,300 200,100').value, [ + 't', + [], + [ + [null, '400'], + [',', []], + [null, '300'], + [[' '], null, []], + [[null, '200'], [',', []], [null, '100']] + ] + ]); + }); + }); + + group('Elliptical arc', () { + final parseEllipticalArc = definition.build( + start: definition.ellipticalArc) + .parse; + + test('can parse a single curve', () { + expect(parseEllipticalArc('A150,150 0 1,0 150,-150').value, [ + 'A', + [], + [ + '150', + [',', []], + '150', + [[' '], null, []], + [null, '0'], + [[' '], null, []], + '1', + [',', []], + '0', + [[' '], null, []], + [[null, '150'], [',', []], ['-', '150']] + ] + ]); + }); + + test('can parse a single curve with fractional values', () { + expect(parseEllipticalArc('a1.50,15.0 0.1 0,1 15.20,-13.10').value, [ + 'a', + [], + [ + [['1', '.', '50'], null], + [',', []], + [['15', '.', '0'], null], + [[' '], null, []], + [null, [['0', '.', '1'], null]], + [[' '], null, []], + '0', + [',', []], + '1', + [[' '], null, []], + [ + [null, [['15', '.', '20'], null]], + [',', []], + ['-', [['13', '.', '10'], null]] + ] + ] + ]); + }); + + test('can parse a multiple curve values', () { + expect(parseEllipticalArc('A150,150 0 1,0 150,-150 120,120 0 0,1 120,-120').value, [ + 'A', + [], + [ + '150', + [',', []], + '150', + [[' '], null, []], + [null, '0'], + [[' '], null, []], + '1', + [',', []], + '0', + [[' '], null, []], + [[null, '150'], [',', []], ['-', '150']], + [[' '], null, []], + [ + '120', + [',', []], + '120', + [[' '], null, []], + [null, '0'], + [[' '], null, []], + '0', + [',', []], + '1', + [[' '], null, []], + [[null, '120'], [',', []], ['-', '120']] + ] + ] + ]); + }); + }); + group('Draw to', () { final parseDraw = definition.build( start: definition.drawToCommand) From 392ac44a6fec14e8428422f5cbbe5f729cd249a9 Mon Sep 17 00:00:00 2001 From: Mart Roosmaa Date: Sun, 20 Aug 2017 16:20:59 +0200 Subject: [PATCH 4/7] Test parsing 3 collapsed commands --- test/parser_test.dart | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/test/parser_test.dart b/test/parser_test.dart index f307162..ab53d70 100644 --- a/test/parser_test.dart +++ b/test/parser_test.dart @@ -8,7 +8,7 @@ import 'package:svg/src/parser.dart'; import 'package:test/test.dart'; void main() { - group('SvgParserDefinitiin', () { + group('SvgParserDefinition', () { final definition = const SvgParserDefinition(); group('Fractional constants', () { @@ -110,9 +110,10 @@ void main() { test('can parse multiple line values', () { expect( - parseLine('l1,1,2,2').value, [ + parseLine('l1,1,2,2,3,3').value, [ const SvgPathLineSegment(1, 1), - const SvgPathLineSegment(2, 2) + const SvgPathLineSegment(2, 2), + const SvgPathLineSegment(3, 3) ]); }); }); @@ -130,10 +131,11 @@ void main() { test('can parse followed by additional moves', () { expect( - parseMove('m0,0,5,5').value, + parseMove('m0,0,5,5,10,10').value, [ const SvgPathMoveSegment(0, 0), - const SvgPathMoveSegment(5, 5) + const SvgPathMoveSegment(5, 5), + const SvgPathMoveSegment(10, 10) ]); }); }); From 1fc8f07b9b81c9b8a4f8dfa09652ddc95b7255c3 Mon Sep 17 00:00:00 2001 From: Mart Roosmaa Date: Sun, 20 Aug 2017 16:21:38 +0200 Subject: [PATCH 5/7] Fix parsing of more than 2 collapsed commands --- lib/src/parser.dart | 52 ++++++++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/lib/src/parser.dart b/lib/src/parser.dart index 62efabb..8fe5038 100644 --- a/lib/src/parser.dart +++ b/lib/src/parser.dart @@ -28,34 +28,18 @@ class SvgParserDefinition extends SvgGrammarDefinition { @override moveTo() => super.moveTo().map((List result) { - // Single move. - if (result[2] is Point) { - Point point = result[2]; - return [new SvgPathMoveSegment(point.x, point.y)]; - } - - // Multiple move. - if (result[2] is Iterable) { - return (result[2] as Iterable).where((e) => e is Point).map((Point p) { - return new SvgPathMoveSegment(p.x, p.y); - }); - } + return _argsParser(result[2], 1).map((List args) { + Point point = args[0]; + return new SvgPathMoveSegment(point.x, point.y); + }); }); @override lineTo() => super.lineTo().map((List result) { - // Single line. - if (result[2] is Point) { - Point point = result[2]; - return [new SvgPathLineSegment(point.x, point.y)]; - } - - // Multiple lines. - if (result[2] is Iterable) { - return (result[2] as Iterable).where((e) => e is Point).map((Point p) { - return new SvgPathLineSegment(p.x, p.y); - }).toList(growable: false); - } + return _argsParser(result[2], 1).map((List args) { + Point point = args[0]; + return new SvgPathLineSegment(point.x, point.y); + }); }); @override @@ -88,4 +72,24 @@ class SvgParserDefinition extends SvgGrammarDefinition { fractionalConstant() { return super.fractionalConstant().flatten().map(double.parse); } + + List _argsParser(seq, num argCount) { + Iterable it = seq is Iterable ? seq : [seq]; + + var arr = []; + + while (it != null) { + arr.add(it.take(argCount).toList(growable: false)); + + var next = it.skip(argCount + 1); + if (next.isEmpty) { + it = null; + } else { + var seq = next.single; + it = seq is Iterable ? seq : [seq]; + } + } + + return arr; + } } From 295228fbc2cb7b9a61c822a8e594fac6d3eda590 Mon Sep 17 00:00:00 2001 From: Mart Roosmaa Date: Sun, 20 Aug 2017 19:35:57 +0200 Subject: [PATCH 6/7] Implement parsid on missing SVG path commands --- README.md | 2 +- lib/src/command.dart | 7 +- lib/src/parser.dart | 104 +++++++++++++++++++++ test/parser_test.dart | 211 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 321 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ff216cf..4c565cb 100644 --- a/README.md +++ b/README.md @@ -49,5 +49,5 @@ svgPathToSource('M0,15,L15,15L7.5,0z') Currently supported --- -- Parsing a single SVG path that contains move, line, and close commands. +- Parsing a single SVG spec compatible path. - Converting a single SVG path to raw Dart (generated) code. diff --git a/lib/src/command.dart b/lib/src/command.dart index acfed3c..f83988b 100644 --- a/lib/src/command.dart +++ b/lib/src/command.dart @@ -105,6 +105,9 @@ class SvgPathCurveQuadraticSegment extends SvgPathPositionSegment { bool isRelative: false}) : super(x, y, SvgPathSegmentType.CurveTo, isRelative: isRelative); + /// Whether there is no control point (a smooth curve) + bool get isSmooth => x1 == null && y1 == null; + @override bool operator==(o) { if (o is! SvgPathCurveQuadraticSegment) return false; @@ -117,7 +120,7 @@ class SvgPathCurveQuadraticSegment extends SvgPathPositionSegment { } @override - String toString() => 'SvgPathCurveCubicSegment ' + { + String toString() => 'SvgPathCurveQuadraticSegment ' + { 'x': x, 'y': y, 'x1': x1, @@ -186,7 +189,7 @@ class SvgPathArcSegment extends SvgPathPositionSegment { /// The value of the sweep-flag parameter. final bool isSweep; - SvgPathArcSegment( + const SvgPathArcSegment( num x, num y, this.r1, diff --git a/lib/src/parser.dart b/lib/src/parser.dart index 9c9e574..c50c230 100644 --- a/lib/src/parser.dart +++ b/lib/src/parser.dart @@ -42,6 +42,107 @@ class SvgParserDefinition extends SvgGrammarDefinition { }); }); + @override + horizontalLineTo() => super.horizontalLineTo().map((List result) { + var isRelative = result[0] == 'h'; + + return _argsParser(result[2], 1).map((List args) { + num x = args[0]; + return new SvgPathLineSegment(x, null, isRelative: isRelative); + }); + }); + + @override + verticalLineTo() => super.verticalLineTo().map((List result) { + var isRelative = result[0] == 'v'; + + return _argsParser(result[2], 1).map((List args) { + num y = args[0]; + return new SvgPathLineSegment(null, y, isRelative: isRelative); + }); + }); + + @override + curveTo() => super.curveTo().map((List result) { + var isRelative = result[0] == 'c'; + + return _argsParser(result[2], 5).map((List args) { + var c1 = args[0]; + var c2 = args[2]; + var point = args[4]; + + return new SvgPathCurveCubicSegment( + point.x, point.y, + c1.x, c1.y, + c2.x, c2.y, + isRelative: isRelative); + }); + }); + + @override + smoothCurveTo() => super.smoothCurveTo().map((List result) { + var isRelative = result[0] == 's'; + + return _argsParser(result[2], 3).map((List args) { + var c2 = args[0]; + var point = args[2]; + + return new SvgPathCurveCubicSegment( + point.x, point.y, + null, null, + c2.x, c2.y, + isRelative: isRelative); + }); + }); + + @override + quadraticBezierCurveTo() => super.quadraticBezierCurveTo().map((List result) { + var isRelative = result[0] == 'q'; + + return _argsParser(result[2], 3).map((List args) { + var c1 = args[0]; + var point = args[2]; + + return new SvgPathCurveQuadraticSegment( + point.x, point.y, + c1.x, c1.y, + isRelative: isRelative); + }); + }); + + @override + smoothQuadraticBezierCurveTo() => super.smoothQuadraticBezierCurveTo().map((List result) { + var isRelative = result[0] == 't'; + + return _argsParser(result[2], 1).map((List args) { + var point = args[0]; + + return new SvgPathCurveQuadraticSegment( + point.x, point.y, + null, null, + isRelative: isRelative); + }); + }); + + @override + ellipticalArc() => super.ellipticalArc().map((List result) { + var isRelative = result[0] == 'a'; + + return _argsParser(result[2], 11).map((List args) { + num r1 = args[0]; + num r2 = args[2]; + num angle = args[4]; + bool isLargeArc = args[6]; + bool isSweep = args[8]; + Point point = args[10]; + + return new SvgPathArcSegment(point.x, point.y, r1, r2, angle, + isLargeArc: isLargeArc, + isSweep: isSweep, + isRelative: isRelative); + }); + }); + @override coordinatePair() => super.coordinatePair().map((result) { return new Point(result[0], result[2]); @@ -57,6 +158,9 @@ class SvgParserDefinition extends SvgGrammarDefinition { return number; }); + @override + flag() => super.flag().map((v) => v == '1'); + @override commaWsp() => super.commaWsp().map((_) => null); diff --git a/test/parser_test.dart b/test/parser_test.dart index ab53d70..cee6b7a 100644 --- a/test/parser_test.dart +++ b/test/parser_test.dart @@ -140,6 +140,217 @@ void main() { }); }); + group('Horizontal line to', () { + final parseHorizontalLine = definition.build( + start: definition.horizontalLineTo) + .parse; + + test('can parse a single line', () { + expect( + parseHorizontalLine('H5').value, + [const SvgPathLineSegment(5, null, isRelative: false)]); + }); + + test('can parse a single line with fractional values', () { + expect( + parseHorizontalLine('h1.2').value, + [const SvgPathLineSegment(1.2, null, isRelative: true)]); + }); + + test('can parse multiple line values', () { + expect( + parseHorizontalLine('h1,2,3').value, [ + const SvgPathLineSegment(1, null, isRelative: true), + const SvgPathLineSegment(2, null, isRelative: true), + const SvgPathLineSegment(3, null, isRelative: true) + ]); + }); + }); + + group('Vertical line to', () { + final parseVerticalLine = definition.build( + start: definition.verticalLineTo) + .parse; + + test('can parse a single line', () { + expect( + parseVerticalLine('V5').value, + [const SvgPathLineSegment(null, 5, isRelative: false)]); + }); + + test('can parse a single line with fractional values', () { + expect( + parseVerticalLine('v1.2').value, + [const SvgPathLineSegment(null, 1.2, isRelative: true)]); + }); + + test('can parse multiple line values', () { + expect( + parseVerticalLine('v1,2,3').value, [ + const SvgPathLineSegment(null, 1, isRelative: true), + const SvgPathLineSegment(null, 2, isRelative: true), + const SvgPathLineSegment(null, 3, isRelative: true) + ]); + }); + }); + + group('Curve to', () { + final parseCurve = definition.build( + start: definition.curveTo) + .parse; + + test('can parse a single line', () { + expect( + parseCurve('C101,102 253,104 255,206').value, + [const SvgPathCurveCubicSegment(255, 206, 101, 102, 253, 104, + isRelative: false)]); + }); + + test('can parse a single line with fractional values', () { + expect( + parseCurve('c10.1,10.2 25.3,10.4 25.5,20.6').value, + [const SvgPathCurveCubicSegment(25.5, 20.6, 10.1, 10.2, 25.3, 10.4, + isRelative: true)]); + }); + + test('can parse multiple line values', () { + expect( + parseCurve('C101,102 253,104 255,206 100,100 250,100 250,200 200,200 350,200 350,300').value, [ + const SvgPathCurveCubicSegment(255, 206, 101, 102, 253, 104, + isRelative: false), + const SvgPathCurveCubicSegment(250, 200, 100, 100, 250, 100, + isRelative: false), + const SvgPathCurveCubicSegment(350, 300, 200, 200, 350, 200, + isRelative: false) + ]); + }); + }); + + group('Smooth curve to', () { + final parseSmoothCurve = definition.build( + start: definition.smoothCurveTo) + .parse; + + test('can parse a single line', () { + expect( + parseSmoothCurve('S253,104 255,206').value, + [const SvgPathCurveCubicSegment(255, 206, null, null, 253, 104, + isRelative: false)]); + }); + + test('can parse a single line with fractional values', () { + expect( + parseSmoothCurve('s25.3,10.4 25.5,20.6').value, + [const SvgPathCurveCubicSegment(25.5, 20.6, null, null, 25.3, 10.4, + isRelative: true)]); + }); + + test('can parse multiple line values', () { + expect( + parseSmoothCurve('S253,104 255,206 250,100 250,200 350,200 350,300').value, [ + const SvgPathCurveCubicSegment(255, 206, null, null, 253, 104, + isRelative: false), + const SvgPathCurveCubicSegment(250, 200, null, null, 250, 100, + isRelative: false), + const SvgPathCurveCubicSegment(350, 300, null, null, 350, 200, + isRelative: false) + ]); + }); + }); + + group('Quadratic bezier curve to', () { + final parseQuadraticBezierCurve = definition.build( + start: definition.quadraticBezierCurveTo) + .parse; + + test('can parse a single line', () { + expect( + parseQuadraticBezierCurve('Q401,52 603,304').value, + [const SvgPathCurveQuadraticSegment(603, 304, 401, 52, + isRelative: false)]); + }); + + test('can parse a single line with fractional values', () { + expect( + parseQuadraticBezierCurve('q40.1,5.2 60.3,30.4').value, + [const SvgPathCurveQuadraticSegment(60.3, 30.4, 40.1, 5.2, + isRelative: true)]); + }); + + test('can parse multiple line values', () { + expect( + parseQuadraticBezierCurve('Q401,52 603,304 400,50 600,300 500,150 700,400').value, [ + const SvgPathCurveQuadraticSegment(603, 304, 401, 52, + isRelative: false), + const SvgPathCurveQuadraticSegment(600, 300, 400, 50, + isRelative: false), + const SvgPathCurveQuadraticSegment(700, 400, 500, 150, + isRelative: false) + ]); + }); + }); + + group('Smooth quadratic bezier curve to', () { + final parseSmoothQuadraticBezierCurve = definition.build( + start: definition.smoothQuadraticBezierCurveTo) + .parse; + + test('can parse a single line', () { + expect( + parseSmoothQuadraticBezierCurve('T603,304').value, + [const SvgPathCurveQuadraticSegment(603, 304, null, null, + isRelative: false)]); + }); + + test('can parse a single line with fractional values', () { + expect( + parseSmoothQuadraticBezierCurve('t60.3,30.4').value, + [const SvgPathCurveQuadraticSegment(60.3, 30.4, null, null, + isRelative: true)]); + }); + + test('can parse multiple line values', () { + expect( + parseSmoothQuadraticBezierCurve('T603,304 600,300 700,400').value, [ + const SvgPathCurveQuadraticSegment(603, 304, null, null, + isRelative: false), + const SvgPathCurveQuadraticSegment(600, 300, null, null, + isRelative: false), + const SvgPathCurveQuadraticSegment(700, 400, null, null, + isRelative: false) + ]); + }); + }); + + group('Elliptical arc', () { + final parseEllipticalArc = definition.build( + start: definition.ellipticalArc) + .parse; + + test('can parse a single arc', () { + expect( + parseEllipticalArc('a20,25 -30 0,1 50,-25').value, + [const SvgPathArcSegment(50, -25, 20, 25, -30, + isLargeArc: false, + isSweep: true, + isRelative: true)]); + }); + + test('can parse multiple arcs', () { + expect( + parseEllipticalArc('A20,25 -30 0,1 50,-25 10,15 -20 1,0 13,11').value, [ + const SvgPathArcSegment(50, -25, 20, 25, -30, + isLargeArc: false, + isSweep: true, + isRelative: false), + const SvgPathArcSegment(13, 11, 10, 15, -20, + isLargeArc: true, + isSweep: false, + isRelative: false) + ]); + }); + }); + group('Draw to', () { final parseDraw = definition.build( start: definition.drawToCommand) From 29be7c55c8c0a90475a488caa5164d731e12eeb5 Mon Sep 17 00:00:00 2001 From: Mart Roosmaa Date: Sun, 20 Aug 2017 20:15:19 +0200 Subject: [PATCH 7/7] Add new parsing functions to drawToCommand --- lib/src/grammar.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/src/grammar.dart b/lib/src/grammar.dart index 5a59d7b..cf9e0de 100644 --- a/lib/src/grammar.dart +++ b/lib/src/grammar.dart @@ -51,7 +51,11 @@ class SvgGrammarDefinition extends pp.GrammarDefinition { /// | quadratic-bezier-curveto /// | smooth-quadratic-bezier-curveto /// | elliptical-arc - drawToCommand() => closePath() | lineTo(); + drawToCommand() => + closePath() | lineTo() | horizontalLineTo() + | verticalLineTo() | curveTo() | smoothCurveTo() + | quadraticBezierCurveTo() | smoothQuadraticBezierCurveTo() + | ellipticalArc(); /// moveto: /// ( "M" | "m" ) wsp* moveto-argument-sequence