//! Directive splitter for master/spec.md. //! //! Recognises three directives, one per line, no nesting: //! {form-only: json} … {/form-only} //! {form-only: ail} … {/form-only} //! {example: } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Form { Json, Ail, } impl Form { fn parse(name: &str) -> Option
{ match name { "json" => Some(Form::Json), "ail" => Some(Form::Ail), _ => None, } } } #[derive(Debug, Clone, PartialEq, Eq)] pub enum Segment { Prose(String), FormOnly { form: Form, body: String }, Example(String), } /// Split a master/spec.md text into a sequence of segments. /// /// Panics on malformed input (unknown form name, unclosed form-only block). /// The renderer treats these as authoring bugs that should fail the test /// suite rather than silently produce diverging mini-specs. pub fn split(input: &str) -> Vec { let mut out: Vec = Vec::new(); let mut current_prose = String::new(); let mut current_block: Option<(Form, String)> = None; for line in input.split_inclusive('\n') { let trimmed = line.trim_end_matches('\n'); // Inside a form-only block: collect until the closing marker. if let Some((form, body)) = current_block.as_mut() { if trimmed == "{/form-only}" { let body_owned = std::mem::take(body); out.push(Segment::FormOnly { form: *form, body: body_owned }); current_block = None; } else { body.push_str(line); } continue; } // {form-only: } if let Some(rest) = trimmed.strip_prefix("{form-only: ").and_then(|r| r.strip_suffix("}")) { flush_prose(&mut current_prose, &mut out); let form = Form::parse(rest) .unwrap_or_else(|| panic!("unknown form name in directive: {}", rest)); current_block = Some((form, String::new())); continue; } // {example: } if let Some(id) = trimmed.strip_prefix("{example: ").and_then(|r| r.strip_suffix("}")) { flush_prose(&mut current_prose, &mut out); out.push(Segment::Example(id.to_string())); continue; } // Stray closer with no opener. if trimmed == "{/form-only}" { panic!("stray {{/form-only}} without matching opener"); } // Plain prose line. current_prose.push_str(line); } if current_block.is_some() { panic!("unclosed form-only block at end of input"); } flush_prose(&mut current_prose, &mut out); out } fn flush_prose(buf: &mut String, out: &mut Vec) { if !buf.is_empty() { out.push(Segment::Prose(std::mem::take(buf))); } }