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
62/// Deferred work a stage hands back, finished only by the asynchronous API.
63pub type Job<'a> = Box<dyn FnOnce(&mut File) -> Result<Tree<'a>, Error> + 'a>;
64
65/// What starting a stage gives: its tree, or a job that yields it.
66pub enum Step<'a> {
67    Done(Tree<'a>),
68    Job(Job<'a>),
69}
70
71/// Turns a tree into output text, whose kind `extension` names (`html`, `md`).
72pub trait Compiler: Send + Sync {
73    fn input(&self) -> TreeKind;
74    fn extension(&self) -> &'static str;
75    fn compile(&self, tree: &Tree<'_>, file: &mut File) -> Result<String, Error>;
76}
77
78/// A natural-language parser, as parse-latin's instance is to mdast-util-to-nlcst and
79/// hast-util-to-nlcst: bridges into nlcst tokenize text with it and run its plugins.
80pub trait NaturalLanguageParser: Send + Sync {
81    /// Split `value` into sentence content: words, white space, punctuation and symbols, each
82    /// spanning its bytes from `offset` on.
83    fn tokenize<'a>(
84        &self,
85        value: &'a str,
86        offset: usize,
87        alloc: &'a Allocator,
88    ) -> ArenaVec<'a, Node<'a>>;
89
90    /// Run the sentence plugins over `content` as one sentence, split it into sentences after
91    /// terminal markers and run the paragraph plugins: a `ParagraphNode`. The sentence and the
92    /// paragraph start at `position`; without one, at their children's.
93    fn tokenize_paragraph<'a>(
94        &self,
95        content: ArenaVec<'a, Node<'a>>,
96        position: Option<Position<'a>>,
97        alloc: &'a Allocator,
98    ) -> Node<'a>;
99
100    /// Split a paragraph into paragraphs at white space holding a line ending and run the
101    /// root plugins: a `RootNode`, at `position` or its children's.
102    fn tokenize_root<'a>(
103        &self,
104        paragraph: Node<'a>,
105        position: Option<Position<'a>>,
106        alloc: &'a Allocator,
107    ) -> Node<'a>;
108}
109
110/// The slot holding the natural-language parser bridges into nlcst read when their stage is built; the
111/// textlane processor seeds it with `nl.parse`'s Latin parser before plugins attach.
112#[derive(Clone, Default)]
113pub struct NaturalLanguageParserSlot {
114    pub parser: Option<Arc<dyn NaturalLanguageParser>>,
115}
116
117/// A JavaScript parser, as acorn is to micromark's MDX extensions: `md.mdx` asks it where
118/// an expression or a block of imports and exports is valid, and keeps only the source.
119pub trait ScriptParser: Send + Sync {
120    /// Parse `source` whole as one expression (JSX included), comments and white space
121    /// around it allowed, and describe it.
122    fn parse_expression(&self, source: &str) -> Result<ScriptExpression, ScriptError>;
123
124    /// Parse `source` as a module and list its top-level statements.
125    fn parse_module(&self, source: &str) -> Result<Vec<ScriptStatement>, ScriptError>;
126}
127
128/// What an expression is, as far as MDX checks it: the byte offset where it ends, before
129/// any comments after it, and, for an object literal (parentheses dropped), the ESTree type
130/// and byte offset of each property.
131#[derive(Clone, Debug, PartialEq, Eq)]
132pub struct ScriptExpression {
133    pub end: usize,
134    pub object: Option<Vec<(String, usize)>>,
135}
136
137/// A top-level statement of a module: its ESTree type, where it starts and, for an
138/// import, the local names it declares.
139#[derive(Clone, Debug, PartialEq, Eq)]
140pub struct ScriptStatement {
141    pub kind: String,
142    pub offset: usize,
143    pub imports: Vec<String>,
144}
145
146/// A syntax error: the byte span of what the parser stopped at, which ends at the end of
147/// the source when more source might still make it valid.
148#[derive(Clone, Debug, PartialEq, Eq)]
149pub struct ScriptError {
150    pub start: usize,
151    pub end: usize,
152    pub reason: String,
153}
154
155/// The slot holding the script parser `md.mdx` validates JavaScript with; the textlane
156/// processor seeds it when built with `mdx`. Without one, expressions end at the brace
157/// that balances their opening one and imports and exports at a blank line.
158#[derive(Clone, Default)]
159pub struct ScriptParserSlot {
160    pub parser: Option<Arc<dyn ScriptParser>>,
161}
162
163type ParserFactory = Box<dyn FnOnce(&Slots) -> Result<Box<dyn Parser>, Error>>;
164type CompilerFactory = Box<dyn FnOnce(&Slots) -> Result<Box<dyn Compiler>, Error>>;
165
166/// What plugins register while a processor freezes.
167#[derive(Default)]
168pub struct Attach {
169    parser: Option<ParserFactory>,
170    compiler: Option<CompilerFactory>,
171    transforms: Vec<Box<dyn Transform>>,
172    slots: Slots,
173}
174
175/// Data plugins share while attaching, one value per type.
176#[derive(Default)]
177pub struct Slots {
178    values: HashMap<TypeId, Box<dyn Any>>,
179}
180
181impl Attach {
182    /// Set the parser, built once every plugin has attached, so it sees all their slots.
183    pub fn set_parser(
184        &mut self,
185        factory: impl FnOnce(&Slots) -> Result<Box<dyn Parser>, Error> + 'static,
186    ) {
187        self.parser = Some(Box::new(factory));
188    }
189
190    /// Set the compiler, built once every plugin has attached.
191    pub fn set_compiler(
192        &mut self,
193        factory: impl FnOnce(&Slots) -> Result<Box<dyn Compiler>, Error> + 'static,
194    ) {
195        self.compiler = Some(Box::new(factory));
196    }
197
198    pub fn add_transform(&mut self, transform: Box<dyn Transform>) {
199        self.transforms.push(transform);
200    }
201
202    pub fn slots_mut(&mut self) -> &mut Slots {
203        &mut self.slots
204    }
205
206    /// Build the parser and compiler and hand back every stage.
207    #[allow(clippy::type_complexity)]
208    pub fn finish(
209        self,
210    ) -> Result<
211        (
212            Option<Box<dyn Parser>>,
213            Vec<Box<dyn Transform>>,
214            Option<Box<dyn Compiler>>,
215        ),
216        Error,
217    > {
218        let parser = self
219            .parser
220            .map(|factory| factory(&self.slots))
221            .transpose()?;
222        let compiler = self
223            .compiler
224            .map(|factory| factory(&self.slots))
225            .transpose()?;
226        Ok((parser, self.transforms, compiler))
227    }
228}
229
230impl Slots {
231    pub fn get<T: 'static>(&self) -> Option<&T> {
232        self.values
233            .get(&TypeId::of::<T>())
234            .and_then(|value| value.downcast_ref())
235    }
236
237    pub fn get_or_default<T: Default + 'static>(&mut self) -> &mut T {
238        let value = self
239            .values
240            .entry(TypeId::of::<T>())
241            .or_insert_with(|| Box::new(T::default()));
242        match value.downcast_mut() {
243            Some(value) => value,
244            None => unreachable!("a slot holds the type it is keyed by"),
245        }
246    }
247}