1use 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
30pub 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
42const DOC_FORMATS: &[&str] = &["antlr", "gbnf", "lark"];
45
46#[derive(Clone, Debug, PartialEq, Eq)]
48pub struct GrammarSourceDefinition {
49 pub name: String,
51 pub text: String,
54 pub gap: String,
56}
57
58#[derive(Clone, Debug, PartialEq, Eq)]
61pub struct GrammarSourceSplit {
62 pub prefix: String,
64 pub members: Vec<GrammarSourceDefinition>,
66}
67
68#[derive(Clone, Debug, PartialEq, Eq)]
70pub struct GrammarLayoutDefinition {
71 pub name: String,
73 pub text: String,
75 pub gap: String,
77 pub fingerprint: String,
79}
80
81#[derive(Clone, Debug, PartialEq, Eq)]
83pub struct GrammarLayoutImplicit {
84 pub name: String,
86 pub fingerprint: String,
88}
89
90#[derive(Clone, Debug, PartialEq, Eq)]
92pub struct GrammarLayout {
93 pub format: String,
95 pub prefix: String,
97 pub members: Vec<GrammarLayoutDefinition>,
99 pub implicit: Vec<GrammarLayoutImplicit>,
101}
102
103#[derive(Clone, Debug, PartialEq, Eq)]
105pub enum GrammarLosslessError {
106 UnsupportedFormat(String),
108 Import(GrammarImportError),
110 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
156struct Span {
158 name: String,
159 start: usize,
160 end: usize,
161}
162
163pub 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
204fn 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
220fn 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
246fn 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
324fn 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
483struct 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
550pub 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
595pub 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
613struct Item {
616 text: String,
617 index: Option<usize>,
618}
619
620pub 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 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
745fn 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#[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
821fn 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
843pub 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}