Skip to main content

twig/
language.rs

1//! Languages registered at runtime: a format this library did not compile in,
2//! supplied as a Rust [`Language`] and registered with [`register`].
3//!
4//! A language **reads**: its [`Language::parse`] turns source into the node
5//! table — the JSON `twig convert -o table` prints for a compiled format, one
6//! row per node in pre-order, each naming its parent, with its spans and its
7//! kind's fields. It may **write**: [`Language::print`] turns a node table
8//! (without positions) back into source. It does not author; an [`Editor`]
9//! opens over it with every gesture unsupported.
10//!
11//! Registration runs the load check over the language before it exists —
12//! every sample parses to a table the library accepts, a language that prints
13//! reparses every sample's print to the same tree, and the fidelity probe
14//! measures what a conversion into it loses — and after that the returned
15//! [`Format`] works everywhere a compiled one does: [`Document::parse`],
16//! [`Document::render_html`], [`Document::serialize_to`], the diagnostics.
17//!
18//! [`Editor`]: crate::Editor
19//! [`Document::parse`]: crate::Document::parse
20//! [`Document::render_html`]: crate::Document::render_html
21//! [`Document::serialize_to`]: crate::Document::serialize_to
22
23use std::ffi::{c_void, CStr};
24use std::fmt;
25use std::os::raw::{c_char, c_int};
26use std::panic::{catch_unwind, AssertUnwindSafe};
27
28use crate::{ffi, Error, Format, RuntimeId};
29
30/// What a language says about itself.
31#[derive(Clone, Debug, Default, Eq, PartialEq)]
32pub struct Description {
33    /// A lowercase identifier no format already answers to.
34    pub name: String,
35    /// Dot-less and lowercase, and no format's already.
36    pub extensions: Vec<String>,
37    pub aliases: Vec<String>,
38    /// Whether the language prints — [`Language::print`] is then called.
39    pub write: bool,
40    /// Small documents the load check holds the language to. At least one.
41    pub samples: Vec<String>,
42}
43
44impl Description {
45    /// The `describe` document the C ABI reads.
46    fn to_json(&self) -> String {
47        let mut out = String::from("{\"name\":");
48        json_string(&self.name, &mut out);
49        for (key, list) in [
50            ("extensions", &self.extensions),
51            ("aliases", &self.aliases),
52            ("samples", &self.samples),
53        ] {
54            out.push_str(",\"");
55            out.push_str(key);
56            out.push_str("\":[");
57            for (i, item) in list.iter().enumerate() {
58                if i > 0 {
59                    out.push(',');
60                }
61                json_string(item, &mut out);
62            }
63            out.push(']');
64        }
65        out.push_str(",\"caps\":{\"read\":true,\"write\":");
66        out.push_str(if self.write { "true" } else { "false" });
67        out.push_str("}}");
68        out
69    }
70}
71
72fn json_string(s: &str, out: &mut String) {
73    out.push('"');
74    for c in s.chars() {
75        match c {
76            '"' => out.push_str("\\\""),
77            '\\' => out.push_str("\\\\"),
78            '\n' => out.push_str("\\n"),
79            '\r' => out.push_str("\\r"),
80            '\t' => out.push_str("\\t"),
81            c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
82            c => out.push(c),
83        }
84    }
85    out.push('"');
86}
87
88/// A language supplied at runtime. Called from whichever thread parses or
89/// prints, hence `Send + Sync`; kept for the life of the process, since there
90/// is no unregistration.
91pub trait Language: Send + Sync + 'static {
92    fn describe(&self) -> Description;
93
94    /// `source` to the node table of its parse, as JSON. `row` is the name
95    /// the language registered under. An `Err` is a parse error whose message
96    /// the library reports.
97    fn parse(&self, row: &str, source: &[u8]) -> Result<Vec<u8>, String>;
98
99    /// A node table without positions to source. Called only for a language
100    /// whose description declares `write`.
101    fn print(&self, row: &str, table: &[u8]) -> Result<Vec<u8>, String> {
102        let _ = (row, table);
103        Err("this language does not print".to_owned())
104    }
105}
106
107/// Why [`register`] refused a language.
108#[derive(Clone, Debug, Eq, PartialEq)]
109pub struct RegisterError {
110    /// [`Error::InvalidLanguage`] for a refusal; otherwise what the call
111    /// failed with.
112    pub error: Error,
113    /// The library's reason, naming the field or sample at fault.
114    pub message: String,
115}
116
117impl fmt::Display for RegisterError {
118    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119        if self.message.is_empty() {
120            write!(f, "{}", self.error)
121        } else {
122            write!(f, "{}: {}", self.error, self.message)
123        }
124    }
125}
126
127impl std::error::Error for RegisterError {}
128
129/// Register `language` and return the [`Format`] it answers to in this
130/// process. On refusal nothing is registered and the language is dropped.
131pub fn register<L: Language>(language: L) -> Result<Format, RegisterError> {
132    let description = language.describe();
133    let json = description.to_json();
134    let user_data = Box::into_raw(Box::new(language)) as *mut c_void;
135    let vtable = ffi::TwigLanguageVTable {
136        version: ffi::TWIG_LANGUAGE_VTABLE_VERSION,
137        user_data,
138        description: json.as_ptr(),
139        description_len: json.len(),
140        parse: Some(parse_trampoline::<L>),
141        print: if description.write {
142            Some(print_trampoline::<L>)
143        } else {
144            None
145        },
146        free: Some(free_trampoline),
147    };
148    let mut code: c_int = 0;
149    let mut err = [0 as c_char; 512];
150    let status =
151        unsafe { ffi::twig_language_register(&vtable, &mut code, err.as_mut_ptr(), err.len()) };
152    match Error::from_status(status) {
153        Ok(()) => Ok(Format::Runtime(RuntimeId(code))),
154        Err(error) => {
155            // Refused: the library kept nothing that points at it.
156            drop(unsafe { Box::from_raw(user_data as *mut L) });
157            let message = unsafe { CStr::from_ptr(err.as_ptr()) }
158                .to_string_lossy()
159                .into_owned();
160            Err(RegisterError { error, message })
161        }
162    }
163}
164
165pub(crate) unsafe fn bytes<'a>(ptr: *const u8, len: usize) -> &'a [u8] {
166    if len == 0 || ptr.is_null() {
167        &[]
168    } else {
169        unsafe { std::slice::from_raw_parts(ptr, len) }
170    }
171}
172
173pub(crate) unsafe fn give(bytes: Vec<u8>, out: *mut *mut u8, out_len: *mut usize) {
174    let boxed = bytes.into_boxed_slice();
175    unsafe {
176        *out_len = boxed.len();
177        *out = Box::into_raw(boxed) as *mut u8;
178    }
179}
180
181/// Run one of the language's functions across the C boundary: its answer or
182/// its message out through `out`, and a panic turned into a failure rather
183/// than unwound into the library.
184unsafe fn call(
185    f: impl FnOnce() -> Result<Vec<u8>, String>,
186    out: *mut *mut u8,
187    out_len: *mut usize,
188) -> c_int {
189    match catch_unwind(AssertUnwindSafe(f)) {
190        Ok(Ok(answer)) => {
191            unsafe { give(answer, out, out_len) };
192            0
193        }
194        Ok(Err(message)) => {
195            unsafe { give(message.into_bytes(), out, out_len) };
196            1
197        }
198        Err(_) => {
199            unsafe { give(b"the language panicked".to_vec(), out, out_len) };
200            1
201        }
202    }
203}
204
205unsafe extern "C" fn parse_trampoline<L: Language>(
206    user_data: *mut c_void,
207    row: *const u8,
208    row_len: usize,
209    input: *const u8,
210    input_len: usize,
211    out: *mut *mut u8,
212    out_len: *mut usize,
213) -> c_int {
214    let language = unsafe { &*(user_data as *const L) };
215    let row = std::str::from_utf8(unsafe { bytes(row, row_len) }).unwrap_or("");
216    let input = unsafe { bytes(input, input_len) };
217    unsafe { call(|| language.parse(row, input), out, out_len) }
218}
219
220unsafe extern "C" fn print_trampoline<L: Language>(
221    user_data: *mut c_void,
222    row: *const u8,
223    row_len: usize,
224    input: *const u8,
225    input_len: usize,
226    out: *mut *mut u8,
227    out_len: *mut usize,
228) -> c_int {
229    let language = unsafe { &*(user_data as *const L) };
230    let row = std::str::from_utf8(unsafe { bytes(row, row_len) }).unwrap_or("");
231    let input = unsafe { bytes(input, input_len) };
232    unsafe { call(|| language.print(row, input), out, out_len) }
233}
234
235pub(crate) unsafe extern "C" fn free_trampoline(_: *mut c_void, ptr: *mut u8, len: usize) {
236    if !ptr.is_null() {
237        drop(unsafe { Box::from_raw(std::ptr::slice_from_raw_parts_mut(ptr, len)) });
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244    use crate::{Document, Gesture, Target};
245
246    /// Every non-empty line a paragraph of one `str`; printed back one per
247    /// line. Reads the print table with a scan for `"text":` values, which is
248    /// enough for the tables it prints from.
249    struct Lines(&'static str);
250
251    fn escape(s: &str) -> String {
252        let mut out = String::new();
253        json_string(s, &mut out);
254        out
255    }
256
257    impl Language for Lines {
258        fn describe(&self) -> Description {
259            Description {
260                name: self.0.to_owned(),
261                extensions: vec![format!("{}-ext", self.0)],
262                write: true,
263                samples: vec!["one\ntwo\n".to_owned(), "x\n".to_owned()],
264                ..Description::default()
265            }
266        }
267
268        fn parse(&self, _row: &str, source: &[u8]) -> Result<Vec<u8>, String> {
269            let src = std::str::from_utf8(source).map_err(|_| "not UTF-8".to_owned())?;
270            if src.contains('\0') {
271                return Err("a NUL byte is not a line".to_owned());
272            }
273            let mut rows = vec![format!("{{\"kind\":\"doc\",\"span\":[0,{}]}}", src.len())];
274            let mut start = 0;
275            for line in src.split_inclusive('\n') {
276                let text = line.trim_end_matches('\n');
277                if !text.is_empty() {
278                    let para = rows.len();
279                    let end = start + text.len();
280                    rows.push(format!(
281                        "{{\"kind\":\"para\",\"parent\":0,\"span\":[{start},{end}]}}"
282                    ));
283                    rows.push(format!(
284                        "{{\"kind\":\"str\",\"parent\":{para},\"span\":[{start},{end}],\"text\":{}}}",
285                        escape(text)
286                    ));
287                }
288                start += line.len();
289            }
290            Ok(format!("{{\"nodes\":[{}]}}", rows.join(",")).into_bytes())
291        }
292
293        fn print(&self, _row: &str, table: &[u8]) -> Result<Vec<u8>, String> {
294            let table = std::str::from_utf8(table).map_err(|e| e.to_string())?;
295            let mut out = String::new();
296            let mut rest = table;
297            while let Some(at) = rest.find("\"text\":\"") {
298                rest = &rest[at + 8..];
299                let end = rest.find('"').ok_or("unterminated text")?;
300                out.push_str(&rest[..end]);
301                out.push('\n');
302                rest = &rest[end..];
303            }
304            Ok(out.into_bytes())
305        }
306    }
307
308    #[test]
309    fn a_registered_language_is_a_format_like_any_other() {
310        let format = register(Lines("rust-lines")).expect("register");
311        assert!(matches!(format, Format::Runtime(_)));
312        assert_eq!(format.name(), "rust-lines");
313        assert_eq!(Format::by_name("rust-lines"), Some(format));
314        assert_eq!(Format::by_name("gfm"), Some(Format::Gfm));
315        assert_eq!(Format::Gfm.name(), "gfm");
316        assert_eq!(Target::from(format).as_format(), Some(format));
317
318        let mut doc = Document::parse_str("alpha\nbeta\n", format).expect("parse");
319        assert_eq!(doc.render_html().unwrap(), b"<p>alpha</p>\n<p>beta</p>\n");
320        assert_eq!(
321            doc.serialize_to(Target::Markdown).unwrap(),
322            b"alpha\n\nbeta\n"
323        );
324        assert_eq!(
325            doc.serialize_to(Target::from(format)).unwrap(),
326            b"alpha\nbeta\n"
327        );
328
329        let mut md = Document::parse_str("# T\n\nx\n", Format::Markdown).expect("markdown");
330        assert_eq!(md.serialize_to(Target::from(format)).unwrap(), b"T\nx\n");
331
332        // It reads and writes, and does not author.
333        assert!(!format.supports(Gesture::SetBlock));
334        assert!(!format.supports(Gesture::InsertLink));
335
336        // The language's own refusal is a parse error.
337        assert_eq!(
338            Document::parse_str("a\0b", format).err(),
339            Some(Error::ParseError)
340        );
341    }
342
343    #[test]
344    fn a_refusal_says_why_and_registers_nothing() {
345        struct Taken;
346        impl Language for Taken {
347            fn describe(&self) -> Description {
348                Description {
349                    name: "markdown".into(),
350                    samples: vec!["x".into()],
351                    ..Description::default()
352                }
353            }
354            fn parse(&self, _: &str, _: &[u8]) -> Result<Vec<u8>, String> {
355                Ok(br#"{"nodes":[{"kind":"doc","span":[0,1]}]}"#.to_vec())
356            }
357        }
358        let err = register(Taken).unwrap_err();
359        assert_eq!(err.error, Error::InvalidLanguage);
360        assert!(
361            err.message.contains("already a format's"),
362            "{}",
363            err.message
364        );
365
366        struct Panics;
367        impl Language for Panics {
368            fn describe(&self) -> Description {
369                Description {
370                    name: "rust-panics".into(),
371                    samples: vec!["x".into()],
372                    ..Description::default()
373                }
374            }
375            fn parse(&self, _: &str, _: &[u8]) -> Result<Vec<u8>, String> {
376                panic!("boom")
377            }
378        }
379        let err = register(Panics).unwrap_err();
380        assert!(
381            err.message.contains("the language panicked"),
382            "{}",
383            err.message
384        );
385        assert_eq!(Format::by_name("rust-panics"), None);
386    }
387}