1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
//! A document as the engine reads it on its own: its frontmatter and its
//! body, parsed in a dialect, with nothing of a host.
use std::sync::LazyLock;
use crate::{
Finding, Locator, RepoPath, Result, Spanned, Yaml,
check::{self, CheckOptions, CheckOutput, DocView, Host},
edit::{self, AnchorEdit},
grammar::Schema,
markdown::{self, Body, Dialect, Segments, split_frontmatter},
yaml,
};
// sokf:begin document
/// One document, parsed.
#[derive(Debug, Clone, PartialEq)]
pub struct Document {
/// The file.
pub path: RepoPath,
/// The text, LF.
pub text: String,
/// The frontmatter, when present and read.
pub frontmatter: Option<Spanned>,
/// Why the frontmatter could not be read, when it could not.
pub problem: Option<Finding>,
/// The body.
pub body: Body,
/// The markers.
pub segments: Segments,
/// The line the body starts on.
pub body_line: usize,
}
// sokf:end document
impl Document {
/// Parse `text`, the document at `path`, its markers as `dialect`
/// spells them. A CRLF text is read as LF.
// sokf:begin document
pub fn parse(path: RepoPath, text: &str, dialect: &Dialect) -> Self {
// sokf:end document
// sokf-implements: MD-3_AC-1
// sokf-implements: CLI-8_AC-1
let text = text.replace("\r\n", "\n");
let split = split_frontmatter(&text);
let (frontmatter, problem) = match split.frontmatter {
// sokf-implements: MD-3_AC-4
None => (None, None),
Some((written, line)) => match yaml::parse(written, line) {
Ok(y) => (Some(y), None),
// YAML that parses and uses what the frontmatter refuses is
// a fault of its own.
// sokf-implements: MD-3_AC-3
// sokf-implements: TL_53
// sokf-implements: TL_123
Err(e) => {
let criterion = if e.is_refused() { "TL_123" } else { "TL_53" };
(
None,
Some(Finding::error(
path.clone(),
Some(line - 1),
criterion,
format!("the frontmatter does not read: {e}"),
)),
)
}
},
};
// sokf-implements: MD-3_AC-2
let segments = markdown::segments(split.body, split.body_line, dialect);
let body = markdown::parse(split.body, split.body_line, &segments, dialect);
let body_line = split.body_line;
Document {
path,
frontmatter,
problem,
body,
segments,
body_line,
text,
}
}
/// The document checked against `schema` at the states its frontmatter
/// holds, each finding ending with `locator`. A frontmatter that does
/// not read is the first finding.
// sokf:begin document
pub fn check(&self, schema: &Schema, host: &dyn Host, locator: Locator) -> CheckOutput {
// sokf:end document
// sokf-implements: MD-3_AC-9
let states = schema.states(self.frontmatter.as_ref());
let mut out = check::check_against(
self.view(),
schema,
&states,
host,
CheckOptions::default(),
locator,
);
if let Some(problem) = &self.problem {
out.findings.insert(0, problem.clone());
}
out
}
/// The text with the repairs of `out` and the anchors its keys need
/// applied, or `None` where they change nothing.
// sokf:begin document
pub fn repair(&self, out: &CheckOutput) -> Result<Option<String>> {
// sokf:end document
// sokf-implements: MD-3_AC-10
let anchors = AnchorEdit {
body: &self.body,
placements: &out.anchors,
text: &self.text,
renames: &[],
};
let edits = edit::line_edits(&self.text, &self.segments, &out.repairs, Some(&anchors));
if edits.is_empty() {
return Ok(None);
}
let repaired = edit::apply(&self.text, &edits)?;
Ok((repaired != self.text).then_some(repaired))
}
/// The number of lines.
pub fn line_count(&self) -> usize {
self.text.lines().count()
}
/// What a check sees of the document. A markdown document always has
/// frontmatter to check: one that is absent, or that is not a map, is
/// checked as an empty map, so each key the schema demands is reported.
/// One that does not read is not checked; its problem is the finding.
// sokf:begin document
pub fn view(&self) -> DocView<'_> {
// sokf:end document
// sokf-implements: TL_54
// sokf-implements: TL_94
// sokf-implements: MD-3_AC-5
// sokf-implements: MD-3_AC-6
// sokf-implements: MD-3_AC-7
static EMPTY: LazyLock<Spanned> = LazyLock::new(|| Spanned::new(Yaml::Map(vec![]), 1));
let frontmatter = match (&self.frontmatter, &self.problem) {
(_, Some(_)) => None,
(Some(f), None) if f.as_map().is_some() => Some(f),
_ => Some(&*EMPTY),
};
DocView {
path: &self.path,
frontmatter,
body: &self.body,
segments: &self.segments,
line_count: self.line_count(),
text: &self.text,
body_line: self.body_line,
}
}
}