Skip to main content

brink_format/inkt/
read.rs

1//! Pest-based reader for the `.inkt` textual format.
2
3use pest::Parser;
4use pest_derive::Parser;
5
6use crate::counting::CountingFlags;
7use crate::definition::{
8    AddressDef, AddressPath, ContainerDef, ExternalFnDef, GlobalVarDef, LineEntry, ListDef,
9    ListItemDef, ScopeLineTable, SlotInfo, SourceLocation,
10};
11use crate::id::{DefinitionId, NameId};
12use crate::line::{LineContent, LinePart, PluralCategory, SelectKey};
13use crate::opcode::{ChoiceFlags, Opcode, SequenceKind};
14use crate::story::StoryData;
15use crate::value::{ListValue, Value, ValueType};
16
17#[derive(Parser)]
18#[grammar = "inkt/inkt.pest"]
19struct InktParser;
20
21/// Error returned when parsing `.inkt` text fails.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct InktParseError {
24    pub message: String,
25    pub line: usize,
26    pub col: usize,
27}
28
29impl core::fmt::Display for InktParseError {
30    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
31        write!(f, "{}:{}: {}", self.line, self.col, self.message)
32    }
33}
34
35impl std::error::Error for InktParseError {}
36
37/// Parse `.inkt` text into a [`StoryData`].
38pub fn read_inkt(input: &str) -> Result<StoryData, InktParseError> {
39    let pairs = InktParser::parse(Rule::story, input).map_err(|e| {
40        let (line, col) = match e.line_col {
41            pest::error::LineColLocation::Pos(pos) => pos,
42            pest::error::LineColLocation::Span(start, _) => start,
43        };
44        InktParseError {
45            message: e.to_string(),
46            line,
47            col,
48        }
49    })?;
50
51    let story_pair = pairs.into_iter().next().ok_or_else(|| InktParseError {
52        message: "no story node".into(),
53        line: 1,
54        col: 1,
55    })?;
56
57    parse_story(story_pair)
58}
59
60type P<'a> = pest::iterators::Pair<'a, Rule>;
61
62fn err(pair: &P<'_>, msg: impl Into<String>) -> InktParseError {
63    let (line, col) = pair.line_col();
64    InktParseError {
65        message: msg.into(),
66        line,
67        col,
68    }
69}
70
71fn parse_story(pair: P<'_>) -> Result<StoryData, InktParseError> {
72    let mut name_table = Vec::new();
73    let mut variables = Vec::new();
74    let mut list_defs = Vec::new();
75    let mut list_items = Vec::new();
76    let mut externals = Vec::new();
77    let mut addresses = Vec::new();
78    let mut address_paths = Vec::new();
79    let mut containers = Vec::new();
80    let mut line_tables = Vec::new();
81    let mut list_literals = Vec::new();
82    let mut source_checksum = 0u32;
83
84    for inner in pair.into_inner() {
85        match inner.as_rule() {
86            Rule::story_checksum => {
87                if let Some(hex_pair) = inner.into_inner().next() {
88                    source_checksum = parse_hex_u32(hex_pair.as_str());
89                }
90            }
91            Rule::name_table => name_table = parse_name_table(inner)?,
92            Rule::globals => variables = parse_globals(inner)?,
93            Rule::lists => list_defs = parse_lists(inner)?,
94            Rule::list_items => list_items = parse_list_items(inner)?,
95            Rule::externals => externals = parse_externals(inner)?,
96            Rule::addresses => addresses = parse_addresses(inner)?,
97            Rule::address_paths => address_paths = parse_address_paths(inner)?,
98            Rule::list_literals => list_literals = parse_list_literals(inner)?,
99            Rule::container => {
100                let (container, lt) = parse_container(inner)?;
101                let is_scope_owner = container.scope_id == container.id;
102                containers.push(container);
103                // Only add line tables for scope-owning containers.
104                // Child containers (scope_id != id) have no lines in the text.
105                if is_scope_owner {
106                    line_tables.push(lt);
107                }
108            }
109            _ => {}
110        }
111    }
112
113    // Sort line tables by scope_id for deterministic ordering,
114    // matching the converter's output.
115    line_tables.sort_by_key(|lt| lt.scope_id.to_raw());
116
117    Ok(StoryData {
118        containers,
119        line_tables,
120        variables,
121        list_defs,
122        list_items,
123        externals,
124        addresses,
125        address_paths,
126        name_table,
127        list_literals,
128        source_checksum,
129    })
130}
131
132// ── Name table ──────────────────────────────────────────────────────────────
133
134fn parse_name_table(pair: P<'_>) -> Result<Vec<String>, InktParseError> {
135    let mut names = Vec::new();
136    for entry in pair.into_inner() {
137        if entry.as_rule() == Rule::name_entry {
138            let mut inner = entry.into_inner();
139            let _index = inner.next(); // integer index (implied by position)
140            let s = inner.next().ok_or_else(|| InktParseError {
141                message: "expected string in name_entry".into(),
142                line: 0,
143                col: 0,
144            })?;
145            names.push(unescape_string(s.as_str()));
146        }
147    }
148    Ok(names)
149}
150
151// ── Globals ─────────────────────────────────────────────────────────────────
152
153fn parse_globals(pair: P<'_>) -> Result<Vec<GlobalVarDef>, InktParseError> {
154    let mut vars = Vec::new();
155    for entry in pair.into_inner() {
156        if entry.as_rule() == Rule::global_entry {
157            vars.push(parse_global_entry(entry)?);
158        }
159    }
160    Ok(vars)
161}
162
163fn parse_global_entry(pair: P<'_>) -> Result<GlobalVarDef, InktParseError> {
164    let mut inner = pair.into_inner();
165    let id = parse_def_id(inner.next().ok_or_else(|| InktParseError {
166        message: "expected def_id in global".into(),
167        line: 0,
168        col: 0,
169    })?)?;
170
171    let type_pair = inner.next().ok_or_else(|| InktParseError {
172        message: "expected type_name in global".into(),
173        line: 0,
174        col: 0,
175    })?;
176    let value_type = parse_value_type(type_pair)?;
177
178    let value_pair = inner.next().ok_or_else(|| InktParseError {
179        message: "expected value in global".into(),
180        line: 0,
181        col: 0,
182    })?;
183    let default_value = parse_value(value_pair, Some(value_type))?;
184
185    let mut mutable = false;
186    let mut name = NameId(0);
187
188    for remaining in inner {
189        match remaining.as_rule() {
190            Rule::mutable_flag => mutable = true,
191            Rule::integer => {
192                name = NameId(parse_u16(&remaining)?);
193            }
194            _ => {}
195        }
196    }
197
198    Ok(GlobalVarDef {
199        id,
200        name,
201        value_type,
202        default_value,
203        mutable,
204    })
205}
206
207#[expect(clippy::needless_pass_by_value)]
208fn parse_value_type(pair: P<'_>) -> Result<ValueType, InktParseError> {
209    let s = pair.as_str();
210    match s {
211        "int" => Ok(ValueType::Int),
212        "float" => Ok(ValueType::Float),
213        "bool" => Ok(ValueType::Bool),
214        "string" => Ok(ValueType::String),
215        "list" => Ok(ValueType::List),
216        "divert_target" => Ok(ValueType::DivertTarget),
217        "var_pointer" => Ok(ValueType::VariablePointer),
218        "temp_pointer" => Ok(ValueType::TempPointer),
219        "fragment_ref" => Ok(ValueType::FragmentRef),
220        "null" => Ok(ValueType::Null),
221        _ => Err(err(&pair, format!("unknown value type: {s}"))),
222    }
223}
224
225fn parse_value(pair: P<'_>, type_hint: Option<ValueType>) -> Result<Value, InktParseError> {
226    let inner = pair.into_inner().next().ok_or_else(|| InktParseError {
227        message: "empty value".into(),
228        line: 0,
229        col: 0,
230    })?;
231
232    match inner.as_rule() {
233        Rule::integer => {
234            // Use the type hint to disambiguate: an integer literal can represent
235            // Int, Float, or Bool depending on the declared value_type.
236            match type_hint {
237                Some(ValueType::Float) => {
238                    let n: f32 = inner
239                        .as_str()
240                        .parse()
241                        .map_err(|_| err(&inner, "invalid float"))?;
242                    Ok(Value::Float(n))
243                }
244                Some(ValueType::Bool) => {
245                    let n: i32 = inner
246                        .as_str()
247                        .parse()
248                        .map_err(|_| err(&inner, "invalid integer"))?;
249                    Ok(Value::Bool(n != 0))
250                }
251                _ => {
252                    let n: i32 = inner
253                        .as_str()
254                        .parse()
255                        .map_err(|_| err(&inner, "invalid integer"))?;
256                    Ok(Value::Int(n))
257                }
258            }
259        }
260        Rule::float => {
261            let n: f32 = inner
262                .as_str()
263                .parse()
264                .map_err(|_| err(&inner, "invalid float"))?;
265            Ok(Value::Float(n))
266        }
267        Rule::bool_value => Ok(Value::Bool(inner.as_str() == "true")),
268        Rule::string => Ok(Value::String(unescape_string(inner.as_str()).into())),
269        Rule::def_id => Ok(Value::DivertTarget(parse_def_id(inner)?)),
270        Rule::null_value => Ok(Value::Null),
271        Rule::list_value => parse_list_value(inner),
272        Rule::var_pointer_value => {
273            let id_pair = inner.into_inner().next().ok_or_else(|| InktParseError {
274                message: "expected def_id in var_pointer".into(),
275                line: 0,
276                col: 0,
277            })?;
278            Ok(Value::VariablePointer(parse_def_id(id_pair)?))
279        }
280        Rule::fragment_ref_value => {
281            let idx_pair = inner.into_inner().next().ok_or_else(|| InktParseError {
282                message: "expected integer in fragment_ref".into(),
283                line: 0,
284                col: 0,
285            })?;
286            let idx: u32 = idx_pair.as_str().parse().map_err(|_| InktParseError {
287                message: "invalid fragment_ref index".into(),
288                line: 0,
289                col: 0,
290            })?;
291            Ok(Value::FragmentRef(idx))
292        }
293        _ => Err(err(
294            &inner,
295            format!("unexpected value rule: {:?}", inner.as_rule()),
296        )),
297    }
298}
299
300fn parse_list_value(pair: P<'_>) -> Result<Value, InktParseError> {
301    let mut items = Vec::new();
302    let mut origins = Vec::new();
303
304    for child in pair.into_inner() {
305        match child.as_rule() {
306            Rule::list_value_items => {
307                for def_pair in child.into_inner() {
308                    if def_pair.as_rule() == Rule::def_id {
309                        items.push(parse_def_id(def_pair)?);
310                    }
311                }
312            }
313            Rule::list_value_origins => {
314                for def_pair in child.into_inner() {
315                    if def_pair.as_rule() == Rule::def_id {
316                        origins.push(parse_def_id(def_pair)?);
317                    }
318                }
319            }
320            _ => {}
321        }
322    }
323
324    Ok(Value::List(ListValue { items, origins }.into()))
325}
326
327// ── Lists ───────────────────────────────────────────────────────────────────
328
329fn parse_lists(pair: P<'_>) -> Result<Vec<ListDef>, InktParseError> {
330    let mut defs = Vec::new();
331    for entry in pair.into_inner() {
332        if entry.as_rule() == Rule::list_entry {
333            defs.push(parse_list_entry(entry)?);
334        }
335    }
336    Ok(defs)
337}
338
339fn parse_list_entry(pair: P<'_>) -> Result<ListDef, InktParseError> {
340    let mut inner = pair.into_inner();
341    let id = parse_def_id(inner.next().ok_or_else(|| InktParseError {
342        message: "expected def_id in list".into(),
343        line: 0,
344        col: 0,
345    })?)?;
346
347    let name_int = inner.next().ok_or_else(|| InktParseError {
348        message: "expected name integer in list".into(),
349        line: 0,
350        col: 0,
351    })?;
352    let name = NameId(parse_u16(&name_int)?);
353
354    let mut items = Vec::new();
355    for remaining in inner {
356        if remaining.as_rule() == Rule::list_item_inline {
357            let mut li_inner = remaining.into_inner();
358            let item_name_id = parse_u16(&li_inner.next().ok_or_else(|| InktParseError {
359                message: "expected name in list item".into(),
360                line: 0,
361                col: 0,
362            })?)?;
363            let ordinal: i32 = li_inner
364                .next()
365                .ok_or_else(|| InktParseError {
366                    message: "expected ordinal in list item".into(),
367                    line: 0,
368                    col: 0,
369                })?
370                .as_str()
371                .parse()
372                .map_err(|_| InktParseError {
373                    message: "invalid ordinal".into(),
374                    line: 0,
375                    col: 0,
376                })?;
377            items.push((NameId(item_name_id), ordinal));
378        }
379    }
380
381    Ok(ListDef { id, name, items })
382}
383
384// ── List items ──────────────────────────────────────────────────────────────
385
386fn parse_list_items(pair: P<'_>) -> Result<Vec<ListItemDef>, InktParseError> {
387    let mut items = Vec::new();
388    for entry in pair.into_inner() {
389        if entry.as_rule() == Rule::list_item_entry {
390            items.push(parse_list_item_entry(entry)?);
391        }
392    }
393    Ok(items)
394}
395
396fn parse_list_item_entry(pair: P<'_>) -> Result<ListItemDef, InktParseError> {
397    let mut inner = pair.into_inner();
398    let id = parse_def_id(next_rule(&mut inner, Rule::def_id, "list_item id")?)?;
399    let origin = parse_def_id(next_rule(&mut inner, Rule::def_id, "list_item origin")?)?;
400    let ordinal: i32 = next_rule(&mut inner, Rule::integer, "list_item ordinal")?
401        .as_str()
402        .parse()
403        .map_err(|_| InktParseError {
404            message: "invalid ordinal".into(),
405            line: 0,
406            col: 0,
407        })?;
408    let name_val =
409        next_rule(&mut inner, Rule::integer, "list_item name").map_or(Ok(0), |p| parse_u16(&p))?;
410    Ok(ListItemDef {
411        id,
412        origin,
413        ordinal,
414        name: NameId(name_val),
415    })
416}
417
418// ── Externals ───────────────────────────────────────────────────────────────
419
420fn parse_externals(pair: P<'_>) -> Result<Vec<ExternalFnDef>, InktParseError> {
421    let mut exts = Vec::new();
422    for entry in pair.into_inner() {
423        if entry.as_rule() == Rule::extern_entry {
424            exts.push(parse_extern_entry(entry)?);
425        }
426    }
427    Ok(exts)
428}
429
430fn parse_extern_entry(pair: P<'_>) -> Result<ExternalFnDef, InktParseError> {
431    let mut inner = pair.into_inner();
432    let id = parse_def_id(inner.next().ok_or_else(|| InktParseError {
433        message: "expected def_id in extern".into(),
434        line: 0,
435        col: 0,
436    })?)?;
437
438    let argc_pair = inner.next().ok_or_else(|| InktParseError {
439        message: "expected argc in extern".into(),
440        line: 0,
441        col: 0,
442    })?;
443    let arg_count: u8 = argc_pair
444        .as_str()
445        .parse()
446        .map_err(|_| err(&argc_pair, "invalid argc"))?;
447
448    let name_int = inner.next().ok_or_else(|| InktParseError {
449        message: "expected name in extern".into(),
450        line: 0,
451        col: 0,
452    })?;
453    let name = NameId(parse_u16(&name_int)?);
454
455    let mut fallback = None;
456    for remaining in inner {
457        if remaining.as_rule() == Rule::fallback {
458            let fb_inner = remaining
459                .into_inner()
460                .next()
461                .ok_or_else(|| InktParseError {
462                    message: "expected def_id in fallback".into(),
463                    line: 0,
464                    col: 0,
465                })?;
466            fallback = Some(parse_def_id(fb_inner)?);
467        }
468    }
469
470    Ok(ExternalFnDef {
471        id,
472        name,
473        arg_count,
474        fallback,
475    })
476}
477
478// ── Addresses ───────────────────────────────────────────────────────────────
479
480fn parse_addresses(pair: P<'_>) -> Result<Vec<AddressDef>, InktParseError> {
481    let mut addresses = Vec::new();
482    for entry in pair.into_inner() {
483        if entry.as_rule() == Rule::address_entry {
484            addresses.push(parse_address_entry(entry)?);
485        }
486    }
487    Ok(addresses)
488}
489
490fn parse_address_entry(pair: P<'_>) -> Result<AddressDef, InktParseError> {
491    let mut inner = pair.into_inner();
492    let id = parse_def_id(inner.next().ok_or_else(|| InktParseError {
493        message: "expected def_id in address".into(),
494        line: 0,
495        col: 0,
496    })?)?;
497    let container_id = parse_def_id(inner.next().ok_or_else(|| InktParseError {
498        message: "expected container_id in address".into(),
499        line: 0,
500        col: 0,
501    })?)?;
502    let offset_pair = inner.next().ok_or_else(|| InktParseError {
503        message: "expected byte_offset in address".into(),
504        line: 0,
505        col: 0,
506    })?;
507    let byte_offset: u32 = offset_pair
508        .as_str()
509        .parse()
510        .map_err(|_| err(&offset_pair, "invalid byte_offset"))?;
511    Ok(AddressDef {
512        id,
513        container_id,
514        byte_offset,
515    })
516}
517
518fn parse_address_paths(pair: P<'_>) -> Result<Vec<AddressPath>, InktParseError> {
519    let mut paths = Vec::new();
520    for entry in pair.into_inner() {
521        if entry.as_rule() == Rule::address_path_entry {
522            paths.push(parse_address_path_entry(entry)?);
523        }
524    }
525    Ok(paths)
526}
527
528fn parse_address_path_entry(pair: P<'_>) -> Result<AddressPath, InktParseError> {
529    let mut inner = pair.into_inner();
530    let path_int = inner.next().ok_or_else(|| InktParseError {
531        message: "expected path index in address_path".into(),
532        line: 0,
533        col: 0,
534    })?;
535    let path = NameId(parse_u16(&path_int)?);
536    let target = parse_def_id(inner.next().ok_or_else(|| InktParseError {
537        message: "expected target def_id in address_path".into(),
538        line: 0,
539        col: 0,
540    })?)?;
541    Ok(AddressPath { path, target })
542}
543
544// ── List literals ────────────────────────────────────────────────────────────
545
546fn parse_list_literals(pair: P<'_>) -> Result<Vec<ListValue>, InktParseError> {
547    let mut literals = Vec::new();
548    for entry in pair.into_inner() {
549        if entry.as_rule() == Rule::list_literal_entry {
550            literals.push(parse_list_literal_entry(entry)?);
551        }
552    }
553    Ok(literals)
554}
555
556fn parse_list_literal_entry(pair: P<'_>) -> Result<ListValue, InktParseError> {
557    let mut items = Vec::new();
558    let mut origins = Vec::new();
559
560    for child in pair.into_inner() {
561        match child.as_rule() {
562            Rule::list_value_items => {
563                for def_pair in child.into_inner() {
564                    if def_pair.as_rule() == Rule::def_id {
565                        items.push(parse_def_id(def_pair)?);
566                    }
567                }
568            }
569            Rule::list_value_origins => {
570                for def_pair in child.into_inner() {
571                    if def_pair.as_rule() == Rule::def_id {
572                        origins.push(parse_def_id(def_pair)?);
573                    }
574                }
575            }
576            _ => {}
577        }
578    }
579
580    Ok(ListValue { items, origins })
581}
582
583// ── Containers ──────────────────────────────────────────────────────────────
584
585fn parse_container(pair: P<'_>) -> Result<(ContainerDef, ScopeLineTable), InktParseError> {
586    let mut inner = pair.into_inner();
587    let id = parse_def_id(inner.next().ok_or_else(|| InktParseError {
588        message: "expected def_id in container".into(),
589        line: 0,
590        col: 0,
591    })?)?;
592
593    let mut counting_flags = CountingFlags::empty();
594    let mut path_hash = 0i32;
595    let mut param_count = 0u8;
596    let mut lines = Vec::new();
597    let mut bytecode = Vec::new();
598    let mut name: Option<NameId> = None;
599
600    let mut scope_id = id;
601
602    for child in inner {
603        match child.as_rule() {
604            Rule::scope_field => {
605                let scope_pair = child.into_inner().next().ok_or_else(|| InktParseError {
606                    message: "expected def_id in scope".into(),
607                    line: 0,
608                    col: 0,
609                })?;
610                scope_id = parse_def_id(scope_pair)?;
611            }
612            Rule::container_name_field => {
613                let val = child.into_inner().next().ok_or_else(|| InktParseError {
614                    message: "expected integer in container name".into(),
615                    line: 0,
616                    col: 0,
617                })?;
618                name = Some(NameId(parse_u16(&val)?));
619            }
620            Rule::flags_field => {
621                for flag in child.into_inner() {
622                    if flag.as_rule() == Rule::flag_name {
623                        match flag.as_str() {
624                            "visits" => counting_flags |= CountingFlags::VISITS,
625                            "turns" => counting_flags |= CountingFlags::TURNS,
626                            "start_only" => counting_flags |= CountingFlags::COUNT_START_ONLY,
627                            _ => {}
628                        }
629                    }
630                }
631            }
632            Rule::path_hash_field => {
633                let val = child.into_inner().next().ok_or_else(|| InktParseError {
634                    message: "expected integer in path_hash".into(),
635                    line: 0,
636                    col: 0,
637                })?;
638                path_hash = val.as_str().parse().map_err(|_| InktParseError {
639                    message: "invalid path_hash integer".into(),
640                    line: 0,
641                    col: 0,
642                })?;
643            }
644            Rule::params_field => {
645                let val = child.into_inner().next().ok_or_else(|| InktParseError {
646                    message: "expected integer in params".into(),
647                    line: 0,
648                    col: 0,
649                })?;
650                param_count = val.as_str().parse().map_err(|_| InktParseError {
651                    message: "invalid params integer".into(),
652                    line: 0,
653                    col: 0,
654                })?;
655            }
656            Rule::lines_field => {
657                lines = parse_lines_field(child)?;
658            }
659            Rule::code_field => {
660                bytecode = parse_code_field(child)?;
661            }
662            _ => {}
663        }
664    }
665
666    let container = ContainerDef {
667        id,
668        scope_id,
669        name,
670        bytecode,
671        counting_flags,
672        path_hash,
673        param_count,
674    };
675    let line_table = ScopeLineTable { scope_id, lines };
676    Ok((container, line_table))
677}
678
679fn parse_lines_field(pair: P<'_>) -> Result<Vec<LineEntry>, InktParseError> {
680    let mut entries = Vec::new();
681    for entry in pair.into_inner() {
682        if entry.as_rule() == Rule::line_entry {
683            entries.push(parse_line_entry(entry)?);
684        }
685    }
686    Ok(entries)
687}
688
689fn parse_line_entry(pair: P<'_>) -> Result<LineEntry, InktParseError> {
690    let mut inner = pair.into_inner();
691    let _index = inner.next(); // integer index (implied by position)
692    let content_pair = inner.next().ok_or_else(|| InktParseError {
693        message: "expected line content".into(),
694        line: 0,
695        col: 0,
696    })?;
697    let content = parse_line_content(content_pair)?;
698    let hash_pair = inner.next().ok_or_else(|| InktParseError {
699        message: "expected source_hash".into(),
700        line: 0,
701        col: 0,
702    })?;
703    // source_hash is @HHHHHHHHHHHHHHHH
704    let hash_str = hash_pair.as_str();
705    let source_hash = parse_hex_u64(&format!("0x{}", &hash_str[1..]))?;
706
707    let mut audio_ref = None;
708    let mut slot_info = Vec::new();
709    let mut source_location = None;
710
711    for remaining in inner {
712        match remaining.as_rule() {
713            Rule::audio_field => {
714                let s = remaining
715                    .into_inner()
716                    .next()
717                    .ok_or_else(|| InktParseError {
718                        message: "expected audio string".into(),
719                        line: 0,
720                        col: 0,
721                    })?;
722                audio_ref = Some(unescape_string(s.as_str()));
723            }
724            Rule::slots_field => {
725                for slot_entry in remaining.into_inner() {
726                    if slot_entry.as_rule() == Rule::slot_entry {
727                        let mut parts = slot_entry.into_inner();
728                        let idx_str = parts.next().map_or("0", |p| p.as_str());
729                        let idx: u8 = idx_str.parse().unwrap_or(0);
730                        let name_str = parts
731                            .next()
732                            .map_or_else(String::new, |p| unescape_string(p.as_str()));
733                        slot_info.push(SlotInfo {
734                            index: idx,
735                            name: name_str,
736                        });
737                    }
738                }
739            }
740            Rule::source_field => {
741                let mut parts = remaining.into_inner();
742                let file = parts
743                    .next()
744                    .map_or_else(String::new, |p| unescape_string(p.as_str()));
745                let start: u32 = parts
746                    .next()
747                    .and_then(|p| p.as_str().parse().ok())
748                    .unwrap_or(0);
749                let end: u32 = parts
750                    .next()
751                    .and_then(|p| p.as_str().parse().ok())
752                    .unwrap_or(0);
753                source_location = Some(SourceLocation {
754                    file,
755                    range_start: start,
756                    range_end: end,
757                });
758            }
759            _ => {}
760        }
761    }
762
763    let flags = crate::LineFlags::from_content(&content);
764    Ok(LineEntry {
765        content,
766        flags,
767        source_hash,
768        audio_ref,
769        slot_info,
770        source_location,
771    })
772}
773
774fn parse_line_content(pair: P<'_>) -> Result<LineContent, InktParseError> {
775    let inner = pair.into_inner().next().ok_or_else(|| InktParseError {
776        message: "empty line content".into(),
777        line: 0,
778        col: 0,
779    })?;
780    match inner.as_rule() {
781        Rule::string => Ok(LineContent::Plain(unescape_string(inner.as_str()))),
782        Rule::template => parse_template(inner),
783        _ => Err(err(
784            &inner,
785            format!("unexpected line content: {:?}", inner.as_rule()),
786        )),
787    }
788}
789
790fn parse_template(pair: P<'_>) -> Result<LineContent, InktParseError> {
791    let mut parts = Vec::new();
792    for child in pair.into_inner() {
793        if child.as_rule() == Rule::template_part {
794            parts.push(parse_template_part(child)?);
795        }
796    }
797    Ok(LineContent::Template(parts))
798}
799
800fn parse_template_part(pair: P<'_>) -> Result<LinePart, InktParseError> {
801    let inner = pair.into_inner().next().ok_or_else(|| InktParseError {
802        message: "empty template part".into(),
803        line: 0,
804        col: 0,
805    })?;
806    match inner.as_rule() {
807        Rule::literal_part => {
808            let s = inner.into_inner().next().ok_or_else(|| InktParseError {
809                message: "expected string in literal".into(),
810                line: 0,
811                col: 0,
812            })?;
813            Ok(LinePart::Literal(unescape_string(s.as_str())))
814        }
815        Rule::slot_part => {
816            let idx = inner.into_inner().next().ok_or_else(|| InktParseError {
817                message: "expected integer in slot".into(),
818                line: 0,
819                col: 0,
820            })?;
821            let n: u8 = idx
822                .as_str()
823                .parse()
824                .map_err(|_| err(&idx, "invalid slot index"))?;
825            Ok(LinePart::Slot(n))
826        }
827        Rule::select_part => parse_select_part(inner),
828        _ => Err(err(
829            &inner,
830            format!("unexpected template part: {:?}", inner.as_rule()),
831        )),
832    }
833}
834
835fn parse_select_part(pair: P<'_>) -> Result<LinePart, InktParseError> {
836    let mut inner = pair.into_inner();
837    let slot_pair = inner.next().ok_or_else(|| InktParseError {
838        message: "expected slot in select".into(),
839        line: 0,
840        col: 0,
841    })?;
842    let slot: u8 = slot_pair
843        .as_str()
844        .parse()
845        .map_err(|_| err(&slot_pair, "invalid slot"))?;
846
847    let mut variants = Vec::new();
848    let mut default = String::new();
849
850    for child in inner {
851        match child.as_rule() {
852            Rule::select_variant => {
853                let mut vi = child.into_inner();
854                let key_pair = vi.next().ok_or_else(|| InktParseError {
855                    message: "expected key in variant".into(),
856                    line: 0,
857                    col: 0,
858                })?;
859                let key = parse_select_key(key_pair)?;
860                let text = vi.next().ok_or_else(|| InktParseError {
861                    message: "expected text in variant".into(),
862                    line: 0,
863                    col: 0,
864                })?;
865                variants.push((key, unescape_string(text.as_str())));
866            }
867            Rule::select_default => {
868                let s = child.into_inner().next().ok_or_else(|| InktParseError {
869                    message: "expected string in default".into(),
870                    line: 0,
871                    col: 0,
872                })?;
873                default = unescape_string(s.as_str());
874            }
875            _ => {}
876        }
877    }
878
879    Ok(LinePart::Select {
880        slot,
881        variants,
882        default,
883    })
884}
885
886fn parse_select_key(pair: P<'_>) -> Result<SelectKey, InktParseError> {
887    let inner = pair.into_inner().next().ok_or_else(|| InktParseError {
888        message: "empty select key".into(),
889        line: 0,
890        col: 0,
891    })?;
892    match inner.as_rule() {
893        Rule::cardinal_key => {
894            let cat = inner.into_inner().next().ok_or_else(|| InktParseError {
895                message: "expected plural_cat".into(),
896                line: 0,
897                col: 0,
898            })?;
899            Ok(SelectKey::Cardinal(parse_plural_cat(cat)?))
900        }
901        Rule::ordinal_key => {
902            let cat = inner.into_inner().next().ok_or_else(|| InktParseError {
903                message: "expected plural_cat".into(),
904                line: 0,
905                col: 0,
906            })?;
907            Ok(SelectKey::Ordinal(parse_plural_cat(cat)?))
908        }
909        Rule::exact_key => {
910            let n = inner.into_inner().next().ok_or_else(|| InktParseError {
911                message: "expected integer".into(),
912                line: 0,
913                col: 0,
914            })?;
915            let v: i32 = n
916                .as_str()
917                .parse()
918                .map_err(|_| err(&n, "invalid exact key"))?;
919            Ok(SelectKey::Exact(v))
920        }
921        Rule::keyword_key => {
922            let ident = inner.into_inner().next().ok_or_else(|| InktParseError {
923                message: "expected ident".into(),
924                line: 0,
925                col: 0,
926            })?;
927            Ok(SelectKey::Keyword(ident.as_str().to_owned()))
928        }
929        _ => Err(err(
930            &inner,
931            format!("unexpected select key: {:?}", inner.as_rule()),
932        )),
933    }
934}
935
936#[expect(clippy::needless_pass_by_value)]
937fn parse_plural_cat(pair: P<'_>) -> Result<PluralCategory, InktParseError> {
938    match pair.as_str() {
939        "Zero" => Ok(PluralCategory::Zero),
940        "One" => Ok(PluralCategory::One),
941        "Two" => Ok(PluralCategory::Two),
942        "Few" => Ok(PluralCategory::Few),
943        "Many" => Ok(PluralCategory::Many),
944        "Other" => Ok(PluralCategory::Other),
945        _ => Err(err(
946            &pair,
947            format!("unknown plural category: {}", pair.as_str()),
948        )),
949    }
950}
951
952// ── Code field ──────────────────────────────────────────────────────────────
953
954fn parse_code_field(pair: P<'_>) -> Result<Vec<u8>, InktParseError> {
955    let mut bytecode = Vec::new();
956    for child in pair.into_inner() {
957        if child.as_rule() == Rule::instruction {
958            let op = parse_instruction(child)?;
959            op.encode(&mut bytecode);
960        }
961    }
962    Ok(bytecode)
963}
964
965#[expect(clippy::too_many_lines)]
966fn parse_instruction(pair: P<'_>) -> Result<Opcode, InktParseError> {
967    let mut inner = pair.into_inner();
968    let mnemonic_pair = inner.next().ok_or_else(|| InktParseError {
969        message: "expected opcode mnemonic".into(),
970        line: 0,
971        col: 0,
972    })?;
973    let mnemonic = mnemonic_pair.as_str();
974
975    let operands: Vec<P<'_>> = inner.collect();
976
977    match mnemonic {
978        // Stack & literals
979        "push_int" => Ok(Opcode::PushInt(parse_operand_i32(&operands, 0, mnemonic)?)),
980        "push_float" => Ok(Opcode::PushFloat(parse_operand_f32(
981            &operands, 0, mnemonic,
982        )?)),
983        "push_bool" => {
984            let s = operand_str(&operands, 0, mnemonic)?;
985            Ok(Opcode::PushBool(s == "true"))
986        }
987        "push_string" => Ok(Opcode::PushString(parse_operand_u16(
988            &operands, 0, mnemonic,
989        )?)),
990        "push_list" => Ok(Opcode::PushList(parse_operand_u16(&operands, 0, mnemonic)?)),
991        "push_divert_target" => Ok(Opcode::PushDivertTarget(parse_operand_def_id(
992            &operands, 0, mnemonic,
993        )?)),
994        "push_null" => Ok(Opcode::PushNull),
995        "pop" => Ok(Opcode::Pop),
996        "duplicate" => Ok(Opcode::Duplicate),
997
998        // Arithmetic
999        "add" => Ok(Opcode::Add),
1000        "subtract" => Ok(Opcode::Subtract),
1001        "multiply" => Ok(Opcode::Multiply),
1002        "divide" => Ok(Opcode::Divide),
1003        "modulo" => Ok(Opcode::Modulo),
1004        "negate" => Ok(Opcode::Negate),
1005
1006        // Comparison
1007        "equal" => Ok(Opcode::Equal),
1008        "not_equal" => Ok(Opcode::NotEqual),
1009        "greater" => Ok(Opcode::Greater),
1010        "greater_or_equal" => Ok(Opcode::GreaterOrEqual),
1011        "less" => Ok(Opcode::Less),
1012        "less_or_equal" => Ok(Opcode::LessOrEqual),
1013
1014        // Logic
1015        "not" => Ok(Opcode::Not),
1016        "and" => Ok(Opcode::And),
1017        "or" => Ok(Opcode::Or),
1018
1019        // Global vars
1020        "get_global" => Ok(Opcode::GetGlobal(parse_operand_def_id(
1021            &operands, 0, mnemonic,
1022        )?)),
1023        "set_global" => Ok(Opcode::SetGlobal(parse_operand_def_id(
1024            &operands, 0, mnemonic,
1025        )?)),
1026
1027        // Temp vars
1028        "declare_temp" => Ok(Opcode::DeclareTemp(parse_operand_u16(
1029            &operands, 0, mnemonic,
1030        )?)),
1031        "get_temp" => Ok(Opcode::GetTemp(parse_operand_u16(&operands, 0, mnemonic)?)),
1032        "set_temp" => Ok(Opcode::SetTemp(parse_operand_u16(&operands, 0, mnemonic)?)),
1033        "get_temp_raw" => Ok(Opcode::GetTempRaw(parse_operand_u16(
1034            &operands, 0, mnemonic,
1035        )?)),
1036
1037        // Variable pointers
1038        "push_var_pointer" => Ok(Opcode::PushVarPointer(parse_operand_def_id(
1039            &operands, 0, mnemonic,
1040        )?)),
1041        "push_temp_pointer" => Ok(Opcode::PushTempPointer(parse_operand_u16(
1042            &operands, 0, mnemonic,
1043        )?)),
1044
1045        // Control flow
1046        "jump" => Ok(Opcode::Jump(parse_operand_i32(&operands, 0, mnemonic)?)),
1047        "jump_if_false" => Ok(Opcode::JumpIfFalse(parse_operand_i32(
1048            &operands, 0, mnemonic,
1049        )?)),
1050        "goto" => Ok(Opcode::Goto(parse_operand_def_id(&operands, 0, mnemonic)?)),
1051        "goto_if" => Ok(Opcode::GotoIf(parse_operand_def_id(
1052            &operands, 0, mnemonic,
1053        )?)),
1054        "goto_variable" => Ok(Opcode::GotoVariable),
1055
1056        // Container flow
1057        "enter_container" => Ok(Opcode::EnterContainer(parse_operand_def_id(
1058            &operands, 0, mnemonic,
1059        )?)),
1060        "exit_container" => Ok(Opcode::ExitContainer),
1061
1062        // Functions / tunnels
1063        "call" => Ok(Opcode::Call(parse_operand_def_id(&operands, 0, mnemonic)?)),
1064        "return" => Ok(Opcode::Return),
1065        "tunnel_call" => Ok(Opcode::TunnelCall(parse_operand_def_id(
1066            &operands, 0, mnemonic,
1067        )?)),
1068        "tunnel_return" => Ok(Opcode::TunnelReturn),
1069        "tunnel_call_variable" => Ok(Opcode::TunnelCallVariable),
1070        "call_variable" => Ok(Opcode::CallVariable),
1071
1072        // Threads
1073        "thread_call" => Ok(Opcode::ThreadCall(parse_operand_def_id(
1074            &operands, 0, mnemonic,
1075        )?)),
1076        "thread_start" => Ok(Opcode::ThreadStart),
1077        "thread_done" => Ok(Opcode::ThreadDone),
1078
1079        // Output
1080        "emit_line" => {
1081            let idx = parse_operand_u16(&operands, 0, mnemonic)?;
1082            let slots = parse_operand_u8(&operands, 1, mnemonic)?;
1083            Ok(Opcode::EmitLine(idx, slots))
1084        }
1085        "emit_value" => Ok(Opcode::EmitValue),
1086        "emit_newline" => Ok(Opcode::EmitNewline),
1087        "spring" => Ok(Opcode::Spring),
1088        "glue" => Ok(Opcode::Glue),
1089        "begin_tag" => Ok(Opcode::BeginTag),
1090        "end_tag" => Ok(Opcode::EndTag),
1091        "eval_line" => {
1092            let idx = parse_operand_u16(&operands, 0, mnemonic)?;
1093            let slots = parse_operand_u8(&operands, 1, mnemonic)?;
1094            Ok(Opcode::EvalLine(idx, slots))
1095        }
1096
1097        // Choices
1098        "begin_choice" => {
1099            let flags = parse_choice_flags_operand(&operands, 0, mnemonic)?;
1100            let target = parse_operand_def_id(&operands, 1, mnemonic)?;
1101            Ok(Opcode::BeginChoice(flags, target))
1102        }
1103        "end_choice" => Ok(Opcode::EndChoice),
1104
1105        // Sequences
1106        "sequence" => {
1107            let kind_str = operand_str(&operands, 0, mnemonic)?;
1108            let kind = match kind_str {
1109                "cycle" => SequenceKind::Cycle,
1110                "stopping" => SequenceKind::Stopping,
1111                "once_only" => SequenceKind::OnceOnly,
1112                "shuffle" => SequenceKind::Shuffle,
1113                _ => {
1114                    return Err(InktParseError {
1115                        message: format!("unknown sequence kind: {kind_str}"),
1116                        line: 0,
1117                        col: 0,
1118                    });
1119                }
1120            };
1121            let count: u8 =
1122                operand_str(&operands, 1, mnemonic)?
1123                    .parse()
1124                    .map_err(|_| InktParseError {
1125                        message: "invalid sequence count".into(),
1126                        line: 0,
1127                        col: 0,
1128                    })?;
1129            Ok(Opcode::Sequence(kind, count))
1130        }
1131        "sequence_branch" => Ok(Opcode::SequenceBranch(parse_operand_i32(
1132            &operands, 0, mnemonic,
1133        )?)),
1134
1135        // Intrinsics
1136        "visit_count" => Ok(Opcode::VisitCount),
1137        "current_visit_count" => Ok(Opcode::CurrentVisitCount),
1138        "turns_since" => Ok(Opcode::TurnsSince),
1139        "turn_index" => Ok(Opcode::TurnIndex),
1140        "choice_count" => Ok(Opcode::ChoiceCount),
1141        "random" => Ok(Opcode::Random),
1142        "seed_random" => Ok(Opcode::SeedRandom),
1143
1144        // Casts / math
1145        "cast_to_int" => Ok(Opcode::CastToInt),
1146        "cast_to_float" => Ok(Opcode::CastToFloat),
1147        "floor" => Ok(Opcode::Floor),
1148        "ceiling" => Ok(Opcode::Ceiling),
1149        "pow" => Ok(Opcode::Pow),
1150        "min" => Ok(Opcode::Min),
1151        "max" => Ok(Opcode::Max),
1152
1153        // External fns
1154        "call_external" => {
1155            let id = parse_operand_def_id(&operands, 0, mnemonic)?;
1156            // "argc=N" is parsed as a kv_operand. Extract the value after "=".
1157            let kv_str = operand_str(&operands, 1, mnemonic)?;
1158            let argc_str = kv_str.strip_prefix("argc=").unwrap_or(kv_str);
1159            let argc: u8 = argc_str.parse().map_err(|_| InktParseError {
1160                message: format!("invalid argc in call_external: {kv_str}"),
1161                line: 0,
1162                col: 0,
1163            })?;
1164            Ok(Opcode::CallExternal(id, argc))
1165        }
1166
1167        // List ops
1168        "list_contains" => Ok(Opcode::ListContains),
1169        "list_not_contains" => Ok(Opcode::ListNotContains),
1170        "list_intersect" => Ok(Opcode::ListIntersect),
1171        "list_all" => Ok(Opcode::ListAll),
1172        "list_invert" => Ok(Opcode::ListInvert),
1173        "list_count" => Ok(Opcode::ListCount),
1174        "list_min" => Ok(Opcode::ListMin),
1175        "list_max" => Ok(Opcode::ListMax),
1176        "list_value" => Ok(Opcode::ListValue),
1177        "list_range" => Ok(Opcode::ListRange),
1178        "list_from_int" => Ok(Opcode::ListFromInt),
1179        "list_random" => Ok(Opcode::ListRandom),
1180
1181        // Lifecycle
1182        "done" => Ok(Opcode::Done),
1183        "yield" => Ok(Opcode::Yield),
1184        "end" => Ok(Opcode::End),
1185        "nop" => Ok(Opcode::Nop),
1186
1187        // String eval
1188        "begin_string_eval" => Ok(Opcode::BeginStringEval),
1189        "end_string_eval" => Ok(Opcode::EndStringEval),
1190
1191        // Fragment capture
1192        "begin_fragment" => Ok(Opcode::BeginFragment),
1193        "end_fragment" => Ok(Opcode::EndFragment),
1194
1195        // Debug
1196        "source_location" => {
1197            // Written as "source_location LINE:COL" — parsed as source_loc operand
1198            let s = operand_str(&operands, 0, mnemonic)?;
1199            let parts: Vec<&str> = s.split(':').collect();
1200            if parts.len() != 2 {
1201                return Err(InktParseError {
1202                    message: format!("invalid source_location: {s}"),
1203                    line: 0,
1204                    col: 0,
1205                });
1206            }
1207            let line: u32 = parts[0].parse().map_err(|_| InktParseError {
1208                message: "invalid line".into(),
1209                line: 0,
1210                col: 0,
1211            })?;
1212            let col: u32 = parts[1].parse().map_err(|_| InktParseError {
1213                message: "invalid col".into(),
1214                line: 0,
1215                col: 0,
1216            })?;
1217            Ok(Opcode::SourceLocation(line, col))
1218        }
1219
1220        _ => Err(InktParseError {
1221            message: format!("unknown opcode: {mnemonic}"),
1222            line: mnemonic_pair.line_col().0,
1223            col: mnemonic_pair.line_col().1,
1224        }),
1225    }
1226}
1227
1228fn parse_choice_flags_operand(
1229    operands: &[P<'_>],
1230    idx: usize,
1231    context: &str,
1232) -> Result<ChoiceFlags, InktParseError> {
1233    let s = operand_str(operands, idx, context)?;
1234    let mut flags = ChoiceFlags {
1235        has_condition: false,
1236        has_start_content: false,
1237        has_choice_only_content: false,
1238        once_only: false,
1239        is_invisible_default: false,
1240    };
1241    if s == "none" {
1242        return Ok(flags);
1243    }
1244    for part in s.split('+') {
1245        match part {
1246            "cond" => flags.has_condition = true,
1247            "start" => flags.has_start_content = true,
1248            "choice_only" => flags.has_choice_only_content = true,
1249            "once" => flags.once_only = true,
1250            "invis_default" => flags.is_invisible_default = true,
1251            _ => {
1252                return Err(InktParseError {
1253                    message: format!("unknown choice flag: {part}"),
1254                    line: 0,
1255                    col: 0,
1256                });
1257            }
1258        }
1259    }
1260    Ok(flags)
1261}
1262
1263// ── Operand helpers ─────────────────────────────────────────────────────────
1264
1265fn operand_str<'a>(
1266    operands: &'a [P<'_>],
1267    idx: usize,
1268    context: &str,
1269) -> Result<&'a str, InktParseError> {
1270    let op = operands.get(idx).ok_or_else(|| InktParseError {
1271        message: format!("missing operand {idx} for {context}"),
1272        line: 0,
1273        col: 0,
1274    })?;
1275    // The operand rule wraps the actual value. Get the inner pair.
1276    let inner = op.clone().into_inner().next();
1277    match inner {
1278        Some(p) => Ok(p.as_str()),
1279        None => Ok(op.as_str()),
1280    }
1281}
1282
1283fn parse_operand_i32(operands: &[P<'_>], idx: usize, context: &str) -> Result<i32, InktParseError> {
1284    let s = operand_str(operands, idx, context)?;
1285    s.parse().map_err(|_| InktParseError {
1286        message: format!("invalid i32 operand for {context}: {s}"),
1287        line: 0,
1288        col: 0,
1289    })
1290}
1291
1292fn parse_operand_f32(operands: &[P<'_>], idx: usize, context: &str) -> Result<f32, InktParseError> {
1293    let s = operand_str(operands, idx, context)?;
1294    s.parse().map_err(|_| InktParseError {
1295        message: format!("invalid f32 operand for {context}: {s}"),
1296        line: 0,
1297        col: 0,
1298    })
1299}
1300
1301fn parse_operand_u8(operands: &[P<'_>], idx: usize, context: &str) -> Result<u8, InktParseError> {
1302    let s = operand_str(operands, idx, context)?;
1303    s.parse().map_err(|_| InktParseError {
1304        message: format!("invalid u8 operand for {context}: {s}"),
1305        line: 0,
1306        col: 0,
1307    })
1308}
1309
1310fn parse_operand_u16(operands: &[P<'_>], idx: usize, context: &str) -> Result<u16, InktParseError> {
1311    let s = operand_str(operands, idx, context)?;
1312    s.parse().map_err(|_| InktParseError {
1313        message: format!("invalid u16 operand for {context}: {s}"),
1314        line: 0,
1315        col: 0,
1316    })
1317}
1318
1319fn parse_operand_def_id(
1320    operands: &[P<'_>],
1321    idx: usize,
1322    context: &str,
1323) -> Result<DefinitionId, InktParseError> {
1324    let op = operands.get(idx).ok_or_else(|| InktParseError {
1325        message: format!("missing operand {idx} for {context}"),
1326        line: 0,
1327        col: 0,
1328    })?;
1329    // Drill into the operand to get the def_id inner pair
1330    let inner = op.clone().into_inner().next().unwrap_or_else(|| op.clone());
1331    parse_def_id(inner)
1332}
1333
1334// ── Shared parse helpers ────────────────────────────────────────────────────
1335
1336#[expect(clippy::needless_pass_by_value)]
1337fn parse_def_id(pair: P<'_>) -> Result<DefinitionId, InktParseError> {
1338    let s = pair.as_str();
1339    // Format: $TT_HHHHHHHHHHHHHH
1340    if !s.starts_with('$') || s.len() < 4 {
1341        return Err(err(&pair, format!("invalid def_id: {s}")));
1342    }
1343    let tag_str = &s[1..3];
1344    let hash_str = &s[4..]; // skip $TT_
1345
1346    let tag_byte = u8::from_str_radix(tag_str, 16)
1347        .map_err(|_| err(&pair, format!("invalid tag: {tag_str}")))?;
1348    let hash = u64::from_str_radix(hash_str, 16)
1349        .map_err(|_| err(&pair, format!("invalid hash: {hash_str}")))?;
1350
1351    let tag = crate::id::DefinitionTag::from_u8(tag_byte)
1352        .ok_or_else(|| err(&pair, format!("unknown tag byte: {tag_byte:#04x}")))?;
1353
1354    Ok(DefinitionId::new(tag, hash))
1355}
1356
1357fn parse_hex_u32(s: &str) -> u32 {
1358    let hex = s.strip_prefix("0x").unwrap_or(s);
1359    u32::from_str_radix(hex, 16).unwrap_or(0)
1360}
1361
1362fn parse_hex_u64(s: &str) -> Result<u64, InktParseError> {
1363    let hex = s.strip_prefix("0x").unwrap_or(s);
1364    u64::from_str_radix(hex, 16).map_err(|_| InktParseError {
1365        message: format!("invalid hex: {s}"),
1366        line: 0,
1367        col: 0,
1368    })
1369}
1370
1371fn parse_u16(pair: &P<'_>) -> Result<u16, InktParseError> {
1372    pair.as_str().parse().map_err(|_| err(pair, "invalid u16"))
1373}
1374
1375fn unescape_string(s: &str) -> String {
1376    // Strip surrounding quotes
1377    let inner = &s[1..s.len() - 1];
1378    let mut out = String::with_capacity(inner.len());
1379    let mut chars = inner.chars();
1380    while let Some(c) = chars.next() {
1381        if c == '\\' {
1382            match chars.next() {
1383                Some('\\') | None => out.push('\\'),
1384                Some('"') => out.push('"'),
1385                Some('n') => out.push('\n'),
1386                Some('t') => out.push('\t'),
1387                Some('r') => out.push('\r'),
1388                Some(other) => {
1389                    out.push('\\');
1390                    out.push(other);
1391                }
1392            }
1393        } else {
1394            out.push(c);
1395        }
1396    }
1397    out
1398}
1399
1400fn next_rule<'a>(
1401    iter: &mut impl Iterator<Item = P<'a>>,
1402    expected: Rule,
1403    context: &str,
1404) -> Result<P<'a>, InktParseError> {
1405    for pair in iter.by_ref() {
1406        if pair.as_rule() == expected {
1407            return Ok(pair);
1408        }
1409    }
1410    Err(InktParseError {
1411        message: format!("expected {expected:?} in {context}"),
1412        line: 0,
1413        col: 0,
1414    })
1415}