Skip to content

Commit ac0efe8

Browse files
authored
Fix parsing of documents with only top-level Sections (#73)
Encountered a nasty bug that a document with only top-level Sections, which by design is a valid and admissible Technique (and is even how `struct Technique` is defined) was not being parsed but worse was being silently discarded. Fixed an omission in the formatter where sections were not being preceded by a blank line. Added an example to the golden tests to enforce this remaining correct in the future.
2 parents e4c87fa + f68758d commit ac0efe8

File tree

3 files changed

+89
-20
lines changed

3 files changed

+89
-20
lines changed

examples/golden/Bedtime.t

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
% technique v1
2+
! Private; © 2025 My Lovely Family
3+
& childrens-task-list
4+
5+
I. Before school
6+
7+
1. Drink glass of water
8+
2. Eat breakfast
9+
3. Put dishes into sink
10+
4. Get changed
11+
5. Pack school bag
12+
6. Clean teeth
13+
7. Shoes on, wait by front door!
14+
15+
II. After school
16+
17+
1. Shower
18+
2. Get changed into home clothes
19+
3. Bring lunchbox to kitchen
20+
4. Put dirty school clothes at washing machine
21+
5. Turn on heater
22+
6. Homework (as required)
23+
24+
III. Dinner
25+
26+
1. Set table
27+
2. Eat dinner
28+
3. Put dishes into dishwasher
29+
30+
IV. Bedtime
31+
32+
1. Get changed into pyjamas
33+
2. Clean teeth
34+
3. Read story
35+
4. Into bed!

src/formatting/formatter.rs

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -220,11 +220,12 @@ impl<'i> Formatter<'i> {
220220
}
221221

222222
fn switch_syntax(&mut self, new_syntax: Syntax) {
223-
if !self.buffer.is_empty() {
224-
self.fragments.push((
225-
self.current,
226-
Cow::Owned(std::mem::take(&mut self.buffer)),
227-
));
223+
if !self
224+
.buffer
225+
.is_empty()
226+
{
227+
self.fragments
228+
.push((self.current, Cow::Owned(std::mem::take(&mut self.buffer))));
228229
}
229230
self.current = new_syntax;
230231
}
@@ -234,11 +235,12 @@ impl<'i> Formatter<'i> {
234235
}
235236

236237
pub fn flush_current(&mut self) {
237-
if !self.buffer.is_empty() {
238-
self.fragments.push((
239-
self.current,
240-
Cow::Owned(std::mem::take(&mut self.buffer)),
241-
));
238+
if !self
239+
.buffer
240+
.is_empty()
241+
{
242+
self.fragments
243+
.push((self.current, Cow::Owned(std::mem::take(&mut self.buffer))));
242244
}
243245
}
244246

@@ -410,6 +412,11 @@ impl<'i> Formatter<'i> {
410412
fn format_technique(&mut self, technique: &'i Technique) {
411413
match technique {
412414
Technique::Steps(steps) => {
415+
// if a header has already been added,
416+
// separate the upcoming steps with a blank line.
417+
if !self.is_empty() {
418+
self.append_char('\n');
419+
}
413420
self.append_steps(steps);
414421
}
415422
Technique::Procedures(procedures) => {
@@ -1242,7 +1249,10 @@ impl<'a, 'i> Line<'a, 'i> {
12421249

12431250
fn wrap_line(&mut self) {
12441251
// Emit all current fragments to the output
1245-
for (syntax, content) in self.current.drain(..) {
1252+
for (syntax, content) in self
1253+
.current
1254+
.drain(..)
1255+
{
12461256
self.output
12471257
.add_fragment(syntax, content);
12481258
}
@@ -1261,7 +1271,10 @@ impl<'a, 'i> Line<'a, 'i> {
12611271
.is_empty()
12621272
{
12631273
// Emit all current fragments to the output
1264-
for (syntax, content) in self.current.drain(..) {
1274+
for (syntax, content) in self
1275+
.current
1276+
.drain(..)
1277+
{
12651278
self.output
12661279
.add_fragment(syntax, content);
12671280
}

src/parsing/parser.rs

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ impl<'i> Parser<'i> {
124124

125125
// Parse zero or more procedures, handling sections if they exist
126126
let mut procedures = Vec::new();
127+
let mut sections = Vec::new();
127128

128129
while !self.is_finished() {
129130
self.trim_whitespace();
@@ -132,7 +133,25 @@ impl<'i> Parser<'i> {
132133
break;
133134
}
134135

135-
if is_procedure_declaration(self.source) {
136+
// Check if this Technique is a a single set of one or more
137+
// top-level Scope::SectionChunk
138+
139+
if is_section(self.source) && procedures.is_empty() {
140+
while !self.is_finished() {
141+
self.trim_whitespace();
142+
if self.is_finished() {
143+
break;
144+
}
145+
146+
if is_section(self.source) {
147+
let section = self.read_section()?;
148+
sections.push(section);
149+
} else {
150+
return Err(ParsingError::Unrecognized(self.offset));
151+
}
152+
}
153+
break;
154+
} else if is_procedure_declaration(self.source) {
136155
let mut procedure = self.take_block_lines(
137156
is_procedure_declaration,
138157
|line| is_section(line) || is_procedure_declaration(line),
@@ -186,10 +205,12 @@ impl<'i> Parser<'i> {
186205
}
187206
}
188207

189-
let body = if procedures.is_empty() {
190-
None
191-
} else {
208+
let body = if !sections.is_empty() {
209+
Some(Technique::Steps(sections))
210+
} else if !procedures.is_empty() {
192211
Some(Technique::Procedures(procedures))
212+
} else {
213+
None
193214
};
194215

195216
Ok(Document { header, body })
@@ -890,11 +911,11 @@ impl<'i> Parser<'i> {
890911
}
891912

892913
// Try to parse steps
893-
if is_substep_dependent(outer.source) {
894-
let step = outer.read_substep_dependent()?;
914+
if is_step_dependent(outer.source) {
915+
let step = outer.read_step_dependent()?;
895916
steps.push(step);
896-
} else if is_substep_parallel(outer.source) {
897-
let step = outer.read_substep_parallel()?;
917+
} else if is_step_parallel(outer.source) {
918+
let step = outer.read_step_parallel()?;
898919
steps.push(step);
899920
} else {
900921
// Skip unrecognized content line by line

0 commit comments

Comments
 (0)