Skip to content

Commit 973ef55

Browse files
committed
Fix handling of empty paragraph
1 parent 531c819 commit 973ef55

5 files changed

Lines changed: 172 additions & 0 deletions

File tree

lib/src/org/grammar.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ class OrgContentGrammarDefinition extends GrammarDefinition {
110110
ref0(localVariables) |
111111
ref0(pgpBlock) |
112112
ref0(comment) |
113+
ref0(whitespaceOnlyParagraph) |
113114
ref0(paragraph);
114115

115116
Parser elements() => ref0(element).star();
@@ -124,6 +125,11 @@ class OrgContentGrammarDefinition extends GrammarDefinition {
124125
ref1(textRun, ref0(paragraphEnd)).plusLazy(ref0(paragraphEnd)) &
125126
ref0(blankLines);
126127

128+
Parser whitespaceOnlyParagraph() =>
129+
(lineStart() & insignificantWhitespace().plusString())
130+
.flatten(message: 'Paragraph indent expected') &
131+
endOfInput();
132+
127133
Parser nonParagraphElement() =>
128134
element()..replace(ref0(paragraph), noOpFail());
129135

lib/src/org/parser.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,12 @@ class OrgContentParserDefinition extends OrgContentGrammarDefinition {
191191
return OrgParagraph(indent, body, trailing);
192192
});
193193

194+
@override
195+
Parser whitespaceOnlyParagraph() => super.whitespaceOnlyParagraph().map((items) {
196+
final indent = items[0] as String;
197+
return OrgParagraph(indent, OrgContent([]), '');
198+
});
199+
194200
@override
195201
Parser plainText([Parser? limit]) =>
196202
super.plainText(limit).map((value) => OrgPlainText(value as String));

test/org/model/parser_test.dart

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,35 @@ baz''', interpretEmbeddedSettings: true);
552552
final radioLink = doc.find<OrgRadioLink>((_) => true);
553553
expect(radioLink!.node.content, 'baz');
554554
});
555+
group('whitespace-only content', () {
556+
test('round-trips document containing only spaces', () {
557+
const markup = ' ';
558+
final doc = OrgDocument.parse(markup);
559+
final paragraph = doc.content!.children.single as OrgParagraph;
560+
expect(paragraph.indent, ' ');
561+
expect(paragraph.body.children, isEmpty);
562+
expect(doc.toMarkup(), markup);
563+
});
564+
565+
test('round-trips document containing only tabs', () {
566+
const markup = '\t';
567+
final doc = OrgDocument.parse(markup);
568+
final paragraph = doc.content!.children.single as OrgParagraph;
569+
expect(paragraph.indent, '\t');
570+
expect(paragraph.body.children, isEmpty);
571+
expect(doc.toMarkup(), markup);
572+
});
573+
574+
test('round-trips section content ending with spaces only', () {
575+
const markup = '* x\n ';
576+
final doc = OrgDocument.parse(markup);
577+
final paragraph =
578+
doc.sections.single.content!.children.single as OrgParagraph;
579+
expect(paragraph.indent, ' ');
580+
expect(paragraph.body.children, isEmpty);
581+
expect(doc.toMarkup(), markup);
582+
});
583+
});
555584
});
556585
}
557586

test/org/parser/parser_test.dart

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,5 +135,37 @@ r0SZNYouvk7tY6rDz0Z62WRwOtrBx0D/5T0E9kT3rpnB
135135
baz buzz''');
136136
expect(doc.sections[0].headline.keyword?.value, 'TODO');
137137
});
138+
group('whitespace-only content', () {
139+
test('document containing only spaces', () {
140+
final result = parser.parse(' ');
141+
expect(result, isA<Success<dynamic>>());
142+
final document = result.value as OrgDocument;
143+
final paragraph = document.content!.children.single as OrgParagraph;
144+
expect(paragraph.indent, ' ');
145+
expect(paragraph.body.children, isEmpty);
146+
expect(paragraph.trailing, '');
147+
});
148+
149+
test('document containing only tabs', () {
150+
final result = parser.parse('\t');
151+
expect(result, isA<Success<dynamic>>());
152+
final document = result.value as OrgDocument;
153+
final paragraph = document.content!.children.single as OrgParagraph;
154+
expect(paragraph.indent, '\t');
155+
expect(paragraph.body.children, isEmpty);
156+
expect(paragraph.trailing, '');
157+
});
158+
159+
test('section content ending with spaces only', () {
160+
final result = parser.parse('* x\n ');
161+
expect(result, isA<Success<dynamic>>());
162+
final document = result.value as OrgDocument;
163+
final paragraph = document.sections.single.content!.children.single
164+
as OrgParagraph;
165+
expect(paragraph.indent, ' ');
166+
expect(paragraph.body.children, isEmpty);
167+
expect(paragraph.trailing, '');
168+
});
169+
});
138170
});
139171
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import 'dart:math';
2+
3+
import 'package:org_parser/org_parser.dart';
4+
import 'package:test/test.dart';
5+
6+
void main() {
7+
group('robustness', () {
8+
test('OrgDocument.parse does not throw on short punctuation-heavy inputs',
9+
() {
10+
const alphabet = [
11+
'*',
12+
' ',
13+
'\n',
14+
':',
15+
'[',
16+
']',
17+
'#',
18+
'+',
19+
'-',
20+
'/',
21+
'\\',
22+
'{',
23+
'}',
24+
'<',
25+
'>',
26+
'|',
27+
'a',
28+
'1',
29+
'\r',
30+
'\t',
31+
];
32+
33+
for (var len = 0; len <= 4; len++) {
34+
final total = pow(alphabet.length, len).toInt();
35+
for (var i = 0; i < total; i++) {
36+
var n = i;
37+
final buf = StringBuffer();
38+
for (var j = 0; j < len; j++) {
39+
buf.write(alphabet[n % alphabet.length]);
40+
n ~/= alphabet.length;
41+
}
42+
final input = buf.toString();
43+
expect(
44+
() => OrgDocument.parse(input),
45+
returnsNormally,
46+
reason: 'input=${_escape(input)}',
47+
);
48+
}
49+
}
50+
});
51+
52+
test('OrgDocument.parse does not throw on deterministic randomized inputs',
53+
() {
54+
const alphabet = [
55+
'*',
56+
' ',
57+
'\n',
58+
':',
59+
'[',
60+
']',
61+
'#',
62+
'+',
63+
'-',
64+
'/',
65+
'\\',
66+
'{',
67+
'}',
68+
'<',
69+
'>',
70+
'|',
71+
'a',
72+
'1',
73+
'\r',
74+
'\t',
75+
];
76+
final rand = Random(0);
77+
78+
for (var i = 0; i < 5000; i++) {
79+
final len = rand.nextInt(120);
80+
final buf = StringBuffer();
81+
for (var j = 0; j < len; j++) {
82+
buf.write(alphabet[rand.nextInt(alphabet.length)]);
83+
}
84+
final input = buf.toString();
85+
expect(
86+
() => OrgDocument.parse(input),
87+
returnsNormally,
88+
reason: 'case=$i input=${_escape(input)}',
89+
);
90+
}
91+
});
92+
});
93+
}
94+
95+
String _escape(String input) => input
96+
.replaceAll('\\', r'\\')
97+
.replaceAll('\n', r'\n')
98+
.replaceAll('\r', r'\r')
99+
.replaceAll('\t', r'\t');

0 commit comments

Comments
 (0)