snapper_fmt/parser/
mod.rs1pub mod latex;
2pub mod markdown;
3pub mod org;
4#[cfg(feature = "pandoc")]
5pub mod pandoc;
6pub mod plaintext;
7pub mod rst;
8pub mod span;
9
10pub use span::{
11 ByteSpan, CodeSpans, Line, RegionOrigin, SpannedRegion, flush_prose_spanned, iter_lines,
12 push_prose_line,
13};
14
15#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum Region {
18 Prose(String),
20 Structure(String),
22 BlankLines(String),
24 Code {
30 lang: Option<String>,
31 header: String,
32 body: String,
33 footer: String,
34 },
35}
36
37pub trait FormatParser {
39 fn parse_full(&self, input: &str) -> Vec<SpannedRegion>;
41
42 fn parse(&self, input: &str) -> Vec<Region> {
44 self.parse_full(input)
45 .into_iter()
46 .map(|s| s.region)
47 .collect()
48 }
49}
50
51pub fn source_line_payloads(
58 input: &str,
59 format: crate::format::Format,
60 config: Option<&crate::FormatConfig>,
61) -> Vec<Option<String>> {
62 let spanned = parser_for_format_config(format, config).parse_full(input);
63 iter_lines(input)
64 .into_iter()
65 .map(|line| line_prose_payload(input, line, &spanned))
66 .collect()
67}
68
69fn line_prose_payload(input: &str, line: Line<'_>, spanned: &[SpannedRegion]) -> Option<String> {
70 let lo = line.start;
71 let hi = line.start + line.text.len();
72 let mut out = String::new();
73 for sr in spanned {
74 if !matches!(sr.region, Region::Prose(_)) {
75 continue;
76 }
77 let Some(origin) = sr.origin else {
78 continue;
79 };
80 let span = origin.whole();
81 let start = span.start.max(lo);
82 let end = span.end.min(hi);
83 if start < end {
84 out.push_str(&input[start..end]);
85 }
86 }
87 if out.trim().is_empty() {
88 None
89 } else {
90 Some(out)
91 }
92}
93
94pub fn parser_for_format(format: crate::format::Format) -> Box<dyn FormatParser> {
96 parser_for_format_config(format, None)
97}
98
99pub fn parser_for_format_config(
101 format: crate::format::Format,
102 config: Option<&crate::FormatConfig>,
103) -> Box<dyn FormatParser> {
104 use crate::format::Format;
105 match format {
106 Format::Org => Box::new(org::OrgParser),
107 Format::Latex => Box::new(latex::LatexParser::from_config(config)),
108 Format::Markdown => Box::new(markdown::MarkdownParser),
109 Format::Rst => Box::new(rst::RstParser),
110 Format::Plaintext => Box::new(plaintext::PlaintextParser),
111 }
112}
113
114pub fn flush_prose(prose: &mut String, regions: &mut Vec<Region>) {
119 if !prose.is_empty() {
120 regions.push(Region::Prose(prose.clone()));
121 prose.clear();
122 }
123}
124
125pub fn check_pragma(line: &str) -> Option<bool> {
128 let trimmed = line.trim();
129 let content = trimmed
131 .strip_prefix("# ") .or_else(|| trimmed.strip_prefix("% ")) .or_else(|| {
134 trimmed
136 .strip_prefix("<!-- ")
137 .and_then(|s| s.strip_suffix(" -->"))
138 })
139 .unwrap_or(trimmed); let content = content.trim();
141 if content == "snapper:off" {
142 Some(false)
143 } else if content == "snapper:on" {
144 Some(true)
145 } else {
146 None
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn pragma_org_comment() {
156 assert_eq!(check_pragma("# snapper:off"), Some(false));
157 assert_eq!(check_pragma("# snapper:on"), Some(true));
158 }
159
160 #[test]
161 fn pragma_latex_comment() {
162 assert_eq!(check_pragma("% snapper:off"), Some(false));
163 assert_eq!(check_pragma("% snapper:on"), Some(true));
164 }
165
166 #[test]
167 fn pragma_html_comment() {
168 assert_eq!(check_pragma("<!-- snapper:off -->"), Some(false));
169 assert_eq!(check_pragma("<!-- snapper:on -->"), Some(true));
170 }
171
172 #[test]
173 fn pragma_bare() {
174 assert_eq!(check_pragma("snapper:off"), Some(false));
175 assert_eq!(check_pragma("snapper:on"), Some(true));
176 }
177
178 #[test]
179 fn pragma_none() {
180 assert_eq!(check_pragma("regular text"), None);
181 assert_eq!(check_pragma("# a comment"), None);
182 assert_eq!(check_pragma(""), None);
183 }
184}