Skip to main content

textlane_core/
plugin.rs

1use std::any::{Any, TypeId};
2use std::collections::HashMap;
3use std::sync::Arc;
4
5use crate::models::error::Error;
6use crate::models::file::File;
7use crate::models::nlcst::Node;
8use crate::models::position::Position;
9use crate::models::tree::{Tree, TreeKind};
10use crate::{Allocator, ArenaVec};
11
12/// A pipeline stage: attached once when a processor freezes, where it registers what it does.
13pub trait Plugin: Send + Sync {
14    fn attach(&self, context: &mut Attach) -> Result<(), Error>;
15}
16
17/// Turns input text into a tree.
18pub trait Parser: Send + Sync {
19    fn output(&self) -> TreeKind;
20
21    /// The part of `input` the parser reads and positions count from: all of it by default.
22    fn source<'a>(&self, input: &'a str) -> &'a str {
23        input
24    }
25
26    fn parse<'a>(
27        &self,
28        input: &'a str,
29        file: &mut File,
30        alloc: &'a Allocator,
31    ) -> Result<Tree<'a>, Error>;
32}
33
34/// Changes a tree, or bridges it into another kind of tree.
35pub trait Transform: Send + Sync {
36    fn input(&self) -> TreeKind;
37    fn output(&self) -> TreeKind;
38    fn run<'a>(
39        &self,
40        tree: Tree<'a>,
41        file: &mut File,
42        alloc: &'a Allocator,
43    ) -> Result<Tree<'a>, Error>;
44
45    /// Start the stage: a finished tree, or a job when its work is asynchronous.
46    fn start<'a>(
47        &self,
48        tree: Tree<'a>,
49        file: &mut File,
50        alloc: &'a Allocator,
51    ) -> Result<Step<'a>, Error> {
52        self.run(tree, file, alloc).map(Step::Done)
53    }
54
55    /// The id of the binding function this stage stands for, which takes and returns
56    /// whatever tree reaches it; `None` for a stage the crate runs.
57    fn host(&self) -> Option<u32> {
58        None
59    }
60
61    /// Something that writes, from this stage's input, the HTML a compiler aiming at
62    /// `target` would write from its output, when the stage can skip building that tree.
63    fn html_writer(&self, _target: HtmlTarget) -> Option<Box<dyn HtmlWriter>> {
64        None
65    }
66}
67
68/// What an HTML compiler writes that a stage writing HTML itself must match.
69#[derive(Clone, Copy, Debug, PartialEq)]
70pub struct HtmlTarget {
71    /// Whether raw HTML is written as it is.
72    pub allow_dangerous_html: bool,
73}
74
75/// Writes HTML straight from a tree, or `None` to have the stages run as declared.
76pub trait HtmlWriter: Send + Sync {
77    fn write(&self, tree: &Tree<'_>) -> Option<String>;
78}
79
80/// Deferred work a stage hands back, finished only by the asynchronous API.
81pub type Job<'a> = Box<dyn FnOnce(&mut File) -> Result<Tree<'a>, Error> + 'a>;
82
83/// What starting a stage gives: its tree, or a job that yields it.
84pub enum Step<'a> {
85    Done(Tree<'a>),
86    Job(Job<'a>),
87}
88
89/// Turns a tree into output text, whose kind `extension` names (`html`, `md`).
90pub trait Compiler: Send + Sync {
91    fn input(&self) -> TreeKind;
92    fn extension(&self) -> &'static str;
93    fn compile(&self, tree: &Tree<'_>, file: &mut File) -> Result<String, Error>;
94
95    /// What a stage before this compiler must match to write its HTML in its place.
96    fn html_target(&self) -> Option<HtmlTarget> {
97        None
98    }
99}
100
101/// A natural-language parser, as parse-latin's instance is to mdast-util-to-nlcst and
102/// hast-util-to-nlcst: bridges into nlcst tokenize text with it and run its plugins.
103pub trait NaturalLanguageParser: Send + Sync {
104    /// Split `value` into sentence content: words, white space, punctuation and symbols, each
105    /// spanning its bytes from `offset` on.
106    fn tokenize<'a>(
107        &self,
108        value: &'a str,
109        offset: usize,
110        alloc: &'a Allocator,
111    ) -> ArenaVec<'a, Node<'a>>;
112
113    /// Run the sentence plugins over `content` as one sentence, split it into sentences after
114    /// terminal markers and run the paragraph plugins: a `ParagraphNode`. The sentence and the
115    /// paragraph start at `position`; without one, at their children's.
116    fn tokenize_paragraph<'a>(
117        &self,
118        content: ArenaVec<'a, Node<'a>>,
119        position: Option<Position<'a>>,
120        alloc: &'a Allocator,
121    ) -> Node<'a>;
122
123    /// Split a paragraph into paragraphs at white space holding a line ending and run the
124    /// root plugins: a `RootNode`, at `position` or its children's.
125    fn tokenize_root<'a>(
126        &self,
127        paragraph: Node<'a>,
128        position: Option<Position<'a>>,
129        alloc: &'a Allocator,
130    ) -> Node<'a>;
131}
132
133/// The slot holding the natural-language parser bridges into nlcst read when their stage is built; the
134/// textlane processor seeds it with `nl.parse`'s Latin parser before plugins attach.
135#[derive(Clone, Default)]
136pub struct NaturalLanguageParserSlot {
137    pub parser: Option<Arc<dyn NaturalLanguageParser>>,
138}
139
140/// A JavaScript parser, as acorn is to micromark's MDX extensions: `md.mdx` asks it where
141/// an expression or a block of imports and exports is valid, and keeps only the source.
142pub trait ScriptParser: Send + Sync {
143    /// Parse `source` whole as one expression (JSX included), comments and white space
144    /// around it allowed, and describe it.
145    fn parse_expression(&self, source: &str) -> Result<ScriptExpression, ScriptError>;
146
147    /// Parse `source` as a module and list its top-level statements.
148    fn parse_module(&self, source: &str) -> Result<Vec<ScriptStatement>, ScriptError>;
149}
150
151/// What an expression is, as far as MDX checks it: the byte offset where it ends, before
152/// any comments after it, and, for an object literal (parentheses dropped), the ESTree type
153/// and byte offset of each property.
154#[derive(Clone, Debug, PartialEq, Eq)]
155pub struct ScriptExpression {
156    pub end: usize,
157    pub object: Option<Vec<(String, usize)>>,
158}
159
160/// A top-level statement of a module: its ESTree type, where it starts and, for an
161/// import, the local names it declares.
162#[derive(Clone, Debug, PartialEq, Eq)]
163pub struct ScriptStatement {
164    pub kind: String,
165    pub offset: usize,
166    pub imports: Vec<String>,
167}
168
169/// A syntax error: the byte span of what the parser stopped at, which ends at the end of
170/// the source when more source might still make it valid.
171#[derive(Clone, Debug, PartialEq, Eq)]
172pub struct ScriptError {
173    pub start: usize,
174    pub end: usize,
175    pub reason: String,
176}
177
178/// The slot holding the script parser `md.mdx` validates JavaScript with; the textlane
179/// processor seeds it when built with `mdx`. Without one, expressions end at the brace
180/// that balances their opening one and imports and exports at a blank line.
181#[derive(Clone, Default)]
182pub struct ScriptParserSlot {
183    pub parser: Option<Arc<dyn ScriptParser>>,
184}
185
186type ParserFactory = Box<dyn FnOnce(&Slots) -> Result<Box<dyn Parser>, Error>>;
187type CompilerFactory = Box<dyn FnOnce(&Slots) -> Result<Box<dyn Compiler>, Error>>;
188
189/// What plugins register while a processor freezes.
190#[derive(Default)]
191pub struct Attach {
192    parser: Option<ParserFactory>,
193    compiler: Option<CompilerFactory>,
194    transforms: Vec<Box<dyn Transform>>,
195    slots: Slots,
196}
197
198/// Data plugins share while attaching, one value per type.
199#[derive(Default)]
200pub struct Slots {
201    values: HashMap<TypeId, Box<dyn Any>>,
202}
203
204impl Attach {
205    /// Set the parser, built once every plugin has attached, so it sees all their slots.
206    pub fn set_parser(
207        &mut self,
208        factory: impl FnOnce(&Slots) -> Result<Box<dyn Parser>, Error> + 'static,
209    ) {
210        self.parser = Some(Box::new(factory));
211    }
212
213    /// Set the compiler, built once every plugin has attached.
214    pub fn set_compiler(
215        &mut self,
216        factory: impl FnOnce(&Slots) -> Result<Box<dyn Compiler>, Error> + 'static,
217    ) {
218        self.compiler = Some(Box::new(factory));
219    }
220
221    pub fn add_transform(&mut self, transform: Box<dyn Transform>) {
222        self.transforms.push(transform);
223    }
224
225    pub fn slots_mut(&mut self) -> &mut Slots {
226        &mut self.slots
227    }
228
229    /// Build the parser and compiler and hand back every stage.
230    #[allow(clippy::type_complexity)]
231    pub fn finish(
232        self,
233    ) -> Result<
234        (
235            Option<Box<dyn Parser>>,
236            Vec<Box<dyn Transform>>,
237            Option<Box<dyn Compiler>>,
238        ),
239        Error,
240    > {
241        let parser = self
242            .parser
243            .map(|factory| factory(&self.slots))
244            .transpose()?;
245        let compiler = self
246            .compiler
247            .map(|factory| factory(&self.slots))
248            .transpose()?;
249        Ok((parser, self.transforms, compiler))
250    }
251}
252
253impl Slots {
254    pub fn get<T: 'static>(&self) -> Option<&T> {
255        self.values
256            .get(&TypeId::of::<T>())
257            .and_then(|value| value.downcast_ref())
258    }
259
260    pub fn get_or_default<T: Default + 'static>(&mut self) -> &mut T {
261        let value = self
262            .values
263            .entry(TypeId::of::<T>())
264            .or_insert_with(|| Box::new(T::default()));
265        match value.downcast_mut() {
266            Some(value) => value,
267            None => unreachable!("a slot holds the type it is keyed by"),
268        }
269    }
270}