use textlane_core::{
Allocator, Attach, Compiler, Error, File, HtmlWriter, Parser, Plugin, Step,
Transform, Tree, TreeKind,
};
use crate::actions::allocator::reuse::with_allocator;
use crate::models::host::missing_host;
use crate::models::pipeline::Api;
#[derive(Default)]
pub struct Processor {
plugins: Vec<Box<dyn Plugin>>,
}
pub struct FrozenProcessor {
parser: Option<Box<dyn Parser>>,
transforms: Vec<Box<dyn Transform>>,
compiler: Option<Box<dyn Compiler>>,
writer: Option<(usize, Box<dyn HtmlWriter>)>,
}
pub(crate) enum Advance<'a> {
Done(Tree<'a>),
Host {
hosts: Vec<u32>,
next: usize,
tree: Tree<'a>,
},
}
pub(crate) enum Finish<'a> {
Text(String),
Host {
hosts: Vec<u32>,
next: usize,
tree: Tree<'a>,
},
}
#[derive(Debug)]
pub struct Output {
pub value: String,
pub extension: &'static str,
pub file: File,
}
impl Processor {
pub fn new() -> Self {
Self::default()
}
pub fn with(mut self, plugin: impl Plugin + 'static) -> Self {
self.plugins.push(Box::new(plugin));
self
}
pub(crate) fn with_boxed(mut self, plugin: Box<dyn Plugin>) -> Self {
self.plugins.push(plugin);
self
}
pub fn freeze(self) -> Result<FrozenProcessor, Error> {
self.freeze_with(|_| {}, |_| {})
}
pub(crate) fn freeze_with(
self,
prepare: impl FnOnce(&mut Attach),
collect: impl FnOnce(&mut Attach),
) -> Result<FrozenProcessor, Error> {
let mut context = Attach::default();
seed_slots(&mut context);
prepare(&mut context);
for plugin in &self.plugins {
plugin.attach(&mut context)?;
}
collect(&mut context);
let (parser, transforms, compiler) = context.finish()?;
let mut kind = parser.as_ref().map(|parser| parser.output());
for transform in transforms
.iter()
.filter(|transform| transform.host().is_none())
{
check_kind(kind, transform.input())?;
kind = Some(transform.output());
}
if let Some(compiler) = &compiler {
check_kind(kind, compiler.input())?;
}
let writer = find_writer(&transforms, compiler.as_deref());
Ok(FrozenProcessor {
parser,
transforms,
compiler,
writer,
})
}
}
impl FrozenProcessor {
pub fn process(&self, input: &str) -> Result<Output, Error> {
self.process_file(input, File::new())
}
pub fn process_file(&self, input: &str, mut file: File) -> Result<Output, Error> {
let value = with_allocator(|alloc| {
let tree = self.parse(input, &mut file, alloc)?;
match self.finish(tree, &mut file, alloc, 0, Api::Async)? {
Finish::Text(value) => Ok(value),
Finish::Host { hosts, .. } => {
Err(missing_host(hosts.first().copied().unwrap_or_default()))
}
}
})?;
Ok(Output {
value,
extension: self.extension().unwrap_or_default(),
file,
})
}
pub fn parse<'a>(
&self,
input: &'a str,
file: &mut File,
alloc: &'a Allocator,
) -> Result<Tree<'a>, Error> {
match &self.parser {
Some(parser) => {
let source = parser.source(input);
file.value = Some(source.to_string());
parser.parse(source, file, alloc)
}
None => Err(Error::new("missing-parser", "the processor has no parser")),
}
}
pub fn run<'a>(
&self,
tree: Tree<'a>,
file: &mut File,
alloc: &'a Allocator,
) -> Result<Tree<'a>, Error> {
self.run_all(tree, file, alloc, Api::Async)
}
pub fn run_sync<'a>(
&self,
tree: Tree<'a>,
file: &mut File,
alloc: &'a Allocator,
) -> Result<Tree<'a>, Error> {
self.run_all(tree, file, alloc, Api::Sync)
}
pub(crate) fn advance<'a>(
&self,
tree: Tree<'a>,
file: &mut File,
alloc: &'a Allocator,
from: usize,
api: Api,
) -> Result<Advance<'a>, Error> {
self.advance_until(tree, file, alloc, from, self.transforms.len(), api)
}
pub(crate) fn finish<'a>(
&self,
tree: Tree<'a>,
file: &mut File,
alloc: &'a Allocator,
from: usize,
api: Api,
) -> Result<Finish<'a>, Error> {
let (tree, from) = match &self.writer {
Some((stage, writer)) if from <= *stage => {
match self.advance_until(tree, file, alloc, from, *stage, api)? {
Advance::Done(tree) => match writer.write(&tree) {
Some(text) => return Ok(Finish::Text(text)),
None => (tree, *stage),
},
Advance::Host { hosts, next, tree } => {
return Ok(Finish::Host { hosts, next, tree });
}
}
}
_ => (tree, from),
};
match self.advance(tree, file, alloc, from, api)? {
Advance::Done(tree) => self.stringify(&tree, file).map(Finish::Text),
Advance::Host { hosts, next, tree } => Ok(Finish::Host { hosts, next, tree }),
}
}
fn advance_until<'a>(
&self,
mut tree: Tree<'a>,
file: &mut File,
alloc: &'a Allocator,
from: usize,
until: usize,
api: Api,
) -> Result<Advance<'a>, Error> {
let mut index = from;
while index < until
&& let Some(transform) = self.transforms.get(index)
{
if transform.host().is_some() {
let hosts: Vec<u32> = self.transforms[index..]
.iter()
.map_while(|transform| transform.host())
.collect();
let next = index + hosts.len();
return Ok(Advance::Host { hosts, next, tree });
}
tree = match (transform.start(tree, file, alloc)?, api) {
(Step::Done(tree), _) => tree,
(Step::Job(job), Api::Async) => job(file)?,
(Step::Job(_), Api::Sync) => {
return Err(Error::new("async-in-sync", "a stage finished async"));
}
};
index += 1;
}
Ok(Advance::Done(tree))
}
fn run_all<'a>(
&self,
tree: Tree<'a>,
file: &mut File,
alloc: &'a Allocator,
api: Api,
) -> Result<Tree<'a>, Error> {
match self.advance(tree, file, alloc, 0, api)? {
Advance::Done(tree) => Ok(tree),
Advance::Host { hosts, .. } => {
Err(missing_host(hosts.first().copied().unwrap_or_default()))
}
}
}
pub fn stringify(&self, tree: &Tree<'_>, file: &mut File) -> Result<String, Error> {
match &self.compiler {
Some(compiler) => compiler.compile(tree, file),
None => Err(Error::new(
"missing-compiler",
"the processor has no compiler",
)),
}
}
pub fn source<'a>(&self, input: &'a str) -> &'a str {
match &self.parser {
Some(parser) => parser.source(input),
None => input,
}
}
pub fn has_parser(&self) -> bool {
self.parser.is_some()
}
pub fn has_compiler(&self) -> bool {
self.compiler.is_some()
}
pub fn extension(&self) -> Option<&'static str> {
self.compiler.as_ref().map(|compiler| compiler.extension())
}
pub fn compiler_kind(&self) -> Option<TreeKind> {
self.compiler.as_ref().map(|compiler| compiler.input())
}
pub fn input_kind(&self) -> Option<TreeKind> {
self.transforms
.iter()
.find(|transform| transform.host().is_none())
.map(|transform| transform.input())
.or_else(|| self.compiler.as_ref().map(|compiler| compiler.input()))
}
}
fn seed_slots(context: &mut Attach) {
#[cfg(feature = "nl")]
{
let parser = textlane_nl::LanguageParser::new(textlane_nl::Language::Latin);
context
.slots_mut()
.get_or_default::<textlane_core::NaturalLanguageParserSlot>()
.parser = Some(std::sync::Arc::new(parser));
context
.slots_mut()
.get_or_default::<textlane_core::TypographerSlot>()
.typographer = Some(std::sync::Arc::new(textlane_nl::SmartypantsTypographer));
}
#[cfg(feature = "mdx")]
{
context
.slots_mut()
.get_or_default::<textlane_core::ScriptParserSlot>()
.parser = Some(std::sync::Arc::new(textlane_mdx::OxcScriptParser));
}
#[cfg(not(any(feature = "nl", feature = "mdx")))]
let _ = context;
}
fn find_writer(
transforms: &[Box<dyn Transform>],
compiler: Option<&dyn Compiler>,
) -> Option<(usize, Box<dyn HtmlWriter>)> {
let target = compiler?.html_target()?;
let last = transforms.last()?;
if last.host().is_some() {
return None;
}
let writer = last.html_writer(target)?;
Some((transforms.len() - 1, writer))
}
fn check_kind(actual: Option<TreeKind>, expected: TreeKind) -> Result<(), Error> {
match actual {
Some(actual) if actual != expected => Err(Error::new(
"invalid-stage",
format!("a stage takes {expected:?} but gets {actual:?}"),
)),
_ => Ok(()),
}
}
#[cfg(all(test, feature = "md", feature = "html"))]
#[path = "processor_unit.rs"]
mod unit;