use org_parser::parse_org;
use std::borrow::Cow;
use std::fs::read_to_string;
use std::num::ParseIntError;
use std::ops::Range;
use std::path::Path;
use thiserror::Error;
use org_parser::element::HeadingLevel;
use crate::types::{ExportError, ExporterInner, FileError};
#[derive(Debug)]
enum IncludeBlock<'a> {
Export { backend: Option<&'a str> },
Example,
Src { lang: Option<&'a str> },
}
#[derive(Debug)]
pub(crate) struct InclParams<'a> {
file: &'a Path,
block: Option<IncludeBlock<'a>>,
_only_contents: bool,
lines: Option<Range<usize>>,
_min_level: Option<HeadingLevel>,
}
impl<'a> InclParams<'a> {
fn new(value: &'a str) -> Result<Self, IncludeError> {
let mut params = value.split(" ").peekable();
let provided_path;
let file_chunk = params.next().ok_or(IncludeError::NoFile)?;
if let Some((file_name, _search_opts)) = file_chunk.trim_matches('"').split_once("::") {
provided_path = Path::new(file_name);
eprintln!("Search options are not yet supported");
} else {
provided_path = Path::new(file_chunk);
}
let is_not_kwarg = |x: &&str| !x.starts_with(':');
let block: Option<IncludeBlock> = if let Some(potential_block) = params.next_if(is_not_kwarg) {
Some(match potential_block {
"example" => IncludeBlock::Example,
"export" => {
let backend = if let Some(potential_arg) = params.next_if(is_not_kwarg) {
Some(potential_arg)
} else {
None
};
IncludeBlock::Export { backend }
}
"src" => {
let lang = if let Some(potential_lang) = params.next_if(is_not_kwarg) {
Some(potential_lang)
} else {
None
};
IncludeBlock::Src { lang }
}
_ => Err(IncludeError::UnsupportedBlock(potential_block.into()))?,
})
} else {
None
};
let mut only_contents = false;
let mut lines = None;
let mut min_level = None;
while let Some(kwarg) = params.next() {
match kwarg {
":only-contents" => {
only_contents = if let Some(not_kwarg) = params.next_if(is_not_kwarg) {
not_kwarg != "nil"
} else {
true
};
}
":lines" => {
if let Some(not_kwarg) = params.next_if(is_not_kwarg) {
let hyphen_ind = not_kwarg.find('-').ok_or(IncludeError::InvalidSyntax(
"Lines pattern does not contain '-'".into(),
))?;
let start = if hyphen_ind == 0 {
0
} else {
not_kwarg[..hyphen_ind].parse()?
};
let end = if hyphen_ind == (not_kwarg.len() - 1) {
usize::MAX
} else {
not_kwarg[(hyphen_ind + 1)..].parse()?
};
lines = Some(Range { start, end });
}
}
":minlevel" => {
if let Some(not_kwarg) = params.next_if(is_not_kwarg) {
let temp = not_kwarg.parse::<usize>()?;
min_level = match temp {
1 => Some(HeadingLevel::One),
2 => Some(HeadingLevel::Two),
3 => Some(HeadingLevel::Three),
4 => Some(HeadingLevel::Four),
5 => Some(HeadingLevel::Five),
6 => Some(HeadingLevel::Six),
_ => Err(IncludeError::InvalidMinLevel { received: temp })?,
};
}
}
_ => Err(IncludeError::UnsupportedKwarg(kwarg.into()))?,
}
}
Ok(Self {
file: provided_path,
block,
_only_contents: only_contents,
lines,
_min_level: min_level,
})
}
}
pub(crate) fn include_handle<'a>(
value: &str,
writer: &mut impl ExporterInner<'a>,
) -> core::result::Result<(), IncludeError> {
let ret = InclParams::new(value)?;
let target_path: Cow<Path> = if let Some(v) = writer.config_opts().file_path().as_ref() {
let temp_path = v.parent().unwrap().join(ret.file);
temp_path
.canonicalize()
.map_err(|e| FileError {
context: "".into(),
path: temp_path,
source: e,
})?
.into()
} else {
ret.file.into()
};
let mut out_str = read_to_string(&target_path).map_err(|e| FileError {
context: "".into(),
path: target_path.into(),
source: e,
})?;
if let Some(lines) = ret.lines {
out_str = out_str
.lines()
.skip(lines.start)
.take(lines.end - lines.start)
.collect();
}
let feed_str;
if let Some(block) = ret.block {
match block {
IncludeBlock::Export { backend } => {
if let Some(backend) = backend {
feed_str = format!(
r"#+begin_export {backend}
{out_str}
#+end_export"
);
} else {
feed_str = format!(
r"#+begin_export
{out_str}
#+end_export"
);
}
}
IncludeBlock::Example => {
feed_str = format!(
r"#+begin_example
{out_str}
#+end_example"
);
}
IncludeBlock::Src { lang } => {
if let Some(lang) = lang {
feed_str = format!(
r"#+begin_src {lang}
{out_str}
#+end_src"
);
} else {
feed_str = format!(
r"#+begin_src
{out_str}
#+end_src"
);
}
}
}
} else {
feed_str = out_str;
}
let parsed = parse_org(&feed_str);
writer.export_rec(&parsed.pool.root_id(), &parsed);
Ok(())
}
#[derive(Debug, Error)]
pub enum IncludeError {
#[error("Invalid include syntax: {0}")]
InvalidSyntax(String),
#[error("No file provided")]
NoFile,
#[error("block `{0}` is not one of [export, example, src]")]
UnsupportedBlock(String),
#[error("kwarg `{0}` is not one of [:only-contents, :lines, :minlevel]")]
UnsupportedKwarg(String),
#[error("lines provided are not in base 10: {0}")]
LinesError(#[from] ParseIntError),
#[error("expected a minlevel of 1-6, received: {received}")]
InvalidMinLevel { received: usize },
#[error("minlevel was not a number: {0}")]
NotStringMinlevel(String),
#[error("{0}")]
IoError(#[from] FileError),
#[error("failure while handling file {0}")]
FileExport(#[from] Box<ExportError>),
}