Skip to main content

meta_language/grammar/interchange/
lossless.rs

1//! The lossless mode of grammar interchange.
2//!
3//! Importing a source in lossless mode keeps, beside the grammar, the layout
4//! of the source: the text before the first rule, every rule definition's own
5//! text with the fingerprint of the rule it defines (its native links line)
6//! and the text after it up to the next definition. Rules the importer adds
7//! without a definition in the source (the ABNF core rules) are kept as
8//! implicit fingerprints. Emitting in lossless mode walks the layout: a rule
9//! whose fingerprint still matches is written with its original text, a
10//! changed rule is written where it was defined with the text a fresh emission
11//! gives it, a new rule follows the rule before it in grammar order, and an
12//! unchanged implicit rule is left implicit. An unchanged grammar therefore
13//! reconstructs its source exactly, and an edited grammar keeps every
14//! untouched definition, comment and blank line. The layout has its own links
15//! form, so the source is reconstructed from links alone. It mirrors
16//! `js/src/grammar-lossless.js`.
17
18use std::collections::{BTreeMap, BTreeSet};
19use std::error::Error;
20use std::fmt;
21
22use links_notation::{LiNo, ParserConfig, parse_lino_to_links_with_config};
23
24use super::super::emit::{EmitReport, GrammarEmitError};
25use super::super::import::GrammarImportError;
26use super::super::{Grammar, GrammarFormat};
27use super::links::{percent_decode_links_text, percent_encode_links_text, render_rule_link};
28use super::{grammar_emitter, grammar_importer};
29
30/// The formats the lossless mode reads and writes.
31pub const GRAMMAR_LOSSLESS_FORMATS: &[&str] = &[
32    "abnf",
33    "antlr",
34    "bnf",
35    "ebnf",
36    "gbnf",
37    "lark",
38    "pest",
39    "tree-sitter-json",
40];
41
42/// Formats whose importers read the comment lines above a rule as its
43/// documentation.
44const DOC_FORMATS: &[&str] = &["antlr", "gbnf", "lark"];
45
46/// One rule definition of a source and the text after it.
47#[derive(Clone, Debug, PartialEq, Eq)]
48pub struct GrammarSourceDefinition {
49    /// The rule it defines.
50    pub name: String,
51    /// The definition's own text, comments above it included where the
52    /// importer reads them as documentation.
53    pub text: String,
54    /// The text after the definition up to the next one or the end.
55    pub gap: String,
56}
57
58/// A source split into the text before its first definition and the
59/// definitions.
60#[derive(Clone, Debug, PartialEq, Eq)]
61pub struct GrammarSourceSplit {
62    /// The text before the first definition.
63    pub prefix: String,
64    /// The definitions in source order.
65    pub members: Vec<GrammarSourceDefinition>,
66}
67
68/// One definition of a layout with the fingerprint of its rule.
69#[derive(Clone, Debug, PartialEq, Eq)]
70pub struct GrammarLayoutDefinition {
71    /// The rule it defines.
72    pub name: String,
73    /// The definition's own text.
74    pub text: String,
75    /// The text after the definition up to the next one or the end.
76    pub gap: String,
77    /// The native links line of the rule when the source was imported.
78    pub fingerprint: String,
79}
80
81/// A rule the importer adds without a definition in the source.
82#[derive(Clone, Debug, PartialEq, Eq)]
83pub struct GrammarLayoutImplicit {
84    /// The rule name.
85    pub name: String,
86    /// The native links line of the rule when the source was imported.
87    pub fingerprint: String,
88}
89
90/// The layout that reconstructs a source from its grammar.
91#[derive(Clone, Debug, PartialEq, Eq)]
92pub struct GrammarLayout {
93    /// The interchange format tag of the source.
94    pub format: String,
95    /// The text before the first definition.
96    pub prefix: String,
97    /// The definitions in source order.
98    pub members: Vec<GrammarLayoutDefinition>,
99    /// The rules defined nowhere in the source.
100    pub implicit: Vec<GrammarLayoutImplicit>,
101}
102
103/// Why the lossless mode could not run.
104#[derive(Clone, Debug, PartialEq, Eq)]
105pub enum GrammarLosslessError {
106    /// The format is not one of [`GRAMMAR_LOSSLESS_FORMATS`].
107    UnsupportedFormat(String),
108    /// The source, the layout or its links are malformed.
109    Import(GrammarImportError),
110    /// The emitter rejected the grammar.
111    Emit(GrammarEmitError),
112}
113
114impl fmt::Display for GrammarLosslessError {
115    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
116        match self {
117            Self::UnsupportedFormat(format) => write!(
118                formatter,
119                "the lossless mode does not support the format {format}"
120            ),
121            Self::Import(error) => write!(formatter, "{error}"),
122            Self::Emit(error) => write!(formatter, "{error}"),
123        }
124    }
125}
126
127impl Error for GrammarLosslessError {}
128
129impl From<GrammarImportError> for GrammarLosslessError {
130    fn from(error: GrammarImportError) -> Self {
131        Self::Import(error)
132    }
133}
134
135impl From<GrammarEmitError> for GrammarLosslessError {
136    fn from(error: GrammarEmitError) -> Self {
137        Self::Emit(error)
138    }
139}
140
141fn layout_error(detail: impl AsRef<str>) -> GrammarLosslessError {
142    GrammarLosslessError::Import(GrammarImportError::Parse {
143        format: GrammarFormat::MetaLanguage,
144        message: format!("layout: {}", detail.as_ref()),
145    })
146}
147
148fn check_format(format: &str) -> Result<&'static str, GrammarLosslessError> {
149    GRAMMAR_LOSSLESS_FORMATS
150        .iter()
151        .find(|known| **known == format)
152        .copied()
153        .ok_or_else(|| GrammarLosslessError::UnsupportedFormat(format.to_owned()))
154}
155
156/// A rule definition span of a source, in bytes.
157struct Span {
158    name: String,
159    start: usize,
160    end: usize,
161}
162
163/// Splits `source` into the text before the first rule definition and the
164/// definitions of the rules in `names`, each with the text after it.
165///
166/// # Errors
167///
168/// Returns [`GrammarLosslessError::UnsupportedFormat`] for a format outside
169/// [`GRAMMAR_LOSSLESS_FORMATS`] and a layout error for malformed JSON.
170pub fn split_grammar_source(
171    source: &str,
172    format: &str,
173    names: &[&str],
174) -> Result<GrammarSourceSplit, GrammarLosslessError> {
175    let format = check_format(format)?;
176    let spans = if format == "tree-sitter-json" {
177        json_rule_spans(source, names)?
178    } else {
179        line_rule_spans(source, format, names)
180    };
181    let Some(first) = spans.first() else {
182        return Ok(GrammarSourceSplit {
183            prefix: source.to_owned(),
184            members: Vec::new(),
185        });
186    };
187    Ok(GrammarSourceSplit {
188        prefix: source[..first.start].to_owned(),
189        members: spans
190            .iter()
191            .enumerate()
192            .map(|(index, span)| {
193                let next = spans.get(index + 1).map_or(source.len(), |next| next.start);
194                GrammarSourceDefinition {
195                    name: span.name.clone(),
196                    text: source[span.start..span.end].to_owned(),
197                    gap: source[span.end..next].to_owned(),
198                }
199            })
200            .collect(),
201    })
202}
203
204/// Trims what `String.prototype.trim` trims, so both runtimes agree on blank
205/// lines.
206fn js_trim(text: &str) -> &str {
207    text.trim_matches(|value: char| {
208        (value.is_whitespace() && value != '\u{85}') || value == '\u{feff}'
209    })
210}
211
212const fn ident_start(byte: u8) -> bool {
213    byte.is_ascii_alphabetic() || byte == b'_'
214}
215
216const fn ident_rest(byte: u8) -> bool {
217    byte.is_ascii_alphanumeric() || byte == b'_'
218}
219
220/// The end of a run of bytes matching `first` then `rest`, if any.
221fn run(bytes: &[u8], at: usize, first: fn(u8) -> bool, rest: fn(u8) -> bool) -> Option<usize> {
222    if !bytes.get(at).copied().is_some_and(first) {
223        return None;
224    }
225    let mut end = at + 1;
226    while bytes.get(end).copied().is_some_and(rest) {
227        end += 1;
228    }
229    Some(end)
230}
231
232fn skip_blanks(bytes: &[u8], at: usize) -> usize {
233    let mut end = at;
234    while matches!(bytes.get(end), Some(b' ' | b'\t')) {
235        end += 1;
236    }
237    end
238}
239
240fn starts_at(bytes: &[u8], at: usize, text: &str) -> bool {
241    bytes
242        .get(at..)
243        .is_some_and(|rest| rest.starts_with(text.as_bytes()))
244}
245
246/// The rule name a line starting with a rule head of `format` defines.
247fn head_name<'a>(format: &str, line: &'a str) -> Option<&'a str> {
248    let bytes = line.as_bytes();
249    let named = |start: usize, end: usize, follows: bool| follows.then(|| &line[start..end]);
250    match format {
251        "abnf" => {
252            let end = run(
253                bytes,
254                0,
255                |byte| byte.is_ascii_alphabetic(),
256                |byte| byte.is_ascii_alphanumeric() || byte == b'-',
257            )?;
258            named(0, end, starts_at(bytes, skip_blanks(bytes, end), "="))
259        }
260        "antlr" => {
261            let rule_at = |start: usize| {
262                let end = run(bytes, start, ident_start, ident_rest)?;
263                let after = skip_blanks(bytes, end);
264                named(start, end, after == bytes.len() || bytes[after] == b':')
265            };
266            let fragment = b"fragment".len();
267            let spaced = skip_blanks(bytes, fragment);
268            (starts_at(bytes, 0, "fragment") && spaced > fragment)
269                .then(|| rule_at(spaced))
270                .flatten()
271                .or_else(|| rule_at(0))
272        }
273        "bnf" => {
274            if bytes.first() != Some(&b'<') {
275                return None;
276            }
277            let close = 1 + bytes[1..]
278                .iter()
279                .position(|byte| matches!(byte, b'<' | b'>'))?;
280            (bytes[close] == b'>' && starts_at(bytes, skip_blanks(bytes, close + 1), "::="))
281                .then(|| &line[1..close])
282        }
283        "ebnf" => {
284            let end = run(bytes, 0, ident_start, |byte| {
285                ident_rest(byte) || byte == b'-'
286            })?;
287            let after = skip_blanks(bytes, end);
288            named(
289                0,
290                end,
291                starts_at(bytes, after, "::=") || starts_at(bytes, after, "="),
292            )
293        }
294        "gbnf" => {
295            let word = |byte: u8| byte.is_ascii_alphanumeric() || byte == b'-';
296            let end = run(bytes, 0, word, word)?;
297            named(0, end, starts_at(bytes, skip_blanks(bytes, end), "::="))
298        }
299        "lark" => {
300            let start = usize::from(matches!(bytes.first(), Some(b'?' | b'!')));
301            let end = run(bytes, start, ident_start, ident_rest)?;
302            let mut after = end;
303            if bytes.get(after) == Some(&b'.') {
304                let digits = after + 1 + usize::from(bytes.get(after + 1) == Some(&b'-'));
305                if let Some(digits_end) = run(
306                    bytes,
307                    digits,
308                    |byte| byte.is_ascii_digit(),
309                    |byte| byte.is_ascii_digit(),
310                ) {
311                    after = digits_end;
312                }
313            }
314            named(start, end, starts_at(bytes, skip_blanks(bytes, after), ":"))
315        }
316        "pest" => {
317            let end = run(bytes, 0, ident_start, ident_rest)?;
318            named(0, end, starts_at(bytes, skip_blanks(bytes, end), "="))
319        }
320        _ => None,
321    }
322}
323
324/// How a comment line of each line-based format starts.
325fn comment_prefixes(format: &str) -> &'static [&'static str] {
326    match format {
327        "abnf" | "bnf" => &[";"],
328        "antlr" => &["//", "/*", "*"],
329        "ebnf" => &["(*"],
330        "gbnf" => &["#"],
331        _ => &["//"],
332    }
333}
334
335struct Line<'a> {
336    start: usize,
337    end: usize,
338    text: &'a str,
339}
340
341fn line_rule_spans(source: &str, format: &str, names: &[&str]) -> Vec<Span> {
342    let mut known: BTreeMap<String, &str> = BTreeMap::new();
343    for name in names {
344        known.insert((*name).to_owned(), name);
345        if format == "abnf" {
346            known.entry(name.to_lowercase()).or_insert(name);
347        }
348    }
349    let mut lines = Vec::new();
350    let mut start = 0;
351    while start < source.len() {
352        let end = source[start..]
353            .find('\n')
354            .map_or(source.len(), |offset| start + offset);
355        lines.push(Line {
356            start,
357            end,
358            text: &source[start..end],
359        });
360        start = if end < source.len() { end + 1 } else { end };
361    }
362    let prefixes = comment_prefixes(format);
363    let trivia = |line: &Line<'_>| {
364        let text = js_trim(line.text);
365        text.is_empty() || prefixes.iter().any(|prefix| text.starts_with(prefix))
366    };
367    let mut heads: Vec<(usize, &str)> = Vec::new();
368    for (index, line) in lines.iter().enumerate() {
369        let Some(found) = head_name(format, line.text) else {
370            continue;
371        };
372        let name = known.get(found).or_else(|| {
373            (format == "abnf")
374                .then(|| known.get(&found.to_lowercase()))
375                .flatten()
376        });
377        if let Some(name) = name {
378            heads.push((index, name));
379        }
380    }
381    let documented = DOC_FORMATS.contains(&format);
382    let firsts: Vec<usize> = heads
383        .iter()
384        .enumerate()
385        .map(|(position, (index, _))| {
386            let mut first = *index;
387            let floor = if position == 0 {
388                0
389            } else {
390                heads[position - 1].0 + 1
391            };
392            if documented {
393                while first > floor
394                    && !js_trim(lines[first - 1].text).is_empty()
395                    && trivia(&lines[first - 1])
396                {
397                    first -= 1;
398                }
399            }
400            first
401        })
402        .collect();
403    heads
404        .iter()
405        .enumerate()
406        .map(|(position, (index, name))| {
407            let limit = firsts.get(position + 1).copied().unwrap_or(lines.len());
408            let mut last = limit - 1;
409            while last > *index && trivia(&lines[last]) {
410                last -= 1;
411            }
412            Span {
413                name: (*name).to_owned(),
414                start: lines[firsts[position]].start,
415                end: lines[last].end,
416            }
417        })
418        .collect()
419}
420
421fn skip_json_space(source: &[u8], index: usize) -> usize {
422    let mut at = index;
423    while matches!(source.get(at), Some(b' ' | b'\t' | b'\r' | b'\n')) {
424        at += 1;
425    }
426    at
427}
428
429fn json_string_end(source: &[u8], index: usize) -> Result<usize, GrammarLosslessError> {
430    if source.get(index) != Some(&b'"') {
431        return Err(layout_error(format!(
432            "expected a JSON string at offset {index}"
433        )));
434    }
435    let mut at = index + 1;
436    while at < source.len() {
437        match source[at] {
438            b'\\' => at += 1,
439            b'"' => return Ok(at + 1),
440            _ => {}
441        }
442        at += 1;
443    }
444    Err(layout_error("unterminated JSON string"))
445}
446
447fn json_value_end(source: &[u8], index: usize) -> Result<usize, GrammarLosslessError> {
448    match source.get(index) {
449        Some(b'"') => return json_string_end(source, index),
450        Some(b'{' | b'[') => {}
451        _ => {
452            let mut at = index;
453            while at < source.len() && !b",}] \t\r\n".contains(&source[at]) {
454                at += 1;
455            }
456            if at == index {
457                return Err(layout_error(format!(
458                    "expected a JSON value at offset {index}"
459                )));
460            }
461            return Ok(at);
462        }
463    }
464    let mut depth = 0_usize;
465    let mut at = index;
466    while at < source.len() {
467        match source[at] {
468            b'"' => at = json_string_end(source, at)? - 1,
469            b'{' | b'[' => depth += 1,
470            b'}' | b']' => {
471                depth -= 1;
472                if depth == 0 {
473                    return Ok(at + 1);
474                }
475            }
476            _ => {}
477        }
478        at += 1;
479    }
480    Err(layout_error("unterminated JSON value"))
481}
482
483/// One member of a JSON object: its key, where the key starts, where the
484/// value starts and where it ends.
485struct JsonMember {
486    key: String,
487    start: usize,
488    value: usize,
489    end: usize,
490}
491
492fn json_members(source: &str, index: usize) -> Result<Vec<JsonMember>, GrammarLosslessError> {
493    let bytes = source.as_bytes();
494    if bytes.get(index) != Some(&b'{') {
495        return Err(layout_error(format!(
496            "expected a JSON object at offset {index}"
497        )));
498    }
499    let mut members = Vec::new();
500    let mut at = skip_json_space(bytes, index + 1);
501    while bytes.get(at) != Some(&b'}') {
502        let key_end = json_string_end(bytes, at)?;
503        let key: String = serde_json::from_str(&source[at..key_end])
504            .map_err(|error| layout_error(error.to_string()))?;
505        let mut value = skip_json_space(bytes, key_end);
506        if bytes.get(value) != Some(&b':') {
507            return Err(layout_error(format!("expected : at offset {value}")));
508        }
509        value = skip_json_space(bytes, value + 1);
510        let end = json_value_end(bytes, value)?;
511        members.push(JsonMember {
512            key,
513            start: at,
514            value,
515            end,
516        });
517        at = skip_json_space(bytes, end);
518        if bytes.get(at) == Some(&b',') {
519            at = skip_json_space(bytes, at + 1);
520        } else if bytes.get(at) != Some(&b'}') {
521            return Err(layout_error(format!("expected , or }} at offset {at}")));
522        }
523    }
524    Ok(members)
525}
526
527fn json_rule_spans(source: &str, names: &[&str]) -> Result<Vec<Span>, GrammarLosslessError> {
528    let members = json_members(source, skip_json_space(source.as_bytes(), 0))?;
529    let Some(rules) = members.iter().find(|member| member.key == "rules") else {
530        return Ok(Vec::new());
531    };
532    if source.as_bytes()[rules.value] != b'{' {
533        return Ok(Vec::new());
534    }
535    Ok(json_members(source, rules.value)?
536        .into_iter()
537        .filter(|member| names.contains(&member.key.as_str()))
538        .map(|member| Span {
539            name: member.key,
540            start: member.start,
541            end: member.end,
542        })
543        .collect())
544}
545
546fn fingerprint(grammar: &Grammar, name: &str) -> String {
547    render_rule_link(grammar.rule(name).expect("the name comes from the grammar"))
548}
549
550/// The layout of `source` for `grammar`, the grammar imported from it: its
551/// definitions with the fingerprints of their rules, and the fingerprints of
552/// the rules defined nowhere in the source.
553///
554/// # Errors
555///
556/// Returns [`GrammarLosslessError::UnsupportedFormat`] for a format outside
557/// [`GRAMMAR_LOSSLESS_FORMATS`] and a layout error for malformed JSON.
558pub fn capture_grammar_layout(
559    source: &str,
560    format: &str,
561    grammar: &Grammar,
562) -> Result<GrammarLayout, GrammarLosslessError> {
563    let names = grammar.rule_names();
564    let split = split_grammar_source(source, format, &names)?;
565    let defined: BTreeSet<&str> = split
566        .members
567        .iter()
568        .map(|member| member.name.as_str())
569        .collect();
570    let implicit = names
571        .iter()
572        .filter(|name| !defined.contains(**name))
573        .map(|name| GrammarLayoutImplicit {
574            name: (*name).to_owned(),
575            fingerprint: fingerprint(grammar, name),
576        })
577        .collect();
578    Ok(GrammarLayout {
579        format: format.to_owned(),
580        prefix: split.prefix,
581        members: split
582            .members
583            .into_iter()
584            .map(|member| GrammarLayoutDefinition {
585                fingerprint: fingerprint(grammar, &member.name),
586                name: member.name,
587                text: member.text,
588                gap: member.gap,
589            })
590            .collect(),
591        implicit,
592    })
593}
594
595/// Imports `source` of `format` with the layout that reconstructs it.
596///
597/// # Errors
598///
599/// Returns [`GrammarLosslessError::UnsupportedFormat`] for a format outside
600/// [`GRAMMAR_LOSSLESS_FORMATS`] and the importer's error for a malformed
601/// source.
602pub fn import_grammar_lossless(
603    source: &str,
604    format: &str,
605) -> Result<(Grammar, GrammarLayout), GrammarLosslessError> {
606    let format = check_format(format)?;
607    let importer = grammar_importer(format).expect("every lossless format has an importer");
608    let grammar = importer(source)?;
609    let layout = capture_grammar_layout(source, format, &grammar)?;
610    Ok((grammar, layout))
611}
612
613/// One piece of the reconstructed source: its text and, for a definition kept
614/// in place, its index in the layout.
615struct Item {
616    text: String,
617    index: Option<usize>,
618}
619
620/// Emits `grammar` with `layout`, the layout of the source it was imported
621/// from: unchanged definitions keep their original text and only changed or
622/// new rules are written fresh.
623///
624/// The report carries the fresh emission's notes, plus a note when the layout
625/// could not place a rule and the whole grammar was emitted fresh instead.
626///
627/// # Errors
628///
629/// Returns [`GrammarLosslessError::UnsupportedFormat`] for a layout format
630/// outside [`GRAMMAR_LOSSLESS_FORMATS`] and the emitter's error.
631pub fn emit_grammar_lossless(
632    grammar: &Grammar,
633    layout: &GrammarLayout,
634) -> Result<(String, EmitReport), GrammarLosslessError> {
635    let format = check_format(&layout.format)?;
636    let names = grammar.rule_names();
637    let fingerprints: BTreeMap<&str, String> = names
638        .iter()
639        .map(|name| (*name, fingerprint(grammar, name)))
640        .collect();
641    let kept = |name: &str, print: &str| fingerprints.get(name).map(String::as_str) == Some(print);
642    let mut defined_at: BTreeMap<&str, Vec<usize>> = BTreeMap::new();
643    for (index, member) in layout.members.iter().enumerate() {
644        defined_at
645            .entry(member.name.as_str())
646            .or_default()
647            .push(index);
648    }
649    let implicit: BTreeSet<&str> = layout
650        .implicit
651        .iter()
652        .filter(|entry| kept(&entry.name, &entry.fingerprint))
653        .map(|entry| entry.name.as_str())
654        .collect();
655    let unchanged = |name: &str| {
656        defined_at.get(name).is_some_and(|indices| {
657            indices.iter().all(|index| {
658                let member = &layout.members[*index];
659                kept(&member.name, &member.fingerprint)
660            })
661        })
662    };
663    let fresh: BTreeSet<&str> = names
664        .iter()
665        .copied()
666        .filter(|name| !unchanged(name) && !implicit.contains(name))
667        .collect();
668
669    let mut report = EmitReport::default();
670    let mut fresh_text: BTreeMap<String, String> = BTreeMap::new();
671    if !fresh.is_empty() {
672        let emitter = grammar_emitter(format).expect("every lossless format has an emitter");
673        let (whole, whole_report) = emitter(grammar)?;
674        report = whole_report;
675        for member in split_grammar_source(&whole, format, &names)?.members {
676            fresh_text.entry(member.name).or_insert(member.text);
677        }
678        if let Some(missing) = names
679            .iter()
680            .find(|name| fresh.contains(**name) && !fresh_text.contains_key(**name))
681        {
682            report.lossy.push(format!(
683                "the layout could not place rule {missing}, so the whole grammar was emitted fresh"
684            ));
685            return Ok((whole, report));
686        }
687    }
688
689    // New rules follow the nearest rule before them in grammar order.
690    let mut following: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
691    let mut leading = Vec::new();
692    let mut previous: Option<&str> = None;
693    for name in names.iter().copied() {
694        if implicit.contains(name) {
695            continue;
696        }
697        if !defined_at.contains_key(name) {
698            match previous {
699                None => leading.push(name),
700                Some(previous) => following.entry(previous).or_default().push(name),
701            }
702        }
703        previous = Some(name);
704    }
705    let mut items = Vec::new();
706    let place_new = |items: &mut Vec<Item>, names: &[&str]| {
707        let mut stack: Vec<&str> = names.iter().rev().copied().collect();
708        while let Some(name) = stack.pop() {
709            items.push(Item {
710                text: fresh_text[name].clone(),
711                index: None,
712            });
713            if let Some(next) = following.get(name) {
714                stack.extend(next.iter().rev());
715            }
716        }
717    };
718    place_new(&mut items, &leading);
719    for (index, member) in layout.members.iter().enumerate() {
720        if !fingerprints.contains_key(member.name.as_str()) {
721            continue;
722        }
723        let indices = &defined_at[member.name.as_str()];
724        if !fresh.contains(member.name.as_str()) {
725            items.push(Item {
726                text: member.text.clone(),
727                index: Some(index),
728            });
729        } else if indices.first() == Some(&index) {
730            items.push(Item {
731                text: fresh_text[&member.name].clone(),
732                index: Some(index),
733            });
734        }
735        if indices.last() == Some(&index)
736            && let Some(next) = following.get(member.name.as_str())
737        {
738            place_new(&mut items, next);
739        }
740    }
741
742    Ok((join_items(layout, &items), report))
743}
744
745/// Joins the reconstructed pieces. The text between two definitions stays
746/// before the definition it preceded (comments above a rule stay with it); a
747/// definition followed by a new rule keeps the text after it when the
748/// definition after it is gone. Each text is written once, and the text after
749/// the last definition ends the source.
750fn join_items(layout: &GrammarLayout, items: &[Item]) -> String {
751    let last = layout.members.len().checked_sub(1);
752    let fallback = if layout.format == "tree-sitter-json" {
753        match last {
754            Some(last) if last > 0 => layout.members[0].gap.as_str(),
755            _ => ",\n    ",
756        }
757    } else {
758        "\n"
759    };
760    let placed: BTreeSet<usize> = items.iter().filter_map(|item| item.index).collect();
761    let mut used = BTreeSet::new();
762    let mut source = layout.prefix.clone();
763    for (position, item) in items.iter().enumerate() {
764        if position > 0 {
765            let before = &items[position - 1];
766            let gap = match (before.index, item.index) {
767                (_, Some(after)) if after > 0 => Some(after - 1),
768                (Some(before), None)
769                    if last.is_some_and(|last| before < last)
770                        && !placed.contains(&(before + 1)) =>
771                {
772                    Some(before)
773                }
774                _ => None,
775            };
776            match gap {
777                Some(gap) if used.insert(gap) => source.push_str(&layout.members[gap].gap),
778                _ => source.push_str(fallback),
779            }
780        }
781        source.push_str(&item.text);
782    }
783    if let Some(last) = last {
784        source.push_str(&layout.members[last].gap);
785    }
786    source
787}
788
789/// Renders the links form of `layout`, one link per line.
790#[must_use]
791pub fn render_grammar_layout_links(layout: &GrammarLayout) -> String {
792    let text = |value: &str| percent_encode_links_text(value);
793    let mut lines = vec![format!(
794        "(layout {} {})",
795        layout.format,
796        text(&layout.prefix)
797    )];
798    for member in &layout.members {
799        lines.push(format!(
800            "(define {} {} {} {})",
801            text(&member.name),
802            text(&member.fingerprint),
803            text(&member.text),
804            text(&member.gap)
805        ));
806    }
807    for entry in &layout.implicit {
808        lines.push(format!(
809            "(implicit {} {})",
810            text(&entry.name),
811            text(&entry.fingerprint)
812        ));
813    }
814    lines.iter().fold(String::new(), |mut text, line| {
815        text.push_str(line);
816        text.push('\n');
817        text
818    })
819}
820
821/// The words of one layout link.
822fn words(statement: &LiNo<String>) -> Result<Vec<&str>, GrammarLosslessError> {
823    fn word(value: &LiNo<String>) -> Result<&str, GrammarLosslessError> {
824        match value {
825            LiNo::Ref(word) => Ok(word.as_str()),
826            LiNo::Link {
827                id: Some(word),
828                values,
829            } if values.is_empty() => Ok(word.as_str()),
830            LiNo::Link { .. } => Err(layout_error("layout links hold words only")),
831        }
832    }
833    match statement {
834        LiNo::Link { id: None, values } if !values.is_empty() => values.iter().map(word).collect(),
835        LiNo::Link {
836            id: Some(id),
837            values,
838        } if !values.is_empty() => Err(layout_error(format!("unexpected identified link {id}"))),
839        _ => Ok(vec![word(statement)?]),
840    }
841}
842
843/// Reads the links form written by [`render_grammar_layout_links`].
844///
845/// # Errors
846///
847/// Returns a layout error for malformed links, an unknown format or an
848/// unexpected link.
849pub fn parse_grammar_layout_links(source: &str) -> Result<GrammarLayout, GrammarLosslessError> {
850    let statements = parse_lino_to_links_with_config(source, &ParserConfig::without_comments())
851        .map_err(|error| layout_error(error.to_string()))?;
852    let mut statements = statements.iter().map(words);
853    let header = statements.next().transpose()?.unwrap_or_default();
854    let [head, format, prefix] = header.as_slice() else {
855        return Err(layout_error(
856            "the first link must be (layout FORMAT PREFIX)",
857        ));
858    };
859    if *head != "layout" {
860        return Err(layout_error(
861            "the first link must be (layout FORMAT PREFIX)",
862        ));
863    }
864    if !GRAMMAR_LOSSLESS_FORMATS.contains(format) {
865        return Err(layout_error(format!("unknown layout format {format}")));
866    }
867    let mut layout = GrammarLayout {
868        format: (*format).to_owned(),
869        prefix: percent_decode_links_text(prefix)?,
870        members: Vec::new(),
871        implicit: Vec::new(),
872    };
873    for statement in statements {
874        let statement = statement?;
875        let (head, values) = statement
876            .split_first()
877            .expect("a statement has at least one word");
878        let decoded = values
879            .iter()
880            .map(|value| percent_decode_links_text(value))
881            .collect::<Result<Vec<_>, _>>()?;
882        match (*head, decoded.as_slice()) {
883            ("define", [name, fingerprint, text, gap]) if layout.implicit.is_empty() => {
884                layout.members.push(GrammarLayoutDefinition {
885                    name: name.clone(),
886                    text: text.clone(),
887                    gap: gap.clone(),
888                    fingerprint: fingerprint.clone(),
889                });
890            }
891            ("implicit", [name, fingerprint]) => layout.implicit.push(GrammarLayoutImplicit {
892                name: name.clone(),
893                fingerprint: fingerprint.clone(),
894            }),
895            _ => return Err(layout_error(format!("unexpected layout link {head}"))),
896        }
897    }
898    Ok(layout)
899}