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