pub mod latex;
pub mod markdown;
pub mod org;
#[cfg(feature = "pandoc")]
pub mod pandoc;
pub mod plaintext;
pub mod rst;
pub mod span;
pub use span::{
ByteSpan, CodeSpans, Line, RegionOrigin, SpannedRegion, flush_prose_spanned, iter_lines,
push_prose_line,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Region {
Prose(String),
Structure(String),
BlankLines(String),
Code {
lang: Option<String>,
header: String,
body: String,
footer: String,
},
}
pub trait FormatParser {
fn parse_full(&self, input: &str) -> Vec<SpannedRegion>;
fn parse(&self, input: &str) -> Vec<Region> {
self.parse_full(input)
.into_iter()
.map(|s| s.region)
.collect()
}
}
pub fn source_line_payloads(
input: &str,
format: crate::format::Format,
config: Option<&crate::FormatConfig>,
) -> Vec<Option<String>> {
let spanned = parser_for_format_config(format, config).parse_full(input);
iter_lines(input)
.into_iter()
.map(|line| line_prose_payload(input, line, &spanned))
.collect()
}
fn line_prose_payload(input: &str, line: Line<'_>, spanned: &[SpannedRegion]) -> Option<String> {
let lo = line.start;
let hi = line.start + line.text.len();
let mut out = String::new();
for sr in spanned {
if !matches!(sr.region, Region::Prose(_)) {
continue;
}
let Some(origin) = sr.origin else {
continue;
};
let span = origin.whole();
let start = span.start.max(lo);
let end = span.end.min(hi);
if start < end {
out.push_str(&input[start..end]);
}
}
if out.trim().is_empty() {
None
} else {
Some(out)
}
}
pub fn parser_for_format(format: crate::format::Format) -> Box<dyn FormatParser> {
parser_for_format_config(format, None)
}
pub fn parser_for_format_config(
format: crate::format::Format,
config: Option<&crate::FormatConfig>,
) -> Box<dyn FormatParser> {
use crate::format::Format;
match format {
Format::Org => Box::new(org::OrgParser),
Format::Latex => Box::new(latex::LatexParser::from_config(config)),
Format::Markdown => Box::new(markdown::MarkdownParser),
Format::Rst => Box::new(rst::RstParser),
Format::Plaintext => Box::new(plaintext::PlaintextParser),
}
}
pub fn flush_prose(prose: &mut String, regions: &mut Vec<Region>) {
if !prose.is_empty() {
regions.push(Region::Prose(prose.clone()));
prose.clear();
}
}
pub fn check_pragma(line: &str) -> Option<bool> {
let trimmed = line.trim();
let content = trimmed
.strip_prefix("# ") .or_else(|| trimmed.strip_prefix("% ")) .or_else(|| {
trimmed
.strip_prefix("<!-- ")
.and_then(|s| s.strip_suffix(" -->"))
})
.unwrap_or(trimmed); let content = content.trim();
if content == "snapper:off" {
Some(false)
} else if content == "snapper:on" {
Some(true)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pragma_org_comment() {
assert_eq!(check_pragma("# snapper:off"), Some(false));
assert_eq!(check_pragma("# snapper:on"), Some(true));
}
#[test]
fn pragma_latex_comment() {
assert_eq!(check_pragma("% snapper:off"), Some(false));
assert_eq!(check_pragma("% snapper:on"), Some(true));
}
#[test]
fn pragma_html_comment() {
assert_eq!(check_pragma("<!-- snapper:off -->"), Some(false));
assert_eq!(check_pragma("<!-- snapper:on -->"), Some(true));
}
#[test]
fn pragma_bare() {
assert_eq!(check_pragma("snapper:off"), Some(false));
assert_eq!(check_pragma("snapper:on"), Some(true));
}
#[test]
fn pragma_none() {
assert_eq!(check_pragma("regular text"), None);
assert_eq!(check_pragma("# a comment"), None);
assert_eq!(check_pragma(""), None);
}
}