1use std::collections::{BTreeMap, BTreeSet, HashMap};
4use std::fmt;
5
6use thiserror::Error;
7
8use svugy_model::description::{DescriptionTarget, TargetLink};
9use svugy_model::link::Link;
10use svugy_model::model::{Edge, EdgeId, Element, Model, PlacementHint, Position};
11use svugy_model::path::ElementPath;
12use svugy_model::properties::ComponentProperties;
13use svugy_model::style::{ColorKey, NumberKey, NumberValue, StyleValue};
14use svugy_model::workflow::{ElementOverride, Step, StepId, StepItem, Workflow, WorkflowId};
15
16use super::ast::Value;
17use super::parse::ItemLabel;
18use super::property::LineStyle;
19use super::skeleton::{DESCRIPTION_INFO, Skeleton};
20use super::target::{ComponentNumbering, EdgeNumbering};
21
22const FILL: &str = ColorKey::FILL.name();
24const STROKE: &str = ColorKey::STROKE.name();
26const STROKE_DASH: &str = NumberKey::STROKE_DASH.name();
28const SAYABLE_ELEMENT_STYLE_KEYS: [&str; 3] = [FILL, STROKE, STROKE_DASH];
30const SAYABLE_EDGE_STYLE_KEYS: [&str; 2] = [STROKE, STROKE_DASH];
32const LINE_BREAKS: [char; 2] = ['\n', '\r'];
34const COMMENT_OPENING: &str = "<!--";
36const MIN_FENCE_LENGTH: usize = 3;
38
39#[derive(Clone, Debug, PartialEq, Error)]
41#[error(
42 "Markdown cannot say {} part(s) of this model:{}",
43 .unprintables.len(),
44 Self::bulleted(.unprintables)
45)]
46pub struct UnprintableModel {
47 pub unprintables: Vec<Unprintable>,
48}
49
50impl UnprintableModel {
51 fn bulleted(unprintables: &[Unprintable]) -> String {
53 let lines = unprintables
54 .iter()
55 .map(|unprintable| format!("\n- {unprintable}"));
56 lines.collect()
57 }
58}
59
60#[derive(Clone, Debug, PartialEq, Error)]
62#[error("{owner}: {reason}")]
63pub struct Unprintable {
64 pub owner: UnprintableOwner,
65 pub reason: UnprintableReason,
66}
67
68#[derive(Clone, Debug, PartialEq)]
70pub enum UnprintableOwner {
71 Element(ElementPath),
72 Workflow(WorkflowId),
73 Override(WorkflowId, ElementPath),
74 Step(WorkflowId, StepId),
75 Edge(WorkflowId, EdgeId),
76}
77
78impl fmt::Display for UnprintableOwner {
79 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
80 match self {
81 Self::Element(path) => write!(formatter, "element `{path}`"),
82 Self::Workflow(workflow) => write!(formatter, "workflow `{workflow}`"),
83 Self::Override(workflow, path) => {
84 write!(formatter, "override of `{path}` in workflow `{workflow}`")
85 }
86 Self::Step(workflow, step) => write!(formatter, "step `{workflow}/{step}`"),
87 Self::Edge(workflow, id) => write!(formatter, "edge `{id}` of workflow `{workflow}`"),
88 }
89 }
90}
91
92#[derive(Clone, Debug, PartialEq, Error)]
94pub enum UnprintableReason {
95 #[error(
96 "path segment `{0}` is not an ID: ASCII letters, digits, `_` and single inner `-`, starting with a letter or `_`"
97 )]
98 InvalidId(String),
99 #[error("metadata name `{0}` is not an ID, so no `meta.NAME` key spells it")]
100 InvalidMetadataName(String),
101 #[error("its {field} {text:?} holds a line break, which no one-line item can")]
102 LineBreak { field: TextField, text: String },
103 #[error("its {field} {text:?} holds `<!--`, which the language reads as a comment and drops")]
104 CommentOpening { field: TextField, text: String },
105 #[error(
106 "its description does not read back verbatim from a ```description block: Markdown reads a carriage return as a line break"
107 )]
108 UnreadableDescription,
109 #[error(
110 "its prose does not read back verbatim under a heading: outside a code fence it holds no `- ` bullet list, `#` heading or comment, and no blank line opens or closes it"
111 )]
112 UnreadableProse,
113 #[error("stroke-dash `{0}` is neither 5 (`line=dashed`) nor 2 (`line=dotted`)")]
114 UnsayableStrokeDash(String),
115 #[error("style `{0}` has no property in the language")]
116 UnmappedStyleKey(String),
117 #[error("a badge edge already says `line=badge`, so it cannot also have a stroke-dash")]
118 BadgeWithStrokeDash,
119 #[error(
120 "link `{0}` holds no `:`, `/`, `?` or `#`, so the language would read it as a component reference; write it as `./{0}`"
121 )]
122 UrlWithoutUrlCharacter(String),
123 #[error(
124 "link `{link}` spells element `{target}` another way, and the language keeps only the path; write it as `{target}`"
125 )]
126 RespelledElementLink { link: String, target: ElementPath },
127 #[error(
128 "its ordinal is {ordinal}, but the language numbers repeated edges in the order their steps declare them, so it would read back as {reading_ordinal}"
129 )]
130 RenumberedEdge { ordinal: u32, reading_ordinal: u32 },
131}
132
133#[derive(Clone, Copy, Debug, PartialEq, Eq, strum::Display)]
135pub enum TextField {
136 #[strum(to_string = "display name")]
137 DisplayName,
138 #[strum(to_string = "caption")]
139 Caption,
140 #[strum(to_string = "link")]
141 Link,
142 #[strum(to_string = "code")]
143 Code,
144 #[strum(to_string = "metadata value")]
145 MetadataValue,
146 #[strum(to_string = "label")]
147 Label,
148 #[strum(to_string = "title")]
149 Title,
150 #[strum(to_string = "subtitle")]
151 Subtitle,
152}
153
154pub struct MarkdownPrinter<'a> {
156 model: &'a Model,
157 references: ReferenceIndex<'a>,
158 markdown: String,
159 unprintables: Vec<Unprintable>,
160}
161
162struct EdgeDeclarations<'a> {
164 declared_edges: BTreeSet<&'a EdgeId>,
165 declaration_counts: BTreeMap<(&'a ElementPath, &'a ElementPath), u32>,
166 listings_left: BTreeMap<&'a EdgeId, usize>,
167}
168
169impl<'a> MarkdownPrinter<'a> {
170 fn new(model: &'a Model) -> Self {
172 let references = ReferenceIndex::new(model);
173 Self {
174 model,
175 references,
176 markdown: String::new(),
177 unprintables: Vec::new(),
178 }
179 }
180
181 pub fn print(model: &'a Model) -> Result<String, UnprintableModel> {
183 let mut printer = Self::new(model);
184 printer.write_frontmatter();
185 printer.write_components();
186 printer.write_workflows();
187 printer.finish()
188 }
189
190 fn write_frontmatter(&mut self) {
192 let Some(view) = self.model.view() else {
193 return;
194 };
195 let view_spelling: &'static str = view.into();
196 let frontmatter = format!("---\nview: {view_spelling}\n---\n\n");
197 self.markdown.push_str(&frontmatter);
198 }
199
200 fn write_components(&mut self) {
202 self.markdown.push_str("## Components\n");
203 let model = self.model;
204 for (path, element) in model.elements() {
205 let mut reasons = Vec::new();
206 let item = self.component_item(path, element, &mut reasons);
207 self.markdown.push_str(&item);
208 self.report(reasons, || {
209 let element_path = path.clone();
210 UnprintableOwner::Element(element_path)
211 });
212 }
213 }
214
215 fn component_item(
217 &self,
218 path: &ElementPath,
219 element: &Element,
220 reasons: &mut Vec<UnprintableReason>,
221 ) -> String {
222 let id = path.name().unwrap_or_default();
223 let is_invalid_id = !ElementPath::is_component_id(id);
224 if is_invalid_id {
225 let invalid_segment = id.to_owned();
226 let invalid_id = UnprintableReason::InvalidId(invalid_segment);
227 reasons.push(invalid_id);
228 }
229 let depth = path.depth();
230 let nesting_depth = depth.saturating_sub(1);
231 let indent = " ".repeat(nesting_depth);
232 let label = element.label();
233 let is_label_diff_from_id = label != id;
234 let display_name = if is_label_diff_from_id {
235 let label_spelling = Self::spell_label(label, TextField::DisplayName);
236 let display_name = Self::spelling_or_push_reason(label_spelling, reasons);
237 format!(": {display_name}")
238 } else {
239 String::new()
240 };
241 let element_properties = element.properties();
242 let properties = self.component_properties(element_properties, None, None, reasons);
243 let properties_list = Self::spell_properties_list(&properties);
244 let item_line = format!("{indent}- {id}{display_name}{properties_list}\n");
245 let Some(description) = element.description() else {
246 return item_line;
247 };
248 let description_indent = format!("{indent} ");
249 let description_spelling =
250 self.spell_description(description, &description_indent, &ComponentNumbering);
251 let spelled_description = Self::spelling_or_push_reason(description_spelling, reasons);
252 format!("{item_line}{spelled_description}")
253 }
254
255 fn component_properties(
257 &self,
258 component: &ComponentProperties,
259 position: Option<Position>,
260 child_placement: Option<PlacementHint>,
261 reasons: &mut Vec<UnprintableReason>,
262 ) -> Vec<String> {
263 let mut properties = Vec::new();
264 let style = component.style();
265 if let Some(caption) = component.caption() {
266 let caption_spelling = Self::spell_value(caption, TextField::Caption);
267 let caption_value = Self::spelling_or_push_reason(caption_spelling, reasons);
268 let property = format!("caption={caption_value}");
269 properties.push(property);
270 }
271 if let Some(fill) = style.get(FILL) {
272 let property = format!("color={fill}");
273 properties.push(property);
274 }
275 if let Some(stroke) = style.get(STROKE) {
276 let property = format!("stroke={stroke}");
277 properties.push(property);
278 }
279 if let Some(stroke_dash) = style.get(STROKE_DASH) {
280 let line_spelling = Self::spell_line(stroke_dash);
281 let line = Self::spelling_or_push_reason(line_spelling, reasons);
282 let property = format!("line={line}");
283 properties.push(property);
284 }
285 if let Some(shape) = component.shape() {
286 let shape_keyword: &str = shape.into();
287 let property = format!("shape={shape_keyword}");
288 properties.push(property);
289 }
290 if let Some(height) = component.height() {
291 let property = format!("height={height}");
292 properties.push(property);
293 }
294 if let Some(position) = position {
295 let x = position.x;
296 let y = position.y;
297 let property = format!("position={x},{y}");
298 properties.push(property);
299 }
300 if let Some(child_placement) = child_placement {
301 let layout_keyword: &str = child_placement.into();
302 let property = format!("layout={layout_keyword}");
303 properties.push(property);
304 }
305 if let Some(PlacementHint::Grid { columns }) = child_placement {
306 let property = format!("columns={columns}");
307 properties.push(property);
308 }
309 if let Some(raw_link) = component.link() {
310 let link_spelling = self.spell_link(raw_link);
311 let link_target = Self::spelling_or_push_reason(link_spelling, reasons);
312 let property = format!("link={link_target}");
313 properties.push(property);
314 }
315 if let Some(code) = component.code() {
316 let code_spelling = Self::spell_value(code, TextField::Code);
317 let code_value = Self::spelling_or_push_reason(code_spelling, reasons);
318 let property = format!("code={code_value}");
319 properties.push(property);
320 }
321 for (name, value) in component.metadata() {
322 let is_invalid_name = !ElementPath::is_component_id(name);
323 if is_invalid_name {
324 let invalid_metadata_name = name.clone();
325 let invalid_name = UnprintableReason::InvalidMetadataName(invalid_metadata_name);
326 reasons.push(invalid_name);
327 }
328 let metadata_spelling = Self::spell_value(value, TextField::MetadataValue);
329 let metadata_value = Self::spelling_or_push_reason(metadata_spelling, reasons);
330 let property = format!("meta.{name}={metadata_value}");
331 properties.push(property);
332 }
333 let unmapped_style_keys =
334 Self::find_unmapped_style_keys(style, &SAYABLE_ELEMENT_STYLE_KEYS);
335 reasons.extend(unmapped_style_keys);
336 properties
337 }
338
339 fn spell_link(&self, raw_link: &str) -> Result<String, UnprintableReason> {
341 let elements = self.model.elements();
342 let link = Link::resolve(raw_link, elements);
343 match link {
344 Link::Element(target) => {
345 let canonical_link = target.to_string();
346 let is_canonical = canonical_link == raw_link;
347 if is_canonical {
348 let reference = self.references.spell(&target);
349 return Ok(reference);
350 }
351 let link = raw_link.to_owned();
352 let respelled_link = UnprintableReason::RespelledElementLink { link, target };
353 Err(respelled_link)
354 }
355 Link::Url(url) => {
356 let is_url_shaped = Link::is_url_shaped(url);
357 if is_url_shaped {
358 return Self::spell_value(url, TextField::Link);
359 }
360 let unshaped_url = url.to_owned();
361 let url_without_url_character =
362 UnprintableReason::UrlWithoutUrlCharacter(unshaped_url);
363 Err(url_without_url_character)
364 }
365 }
366 }
367
368 fn write_workflows(&mut self) {
370 self.markdown.push_str("\n## Workflows\n");
371 let model = self.model;
372 for workflow in model.workflows() {
373 self.write_workflow(workflow);
374 }
375 }
376
377 fn write_workflow(&mut self, workflow: &'a Workflow) {
379 let workflow_id = workflow.id();
380 let mut reasons = Vec::new();
381 let heading = Self::workflow_heading(workflow, &mut reasons);
382 let prose = match workflow.description() {
383 Some(prose) => {
384 let prose_spelling = self.spell_prose(prose, workflow);
385 let spelled_prose = Self::spelling_or_push_reason(prose_spelling, &mut reasons);
386 Some(spelled_prose)
387 }
388 None => None,
389 };
390 self.report(reasons, || {
391 let owner_id = workflow_id.clone();
392 UnprintableOwner::Workflow(owner_id)
393 });
394 let mut override_items = String::new();
395 for (path, element_override) in workflow.overrides() {
396 let mut reasons = Vec::new();
397 let item = self.override_item(path, element_override, &mut reasons);
398 override_items.push_str(&item);
399 self.report(reasons, || {
400 let owner_id = workflow_id.clone();
401 let overridden_path = path.clone();
402 UnprintableOwner::Override(owner_id, overridden_path)
403 });
404 }
405 self.write_headed_part(&heading, prose.as_deref(), &override_items);
406 let mut declarations = EdgeDeclarations::of(workflow);
407 for step in workflow.steps() {
408 self.write_step(workflow, step, &mut declarations);
409 }
410 }
411
412 fn workflow_heading(workflow: &Workflow, reasons: &mut Vec<UnprintableReason>) -> String {
414 let workflow_id = workflow.id();
415 let workflow_title = workflow.title();
416 let title_spelling = Self::spell_title(workflow_title);
417 let title = Self::spelling_or_push_reason(title_spelling, reasons);
418 let Some(subtitle) = workflow.subtitle() else {
419 return format!("### {workflow_id}: {title}");
420 };
421 let subtitle_spelling = Self::spell_value(subtitle, TextField::Subtitle);
422 let subtitle_value = Self::spelling_or_push_reason(subtitle_spelling, reasons);
423 format!("### {workflow_id}: {title} (subtitle={subtitle_value})")
424 }
425
426 fn override_item(
428 &self,
429 path: &ElementPath,
430 element_override: &ElementOverride,
431 reasons: &mut Vec<UnprintableReason>,
432 ) -> String {
433 let reference = self.references.spell(path);
434 let overridden_properties = element_override.properties();
435 let position = element_override.position();
436 let child_placement = element_override.child_placement();
437 let properties =
438 self.component_properties(overridden_properties, position, child_placement, reasons);
439 let entries = properties.join(", ");
440 format!("- {reference} ({entries})\n")
441 }
442
443 fn write_step(
445 &mut self,
446 workflow: &'a Workflow,
447 step: &'a Step,
448 declarations: &mut EdgeDeclarations<'a>,
449 ) {
450 let workflow_id = workflow.id();
451 let step_id = step.id();
452 let mut reasons = Vec::new();
453 let step_title = step.title();
454 let title_spelling = Self::spell_title(step_title);
455 let title = Self::spelling_or_push_reason(title_spelling, &mut reasons);
456 let heading = format!("#### {step_id}: {title}");
457 let prose = match step.description() {
458 Some(prose) => {
459 let prose_spelling = self.spell_prose(prose, workflow);
460 let spelled_prose = Self::spelling_or_push_reason(prose_spelling, &mut reasons);
461 Some(spelled_prose)
462 }
463 None => None,
464 };
465 self.report(reasons, || {
466 let owner_workflow_id = workflow_id.clone();
467 let owner_step_id = step_id.clone();
468 UnprintableOwner::Step(owner_workflow_id, owner_step_id)
469 });
470 let mut items = String::new();
471 for item in step.items() {
472 let item_line = match item {
473 StepItem::Element(path) => {
474 let reference = self.references.spell(path);
475 format!("- {reference}\n")
476 }
477 StepItem::Edge(id) => self.edge_item(workflow, id, declarations),
478 };
479 items.push_str(&item_line);
480 }
481 self.write_headed_part(&heading, prose.as_deref(), &items);
482 }
483
484 fn edge_item(
486 &mut self,
487 workflow: &'a Workflow,
488 id: &'a EdgeId,
489 declarations: &mut EdgeDeclarations<'a>,
490 ) -> String {
491 let from_path = id.from();
492 let to_path = id.to();
493 let from_reference = self.references.spell(from_path);
494 let to_reference = self.references.spell(to_path);
495 let edge = workflow.edge(id);
496 let reading_ordinal = declarations.declare(id);
497 let (Some(edge), Some(reading_ordinal)) = (edge, reading_ordinal) else {
498 let ordinal = id.ordinal();
499 return format!("- {from_reference} -> {to_reference}[{ordinal}]\n");
500 };
501 let mut reasons = Vec::new();
502 let ordinal = id.ordinal();
503 let is_renumbered = ordinal != reading_ordinal;
504 if is_renumbered {
505 let renumbered_edge = UnprintableReason::RenumberedEdge {
506 ordinal,
507 reading_ordinal,
508 };
509 reasons.push(renumbered_edge);
510 }
511 let label = match edge.label() {
512 Some(label) => {
513 let label_spelling = Self::spell_label(label, TextField::Label);
514 let label = Self::spelling_or_push_reason(label_spelling, &mut reasons);
515 format!(": {label}")
516 }
517 None => String::new(),
518 };
519 let properties = Self::edge_properties(edge, &mut reasons);
520 let properties_list = Self::spell_properties_list(&properties);
521 let item_line = format!("- {from_reference} -> {to_reference}{label}{properties_list}\n");
522 let spelled_item = match edge.description() {
523 Some(description) => {
524 let description_spelling = self.spell_description(description, " ", workflow);
525 let spelled_description =
526 Self::spelling_or_push_reason(description_spelling, &mut reasons);
527 format!("{item_line}{spelled_description}")
528 }
529 None => item_line,
530 };
531 self.report(reasons, || {
532 let owner_workflow_id = workflow.id().clone();
533 let edge_id = id.clone();
534 UnprintableOwner::Edge(owner_workflow_id, edge_id)
535 });
536 spelled_item
537 }
538
539 fn edge_properties(edge: &Edge, reasons: &mut Vec<UnprintableReason>) -> Vec<String> {
541 let mut properties = Vec::new();
542 let style = edge.style();
543 let is_badge = edge.is_badge();
544 let stroke_dash = style.get(STROKE_DASH);
545 match (is_badge, stroke_dash) {
546 (true, Some(_stroke_dash)) => reasons.push(UnprintableReason::BadgeWithStrokeDash),
547 (true, None) => {
548 let badge: &str = LineStyle::Badge.into();
549 let property = format!("line={badge}");
550 properties.push(property);
551 }
552 (false, Some(stroke_dash)) => {
553 let line_spelling = Self::spell_line(stroke_dash);
554 let line = Self::spelling_or_push_reason(line_spelling, reasons);
555 let property = format!("line={line}");
556 properties.push(property);
557 }
558 (false, None) => {}
559 }
560 if let Some(stroke) = style.get(STROKE) {
561 let property = format!("color={stroke}");
562 properties.push(property);
563 }
564 if let Some(traveler) = edge.traveler() {
565 let traveler_keyword: &str = traveler.into();
566 let property = format!("traveler={traveler_keyword}");
567 properties.push(property);
568 }
569 let unmapped_style_keys = Self::find_unmapped_style_keys(style, &SAYABLE_EDGE_STYLE_KEYS);
570 reasons.extend(unmapped_style_keys);
571 properties
572 }
573
574 fn write_headed_part(&mut self, heading: &str, prose: Option<&str>, items: &str) {
576 let heading_line = format!("\n{heading}\n");
577 self.markdown.push_str(&heading_line);
578 if let Some(prose) = prose {
579 let prose_lines = format!("{prose}\n");
580 self.markdown.push_str(&prose_lines);
581 }
582 let has_items = !items.is_empty();
583 let has_prose_before_items = prose.is_some() && has_items;
584 if has_prose_before_items {
585 self.markdown.push('\n');
586 }
587 self.markdown.push_str(items);
588 }
589
590 fn finish(self) -> Result<String, UnprintableModel> {
592 let is_printable = self.unprintables.is_empty();
593 if is_printable {
594 return Ok(self.markdown);
595 }
596 let unprintables = self.unprintables;
597 let unprintable_model = UnprintableModel { unprintables };
598 Err(unprintable_model)
599 }
600
601 fn report(&mut self, reasons: Vec<UnprintableReason>, owner: impl Fn() -> UnprintableOwner) {
603 let unprintables = reasons.into_iter().map(|reason| {
604 let owner = owner();
605 Unprintable { owner, reason }
606 });
607 self.unprintables.extend(unprintables);
608 }
609
610 fn spelling_or_push_reason<T: Default>(
612 spelling: Result<T, UnprintableReason>,
613 reasons: &mut Vec<UnprintableReason>,
614 ) -> T {
615 spelling.unwrap_or_else(|reason| {
616 reasons.push(reason);
617 T::default()
618 })
619 }
620
621 fn find_unmapped_style_keys(
623 style: &BTreeMap<String, String>,
624 sayable_keys: &[&str],
625 ) -> impl Iterator<Item = UnprintableReason> {
626 let style_keys = style.keys();
627 let unmapped_keys = style_keys.filter(|key| {
628 let is_sayable = sayable_keys.contains(&key.as_str());
629 !is_sayable
630 });
631 unmapped_keys
632 .cloned()
633 .map(UnprintableReason::UnmappedStyleKey)
634 }
635
636 fn spell_line(stroke_dash: &str) -> Result<&'static str, UnprintableReason> {
638 let dash_length = NumberValue::parse(stroke_dash);
639 let line_style = dash_length.and_then(LineStyle::drawing_dash);
640 let Some(line_style) = line_style else {
641 let unsayable_dash = stroke_dash.to_owned();
642 let unsayable_stroke_dash = UnprintableReason::UnsayableStrokeDash(unsayable_dash);
643 return Err(unsayable_stroke_dash);
644 };
645 let line_keyword: &'static str = line_style.into();
646 Ok(line_keyword)
647 }
648
649 fn spell_value(text: &str, field: TextField) -> Result<String, UnprintableReason> {
651 let reads_back_bare = Value::reads_back_bare(text);
652 Self::spell_bare_or_quoted(text, reads_back_bare, field)
653 }
654
655 fn spell_label(text: &str, field: TextField) -> Result<String, UnprintableReason> {
657 let reads_back_bare = ItemLabel::reads_back_bare(text);
658 Self::spell_bare_or_quoted(text, reads_back_bare, field)
659 }
660
661 fn spell_title(text: &str) -> Result<String, UnprintableReason> {
663 let reads_back_as_label = ItemLabel::reads_back_bare(text);
664 let is_ending_with_hash = text.ends_with('#');
665 let reads_back_bare = reads_back_as_label && !is_ending_with_hash;
666 Self::spell_bare_or_quoted(text, reads_back_bare, TextField::Title)
667 }
668
669 fn spell_bare_or_quoted(
671 text: &str,
672 reads_back_bare: bool,
673 field: TextField,
674 ) -> Result<String, UnprintableReason> {
675 let one_line_problem = Self::check_one_line_text(text, field);
676 if let Some(one_line_problem) = one_line_problem {
677 return Err(one_line_problem);
678 }
679 if reads_back_bare {
680 let bare_text = text.to_owned();
681 return Ok(bare_text);
682 }
683 let backslash_escaped = text.replace('\\', "\\\\");
684 let escaped = backslash_escaped.replace('"', "\\\"");
685 let quoted = format!("\"{escaped}\"");
686 Ok(quoted)
687 }
688
689 fn check_one_line_text(text: &str, field: TextField) -> Option<UnprintableReason> {
691 let has_line_break = text.contains(LINE_BREAKS);
692 if has_line_break {
693 let text = text.to_owned();
694 let line_break = UnprintableReason::LineBreak { field, text };
695 return Some(line_break);
696 }
697 let has_comment_opening = text.contains(COMMENT_OPENING);
698 if has_comment_opening {
699 let text = text.to_owned();
700 let comment_opening = UnprintableReason::CommentOpening { field, text };
701 return Some(comment_opening);
702 }
703 None
704 }
705
706 fn spell_description(
708 &self,
709 description: &str,
710 indent: &str,
711 numbering: &(impl EdgeNumbering + ?Sized),
712 ) -> Result<String, UnprintableReason> {
713 let respelled_description = self.respell_targets(description, numbering);
714 let description = respelled_description.as_str();
715 let item_spelling = Self::fenced_description(description, " ");
716 let reads_back = Skeleton::reads_back_as_description(description, &item_spelling);
717 let is_lossy = !reads_back;
718 if is_lossy {
719 return Err(UnprintableReason::UnreadableDescription);
720 }
721 let spelled_description = Self::fenced_description(description, indent);
722 Ok(spelled_description)
723 }
724
725 fn spell_prose(&self, prose: &str, workflow: &Workflow) -> Result<String, UnprintableReason> {
727 let respelled_prose = self.respell_targets(prose, workflow);
728 let reads_back = Skeleton::reads_back_as_prose(&respelled_prose);
729 let is_lossy = !reads_back;
730 if is_lossy {
731 return Err(UnprintableReason::UnreadableProse);
732 }
733 Ok(respelled_prose)
734 }
735
736 fn respell_targets(
738 &self,
739 description: &str,
740 numbering: &(impl EdgeNumbering + ?Sized),
741 ) -> String {
742 TargetLink::respell_all(description, |target_link| {
743 let description_target = DescriptionTarget::parse(target_link.target)?;
744 match description_target {
745 DescriptionTarget::Element(path) => {
746 let reference = self.references.spell(&path);
747 Some(reference)
748 }
749 DescriptionTarget::Edge(id) => {
750 let from_reference = self.references.spell(id.from());
751 let to_reference = self.references.spell(id.to());
752 let ordinal = id.ordinal();
753 let reading_ordinal = numbering.number_unnumbered(id.from(), id.to());
754 let is_numbered_on_reading = reading_ordinal == Some(ordinal);
755 if is_numbered_on_reading {
756 let unnumbered_target = format!("{from_reference}->{to_reference}");
757 return Some(unnumbered_target);
758 }
759 let numbered_target = format!("{from_reference}->{to_reference}[{ordinal}]");
760 Some(numbered_target)
761 }
762 DescriptionTarget::UnnumberedEdge { from, to } => {
763 let from_reference = self.references.spell(&from);
764 let to_reference = self.references.spell(&to);
765 let unnumbered_target = format!("{from_reference}->{to_reference}");
766 Some(unnumbered_target)
767 }
768 DescriptionTarget::Step { .. } => None,
769 }
770 })
771 }
772
773 fn fenced_description(description: &str, indent: &str) -> String {
775 let opening_backtick_runs = description.split('\n').map(|line| {
776 let after_spaces = line.trim_start_matches(' ');
777 let after_backticks = after_spaces.trim_start_matches('`');
778 after_spaces.len() - after_backticks.len()
779 });
780 let longest_opening_run = opening_backtick_runs.max().unwrap_or_default();
781 let fence_length = MIN_FENCE_LENGTH.max(longest_opening_run + 1);
782 let fence = "`".repeat(fence_length);
783 let content = Self::indented_lines(description, indent);
784 format!("{indent}{fence}{DESCRIPTION_INFO}\n{content}{indent}{fence}\n")
785 }
786
787 fn indented_lines(text: &str, indent: &str) -> String {
789 let lines = text.split('\n').map(|line| {
790 let is_empty_line = line.is_empty();
791 if is_empty_line {
792 return "\n".to_owned();
793 }
794 format!("{indent}{line}\n")
795 });
796 lines.collect()
797 }
798
799 fn spell_properties_list(properties: &[String]) -> String {
801 let is_empty = properties.is_empty();
802 if is_empty {
803 return String::new();
804 }
805 let entries = properties.join(", ");
806 format!(" ({entries})")
807 }
808}
809
810impl<'a> EdgeDeclarations<'a> {
811 fn of(workflow: &'a Workflow) -> Self {
813 let mut listings_left: BTreeMap<&'a EdgeId, usize> = BTreeMap::new();
814 let items = workflow.steps().iter().flat_map(Step::items);
815 for item in items {
816 let StepItem::Edge(id) = item else {
817 continue;
818 };
819 let listing_count = listings_left.entry(id).or_default();
820 *listing_count += 1;
821 }
822 Self {
823 declared_edges: BTreeSet::new(),
824 declaration_counts: BTreeMap::new(),
825 listings_left,
826 }
827 }
828
829 fn declare(&mut self, id: &'a EdgeId) -> Option<u32> {
831 let listing_count = self.listings_left.entry(id).or_default();
832 *listing_count = listing_count.saturating_sub(1);
833 let has_later_listing = *listing_count > 0;
834 let is_already_declared = self.declared_edges.contains(id);
835 if is_already_declared {
836 return None;
837 }
838 let pair = (id.from(), id.to());
839 let declaration_count = self.declaration_counts.entry(pair).or_default();
840 let reading_ordinal = *declaration_count;
841 let is_declared_out_of_order = id.ordinal() != reading_ordinal;
842 let is_declaration_deferred = is_declared_out_of_order && has_later_listing;
843 if is_declaration_deferred {
844 return None;
845 }
846 *declaration_count += 1;
847 self.declared_edges.insert(id);
848 Some(reading_ordinal)
849 }
850}
851
852struct ReferenceIndex<'a> {
854 element_counts_by_suffix: HashMap<&'a [String], usize>,
855}
856
857impl<'a> ReferenceIndex<'a> {
858 fn new(model: &'a Model) -> Self {
860 let mut element_counts_by_suffix = HashMap::new();
861 let elements = model.elements();
862 for path in elements.keys() {
863 let segments = path.segments();
864 let depth = segments.len();
865 let suffix_starts = 0..depth;
866 for suffix_start in suffix_starts {
867 let suffix = &segments[suffix_start..];
868 let element_count = element_counts_by_suffix.entry(suffix).or_default();
869 *element_count += 1;
870 }
871 }
872 Self {
873 element_counts_by_suffix,
874 }
875 }
876
877 fn spell(&self, path: &ElementPath) -> String {
879 let segments = path.segments();
880 let depth = segments.len();
881 let suffix_starts = 0..depth;
882 let shortest_first_starts = suffix_starts.rev();
883 let suffixes = shortest_first_starts.map(|suffix_start| &segments[suffix_start..]);
884 let mut unique_suffixes = suffixes.filter(|suffix| {
885 let element_count = self.element_counts_by_suffix.get(suffix);
886 element_count == Some(&1)
887 });
888 let unique_suffix = unique_suffixes.next();
889 let reference_segments = unique_suffix.unwrap_or(segments);
890 reference_segments.join(".")
891 }
892}