use std::ops::Range;
use serde::Serialize;
use crate::convert::openspec::ext;
use crate::convert::openspec::read::{fence_mask, heading_level};
use crate::convert::openspec::statement::metadata_entry;
use crate::identity::Key;
use crate::ids::{CriterionId, ElementId, FeatureId, RequirementId, SectionId};
use crate::render::Labels;
use crate::statement::Statement;
use crate::tree::{Feature, Requirement, Section};
const MAX_LEVEL: usize = 6;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub(super) enum Mark {
Requirement,
Scenario,
}
pub(super) struct Marked {
pub mark: Mark,
pub line: usize,
pub element: Option<ElementId>,
}
pub(super) struct Written {
pub key: Option<Key>,
pub name: String,
pub line: usize,
pub lines: Range<usize>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(super) enum Unreadable {
Heading,
Metadata,
LineBreak,
Depth,
}
impl Unreadable {
pub const fn as_str(self) -> &'static str {
match self {
Self::Heading => "a body line that is a heading",
Self::Metadata => "a Requirement body line that opens with a bold label and a colon",
Self::LineBreak => "a name or a text statement that holds a line break",
Self::Depth => "a Section deeper than level 6",
}
}
}
pub(super) struct Unread {
pub key: Option<Key>,
pub line: usize,
pub kind: Unreadable,
}
pub(super) struct WrittenSection {
pub key: Option<Key>,
pub line: usize,
pub level: usize,
pub name: String,
}
pub(super) struct Spec {
pub lines: Vec<String>,
pub purpose_line: usize,
pub purpose: String,
pub marked: Vec<Marked>,
pub requirements: Vec<Written>,
pub sections: Vec<WrittenSection>,
pub unread: Vec<Unread>,
}
pub(super) fn write(feature: &Feature, labels: &Labels) -> Spec {
let mut writer = Writer {
labels,
lines: Vec::new(),
marked: Vec::new(),
requirements: Vec::new(),
sections: Vec::new(),
unread: Vec::new(),
};
let key = feature.id.map(FeatureId::key);
let line = writer.heading(1, &format!("{} Specification", feature.name));
writer.named(&feature.name, key, line);
let purpose_line = writer.heading(2, "Purpose");
let purpose = feature
.details
.body
.as_deref()
.or_else(|| ext::purpose(&feature.details.ext))
.unwrap_or_default();
writer.body(purpose, key, line, false);
writer.heading(2, "Requirements");
for requirement in &feature.requirements {
writer.requirement(requirement);
}
for section in &feature.details.sections {
writer.section(section, 2);
}
Spec {
lines: writer.lines,
purpose_line,
purpose: purpose.to_owned(),
marked: writer.marked,
requirements: writer.requirements,
sections: writer.sections,
unread: writer.unread,
}
}
struct Writer<'a> {
labels: &'a Labels,
lines: Vec<String>,
marked: Vec<Marked>,
requirements: Vec<Written>,
sections: Vec<WrittenSection>,
unread: Vec<Unread>,
}
impl Writer<'_> {
fn heading(&mut self, level: usize, title: &str) -> usize {
self.blank();
self.lines
.push(format!("{} {title}", "#".repeat(level.min(MAX_LEVEL))));
let line = self.lines.len();
if level <= 2 {
self.lines.push(String::new());
}
line
}
fn blank(&mut self) {
if self.lines.last().is_some_and(|line| !line.is_empty()) {
self.lines.push(String::new());
}
}
fn text(&mut self, text: &str) {
self.lines.extend(text.lines().map(str::to_owned));
}
fn body(&mut self, body: &str, key: Option<Key>, line: usize, requirement: bool) {
let lines: Vec<String> = body.lines().map(str::to_owned).collect();
for (text, fenced) in lines.iter().zip(fence_mask(&lines)) {
if fenced {
continue;
}
if heading_level(text).is_some() {
self.note(key, line, Unreadable::Heading);
}
if requirement && metadata_entry(text).is_some() {
self.note(key, line, Unreadable::Metadata);
}
}
self.lines.extend(lines);
}
fn named(&mut self, name: &str, key: Option<Key>, line: usize) {
if name.contains('\n') {
self.note(key, line, Unreadable::LineBreak);
}
}
fn note(&mut self, key: Option<Key>, line: usize, kind: Unreadable) {
self.unread.push(Unread { key, line, kind });
}
fn requirement(&mut self, requirement: &Requirement) {
let key = requirement.id.map(RequirementId::key);
let name = self.name(requirement);
let line = self.heading(3, &format!("Requirement: {name}"));
self.named(&name, key, line);
self.mark(Mark::Requirement, line, requirement.id.map(ElementId::from));
let start = self.lines.len();
match &requirement.statement {
Statement::Form(form) => {
self.text(&form.render_with_modal(ext::modal(&requirement.details.ext)));
}
Statement::Text(text) => {
self.named(text, key, line);
self.text(text);
}
}
for (label, value) in ext::metadata(&requirement.details.ext) {
self.lines.push(format!("**{label}**: {value}"));
}
if let Some(body) = &requirement.details.body {
self.blank();
self.body(body, key, line, true);
}
for criterion in &requirement.criteria {
let key = criterion.id.map(CriterionId::key);
let line = self.heading(4, &format!("Scenario: {}", criterion.name));
self.named(&criterion.name, key, line);
self.mark(Mark::Scenario, line, criterion.id.map(ElementId::from));
match &criterion.details.body {
Some(body) => self.body(body, key, line, false),
None => self.lines.push(format!("- **THEN** {}", criterion.name)),
}
}
for section in &requirement.details.sections {
self.section(section, 4);
}
self.requirements.push(Written {
key,
name,
line,
lines: start..self.lines.len(),
});
}
fn name(&self, requirement: &Requirement) -> String {
if let Some(name) = ext::name(&requirement.details.ext) {
return name.to_owned();
}
requirement
.id
.map_or_else(|| requirement.statement.render(), |id| self.labels.name(id))
}
fn section(&mut self, section: &Section, level: usize) {
let key = section.id.map(SectionId::key);
let line = self.heading(level, §ion.name);
self.named(§ion.name, key, line);
if level > MAX_LEVEL {
self.note(key, line, Unreadable::Depth);
}
self.sections.push(WrittenSection {
key,
line,
level: level.min(MAX_LEVEL),
name: section.name.clone(),
});
self.body(§ion.body, key, line, false);
for child in §ion.sections {
self.section(child, level + 1);
}
}
fn mark(&mut self, mark: Mark, line: usize, element: Option<ElementId>) {
self.marked.push(Marked {
mark,
line,
element,
});
}
}