summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2019-05-24 12:24:10 +0200
committerLaurenz <laurmaedje@gmail.com>2019-05-24 12:24:10 +0200
commitb3734bbc046fe7b14cff54e2dae7014a71014777 (patch)
treed80e97d9f0694cb7e7f61869a1085e64e791a5b8 /src
parente3215fa3b92574e2087c28b1d494d397e6819236 (diff)
Restructure engine into modular layouter 🍂
Diffstat (limited to 'src')
-rw-r--r--src/bin/main.rs13
-rw-r--r--src/doc.rs2
-rw-r--r--src/engine/loader.rs179
-rw-r--r--src/engine/mod.rs303
-rw-r--r--src/export/pdf.rs2
-rw-r--r--src/font.rs238
-rw-r--r--src/func.rs12
-rw-r--r--src/layout/mod.rs118
-rw-r--r--src/layout/size.rs (renamed from src/engine/size.rs)0
-rw-r--r--src/lib.rs116
-rw-r--r--src/parsing.rs58
11 files changed, 435 insertions, 606 deletions
diff --git a/src/bin/main.rs b/src/bin/main.rs
index b548214a..9e1403b2 100644
--- a/src/bin/main.rs
+++ b/src/bin/main.rs
@@ -5,7 +5,7 @@ use std::process;
use std::io::Read;
use std::path::{Path, PathBuf};
-use typeset::Compiler;
+use typeset::Typesetter;
use typeset::{font::FileSystemFontProvider, font_info};
use typeset::export::pdf::PdfExporter;
@@ -33,10 +33,10 @@ fn run() -> Result<(), Box<Error>> {
let mut src = String::new();
file.read_to_string(&mut src).map_err(|_| "failed to read from source file")?;
- // Create a compiler with a font provider that provides three fonts
+ // Create a typesetter with a font provider that provides three fonts
// (two sans-serif fonts and a fallback for the emoji).
- let mut compiler = Compiler::new();
- compiler.add_font_provider(FileSystemFontProvider::new("fonts", vec![
+ let mut typesetter = Typesetter::new();
+ typesetter.add_font_provider(FileSystemFontProvider::new("fonts", vec![
("NotoSans-Regular.ttf", font_info!(["NotoSans", "Noto", SansSerif])),
("NotoSans-Italic.ttf", font_info!(["NotoSans", "Noto", SansSerif], italic)),
("NotoSans-Bold.ttf", font_info!(["NotoSans", "Noto", SansSerif], bold)),
@@ -45,9 +45,8 @@ fn run() -> Result<(), Box<Error>> {
("NotoEmoji-Regular.ttf", font_info!(["NotoEmoji", "Noto", SansSerif, Serif, Monospace])),
]));
- // Compile the source code with the compiler.
- let document = compiler.compile(&src)?;
-
+ // Typeset the source code.
+ let document = typesetter.typeset(&src)?;
// Export the document into a PDF file.
let exporter = PdfExporter::new();
diff --git a/src/doc.rs b/src/doc.rs
index 83a3b300..51583bdc 100644
--- a/src/doc.rs
+++ b/src/doc.rs
@@ -1,7 +1,7 @@
//! Representation of typesetted documents.
use crate::font::Font;
-use crate::engine::Size;
+use crate::layout::Size;
/// A complete typesetted document, which can be exported.
diff --git a/src/engine/loader.rs b/src/engine/loader.rs
deleted file mode 100644
index bae5f51a..00000000
--- a/src/engine/loader.rs
+++ /dev/null
@@ -1,179 +0,0 @@
-//! Loading of fonts by queries.
-
-use std::fmt::{self, Debug, Formatter};
-use std::cell::{RefCell, Ref};
-use std::collections::HashMap;
-use crate::font::{Font, FontProvider, FontFamily, FontInfo};
-
-
-/// Serves matching fonts given a query.
-pub struct FontLoader<'p> {
- /// The font providers.
- providers: &'p [Box<dyn FontProvider + 'p>],
- /// All available fonts indexed by provider.
- provider_fonts: Vec<&'p [FontInfo]>,
- /// The internal state.
- state: RefCell<FontLoaderState<'p>>,
-}
-
-/// Internal state of the font loader (wrapped in a RefCell).
-struct FontLoaderState<'p> {
- /// The loaded fonts along with their external indices.
- fonts: Vec<(Option<usize>, Font)>,
- /// Allows to retrieve cached results for queries.
- query_cache: HashMap<FontQuery<'p>, usize>,
- /// Allows to lookup fonts by their infos.
- info_cache: HashMap<&'p FontInfo, usize>,
- /// Indexed by outside and indices maps to internal indices.
- inner_index: Vec<usize>,
-}
-
-impl<'p> FontLoader<'p> {
- /// Create a new font loader.
- pub fn new(providers: &'p [Box<dyn FontProvider + 'p>]) -> FontLoader {
- let provider_fonts = providers.iter()
- .map(|prov| prov.available()).collect();
-
- FontLoader {
- providers,
- provider_fonts,
- state: RefCell::new(FontLoaderState {
- query_cache: HashMap::new(),
- info_cache: HashMap::new(),
- inner_index: vec![],
- fonts: vec![],
- }),
- }
- }
-
- /// Return the best matching font and it's index (if there is any) given the query.
- pub fn get(&self, query: FontQuery<'p>) -> Option<(usize, Ref<Font>)> {
- // Check if we had the exact same query before.
- let state = self.state.borrow();
- if let Some(&index) = state.query_cache.get(&query) {
- // That this is the query cache means it must has an index as we've served it before.
- let extern_index = state.fonts[index].0.unwrap();
- let font = Ref::map(state, |s| &s.fonts[index].1);
-
- return Some((extern_index, font));
- }
- drop(state);
-
- // Go over all font infos from all font providers that match the query.
- for family in query.families {
- for (provider, infos) in self.providers.iter().zip(&self.provider_fonts) {
- for info in infos.iter() {
- // Check whether this info matches the query.
- if Self::matches(query, family, info) {
- let mut state = self.state.borrow_mut();
-
- // Check if we have already loaded this font before.
- // Otherwise we'll fetch the font from the provider.
- let index = if let Some(&index) = state.info_cache.get(info) {
- index
- } else if let Some(mut source) = provider.get(info) {
- // Read the font program into a vec.
- let mut program = Vec::new();
- source.read_to_end(&mut program).ok()?;
-
- // Create a font from it.
- let font = Font::new(program).ok()?;
-
- // Insert it into the storage.
- let index = state.fonts.len();
- state.info_cache.insert(info, index);
- state.fonts.push((None, font));
-
- index
- } else {
- continue;
- };
-
- // Check whether this font has the character we need.
- let has_char = state.fonts[index].1.mapping.contains_key(&query.character);
- if has_char {
- // We can take this font, so we store the query.
- state.query_cache.insert(query, index);
-
- // Now we have to find out the external index of it, or assign a new
- // one if it has not already one.
- let maybe_extern_index = state.fonts[index].0;
- let extern_index = maybe_extern_index.unwrap_or_else(|| {
- // We have to assign an external index before serving.
- let extern_index = state.inner_index.len();
- state.inner_index.push(index);
- state.fonts[index].0 = Some(extern_index);
- extern_index
- });
-
- // Release the mutable borrow and borrow immutably.
- drop(state);
- let font = Ref::map(self.state.borrow(), |s| &s.fonts[index].1);
-
- // Finally we can return it.
- return Some((extern_index, font));
- }
- }
- }
- }
- }
-
- None
- }
-
- /// Return a loaded font at an index. Panics if the index is out of bounds.
- pub fn get_with_index(&self, index: usize) -> Ref<Font> {
- let state = self.state.borrow();
- let internal = state.inner_index[index];
- Ref::map(state, |s| &s.fonts[internal].1)
- }
-
- /// Return the list of fonts.
- pub fn into_fonts(self) -> Vec<Font> {
- // Sort the fonts by external key so that they are in the correct order.
- let mut fonts = self.state.into_inner().fonts;
- fonts.sort_by_key(|&(maybe_index, _)| match maybe_index {
- Some(index) => index as isize,
- None => -1,
- });
-
- // Remove the fonts that are not used from the outside
- fonts.into_iter().filter_map(|(maybe_index, font)| {
- maybe_index.map(|_| font)
- }).collect()
- }
-
- /// Check whether the query and the current family match the info.
- fn matches(query: FontQuery, family: &FontFamily, info: &FontInfo) -> bool {
- info.families.contains(family)
- && info.italic == query.italic && info.bold == query.bold
- }
-}
-
-impl Debug for FontLoader<'_> {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- let state = self.state.borrow();
- f.debug_struct("FontLoader")
- .field("providers", &self.providers.len())
- .field("provider_fonts", &self.provider_fonts)
- .field("fonts", &state.fonts)
- .field("query_cache", &state.query_cache)
- .field("info_cache", &state.info_cache)
- .field("inner_index", &state.inner_index)
- .finish()
- }
-}
-
-/// A query for a font with specific properties.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub struct FontQuery<'p> {
- /// A fallback list of font families to accept. The first family in this list, that also
- /// satisfies the other conditions, shall be returned.
- pub families: &'p [FontFamily],
- /// Whether the font shall be in italics.
- pub italic: bool,
- /// Whether the font shall be in boldface.
- pub bold: bool,
- /// Which character we need.
- pub character: char,
-}
diff --git a/src/engine/mod.rs b/src/engine/mod.rs
deleted file mode 100644
index a396ed3a..00000000
--- a/src/engine/mod.rs
+++ /dev/null
@@ -1,303 +0,0 @@
-//! Core typesetting engine.
-
-use std::cell::Ref;
-use std::mem::swap;
-
-use smallvec::SmallVec;
-
-use crate::doc::{Document, Page, Text, TextCommand};
-use crate::font::{Font, FontFamily, FontProvider, FontError};
-use crate::syntax::{SyntaxTree, Node};
-use loader::{FontLoader, FontQuery};
-
-mod size;
-mod loader;
-pub use size::Size;
-
-
-/// Typeset a parsed syntax tree.
-pub fn typeset<'p>(tree: &SyntaxTree, style: &Style, font_providers: &[Box<dyn FontProvider + 'p>])
- -> TypesetResult<Document> {
- Engine::new(tree, style, font_providers).typeset()
-}
-
-
-/// The core typesetting engine, transforming an abstract syntax tree into a document.
-struct Engine<'a> {
- // Input
- tree: &'a SyntaxTree,
- style: &'a Style,
-
- // Internal
- font_loader: FontLoader<'a>,
-
- // Output
- text_commands: Vec<TextCommand>,
-
- // Intermediates
- active_font: usize,
- current_text: String,
- current_line_width: Size,
- current_max_vertical_move: Size,
- bold: bool,
- italic: bool,
-}
-
-impl<'a> Engine<'a> {
- /// Create a new generator from a syntax tree.
- fn new(
- tree: &'a SyntaxTree,
- style: &'a Style,
- font_providers: &'a [Box<dyn FontProvider + 'a>]
- ) -> Engine<'a> {
- Engine {
- tree,
- style,
- font_loader: FontLoader::new(font_providers),
- text_commands: vec![],
- active_font: std::usize::MAX,
- current_text: String::new(),
- current_line_width: Size::zero(),
- current_max_vertical_move: Size::zero(),
- italic: false,
- bold: false,
- }
- }
-
- /// Generate the abstract document.
- fn typeset(mut self) -> TypesetResult<Document> {
- // Start by moving to a suitable position.
- self.move_start();
-
- // Iterate through the documents nodes.
- for node in &self.tree.nodes {
- match node {
- Node::Text(text) => self.write_word(text)?,
- Node::Space => self.write_space()?,
- Node::Newline => {
- self.write_buffered_text();
- self.move_newline(self.style.paragraph_spacing);
- },
-
- Node::ToggleItalics => self.italic = !self.italic,
- Node::ToggleBold => self.bold = !self.bold,
-
- Node::ToggleMath => unimplemented!(),
- Node::Func(_) => unimplemented!(),
- }
- }
-
- // Flush the text buffer.
- self.write_buffered_text();
-
- // Create a document with one page from the contents.
- Ok(Document {
- pages: vec![Page {
- width: self.style.width,
- height: self.style.height,
- text: vec![Text {
- commands: self.text_commands,
- }],
- }],
- fonts: self.font_loader.into_fonts(),
- })
- }
-
- /// Write a word.
- fn write_word(&mut self, word: &str) -> TypesetResult<()> {
- // Contains pairs of (characters, font_index, char_width).
- let mut chars_with_widths = SmallVec::<[(char, usize, Size); 12]>::new();
-
- // Find out which font to use for each character in the word and meanwhile
- // calculate the width of the word.
- let mut word_width = Size::zero();
- for c in word.chars() {
- let (index, font) = self.get_font_for(c)?;
- let width = self.char_width(c, &font);
- word_width += width;
- chars_with_widths.push((c, index, width));
- }
-
- // If this would overflow, we move to a new line and finally write the previous one.
- if self.would_overflow(word_width) {
- self.write_buffered_text();
- self.move_newline(1.0);
- }
-
- // Finally write the word.
- for (c, index, width) in chars_with_widths {
- if index != self.active_font {
- // If we will change the font, first write the remaining things.
- self.write_buffered_text();
- self.set_font(index);
- }
-
- self.current_text.push(c);
- self.current_line_width += width;
- }
-
- Ok(())
- }
-
- /// Write the space character: `' '`.
- fn write_space(&mut self) -> TypesetResult<()> {
- let space_width = self.char_width(' ', &self.get_font_for(' ')?.1);
- if !self.would_overflow(space_width) && self.current_line_width > Size::zero() {
- self.write_word(" ")?;
- }
-
- Ok(())
- }
-
- /// Write a text command with the buffered text.
- fn write_buffered_text(&mut self) {
- if !self.current_text.is_empty() {
- let mut current_text = String::new();
- swap(&mut self.current_text, &mut current_text);
- self.text_commands.push(TextCommand::Text(current_text));
- }
- }
-
- /// Move to the starting position defined by the style.
- fn move_start(&mut self) {
- // Move cursor to top-left position
- self.text_commands.push(TextCommand::Move(
- self.style.margin_left,
- self.style.height - self.style.margin_top
- ));
- }
-
- /// Move to a new line.
- fn move_newline(&mut self, factor: f32) {
- if self.active_font == std::usize::MAX {
- return;
- }
-
- let vertical_move = if self.current_max_vertical_move == Size::zero() {
- // If max vertical move is still zero, the line is empty and we take the
- // font size from the previous line.
- self.style.font_size
- * self.style.line_spacing
- * self.get_font_at(self.active_font).metrics.ascender
- * factor
- } else {
- self.current_max_vertical_move
- };
-
- self.text_commands.push(TextCommand::Move(Size::zero(), -vertical_move));
- self.current_max_vertical_move = Size::zero();
- self.current_line_width = Size::zero();
- }
-
- /// Set the current font.
- fn set_font(&mut self, index: usize) {
- self.text_commands.push(TextCommand::SetFont(index, self.style.font_size));
- self.active_font = index;
- }
-
- /// Whether the current line plus the extra `width` would overflow the line.
- fn would_overflow(&self, width: Size) -> bool {
- let max_width = self.style.width
- - self.style.margin_left - self.style.margin_right;
- self.current_line_width + width > max_width
- }
-
- /// Load a font that has the character we need.
- fn get_font_for(&self, character: char) -> TypesetResult<(usize, Ref<Font>)> {
- self.font_loader.get(FontQuery {
- families: &self.style.font_families,
- italic: self.italic,
- bold: self.bold,
- character,
- }).ok_or_else(|| TypesetError::MissingFont)
- }
-
- /// Load a font at an index.
- fn get_font_at(&self, index: usize) -> Ref<Font> {
- self.font_loader.get_with_index(index)
- }
-
- /// The width of a char in a specific font.
- fn char_width(&self, character: char, font: &Font) -> Size {
- font.widths[font.map(character) as usize] * self.style.font_size
- }
-}
-
-/// The context for typesetting a function.
-#[derive(Debug)]
-pub struct TypesetContext {}
-
-/// Default styles for typesetting.
-#[derive(Debug, Clone, PartialEq)]
-pub struct Style {
- /// The width of the paper.
- pub width: Size,
- /// The height of the paper.
- pub height: Size,
-
- /// The left margin of the paper.
- pub margin_left: Size,
- /// The top margin of the paper.
- pub margin_top: Size,
- /// The right margin of the paper.
- pub margin_right: Size,
- /// The bottom margin of the paper.
- pub margin_bottom: Size,
-
- /// A fallback list of font families to use.
- pub font_families: Vec<FontFamily>,
- /// The font size.
- pub font_size: f32,
- /// The line spacing (as a multiple of the font size).
- pub line_spacing: f32,
- /// The spacing for paragraphs (as a multiple of the line spacing).
- pub paragraph_spacing: f32,
-}
-
-impl Default for Style {
- fn default() -> Style {
- use FontFamily::*;
- Style {
- // A4 paper.
- width: Size::from_mm(210.0),
- height: Size::from_mm(297.0),
-
- // Margins. A bit more on top and bottom.
- margin_left: Size::from_cm(3.0),
- margin_top: Size::from_cm(3.0),
- margin_right: Size::from_cm(3.0),
- margin_bottom: Size::from_cm(3.0),
-
- // Default font family, font size and line spacing.
- font_families: vec![SansSerif, Serif, Monospace],
- font_size: 11.0,
- line_spacing: 1.25,
- paragraph_spacing: 1.5,
- }
- }
-}
-
-/// The error type for typesetting.
-pub enum TypesetError {
- /// There was no suitable font.
- MissingFont,
- /// An error occured while gathering font data.
- Font(FontError),
-}
-
-/// The result type for typesetting.
-pub type TypesetResult<T> = Result<T, TypesetError>;
-
-error_type! {
- err: TypesetError,
- show: f => match err {
- TypesetError::MissingFont => write!(f, "missing font"),
- TypesetError::Font(err) => write!(f, "font error: {}", err),
- },
- source: match err {
- TypesetError::Font(err) => Some(err),
- _ => None,
- },
- from: (std::io::Error, TypesetError::Font(FontError::Io(err))),
- from: (FontError, TypesetError::Font(err)),
-}
diff --git a/src/export/pdf.rs b/src/export/pdf.rs
index ee7ce1ef..55e00abb 100644
--- a/src/export/pdf.rs
+++ b/src/export/pdf.rs
@@ -10,7 +10,7 @@ use pdf::font::{GlyphUnit, CMap, CMapEncoding, WidthRecord, FontStream};
use crate::doc::{Document, Text as DocText, TextCommand};
use crate::font::{Font, FontError};
-use crate::engine::Size;
+use crate::layout::Size;
/// Exports documents into _PDFs_.
diff --git a/src/font.rs b/src/font.rs
index 19b09b31..17331234 100644
--- a/src/font.rs
+++ b/src/font.rs
@@ -1,16 +1,18 @@
//! Font loading and transforming.
//!
//! # Font handling
-//! To do the typesetting, the compiler needs font data. To be highly portable the compiler assumes
-//! nothing about the environment. To still work with fonts, the consumer of this library has to
-//! add _font providers_ to their compiler instance. These can be queried for font data given
-//! flexible font filters specifying required font families and styles. A font provider is a type
-//! implementing the [`FontProvider`](crate::font::FontProvider) trait.
+//! To do the typesetting, the typesetting engine needs font data. To be highly portable the engine
+//! itself assumes nothing about the environment. To still work with fonts, the consumer of this
+//! library has to add _font providers_ to their typesetting instance. These can be queried for font
+//! data given flexible font filters specifying required font families and styles. A font provider
+//! is a type implementing the [`FontProvider`](crate::font::FontProvider) trait.
//!
-//! There is one [included font provider](crate::font::FileSystemFontProvider) that serves
-//! fonts from a folder on the file system.
+//! There is one [included font provider](crate::font::FileSystemFontProvider) that serves fonts
+//! from a folder on the file system.
+use std::cell::{RefCell, Ref};
use std::collections::HashMap;
+use std::fmt::{self, Debug, Formatter};
use std::fs::File;
use std::io::{self, Cursor, Read, Seek, SeekFrom, BufReader};
use std::path::PathBuf;
@@ -20,7 +22,7 @@ use opentype::{Error as OpentypeError, OpenTypeReader, Outlines, TableRecord, Ta
use opentype::tables::{Header, Name, CharMap, MaximumProfile, HorizontalMetrics, Post, OS2};
use opentype::global::{MacStyleFlags, NameEntry};
-use crate::engine::Size;
+use crate::layout::Size;
/// A loaded font, containing relevant information for typesetting.
@@ -174,25 +176,6 @@ pub struct FontMetrics {
pub weight_class: u16,
}
-/// A type that provides fonts.
-pub trait FontProvider {
- /// Returns the font with the given info if this provider has it.
- fn get(&self, info: &FontInfo) -> Option<Box<dyn FontData>>;
-
- /// The available fonts this provider can serve. While these should generally be retrievable
- /// through the `get` method, it is not guaranteed that a font info that is contained here
- /// yields a `Some` value when passed into `get`.
- fn available<'a>(&'a self) -> &'a [FontInfo];
-}
-
-/// A wrapper trait around `Read + Seek`.
-///
-/// This type is needed because currently you can't make a trait object
-/// with two traits, like `Box<dyn Read + Seek>`.
-/// Automatically implemented for all types that are [`Read`] and [`Seek`].
-pub trait FontData: Read + Seek {}
-impl<T> FontData for T where T: Read + Seek {}
-
/// Describes a font.
///
/// Can be constructed conveniently with the [`font_info`] macro.
@@ -285,6 +268,25 @@ pub enum FontFamily {
Named(String),
}
+/// A type that provides fonts.
+pub trait FontProvider {
+ /// Returns the font with the given info if this provider has it.
+ fn get(&self, info: &FontInfo) -> Option<Box<dyn FontData>>;
+
+ /// The available fonts this provider can serve. While these should generally be retrievable
+ /// through the `get` method, it is not guaranteed that a font info that is contained here
+ /// yields a `Some` value when passed into `get`.
+ fn available<'a>(&'a self) -> &'a [FontInfo];
+}
+
+/// A wrapper trait around `Read + Seek`.
+///
+/// This type is needed because currently you can't make a trait object
+/// with two traits, like `Box<dyn Read + Seek>`.
+/// Automatically implemented for all types that are [`Read`] and [`Seek`].
+pub trait FontData: Read + Seek {}
+impl<T> FontData for T where T: Read + Seek {}
+
/// A font provider serving fonts from a folder on the local file system.
pub struct FileSystemFontProvider {
base: PathBuf,
@@ -349,10 +351,182 @@ impl FontProvider for FileSystemFontProvider {
}
}
-struct Subsetter<'d> {
+/// Serves matching fonts given a query.
+pub struct FontLoader<'p> {
+ /// The font providers.
+ providers: Vec<&'p (dyn FontProvider + 'p)>,
+ /// All available fonts indexed by provider.
+ provider_fonts: Vec<&'p [FontInfo]>,
+ /// The internal state.
+ state: RefCell<FontLoaderState<'p>>,
+}
+
+/// Internal state of the font loader (wrapped in a RefCell).
+struct FontLoaderState<'p> {
+ /// The loaded fonts along with their external indices.
+ fonts: Vec<(Option<usize>, Font)>,
+ /// Allows to retrieve cached results for queries.
+ query_cache: HashMap<FontQuery<'p>, usize>,
+ /// Allows to lookup fonts by their infos.
+ info_cache: HashMap<&'p FontInfo, usize>,
+ /// Indexed by outside and indices maps to internal indices.
+ inner_index: Vec<usize>,
+}
+
+impl<'p> FontLoader<'p> {
+ /// Create a new font loader.
+ pub fn new<P: 'p>(providers: &'p [P]) -> FontLoader<'p> where P: AsRef<dyn FontProvider + 'p> {
+ let providers: Vec<_> = providers.iter().map(|p| p.as_ref()).collect();
+ let provider_fonts = providers.iter().map(|prov| prov.available()).collect();
+
+ FontLoader {
+ providers,
+ provider_fonts,
+ state: RefCell::new(FontLoaderState {
+ query_cache: HashMap::new(),
+ info_cache: HashMap::new(),
+ inner_index: vec![],
+ fonts: vec![],
+ }),
+ }
+ }
+
+ /// Return the best matching font and it's index (if there is any) given the query.
+ pub fn get(&self, query: FontQuery<'p>) -> Option<(usize, Ref<Font>)> {
+ // Check if we had the exact same query before.
+ let state = self.state.borrow();
+ if let Some(&index) = state.query_cache.get(&query) {
+ // That this is the query cache means it must has an index as we've served it before.
+ let extern_index = state.fonts[index].0.unwrap();
+ let font = Ref::map(state, |s| &s.fonts[index].1);
+
+ return Some((extern_index, font));
+ }
+ drop(state);
+
+ // Go over all font infos from all font providers that match the query.
+ for family in query.families {
+ for (provider, infos) in self.providers.iter().zip(&self.provider_fonts) {
+ for info in infos.iter() {
+ // Check whether this info matches the query.
+ if Self::matches(query, family, info) {
+ let mut state = self.state.borrow_mut();
+
+ // Check if we have already loaded this font before.
+ // Otherwise we'll fetch the font from the provider.
+ let index = if let Some(&index) = state.info_cache.get(info) {
+ index
+ } else if let Some(mut source) = provider.get(info) {
+ // Read the font program into a vec.
+ let mut program = Vec::new();
+ source.read_to_end(&mut program).ok()?;
+
+ // Create a font from it.
+ let font = Font::new(program).ok()?;
+
+ // Insert it into the storage.
+ let index = state.fonts.len();
+ state.info_cache.insert(info, index);
+ state.fonts.push((None, font));
+
+ index
+ } else {
+ continue;
+ };
+
+ // Check whether this font has the character we need.
+ let has_char = state.fonts[index].1.mapping.contains_key(&query.character);
+ if has_char {
+ // We can take this font, so we store the query.
+ state.query_cache.insert(query, index);
+
+ // Now we have to find out the external index of it, or assign a new
+ // one if it has not already one.
+ let maybe_extern_index = state.fonts[index].0;
+ let extern_index = maybe_extern_index.unwrap_or_else(|| {
+ // We have to assign an external index before serving.
+ let extern_index = state.inner_index.len();
+ state.inner_index.push(index);
+ state.fonts[index].0 = Some(extern_index);
+ extern_index
+ });
+
+ // Release the mutable borrow and borrow immutably.
+ drop(state);
+ let font = Ref::map(self.state.borrow(), |s| &s.fonts[index].1);
+
+ // Finally we can return it.
+ return Some((extern_index, font));
+ }
+ }
+ }
+ }
+ }
+
+ None
+ }
+
+ /// Return a loaded font at an index. Panics if the index is out of bounds.
+ pub fn get_with_index(&self, index: usize) -> Ref<Font> {
+ let state = self.state.borrow();
+ let internal = state.inner_index[index];
+ Ref::map(state, |s| &s.fonts[internal].1)
+ }
+
+ /// Return the list of fonts.
+ pub fn into_fonts(self) -> Vec<Font> {
+ // Sort the fonts by external key so that they are in the correct order.
+ let mut fonts = self.state.into_inner().fonts;
+ fonts.sort_by_key(|&(maybe_index, _)| match maybe_index {
+ Some(index) => index as isize,
+ None => -1,
+ });
+
+ // Remove the fonts that are not used from the outside
+ fonts.into_iter().filter_map(|(maybe_index, font)| {
+ maybe_index.map(|_| font)
+ }).collect()
+ }
+
+ /// Check whether the query and the current family match the info.
+ fn matches(query: FontQuery, family: &FontFamily, info: &FontInfo) -> bool {
+ info.families.contains(family)
+ && info.italic == query.italic && info.bold == query.bold
+ }
+}
+
+impl Debug for FontLoader<'_> {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ let state = self.state.borrow();
+ f.debug_struct("FontLoader")
+ .field("providers", &self.providers.len())
+ .field("provider_fonts", &self.provider_fonts)
+ .field("fonts", &state.fonts)
+ .field("query_cache", &state.query_cache)
+ .field("info_cache", &state.info_cache)
+ .field("inner_index", &state.inner_index)
+ .finish()
+ }
+}
+
+/// A query for a font with specific properties.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct FontQuery<'a> {
+ /// A fallback list of font families to accept. The first family in this list, that also
+ /// satisfies the other conditions, shall be returned.
+ pub families: &'a [FontFamily],
+ /// Whether the font shall be in italics.
+ pub italic: bool,
+ /// Whether the font shall be in boldface.
+ pub bold: bool,
+ /// Which character we need.
+ pub character: char,
+}
+
+struct Subsetter<'a> {
// Original font
- font: &'d Font,
- reader: OpenTypeReader<Cursor<&'d [u8]>>,
+ font: &'a Font,
+ reader: OpenTypeReader<Cursor<&'a [u8]>>,
outlines: Outlines,
tables: Vec<TableRecord>,
cmap: Option<CharMap>,
@@ -366,7 +540,7 @@ struct Subsetter<'d> {
body: Vec<u8>,
}
-impl<'d> Subsetter<'d> {
+impl<'a> Subsetter<'a> {
fn subset<I, S>(mut self, needed_tables: I, optional_tables: I) -> FontResult<Font>
where I: IntoIterator<Item=S>, S: AsRef<str> {
// Find out which glyphs to include based on which characters we want
@@ -695,7 +869,7 @@ impl<'d> Subsetter<'d> {
}))
}
- fn get_table_data(&self, tag: Tag) -> FontResult<&'d [u8]> {
+ fn get_table_data(&self, tag: Tag) -> FontResult<&'a [u8]> {
let record = match self.tables.binary_search_by_key(&tag, |r| r.tag) {
Ok(index) => &self.tables[index],
Err(_) => return Err(FontError::MissingTable(tag.to_string())),
diff --git a/src/func.rs b/src/func.rs
index c90e87ad..ef5120df 100644
--- a/src/func.rs
+++ b/src/func.rs
@@ -6,7 +6,7 @@ use std::fmt::{self, Debug, Formatter};
use crate::syntax::FuncHeader;
use crate::parsing::{ParseContext, ParseResult};
-use crate::engine::{TypesetContext, TypesetResult};
+use crate::layout::{Layout, LayoutContext, LayoutResult};
/// Types that act as functions.
@@ -17,12 +17,16 @@ use crate::engine::{TypesetContext, TypesetResult};
/// The trait `FunctionBounds` is automatically implemented for types which can be
/// used as functions, that is they fulfill the bounds `Debug + PartialEq + 'static`.
pub trait Function: FunctionBounds {
- /// Parse the tokens of the context with the given header and scope into self.
+ /// Parse the header and body into this function given this context.
fn parse(header: &FuncHeader, body: Option<&str>, ctx: &ParseContext)
-> ParseResult<Self> where Self: Sized;
- /// Execute the function and optionally yield a return value.
- fn typeset(&self, ctx: &TypesetContext) -> TypesetResult<()>;
+ /// Layout this function given a context.
+ ///
+ /// Returns optionally the resulting layout and a if changes to the context
+ /// should be made new context.
+ fn layout(&self, ctx: &LayoutContext)
+ -> LayoutResult<(Option<Layout>, Option<LayoutContext>)>;
}
impl PartialEq for dyn Function {
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
new file mode 100644
index 00000000..7bc62bd3
--- /dev/null
+++ b/src/layout/mod.rs
@@ -0,0 +1,118 @@
+//! Layouting engine.
+
+use crate::doc::Document;
+use crate::font::{Font, FontLoader, FontFamily, FontError};
+use crate::syntax::SyntaxTree;
+
+mod size;
+pub use size::Size;
+
+
+/// Layout a syntax tree given a context.
+#[allow(unused_variables)]
+pub fn layout(tree: &SyntaxTree, ctx: &LayoutContext) -> LayoutResult<Layout> {
+ Ok(Layout {})
+}
+
+/// A collection of layouted content.
+pub struct Layout {}
+
+impl Layout {
+ /// Convert this layout into a document given the list of fonts referenced by it.
+ pub fn into_document(self, fonts: Vec<Font>) -> Document {
+ Document {
+ pages: vec![],
+ fonts,
+ }
+ }
+}
+
+/// The context for layouting.
+pub struct LayoutContext<'a, 'p> {
+ pub loader: &'a FontLoader<'p>,
+}
+
+/// Default styles for pages.
+#[derive(Debug, Clone, PartialEq)]
+pub struct PageStyle {
+ /// The width of the paper.
+ pub width: Size,
+ /// The height of the paper.
+ pub height: Size,
+
+ /// The left margin of the paper.
+ pub margin_left: Size,
+ /// The top margin of the paper.
+ pub margin_top: Size,
+ /// The right margin of the paper.
+ pub margin_right: Size,
+ /// The bottom margin of the paper.
+ pub margin_bottom: Size,
+}
+
+impl Default for PageStyle {
+ fn default() -> PageStyle {
+ PageStyle {
+ // A4 paper.
+ width: Size::from_mm(210.0),
+ height: Size::from_mm(297.0),
+
+ // Margins. A bit more on top and bottom.
+ margin_left: Size::from_cm(3.0),
+ margin_top: Size::from_cm(3.0),
+ margin_right: Size::from_cm(3.0),
+ margin_bottom: Size::from_cm(3.0),
+ }
+ }
+}
+
+/// Default styles for texts.
+#[derive(Debug, Clone, PartialEq)]
+pub struct TextStyle {
+ /// A fallback list of font families to use.
+ pub font_families: Vec<FontFamily>,
+ /// The font size.
+ pub font_size: f32,
+ /// The line spacing (as a multiple of the font size).
+ pub line_spacing: f32,
+ /// The spacing for paragraphs (as a multiple of the line spacing).
+ pub paragraph_spacing: f32,
+}
+
+impl Default for TextStyle {
+ fn default() -> TextStyle {
+ use FontFamily::*;
+ TextStyle {
+ // Default font family, font size and line spacing.
+ font_families: vec![SansSerif, Serif, Monospace],
+ font_size: 11.0,
+ line_spacing: 1.25,
+ paragraph_spacing: 1.5,
+ }
+ }
+}
+
+/// The error type for layouting.
+pub enum LayoutError {
+ /// There was no suitable font.
+ MissingFont,
+ /// An error occured while gathering font data.
+ Font(FontError),
+}
+
+/// The result type for layouting.
+pub type LayoutResult<T> = Result<T, LayoutError>;
+
+error_type! {
+ err: LayoutError,
+ show: f => match err {
+ LayoutError::MissingFont => write!(f, "missing font"),
+ LayoutError::Font(err) => write!(f, "font error: {}", err),
+ },
+ source: match err {
+ LayoutError::Font(err) => Some(err),
+ _ => None,
+ },
+ from: (std::io::Error, LayoutError::Font(FontError::Io(err))),
+ from: (FontError, LayoutError::Font(err)),
+}
diff --git a/src/engine/size.rs b/src/layout/size.rs
index bf79a3c4..bf79a3c4 100644
--- a/src/engine/size.rs
+++ b/src/layout/size.rs
diff --git a/src/lib.rs b/src/lib.rs
index aa017cad..9a1082c6 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -5,9 +5,9 @@
//! [iterator of tokens](crate::parsing::Tokens). Then the [parser](crate::parsing::Parser)
//! operates on that to construct a syntax tree. The structures describing the tree can be found
//! in the [syntax] module.
-//! - **Typesetting:** The next step is to transform the syntax tree into a portable representation
-//! of the typesetted document. Types for these can be found in the [doc] module. This
-//! representation contains already the finished layout.
+//! - **Layouting:** The next step is to transform the syntax tree into a portable representation
+//! of the typesetted document. Types for these can be found in the [doc] and [layout] modules.
+//! This representation contains already the finished layout.
//! - **Exporting:** The finished document can then be exported into supported formats. Submodules
//! for the supported formats are located in the [export] module. Currently the only supported
//! format is _PDF_.
@@ -15,24 +15,24 @@
//! # Example
//! ```
//! use std::fs::File;
-//! use typeset::Compiler;
+//! use typeset::Typesetter;
//! use typeset::{font::FileSystemFontProvider, font_info};
//! use typeset::export::pdf::PdfExporter;
//!
//! // Simple example source code.
//! let src = "Hello World from __Typeset__! 🌍";
//!
-//! // Create a compiler with a font provider that provides three fonts
+//! // Create a typesetter with a font provider that provides three fonts
//! // (two sans-serif fonts and a fallback for the emoji).
-//! let mut compiler = Compiler::new();
-//! compiler.add_font_provider(FileSystemFontProvider::new("../fonts", vec![
+//! let mut typesetter = Typesetter::new();
+//! typesetter.add_font_provider(FileSystemFontProvider::new("../fonts", vec![
//! ("NotoSans-Regular.ttf", font_info!(["NotoSans", "Noto", SansSerif])),
//! ("NotoSans-Italic.ttf", font_info!(["NotoSans", "Noto", SansSerif], italic)),
//! ("NotoEmoji-Regular.ttf", font_info!(["NotoEmoji", "Noto", SansSerif, Serif, Monospace])),
//! ]));
//!
-//! // Compile the source code into a document with the compiler.
-//! let document = compiler.compile(src).unwrap();
+//! // Typeset the source code into a document.
+//! let document = typesetter.typeset(src).unwrap();
//!
//! // Export the document into a PDF file.
//! # /*
@@ -44,51 +44,61 @@
//! ```
use crate::doc::Document;
-use crate::engine::{typeset, Style, TypesetResult, TypesetError};
use crate::func::Scope;
-use crate::font::FontProvider;
-use crate::parsing::{parse, ParseResult, ParseError};
+use crate::font::{Font, FontLoader, FontProvider};
+use crate::layout::{layout, Layout, LayoutContext, LayoutResult, LayoutError};
+use crate::layout::{PageStyle, TextStyle};
+use crate::parsing::{parse, ParseContext, ParseResult, ParseError};
use crate::syntax::SyntaxTree;
#[macro_use]
mod error;
pub mod doc;
-pub mod engine;
pub mod export;
#[macro_use]
pub mod font;
pub mod func;
+pub mod layout;
pub mod parsing;
pub mod syntax;
/// Transforms source code into typesetted documents.
///
-/// Holds the compilation context, which can be configured through various methods.
-pub struct Compiler<'p> {
- /// Style for typesetting.
- style: Style,
+/// Holds the typesetting context, which can be configured through various methods.
+pub struct Typesetter<'p> {
+ /// The default page style.
+ base_page_style: PageStyle,
+ /// The default text style.
+ base_text_style: TextStyle,
/// Font providers.
font_providers: Vec<Box<dyn FontProvider + 'p>>,
}
-impl<'p> Compiler<'p> {
- /// Create a new compiler.
+impl<'p> Typesetter<'p> {
+ /// Create a new typesetter.
#[inline]
- pub fn new() -> Compiler<'p> {
- Compiler {
- style: Style::default(),
+ pub fn new() -> Typesetter<'p> {
+ Typesetter {
+ base_page_style: PageStyle::default(),
+ base_text_style: TextStyle::default(),
font_providers: vec![],
}
}
- /// Set the default style for the document.
+ /// Set the default page style for the document.
#[inline]
- pub fn set_style(&mut self, style: Style) {
- self.style = style;
+ pub fn set_page_style(&mut self, style: PageStyle) {
+ self.base_page_style = style;
}
- /// Add a font provider to the context of this compiler.
+ /// Set the default text style for the document.
+ #[inline]
+ pub fn set_text_style(&mut self, style: TextStyle) {
+ self.base_text_style = style;
+ }
+
+ /// Add a font provider to the context of this typesetter.
#[inline]
pub fn add_font_provider<P: 'p>(&mut self, provider: P) where P: FontProvider {
self.font_providers.push(Box::new(provider));
@@ -98,45 +108,50 @@ impl<'p> Compiler<'p> {
#[inline]
pub fn parse(&self, src: &str) -> ParseResult<SyntaxTree> {
let scope = Scope::with_std();
- parse(src, &scope)
+ let ctx = ParseContext { scope: &scope };
+ parse(src, &ctx)
}
- /// Typeset a parsed syntax tree into a document.
+ /// Layout a parsed syntax tree and return the layout and the referenced font list.
#[inline]
- pub fn typeset(&self, tree: &SyntaxTree) -> TypesetResult<Document> {
- typeset(&tree, &self.style, &self.font_providers).map_err(Into::into)
+ pub fn layout(&self, tree: &SyntaxTree) -> LayoutResult<(Layout, Vec<Font>)> {
+ let loader = FontLoader::new(&self.font_providers);
+ let ctx = LayoutContext { loader: &loader };
+ let layout = layout(&tree, &ctx)?;
+ Ok((layout, loader.into_fonts()))
}
- /// Compile a portable typesetted document from source code.
+ /// Typeset a portable document from source code.
#[inline]
- pub fn compile(&self, src: &str) -> Result<Document, CompileError> {
+ pub fn typeset(&self, src: &str) -> Result<Document, TypesetError> {
let tree = self.parse(src)?;
- let document = self.typeset(&tree)?;
+ let (layout, fonts) = self.layout(&tree)?;
+ let document = layout.into_document(fonts);
Ok(document)
}
}
-/// The general error type for compilation.
-pub enum CompileError {
+/// The general error type for typesetting.
+pub enum TypesetError {
/// An error that occured while transforming source code into
/// an abstract syntax tree.
- ParseErr(ParseError),
- /// An error that occured while typesetting into an abstract document.
- TypesetErr(TypesetError),
+ Parse(ParseError),
+ /// An error that occured while layouting.
+ Layout(LayoutError),
}
error_type! {
- err: CompileError,
+ err: TypesetError,
show: f => match err {
- CompileError::ParseErr(e) => write!(f, "parse error: {}", e),
- CompileError::TypesetErr(e) => write!(f, "typeset error: {}", e),
+ TypesetError::Parse(e) => write!(f, "parse error: {}", e),
+ TypesetError::Layout(e) => write!(f, "layout error: {}", e),
},
source: match err {
- CompileError::ParseErr(e) => Some(e),
- CompileError::TypesetErr(e) => Some(e),
+ TypesetError::Parse(e) => Some(e),
+ TypesetError::Layout(e) => Some(e),
},
- from: (ParseError, CompileError::ParseErr(err)),
- from: (TypesetError, CompileError::TypesetErr(err)),
+ from: (ParseError, TypesetError::Parse(err)),
+ from: (LayoutError, TypesetError::Layout(err)),
}
@@ -144,15 +159,14 @@ error_type! {
mod test {
use std::fs::File;
use std::io::BufWriter;
- use crate::Compiler;
+ use crate::Typesetter;
use crate::export::pdf::PdfExporter;
use crate::font::FileSystemFontProvider;
/// Create a pdf with a name from the source code.
fn test(name: &str, src: &str) {
- // Create compiler
- let mut compiler = Compiler::new();
- compiler.add_font_provider(FileSystemFontProvider::new("../fonts", vec![
+ let mut typesetter = Typesetter::new();
+ typesetter.add_font_provider(FileSystemFontProvider::new("../fonts", vec![
("NotoSans-Regular.ttf", font_info!(["NotoSans", "Noto", SansSerif])),
("NotoSans-Italic.ttf", font_info!(["NotoSans", "Noto", SansSerif], italic)),
("NotoSans-Bold.ttf", font_info!(["NotoSans", "Noto", SansSerif], bold)),
@@ -161,8 +175,8 @@ mod test {
("NotoEmoji-Regular.ttf", font_info!(["NotoEmoji", "Noto", SansSerif, Serif, Monospace])),
]));
- // Compile into document
- let document = compiler.compile(src).unwrap();
+ // Typeset into document
+ let document = typesetter.typeset(src).unwrap();
// Write to file
let path = format!("../target/typeset-unit-{}.pdf", name);
diff --git a/src/parsing.rs b/src/parsing.rs
index f9ebe49a..594077f7 100644
--- a/src/parsing.rs
+++ b/src/parsing.rs
@@ -324,18 +324,25 @@ impl Iterator for PeekableChars<'_> {
}
}
-/// Parses source code into a syntax tree using function definitions from a scope.
+/// Parses source code into a syntax tree given a context.
#[inline]
-pub fn parse(src: &str, scope: &Scope) -> ParseResult<SyntaxTree> {
- Parser::new(src, scope).parse()
+pub fn parse(src: &str, ctx: &ParseContext) -> ParseResult<SyntaxTree> {
+ Parser::new(src, ctx).parse()
+}
+
+/// The context for parsing.
+#[derive(Debug)]
+pub struct ParseContext<'a> {
+ /// The scope containing function definitions.
+ pub scope: &'a Scope,
}
/// Transforms token streams to syntax trees.
struct Parser<'s> {
src: &'s str,
tokens: PeekableTokens<'s>,
- scope: &'s Scope,
state: ParserState,
+ ctx: &'s ParseContext<'s>,
tree: SyntaxTree,
}
@@ -352,11 +359,11 @@ enum ParserState {
impl<'s> Parser<'s> {
/// Create a new parser from a stream of tokens and a scope of functions.
- fn new(src: &'s str, scope: &'s Scope) -> Parser<'s> {
+ fn new(src: &'s str, ctx: &'s ParseContext) -> Parser<'s> {
Parser {
src,
tokens: PeekableTokens::new(tokenize(src)),
- scope,
+ ctx,
state: ParserState::Body,
tree: SyntaxTree::new(),
}
@@ -454,13 +461,9 @@ impl<'s> Parser<'s> {
}
// Now we want to parse this function dynamically.
- let parser = self.scope.get_parser(&header.name)
+ let parser = self.ctx.scope.get_parser(&header.name)
.ok_or_else(|| ParseError::new(format!("unknown function: '{}'", &header.name)))?;
- let parse_context = ParseContext {
- scope: &self.scope,
- };
-
// Do the parsing dependent on whether the function has a body.
Ok(if has_body {
// Find out the string which makes the body of this function.
@@ -471,7 +474,7 @@ impl<'s> Parser<'s> {
// Parse the body.
let body_string = &self.src[start .. end];
- let body = parser(&header, Some(body_string), &parse_context)?;
+ let body = parser(&header, Some(body_string), self.ctx)?;
// Skip to the end of the function in the token stream.
self.tokens.goto(end);
@@ -481,7 +484,7 @@ impl<'s> Parser<'s> {
body
} else {
- parser(&header, None, &parse_context)?
+ parser(&header, None, self.ctx)?
})
}
@@ -627,13 +630,6 @@ impl<'s> Iterator for PeekableTokens<'s> {
}
}
-/// The context for parsing a function.
-#[derive(Debug)]
-pub struct ParseContext<'s> {
- /// The scope containing function definitions.
- pub scope: &'s Scope,
-}
-
/// Whether this word is a valid unicode identifier.
fn is_identifier(string: &str) -> bool {
let mut chars = string.chars();
@@ -801,7 +797,7 @@ mod token_tests {
mod parse_tests {
use super::*;
use crate::func::{Function, Scope};
- use crate::engine::{TypesetContext, TypesetResult};
+ use crate::layout::{LayoutContext, LayoutResult, Layout};
use Node::{Space as S, Newline as N, Func as F};
use funcs::*;
@@ -818,13 +814,14 @@ mod parse_tests {
fn parse(_: &FuncHeader, body: Option<&str>, ctx: &ParseContext)
-> ParseResult<Self> where Self: Sized {
if let Some(src) = body {
- parse(src, ctx.scope).map(|tree| TreeFn(tree))
+ parse(src, ctx).map(|tree| TreeFn(tree))
} else {
Err(ParseError::new("expected body for tree fn"))
}
}
- fn typeset(&self, _: &TypesetContext) -> TypesetResult<()> { Ok(()) }
+ fn layout(&self, _: &LayoutContext)
+ -> LayoutResult<(Option<Layout>, Option<LayoutContext>)> { Ok((None, None)) }
}
/// A testing function without a body.
@@ -841,28 +838,33 @@ mod parse_tests {
}
}
- fn typeset(&self, _: &TypesetContext) -> TypesetResult<()> { Ok(()) }
+ fn layout(&self, _: &LayoutContext)
+ -> LayoutResult<(Option<Layout>, Option<LayoutContext>)> { Ok((None, None)) }
}
}
/// Test if the source code parses into the syntax tree.
fn test(src: &str, tree: SyntaxTree) {
- assert_eq!(parse(src, &Scope::new()).unwrap(), tree);
+ let ctx = ParseContext { scope: &Scope::new() };
+ assert_eq!(parse(src, &ctx).unwrap(), tree);
}
/// Test with a scope containing function definitions.
fn test_scoped(scope: &Scope, src: &str, tree: SyntaxTree) {
- assert_eq!(parse(src, &scope).unwrap(), tree);
+ let ctx = ParseContext { scope };
+ assert_eq!(parse(src, &ctx).unwrap(), tree);
}
/// Test if the source parses into the error.
fn test_err(src: &str, err: &str) {
- assert_eq!(parse(src, &Scope::new()).unwrap_err().to_string(), err);
+ let ctx = ParseContext { scope: &Scope::new() };
+ assert_eq!(parse(src, &ctx).unwrap_err().to_string(), err);
}
/// Test with a scope if the source parses into the error.
fn test_err_scoped(scope: &Scope, src: &str, err: &str) {
- assert_eq!(parse(src, &scope).unwrap_err().to_string(), err);
+ let ctx = ParseContext { scope };
+ assert_eq!(parse(src, &ctx).unwrap_err().to_string(), err);
}
/// Create a text node.