1use crate::document::{Dependency, Document, Stage, StageItem, Sub, SubTo, Tests};
9use crate::error::{Error, Result};
10use crate::item::{FuncName, Item};
11use crate::stdlib::{Funcall, FuncallName, Prim};
12use crate::tree::{leaf_text, pget, TNode};
13use std::collections::BTreeMap;
14use std::rc::Rc;
15
16pub fn dependency_targets(tree: &TNode) -> Vec<(String, Option<String>)> {
19 let system = tree.get("system").unwrap_or(tree);
20 let mut out = Vec::new();
21 let Some(body) = system.get("body") else { return out };
22 for block in body.ruby_array() {
23 if let Some(pair) = block.get("target") {
24 let target = unquote(pair);
25 let alias = block.get("alias").map(ident);
26 out.push((target, alias));
27 }
28 }
29 out
30}
31
32pub fn build(
33 tree: &TNode,
34 filename_stem: &str,
35 imports: &[Rc<Document>],
36 dep_docs: &BTreeMap<String, Rc<Document>>,
37) -> Result<Document> {
38 let system = tree.get("system").unwrap_or(tree);
39 let system_code = system.get("system_code").map(unquote).unwrap_or_default();
40
41 let body: Vec<&TNode> = match system.get("body") {
42 Some(b) => b.ruby_array().iter().collect(),
43 None => Vec::new(),
44 };
45
46 let mut metadata = Vec::new();
48 let mut alias_nodes: Vec<(String, TNode)> = Vec::new();
49 let mut tests = Tests::default();
50 let mut stage_nodes: Vec<(String, &TNode)> = Vec::new();
51 let mut dependencies: Vec<Dependency> = Vec::new();
52
53 for block in &body {
54 let Some(hash) = block.ruby_array_hash() else { continue };
55 if let Some(fields) = pget(hash, "metadata") {
56 extract_metadata(fields, &mut metadata);
57 } else if let Some(arr) = pget(hash, "aliases") {
58 for a in arr.ruby_array() {
59 if let Some(name_node) = a.get("name") {
60 let name = ident(name_node);
61 let value = a.get("value").cloned().unwrap_or(TNode::Nil);
62 alias_nodes.push((name, value));
63 }
64 }
65 } else if let Some(arr) = pget(hash, "tests") {
66 for t in arr.ruby_array() {
67 let input = t.get("input").map(unquote).unwrap_or_default();
68 let expected = t.get("expected").map(unquote).unwrap_or_default();
69 tests.data.push((input, expected));
70 }
71 } else if let (Some(name_node), Some(stage_node)) = (pget(hash, "stage_name"), pget(hash, "stage")) {
72 stage_nodes.push((ident(name_node), stage_node));
73 } else if let Some(t) = pget(hash, "target") {
74 let target = unquote(t);
75 let alias = pget(hash, "alias").map(ident);
76 dependencies.push(Dependency { target, alias });
77 }
78 }
79
80 let ctx = Ctx {
83 imported: {
84 let mut m = BTreeMap::new();
85 for doc in imports {
86 for (k, v) in &doc.aliases {
87 m.entry(k.clone()).or_insert_with(|| v.clone());
88 }
89 }
90 m
91 },
92 local_raw: alias_nodes.clone(),
93 dep_docs,
94 };
95
96 let mut aliases = BTreeMap::new();
98 for (name, value) in &alias_nodes {
99 aliases.insert(name.clone(), ctx.materialize(value)?);
100 }
101
102 let mut stages = BTreeMap::new();
103 for (name, node) in stage_nodes {
104 let stage = ctx.extract_stage(&name, node)?;
105 stages.insert(stage.name.clone(), stage);
106 }
107
108 Ok(Document {
109 name: filename_stem.to_string(),
110 system_code,
111 aliases,
112 stages,
113 dep_aliases: dep_docs
114 .iter()
115 .filter_map(|(k, v)| {
116 dependencies
117 .iter()
118 .find(|d| d.alias.as_deref() == Some(k.as_str()))
119 .map(|_| (k.clone(), v.clone()))
120 })
121 .collect(),
122 imports: imports.to_vec(),
123 tests,
124 metadata,
125 })
126}
127
128struct Ctx<'a> {
129 imported: BTreeMap<String, Item>,
131 local_raw: Vec<(String, TNode)>,
133 dep_docs: &'a BTreeMap<String, Rc<Document>>,
135}
136
137impl<'a> Ctx<'a> {
138 fn extract_stage(&self, name: &str, node: &TNode) -> Result<Stage> {
143 let mut items: Vec<StageItem> = Vec::new();
144 for entry in node.ruby_array() {
145 let Some(hash) = entry.ruby_array_hash() else { continue };
146 let keys: Vec<&str> = hash.iter().map(|(k, _)| k.as_str()).collect();
147 if keys.contains(&"sequence") {
148 let node = pget(hash, "sequence").unwrap();
149 items.push(StageItem::Sequential(self.extract_rule_list(node)?));
150 } else if keys.contains(&"parallel") {
151 let node = pget(hash, "parallel").unwrap();
152 items.push(StageItem::Parallel(self.extract_rule_list(node)?));
153 } else if keys.contains(&"separate") {
154 let separator = pget(hash, "separator")
155 .map(|v| self.materialize(v))
156 .transpose()?
157 .map(|item| render_str(&item.first_string()));
158 items.push(StageItem::Funcall(Funcall {
159 name: FuncallName::Separate,
160 kwargs: separator
161 .map(|s| vec![("separator".to_string(), s)])
162 .unwrap_or_default(),
163 reverse_run: None,
164 }));
165 } else if keys.contains(&"compose") {
166 let op = leaf_text(pget(hash, "compose").unwrap());
167 let name = if op == "decompose" {
168 FuncallName::Decompose
169 } else {
170 FuncallName::Compose
171 };
172 items.push(StageItem::Funcall(Funcall { name, kwargs: vec![], reverse_run: None }));
173 } else if keys.contains(&"case") {
174 let op = leaf_text(pget(hash, "case").unwrap());
175 let name = match op.as_str() {
176 "upcase" => FuncallName::Upcase,
177 "downcase" => FuncallName::Downcase,
178 "title_case" => FuncallName::TitleCase,
179 other => return Err(Error::MapLogic(format!("unknown case op {other:?}"))),
180 };
181 let kwargs = pget(hash, "case_kwargs").map(extract_kwargs).unwrap_or_default();
182 items.push(StageItem::Funcall(Funcall { name, kwargs, reverse_run: None }));
183 } else if keys.contains(&"funcall_name") {
184 let fname = leaf_text(pget(hash, "funcall_name").unwrap());
185 let kwargs = pget(hash, "funcall_kwargs").map(extract_kwargs).unwrap_or_default();
186 let fname_enum = FuncallName::from_name(&fname)
187 .ok_or_else(|| crate::stdlib::unsupported_funcall(&fname))?;
188 items.push(StageItem::Funcall(Funcall { name: fname_enum, kwargs, reverse_run: None }));
189 } else if keys.contains(&"dep") {
190 let dep = ident(pget(hash, "dep").unwrap());
191 let stage = ident(pget(hash, "stage").unwrap());
192 items.push(StageItem::Run {
193 dep: Some(dep),
194 stage,
195 reverse_run: None,
196 });
197 } else if keys.contains(&"run_stage_only") {
198 let stage = pget(hash, "run_stage_only")
199 .and_then(|v| v.get("stage"))
200 .map(ident)
201 .unwrap_or_default();
202 items.push(StageItem::Run { dep: None, stage, reverse_run: None });
203 } else if keys.contains(&"bare_rule") {
204 let rule = pget(hash, "bare_rule").unwrap();
205 items.push(StageItem::Sequential(vec![self.extract_rule(rule)?]));
206 }
207 }
209 Ok(Stage { name: name.to_string(), items, reverse_run: None })
210 }
211
212 fn extract_rule_list(&self, node: &TNode) -> Result<Vec<Sub>> {
213 let mut rules = Vec::new();
214 for r in node.ruby_array() {
215 if is_none_capture(r.get("from")) && is_none_capture(r.get("to")) {
217 continue;
218 }
219 rules.push(self.extract_rule(r)?);
220 }
221 Ok(rules)
222 }
223
224 fn extract_rule(&self, r: &TNode) -> Result<Sub> {
225 let from = match r.get("from") {
226 Some(n) => self.materialize(n)?,
227 None => Item::None,
228 };
229 let to = match r.get("to") {
230 Some(n) => self.materialize(n)?,
231 None => Item::None,
232 };
233
234 let mut before = None;
235 let mut not_before = None;
236 let mut after = None;
237 let mut not_after = None;
238
239 if let Some(constraints) = r.get("constraints") {
240 for c in constraints.ruby_array() {
241 let Some(hash) = c.ruby_array_hash() else { continue };
242 for (kind, item_node) in hash.iter() {
243 let target = match kind.as_str() {
244 "before" => &mut before,
245 "not_before" => &mut not_before,
246 "after" => &mut after,
247 "not_after" => &mut not_after,
248 _ => continue,
249 };
250 *target = Some(self.materialize_class(item_node)?);
253 }
254 }
255 }
256
257 let to_target = match &to {
260 Item::Func(FuncName::Upcase) => SubTo::Upcase,
261 Item::Func(FuncName::Downcase) => SubTo::Downcase,
262 item => SubTo::Item(item.clone()),
263 };
264
265 let mut sub = Sub {
266 from,
267 to: to_target,
268 before,
269 not_before,
270 after,
271 not_after,
272 group_count: 0,
273 reverse_run: None,
274 };
275 sub.from = self.expand_aliases(&sub.from, 0)?;
280 for c in [&mut sub.before, &mut sub.not_before] {
281 if let Some(i) = c {
282 *i = self.expand_aliases(i, 0)?;
283 }
284 }
285 for c in [&mut sub.not_after, &mut sub.after] {
287 if let Some(i) = c {
288 *i = self.expand_aliases(i, 0)?;
289 }
290 }
291 sub.group_count = assign_group_ids(&mut sub);
292 Ok(sub)
293 }
294
295 fn expand_aliases(&self, item: &Item, depth: usize) -> Result<Item> {
301 if depth > 32 {
302 return Err(Error::MapLogic(
303 "alias expansion too deep (alias cycle?)".into(),
304 ));
305 }
306 Ok(match item {
307 Item::Alias { map, name } => {
308 if map.is_some() {
309 let alias = map.clone().unwrap();
311 let doc = self
312 .dep_docs
313 .get(&alias)
314 .ok_or_else(|| Error::MapLogic(format!("dep alias {alias:?} not found")))?;
315 match doc.lookup_alias(name) {
316 Some(resolved) => self.expand_aliases(&resolved, depth + 1)?,
317 None => {
318 return Err(Error::MapLogic(format!(
319 "alias {name:?} of dep {alias:?} not found"
320 )))
321 }
322 }
323 } else if let Some(prim) = Prim::from_name(name) {
324 Item::Prim(prim)
325 } else if let Some(data) = self.imported.get(name) {
326 self.expand_aliases(data, depth + 1)?
327 } else if let Some((_, raw)) = self
328 .local_raw
329 .iter()
330 .find(|(n, _)| n.as_str() == name)
331 {
332 let resolved = self.materialize(raw)?;
333 self.expand_aliases(&resolved, depth + 1)?
334 } else {
335 return Err(Error::MapLogic(format!(
336 "alias {name:?} not found during expansion"
337 )));
338 }
339 }
340 Item::CaptureGroup { id, inner } => Item::CaptureGroup {
341 id: *id,
342 inner: Box::new(self.expand_aliases(inner, depth)?),
343 },
344 Item::Maybe(inner) => Item::Maybe(Box::new(self.expand_aliases(inner, depth)?)),
345 Item::Some(inner) => Item::Some(Box::new(self.expand_aliases(inner, depth)?)),
346 Item::Concat(parts) => Item::Concat(
347 parts
348 .iter()
349 .map(|p| self.expand_aliases(p, depth))
350 .collect::<Result<Vec<_>>>()?,
351 ),
352 Item::Set(entries) => Item::Set(
353 entries
354 .iter()
355 .map(|p| self.expand_set_entry(p, depth))
356 .collect::<Result<Vec<_>>>()?,
357 ),
358 other => other.clone(),
359 })
360 }
361
362 fn expand_set_entry(&self, entry: &Item, depth: usize) -> Result<Item> {
365 match entry {
366 Item::Alias { .. } => self.expand_aliases(entry, depth),
367 Item::Concat(parts) => Ok(Item::Concat(
368 parts
369 .iter()
370 .map(|p| self.expand_set_entry(p, depth))
371 .collect::<Result<Vec<_>>>()?,
372 )),
373 Item::Set(entries) => Ok(Item::Set(
374 entries
375 .iter()
376 .map(|p| self.expand_set_entry(p, depth))
377 .collect::<Result<Vec<_>>>()?,
378 )),
379 Item::Maybe(inner) => Ok(Item::Maybe(Box::new(self.expand_set_entry(inner, depth)?))),
380 Item::Some(inner) => Ok(Item::Some(Box::new(self.expand_set_entry(inner, depth)?))),
381 other => Ok(other.clone()),
382 }
383 }
384
385 fn materialize(&self, fragment: &TNode) -> Result<Item> {
390 match fragment {
391 TNode::Hash(_) => {
392 if fragment.has_key("concatenation") {
393 let parts = fragment.get("concatenation").unwrap();
394 let items = parts
395 .ruby_array()
396 .iter()
397 .map(|p| self.convert_fragment(p))
398 .collect::<Result<Vec<_>>>()?;
399 Ok(Item::concat_from_parts(items))
400 } else {
401 let item = self.convert_fragment(fragment)?;
402 Ok(Item::concat_from_parts(vec![item]))
403 }
404 }
405 TNode::Array(parts) => {
406 let items = parts
407 .iter()
408 .map(|p| self.convert_fragment(p))
409 .collect::<Result<Vec<_>>>()?;
410 Ok(Item::concat_from_parts(items))
411 }
412 TNode::Nil => Ok(Item::None),
413 TNode::Str(_) => Ok(Item::Str(fragment.as_str().unwrap_or_default().to_string())),
414 }
415 }
416
417 fn convert_fragment(&self, node: &TNode) -> Result<Item> {
419 if let TNode::Hash(pairs) = node {
420 let (key, value) = (&pairs[0].0, &pairs[0].1);
421 match key.as_str() {
422 "string" => return Ok(Item::Str(decode_string_value(value))),
423 "none" => return Ok(Item::None),
424 "any_char" => {
425 return Ok(Item::Alias {
426 map: None,
427 name: "any_character".to_string(),
428 })
429 }
430 "primitive" => {
431 let name = leaf_text(value);
432 return Prim::from_name(&name)
433 .map(Item::Prim)
434 .ok_or_else(|| Error::MapLogic(format!("unknown primitive {name:?}")));
435 }
436 "function" => {
437 let name = leaf_text(value);
438 return match name.as_str() {
439 "upcase" => Ok(Item::Func(FuncName::Upcase)),
440 "downcase" => Ok(Item::Func(FuncName::Downcase)),
441 other => Err(Error::MapLogic(format!(
446 "function item {other:?} is not supported in this position"
447 ))),
448 };
449 }
450 "alias" => {
451 return Ok(match value.get("qualified_name") {
456 Some(q) => Item::Alias {
457 map: value.get("identifier").map(ident),
458 name: ident(q),
459 },
460 None => Item::Alias {
461 map: None,
462 name: match value.get("identifier") {
463 Some(id) => ident(id),
464 None => leaf_text(value),
465 },
466 },
467 });
468 }
469 "alias_ref" => {
470 return Ok(Item::Alias {
471 map: None,
472 name: match value.get("identifier") {
473 Some(id) => ident(id),
474 None => leaf_text(value),
475 },
476 });
477 }
478 "ref" => {
479 let digit = leaf_text(value);
480 return Ok(Item::CaptureRef(digit.parse().map_err(|_| {
481 Error::MapLogic(format!("bad ref({digit:?})"))
482 })?));
483 }
484 "capture_inner" => {
485 return Ok(Item::CaptureGroup {
486 id: 0,
487 inner: Box::new(self.materialize(value)?),
488 })
489 }
490 "maybe_inner" => return Ok(Item::Maybe(Box::new(self.materialize(value)?))),
491 "some_inner" => return Ok(Item::Some(Box::new(self.materialize(value)?))),
492 "any" => return self.convert_any_arg(value),
493 "concatenation" => return self.materialize(node),
494 _ => {}
495 }
496 }
497 self.convert_other(node)
498 }
499
500 fn convert_any_arg(&self, value: &TNode) -> Result<Item> {
503 if let Some(hash) = value.ruby_array_hash() {
504 let keys: Vec<&str> = hash.iter().map(|(k, _)| k.as_str()).collect();
505 let get = |name: &str| pget(hash, name);
506 if keys.contains(&"lo") && keys.contains(&"hi") {
507 let lo = unquote(get("lo").unwrap());
508 let hi = unquote(get("hi").unwrap());
509 return match (chars_of(&lo).next(), chars_of(&hi).next()) {
510 (Some(l), Some(h)) => Ok(Item::Range(l, h)),
511 _ => Err(Error::MapLogic("empty any() range bounds".into())),
512 };
513 }
514 if keys.contains(&"single") {
515 let s = unquote(get("single").unwrap());
516 return Ok(Item::Set(
517 chars_of(&s).map(|c| Item::Str(c.to_string())).collect(),
518 ));
519 }
520 if keys.contains(&"list") {
521 let items = get("list")
522 .unwrap()
523 .ruby_array()
524 .iter()
525 .map(|e| self.convert_set_entry(e))
526 .collect::<Result<Vec<_>>>()?;
527 return Ok(Item::Set(items));
528 }
529 if keys.contains(&"alias_ref") || keys.contains(&"alias") {
530 let alias = self.convert_set_entry(value)?;
533 return Ok(Item::Set(vec![alias]));
534 }
535 if keys.contains(&"any_item") {
536 return self.convert_any_arg(get("any_item").unwrap());
537 }
538 if keys.contains(&"any_char") {
539 return Ok(Item::Set(vec![Item::Alias {
540 map: None,
541 name: "any_character".to_string(),
542 }]));
543 }
544 if keys.contains(&"primitive") {
545 return Ok(Item::Set(vec![self.convert_set_entry(value)?]));
546 }
547 if keys.contains(&"string") {
548 let s = decode_string_value(get("string").unwrap());
549 return Ok(Item::Set(
550 chars_of(&s).map(|c| Item::Str(c.to_string())).collect(),
551 ));
552 }
553 if keys.contains(&"concatenation") {
554 let item = self.materialize(value)?;
555 return Ok(Item::Set(vec![item]));
556 }
557 }
558 self.convert_set_entry(value)
559 }
560
561 fn convert_set_entry(&self, entry: &TNode) -> Result<Item> {
563 if let TNode::Hash(pairs) = entry {
564 let (key, value) = (&pairs[0].0, &pairs[0].1);
565 match key.as_str() {
566 "string" => return Ok(Item::Str(decode_string_value(value))),
567 "primitive" => {
568 let name = leaf_text(value);
569 return Prim::from_name(&name)
570 .map(Item::Prim)
571 .ok_or_else(|| Error::MapLogic(format!("unknown primitive {name:?}")));
572 }
573 "alias_ref" | "alias" => return self.convert_class_alias(value),
574 "concatenation" => return self.materialize(entry),
575 "any" => return self.convert_any_arg(value),
576 _ => {}
577 }
578 }
579 self.materialize(entry)
580 }
581
582 fn convert_class_alias(&self, value: &TNode) -> Result<Item> {
586 let (map, name) = match value.get("qualified_name") {
587 Some(q) => (value.get("identifier").map(ident), ident(q)),
588 None => (None, match value.get("identifier") {
589 Some(id) => ident(id),
590 None => leaf_text(value),
591 }),
592 };
593 if map.is_some() {
597 return Ok(Item::Alias { map, name });
598 }
599 if let Some(prim) = Prim::from_name(&name) {
600 return Ok(Item::Prim(prim));
601 }
602 if let Some(data) = self.imported.get(&name) {
604 return match data {
605 Item::Set(entries) => Ok(Item::Set(entries.clone())),
606 Item::Range(lo, hi) => Ok(Item::Range(*lo, *hi)),
607 Item::Str(s) => Ok(Item::Set(
608 chars_of(s).map(|c| Item::Str(c.to_string())).collect(),
609 )),
610 _ => Ok(vacuous()),
611 };
612 }
613 if let Some((_, raw)) = self.local_raw.iter().find(|(n, _)| *n == name) {
618 let converted = self.materialize(raw)?;
619 return Ok(match converted {
620 Item::Set(_) | Item::Range(_, _) | Item::Str(_) => converted,
621 _ => vacuous(),
622 });
623 }
624 Ok(vacuous())
625 }
626
627 fn materialize_class(&self, fragment: &TNode) -> Result<Item> {
629 match fragment {
630 TNode::Hash(_) if fragment.has_key("concatenation") => {
631 let parts = fragment.get("concatenation").unwrap();
632 let items = parts
633 .ruby_array()
634 .iter()
635 .map(|p| self.convert_class_fragment(p))
636 .collect::<Result<Vec<_>>>()?;
637 Ok(Item::concat_from_parts(items))
638 }
639 TNode::Hash(_) => {
640 let item = self.convert_class_fragment(fragment)?;
641 Ok(Item::concat_from_parts(vec![item]))
642 }
643 TNode::Array(parts) => {
644 let items = parts
645 .iter()
646 .map(|p| self.convert_class_fragment(p))
647 .collect::<Result<Vec<_>>>()?;
648 Ok(Item::concat_from_parts(items))
649 }
650 TNode::Nil => Ok(Item::None),
651 TNode::Str(_) => Ok(Item::Str(fragment.as_str().unwrap_or_default().to_string())),
652 }
653 }
654
655 fn convert_class_fragment(&self, node: &TNode) -> Result<Item> {
656 if let TNode::Hash(pairs) = node {
659 if let Some((key, value)) = pairs.first() {
660 if key == "alias_ref" || key == "alias" {
661 return self.convert_class_alias(value);
662 }
663 }
664 }
665 self.convert_fragment(node)
666 }
667
668 fn convert_other(&self, node: &TNode) -> Result<Item> {
669 let _ = self;
670 if let TNode::Hash(pairs) = node {
671 let keys: Vec<&str> = pairs.iter().map(|(k, _)| k.as_str()).collect();
672 let get = |name: &str| pget(pairs, name);
673 if keys.contains(&"lo") && keys.contains(&"hi") {
674 let lo = unquote(get("lo").unwrap());
675 let hi = unquote(get("hi").unwrap());
676 return match (chars_of(&lo).next(), chars_of(&hi).next()) {
677 (Some(l), Some(h)) => Ok(Item::Range(l, h)),
678 _ => Err(Error::MapLogic("empty range bounds".into())),
679 };
680 }
681 }
682 Ok(Item::Str(leaf_text(node)))
683 }
684}
685
686fn vacuous() -> Item {
687 Item::Str(String::new())
688}
689
690fn is_none_capture(node: Option<&TNode>) -> bool {
695 match node {
696 None => true,
697 Some(TNode::Nil) => true,
698 Some(n) => n.has_key("none"),
699 }
700}
701
702pub fn decode_string_value(value: &TNode) -> String {
704 match value {
705 TNode::Hash(_) => decode_part(value),
706 TNode::Array(parts) => parts.iter().map(decode_part).collect(),
707 TNode::Str(s) => s.clone(),
708 TNode::Nil => String::new(),
709 }
710}
711
712fn decode_part(part: &TNode) -> String {
713 if let Some(pairs) = part.ruby_array_hash() {
714 if let Some((key, value)) = pairs.first() {
715 return match key.as_str() {
716 "run" => leaf_text(value),
717 "newline" => "\n".to_string(),
718 "carriage_return" => "\r".to_string(),
719 "tab" => "\t".to_string(),
720 "dquote" => "\"".to_string(),
721 "backslash" => "\\".to_string(),
722 "u_lone" => "U".to_string(),
723 "unicode" => {
724 let hex = leaf_text(value);
725 match u32::from_str_radix(&hex, 16).ok().and_then(char::from_u32) {
726 Some(c) => c.to_string(),
727 None => hex,
729 }
730 }
731 _ => leaf_text(part),
732 };
733 }
734 return String::new();
735 }
736 match part {
737 TNode::Str(s) => s.clone(),
738 _ => leaf_text(part),
739 }
740}
741
742pub fn unquote(fragment: &TNode) -> String {
744 match fragment {
745 TNode::Nil => String::new(),
746 TNode::Str(s) => s.clone(),
747 TNode::Hash(pairs) => {
748 if let Some((_, v)) = pairs.iter().find(|(k2, _)| k2 == "string") {
749 decode_string_value(v)
750 } else {
751 leaf_text(fragment)
752 }
753 }
754 TNode::Array(_) => leaf_text(fragment),
755 }
756}
757
758pub fn ident(fragment: &TNode) -> String {
760 match fragment {
761 TNode::Nil => String::new(),
762 TNode::Str(s) => s.clone(),
763 _ => match fragment.get("identifier") {
764 Some(id) => leaf_text(id),
765 None => leaf_text(fragment),
766 },
767 }
768}
769
770fn chars_of(s: &str) -> impl Iterator<Item = char> + '_ {
771 s.chars()
772}
773
774fn render_str(item: &Item) -> String {
777 match item {
778 Item::Str(s) => s.clone(),
779 Item::None => String::new(),
780 Item::Concat(parts) => parts.iter().map(render_str).collect(),
781 _ => String::new(),
782 }
783}
784
785pub fn extract_kwargs(node: &TNode) -> Vec<(String, String)> {
787 let mut out = Vec::new();
788 let entries: Vec<&TNode> = match node.get("kwarg") {
789 Some(kw) => kw.ruby_array().iter().collect(),
790 None => vec![node],
791 };
792 for entry in entries {
793 if let (Some(name), Some(value)) = (entry.get("kwarg_name"), entry.get("kwarg_value")) {
794 out.push((ident(name), unquote(value)));
795 }
796 }
797 out
798}
799
800pub fn unescape_braces(text: &str) -> String {
802 let mut out = String::with_capacity(text.len());
803 let mut chars = text.chars();
804 while let Some(c) = chars.next() {
805 if c == '\\' {
806 match chars.next() {
807 Some(n @ ('{' | '}' | '\\')) => out.push(n),
808 Some(other) => {
809 out.push('\\');
810 out.push(other);
811 }
812 None => out.push('\\'),
813 }
814 } else {
815 out.push(c);
816 }
817 }
818 out
819}
820
821pub fn normalize_heredoc(text: &str) -> String {
824 let lines: Vec<&str> = text.lines().collect();
825 let content: Vec<&&str> = lines
826 .iter()
827 .filter(|l| !l.trim().is_empty())
828 .collect();
829 if content.is_empty() {
830 return String::new();
831 }
832 if content.len() == 1 {
833 return content[0].trim().to_string();
834 }
835 let min_indent = content
836 .iter()
837 .skip(1)
838 .map(|l| l.len() - l.trim_start_matches([' ', '\t']).len())
839 .min()
840 .unwrap_or(0);
841 let mut result: Vec<String> = Vec::new();
842 for line in &lines {
843 if line.trim().is_empty() {
844 result.push(String::new());
845 } else {
846 let indent = line.len() - line.trim_start_matches([' ', '\t']).len();
847 if indent >= min_indent {
848 result.push(line[min_indent..].to_string());
849 } else {
850 result.push(line.trim().to_string());
851 }
852 }
853 }
854 let joined = result.join("\n");
855 let trimmed = joined.trim_end().to_string();
856 if text.ends_with('\n') && !text.ends_with("\n\n") {
857 trimmed + "\n"
858 } else {
859 trimmed
860 }
861}
862
863fn extract_metadata(arr: &TNode, out: &mut Vec<(String, String)>) {
865 for field in arr.ruby_array() {
866 let Some(hash) = field.ruby_array_hash() else { continue };
867 if let Some(v) = pget(hash, "field_name") {
868 let name = ident(v);
869 let val = if let Some(b) = pget(hash, "field_block") {
870 normalize_heredoc(&unescape_braces(&leaf_text(b)))
871 } else if let Some(fv) = pget(hash, "field_value") {
872 match fv {
873 TNode::Hash(_) if fv.has_key("string") => unquote(fv),
874 _ => fv.as_str().unwrap_or_default().trim().to_string(),
875 }
876 } else {
877 String::new()
878 };
879 out.push((name, val));
880 } else {
881 for (key, val) in hash.iter() {
882 if matches!(val, TNode::Nil) {
883 continue;
884 }
885 let text = match val {
886 TNode::Hash(_) if val.has_key("string") => unquote(val),
887 _ => leaf_text(val),
888 };
889 out.push((key.clone(), text));
890 }
891 }
892 }
893}
894
895pub fn assign_group_ids(sub: &mut Sub) -> usize {
899 let mut counter = 0usize;
900 for item in [&mut sub.before, &mut sub.not_before] {
901 if let Some(i) = item {
902 assign_in(i, &mut counter);
903 }
904 }
905 assign_in(&mut sub.from, &mut counter);
906 for item in [&mut sub.not_after, &mut sub.after] {
907 if let Some(i) = item {
908 assign_in(i, &mut counter);
909 }
910 }
911 counter
912}
913
914fn assign_in(item: &mut Item, counter: &mut usize) {
915 match item {
916 Item::CaptureGroup { id, inner } => {
917 *counter += 1;
918 *id = *counter;
919 assign_in(inner, counter);
920 }
921 Item::Maybe(inner) | Item::Some(inner) => assign_in(inner, counter),
922 Item::Concat(parts) => parts.iter_mut().for_each(|p| assign_in(p, counter)),
923 Item::Set(entries) => entries.iter_mut().for_each(|e| assign_in(e, counter)),
924 _ => {}
925 }
926}