mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
Restructure engine into modular layouter 🍂
This commit is contained in:
parent
e3215fa3b9
commit
b3734bbc04
@ -5,7 +5,7 @@ use std::process;
|
|||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use typeset::Compiler;
|
use typeset::Typesetter;
|
||||||
use typeset::{font::FileSystemFontProvider, font_info};
|
use typeset::{font::FileSystemFontProvider, font_info};
|
||||||
use typeset::export::pdf::PdfExporter;
|
use typeset::export::pdf::PdfExporter;
|
||||||
|
|
||||||
@ -33,10 +33,10 @@ fn run() -> Result<(), Box<Error>> {
|
|||||||
let mut src = String::new();
|
let mut src = String::new();
|
||||||
file.read_to_string(&mut src).map_err(|_| "failed to read from source file")?;
|
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).
|
// (two sans-serif fonts and a fallback for the emoji).
|
||||||
let mut compiler = Compiler::new();
|
let mut typesetter = Typesetter::new();
|
||||||
compiler.add_font_provider(FileSystemFontProvider::new("fonts", vec![
|
typesetter.add_font_provider(FileSystemFontProvider::new("fonts", vec![
|
||||||
("NotoSans-Regular.ttf", font_info!(["NotoSans", "Noto", SansSerif])),
|
("NotoSans-Regular.ttf", font_info!(["NotoSans", "Noto", SansSerif])),
|
||||||
("NotoSans-Italic.ttf", font_info!(["NotoSans", "Noto", SansSerif], italic)),
|
("NotoSans-Italic.ttf", font_info!(["NotoSans", "Noto", SansSerif], italic)),
|
||||||
("NotoSans-Bold.ttf", font_info!(["NotoSans", "Noto", SansSerif], bold)),
|
("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])),
|
("NotoEmoji-Regular.ttf", font_info!(["NotoEmoji", "Noto", SansSerif, Serif, Monospace])),
|
||||||
]));
|
]));
|
||||||
|
|
||||||
// Compile the source code with the compiler.
|
// Typeset the source code.
|
||||||
let document = compiler.compile(&src)?;
|
let document = typesetter.typeset(&src)?;
|
||||||
|
|
||||||
|
|
||||||
// Export the document into a PDF file.
|
// Export the document into a PDF file.
|
||||||
let exporter = PdfExporter::new();
|
let exporter = PdfExporter::new();
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
//! Representation of typesetted documents.
|
//! Representation of typesetted documents.
|
||||||
|
|
||||||
use crate::font::Font;
|
use crate::font::Font;
|
||||||
use crate::engine::Size;
|
use crate::layout::Size;
|
||||||
|
|
||||||
|
|
||||||
/// A complete typesetted document, which can be exported.
|
/// A complete typesetted document, which can be exported.
|
||||||
|
@ -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,
|
|
||||||
}
|
|
@ -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)),
|
|
||||||
}
|
|
@ -10,7 +10,7 @@ use pdf::font::{GlyphUnit, CMap, CMapEncoding, WidthRecord, FontStream};
|
|||||||
|
|
||||||
use crate::doc::{Document, Text as DocText, TextCommand};
|
use crate::doc::{Document, Text as DocText, TextCommand};
|
||||||
use crate::font::{Font, FontError};
|
use crate::font::{Font, FontError};
|
||||||
use crate::engine::Size;
|
use crate::layout::Size;
|
||||||
|
|
||||||
|
|
||||||
/// Exports documents into _PDFs_.
|
/// Exports documents into _PDFs_.
|
||||||
|
238
src/font.rs
238
src/font.rs
@ -1,16 +1,18 @@
|
|||||||
//! Font loading and transforming.
|
//! Font loading and transforming.
|
||||||
//!
|
//!
|
||||||
//! # Font handling
|
//! # Font handling
|
||||||
//! To do the typesetting, the compiler needs font data. To be highly portable the compiler assumes
|
//! To do the typesetting, the typesetting engine needs font data. To be highly portable the engine
|
||||||
//! nothing about the environment. To still work with fonts, the consumer of this library has to
|
//! itself assumes nothing about the environment. To still work with fonts, the consumer of this
|
||||||
//! add _font providers_ to their compiler instance. These can be queried for font data given
|
//! library has to add _font providers_ to their typesetting instance. These can be queried for font
|
||||||
//! flexible font filters specifying required font families and styles. A font provider is a type
|
//! data given flexible font filters specifying required font families and styles. A font provider
|
||||||
//! implementing the [`FontProvider`](crate::font::FontProvider) trait.
|
//! is a type implementing the [`FontProvider`](crate::font::FontProvider) trait.
|
||||||
//!
|
//!
|
||||||
//! There is one [included font provider](crate::font::FileSystemFontProvider) that serves
|
//! There is one [included font provider](crate::font::FileSystemFontProvider) that serves fonts
|
||||||
//! fonts from a folder on the file system.
|
//! from a folder on the file system.
|
||||||
|
|
||||||
|
use std::cell::{RefCell, Ref};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::fmt::{self, Debug, Formatter};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{self, Cursor, Read, Seek, SeekFrom, BufReader};
|
use std::io::{self, Cursor, Read, Seek, SeekFrom, BufReader};
|
||||||
use std::path::PathBuf;
|
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::tables::{Header, Name, CharMap, MaximumProfile, HorizontalMetrics, Post, OS2};
|
||||||
use opentype::global::{MacStyleFlags, NameEntry};
|
use opentype::global::{MacStyleFlags, NameEntry};
|
||||||
|
|
||||||
use crate::engine::Size;
|
use crate::layout::Size;
|
||||||
|
|
||||||
|
|
||||||
/// A loaded font, containing relevant information for typesetting.
|
/// A loaded font, containing relevant information for typesetting.
|
||||||
@ -174,25 +176,6 @@ pub struct FontMetrics {
|
|||||||
pub weight_class: u16,
|
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.
|
/// Describes a font.
|
||||||
///
|
///
|
||||||
/// Can be constructed conveniently with the [`font_info`] macro.
|
/// Can be constructed conveniently with the [`font_info`] macro.
|
||||||
@ -285,6 +268,25 @@ pub enum FontFamily {
|
|||||||
Named(String),
|
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.
|
/// A font provider serving fonts from a folder on the local file system.
|
||||||
pub struct FileSystemFontProvider {
|
pub struct FileSystemFontProvider {
|
||||||
base: PathBuf,
|
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
|
// Original font
|
||||||
font: &'d Font,
|
font: &'a Font,
|
||||||
reader: OpenTypeReader<Cursor<&'d [u8]>>,
|
reader: OpenTypeReader<Cursor<&'a [u8]>>,
|
||||||
outlines: Outlines,
|
outlines: Outlines,
|
||||||
tables: Vec<TableRecord>,
|
tables: Vec<TableRecord>,
|
||||||
cmap: Option<CharMap>,
|
cmap: Option<CharMap>,
|
||||||
@ -366,7 +540,7 @@ struct Subsetter<'d> {
|
|||||||
body: Vec<u8>,
|
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>
|
fn subset<I, S>(mut self, needed_tables: I, optional_tables: I) -> FontResult<Font>
|
||||||
where I: IntoIterator<Item=S>, S: AsRef<str> {
|
where I: IntoIterator<Item=S>, S: AsRef<str> {
|
||||||
// Find out which glyphs to include based on which characters we want
|
// 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) {
|
let record = match self.tables.binary_search_by_key(&tag, |r| r.tag) {
|
||||||
Ok(index) => &self.tables[index],
|
Ok(index) => &self.tables[index],
|
||||||
Err(_) => return Err(FontError::MissingTable(tag.to_string())),
|
Err(_) => return Err(FontError::MissingTable(tag.to_string())),
|
||||||
|
12
src/func.rs
12
src/func.rs
@ -6,7 +6,7 @@ use std::fmt::{self, Debug, Formatter};
|
|||||||
|
|
||||||
use crate::syntax::FuncHeader;
|
use crate::syntax::FuncHeader;
|
||||||
use crate::parsing::{ParseContext, ParseResult};
|
use crate::parsing::{ParseContext, ParseResult};
|
||||||
use crate::engine::{TypesetContext, TypesetResult};
|
use crate::layout::{Layout, LayoutContext, LayoutResult};
|
||||||
|
|
||||||
|
|
||||||
/// Types that act as functions.
|
/// 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
|
/// The trait `FunctionBounds` is automatically implemented for types which can be
|
||||||
/// used as functions, that is they fulfill the bounds `Debug + PartialEq + 'static`.
|
/// used as functions, that is they fulfill the bounds `Debug + PartialEq + 'static`.
|
||||||
pub trait Function: FunctionBounds {
|
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)
|
fn parse(header: &FuncHeader, body: Option<&str>, ctx: &ParseContext)
|
||||||
-> ParseResult<Self> where Self: Sized;
|
-> ParseResult<Self> where Self: Sized;
|
||||||
|
|
||||||
/// Execute the function and optionally yield a return value.
|
/// Layout this function given a context.
|
||||||
fn typeset(&self, ctx: &TypesetContext) -> TypesetResult<()>;
|
///
|
||||||
|
/// 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 {
|
impl PartialEq for dyn Function {
|
||||||
|
118
src/layout/mod.rs
Normal file
118
src/layout/mod.rs
Normal file
@ -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)),
|
||||||
|
}
|
116
src/lib.rs
116
src/lib.rs
@ -5,9 +5,9 @@
|
|||||||
//! [iterator of tokens](crate::parsing::Tokens). Then the [parser](crate::parsing::Parser)
|
//! [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
|
//! operates on that to construct a syntax tree. The structures describing the tree can be found
|
||||||
//! in the [syntax] module.
|
//! in the [syntax] module.
|
||||||
//! - **Typesetting:** The next step is to transform the syntax tree into a portable representation
|
//! - **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] module. This
|
//! of the typesetted document. Types for these can be found in the [doc] and [layout] modules.
|
||||||
//! representation contains already the finished layout.
|
//! This representation contains already the finished layout.
|
||||||
//! - **Exporting:** The finished document can then be exported into supported formats. Submodules
|
//! - **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
|
//! for the supported formats are located in the [export] module. Currently the only supported
|
||||||
//! format is _PDF_.
|
//! format is _PDF_.
|
||||||
@ -15,24 +15,24 @@
|
|||||||
//! # Example
|
//! # Example
|
||||||
//! ```
|
//! ```
|
||||||
//! use std::fs::File;
|
//! use std::fs::File;
|
||||||
//! use typeset::Compiler;
|
//! use typeset::Typesetter;
|
||||||
//! use typeset::{font::FileSystemFontProvider, font_info};
|
//! use typeset::{font::FileSystemFontProvider, font_info};
|
||||||
//! use typeset::export::pdf::PdfExporter;
|
//! use typeset::export::pdf::PdfExporter;
|
||||||
//!
|
//!
|
||||||
//! // Simple example source code.
|
//! // Simple example source code.
|
||||||
//! let src = "Hello World from __Typeset__! 🌍";
|
//! 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).
|
//! // (two sans-serif fonts and a fallback for the emoji).
|
||||||
//! let mut compiler = Compiler::new();
|
//! let mut typesetter = Typesetter::new();
|
||||||
//! compiler.add_font_provider(FileSystemFontProvider::new("../fonts", vec![
|
//! typesetter.add_font_provider(FileSystemFontProvider::new("../fonts", vec![
|
||||||
//! ("NotoSans-Regular.ttf", font_info!(["NotoSans", "Noto", SansSerif])),
|
//! ("NotoSans-Regular.ttf", font_info!(["NotoSans", "Noto", SansSerif])),
|
||||||
//! ("NotoSans-Italic.ttf", font_info!(["NotoSans", "Noto", SansSerif], italic)),
|
//! ("NotoSans-Italic.ttf", font_info!(["NotoSans", "Noto", SansSerif], italic)),
|
||||||
//! ("NotoEmoji-Regular.ttf", font_info!(["NotoEmoji", "Noto", SansSerif, Serif, Monospace])),
|
//! ("NotoEmoji-Regular.ttf", font_info!(["NotoEmoji", "Noto", SansSerif, Serif, Monospace])),
|
||||||
//! ]));
|
//! ]));
|
||||||
//!
|
//!
|
||||||
//! // Compile the source code into a document with the compiler.
|
//! // Typeset the source code into a document.
|
||||||
//! let document = compiler.compile(src).unwrap();
|
//! let document = typesetter.typeset(src).unwrap();
|
||||||
//!
|
//!
|
||||||
//! // Export the document into a PDF file.
|
//! // Export the document into a PDF file.
|
||||||
//! # /*
|
//! # /*
|
||||||
@ -44,51 +44,61 @@
|
|||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
use crate::doc::Document;
|
use crate::doc::Document;
|
||||||
use crate::engine::{typeset, Style, TypesetResult, TypesetError};
|
|
||||||
use crate::func::Scope;
|
use crate::func::Scope;
|
||||||
use crate::font::FontProvider;
|
use crate::font::{Font, FontLoader, FontProvider};
|
||||||
use crate::parsing::{parse, ParseResult, ParseError};
|
use crate::layout::{layout, Layout, LayoutContext, LayoutResult, LayoutError};
|
||||||
|
use crate::layout::{PageStyle, TextStyle};
|
||||||
|
use crate::parsing::{parse, ParseContext, ParseResult, ParseError};
|
||||||
use crate::syntax::SyntaxTree;
|
use crate::syntax::SyntaxTree;
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod error;
|
mod error;
|
||||||
pub mod doc;
|
pub mod doc;
|
||||||
pub mod engine;
|
|
||||||
pub mod export;
|
pub mod export;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
pub mod font;
|
pub mod font;
|
||||||
pub mod func;
|
pub mod func;
|
||||||
|
pub mod layout;
|
||||||
pub mod parsing;
|
pub mod parsing;
|
||||||
pub mod syntax;
|
pub mod syntax;
|
||||||
|
|
||||||
|
|
||||||
/// Transforms source code into typesetted documents.
|
/// Transforms source code into typesetted documents.
|
||||||
///
|
///
|
||||||
/// Holds the compilation context, which can be configured through various methods.
|
/// Holds the typesetting context, which can be configured through various methods.
|
||||||
pub struct Compiler<'p> {
|
pub struct Typesetter<'p> {
|
||||||
/// Style for typesetting.
|
/// The default page style.
|
||||||
style: Style,
|
base_page_style: PageStyle,
|
||||||
|
/// The default text style.
|
||||||
|
base_text_style: TextStyle,
|
||||||
/// Font providers.
|
/// Font providers.
|
||||||
font_providers: Vec<Box<dyn FontProvider + 'p>>,
|
font_providers: Vec<Box<dyn FontProvider + 'p>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'p> Compiler<'p> {
|
impl<'p> Typesetter<'p> {
|
||||||
/// Create a new compiler.
|
/// Create a new typesetter.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn new() -> Compiler<'p> {
|
pub fn new() -> Typesetter<'p> {
|
||||||
Compiler {
|
Typesetter {
|
||||||
style: Style::default(),
|
base_page_style: PageStyle::default(),
|
||||||
|
base_text_style: TextStyle::default(),
|
||||||
font_providers: vec![],
|
font_providers: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the default style for the document.
|
/// Set the default page style for the document.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn set_style(&mut self, style: Style) {
|
pub fn set_page_style(&mut self, style: PageStyle) {
|
||||||
self.style = style;
|
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]
|
#[inline]
|
||||||
pub fn add_font_provider<P: 'p>(&mut self, provider: P) where P: FontProvider {
|
pub fn add_font_provider<P: 'p>(&mut self, provider: P) where P: FontProvider {
|
||||||
self.font_providers.push(Box::new(provider));
|
self.font_providers.push(Box::new(provider));
|
||||||
@ -98,45 +108,50 @@ impl<'p> Compiler<'p> {
|
|||||||
#[inline]
|
#[inline]
|
||||||
pub fn parse(&self, src: &str) -> ParseResult<SyntaxTree> {
|
pub fn parse(&self, src: &str) -> ParseResult<SyntaxTree> {
|
||||||
let scope = Scope::with_std();
|
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]
|
#[inline]
|
||||||
pub fn typeset(&self, tree: &SyntaxTree) -> TypesetResult<Document> {
|
pub fn layout(&self, tree: &SyntaxTree) -> LayoutResult<(Layout, Vec<Font>)> {
|
||||||
typeset(&tree, &self.style, &self.font_providers).map_err(Into::into)
|
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]
|
#[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 tree = self.parse(src)?;
|
||||||
let document = self.typeset(&tree)?;
|
let (layout, fonts) = self.layout(&tree)?;
|
||||||
|
let document = layout.into_document(fonts);
|
||||||
Ok(document)
|
Ok(document)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The general error type for compilation.
|
/// The general error type for typesetting.
|
||||||
pub enum CompileError {
|
pub enum TypesetError {
|
||||||
/// An error that occured while transforming source code into
|
/// An error that occured while transforming source code into
|
||||||
/// an abstract syntax tree.
|
/// an abstract syntax tree.
|
||||||
ParseErr(ParseError),
|
Parse(ParseError),
|
||||||
/// An error that occured while typesetting into an abstract document.
|
/// An error that occured while layouting.
|
||||||
TypesetErr(TypesetError),
|
Layout(LayoutError),
|
||||||
}
|
}
|
||||||
|
|
||||||
error_type! {
|
error_type! {
|
||||||
err: CompileError,
|
err: TypesetError,
|
||||||
show: f => match err {
|
show: f => match err {
|
||||||
CompileError::ParseErr(e) => write!(f, "parse error: {}", e),
|
TypesetError::Parse(e) => write!(f, "parse error: {}", e),
|
||||||
CompileError::TypesetErr(e) => write!(f, "typeset error: {}", e),
|
TypesetError::Layout(e) => write!(f, "layout error: {}", e),
|
||||||
},
|
},
|
||||||
source: match err {
|
source: match err {
|
||||||
CompileError::ParseErr(e) => Some(e),
|
TypesetError::Parse(e) => Some(e),
|
||||||
CompileError::TypesetErr(e) => Some(e),
|
TypesetError::Layout(e) => Some(e),
|
||||||
},
|
},
|
||||||
from: (ParseError, CompileError::ParseErr(err)),
|
from: (ParseError, TypesetError::Parse(err)),
|
||||||
from: (TypesetError, CompileError::TypesetErr(err)),
|
from: (LayoutError, TypesetError::Layout(err)),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -144,15 +159,14 @@ error_type! {
|
|||||||
mod test {
|
mod test {
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::BufWriter;
|
use std::io::BufWriter;
|
||||||
use crate::Compiler;
|
use crate::Typesetter;
|
||||||
use crate::export::pdf::PdfExporter;
|
use crate::export::pdf::PdfExporter;
|
||||||
use crate::font::FileSystemFontProvider;
|
use crate::font::FileSystemFontProvider;
|
||||||
|
|
||||||
/// Create a pdf with a name from the source code.
|
/// Create a pdf with a name from the source code.
|
||||||
fn test(name: &str, src: &str) {
|
fn test(name: &str, src: &str) {
|
||||||
// Create compiler
|
let mut typesetter = Typesetter::new();
|
||||||
let mut compiler = Compiler::new();
|
typesetter.add_font_provider(FileSystemFontProvider::new("../fonts", vec![
|
||||||
compiler.add_font_provider(FileSystemFontProvider::new("../fonts", vec![
|
|
||||||
("NotoSans-Regular.ttf", font_info!(["NotoSans", "Noto", SansSerif])),
|
("NotoSans-Regular.ttf", font_info!(["NotoSans", "Noto", SansSerif])),
|
||||||
("NotoSans-Italic.ttf", font_info!(["NotoSans", "Noto", SansSerif], italic)),
|
("NotoSans-Italic.ttf", font_info!(["NotoSans", "Noto", SansSerif], italic)),
|
||||||
("NotoSans-Bold.ttf", font_info!(["NotoSans", "Noto", SansSerif], bold)),
|
("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])),
|
("NotoEmoji-Regular.ttf", font_info!(["NotoEmoji", "Noto", SansSerif, Serif, Monospace])),
|
||||||
]));
|
]));
|
||||||
|
|
||||||
// Compile into document
|
// Typeset into document
|
||||||
let document = compiler.compile(src).unwrap();
|
let document = typesetter.typeset(src).unwrap();
|
||||||
|
|
||||||
// Write to file
|
// Write to file
|
||||||
let path = format!("../target/typeset-unit-{}.pdf", name);
|
let path = format!("../target/typeset-unit-{}.pdf", name);
|
||||||
|
@ -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]
|
#[inline]
|
||||||
pub fn parse(src: &str, scope: &Scope) -> ParseResult<SyntaxTree> {
|
pub fn parse(src: &str, ctx: &ParseContext) -> ParseResult<SyntaxTree> {
|
||||||
Parser::new(src, scope).parse()
|
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.
|
/// Transforms token streams to syntax trees.
|
||||||
struct Parser<'s> {
|
struct Parser<'s> {
|
||||||
src: &'s str,
|
src: &'s str,
|
||||||
tokens: PeekableTokens<'s>,
|
tokens: PeekableTokens<'s>,
|
||||||
scope: &'s Scope,
|
|
||||||
state: ParserState,
|
state: ParserState,
|
||||||
|
ctx: &'s ParseContext<'s>,
|
||||||
tree: SyntaxTree,
|
tree: SyntaxTree,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -352,11 +359,11 @@ enum ParserState {
|
|||||||
|
|
||||||
impl<'s> Parser<'s> {
|
impl<'s> Parser<'s> {
|
||||||
/// Create a new parser from a stream of tokens and a scope of functions.
|
/// 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 {
|
Parser {
|
||||||
src,
|
src,
|
||||||
tokens: PeekableTokens::new(tokenize(src)),
|
tokens: PeekableTokens::new(tokenize(src)),
|
||||||
scope,
|
ctx,
|
||||||
state: ParserState::Body,
|
state: ParserState::Body,
|
||||||
tree: SyntaxTree::new(),
|
tree: SyntaxTree::new(),
|
||||||
}
|
}
|
||||||
@ -454,13 +461,9 @@ impl<'s> Parser<'s> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Now we want to parse this function dynamically.
|
// 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)))?;
|
.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.
|
// Do the parsing dependent on whether the function has a body.
|
||||||
Ok(if has_body {
|
Ok(if has_body {
|
||||||
// Find out the string which makes the body of this function.
|
// Find out the string which makes the body of this function.
|
||||||
@ -471,7 +474,7 @@ impl<'s> Parser<'s> {
|
|||||||
|
|
||||||
// Parse the body.
|
// Parse the body.
|
||||||
let body_string = &self.src[start .. end];
|
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.
|
// Skip to the end of the function in the token stream.
|
||||||
self.tokens.goto(end);
|
self.tokens.goto(end);
|
||||||
@ -481,7 +484,7 @@ impl<'s> Parser<'s> {
|
|||||||
|
|
||||||
body
|
body
|
||||||
} else {
|
} 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.
|
/// Whether this word is a valid unicode identifier.
|
||||||
fn is_identifier(string: &str) -> bool {
|
fn is_identifier(string: &str) -> bool {
|
||||||
let mut chars = string.chars();
|
let mut chars = string.chars();
|
||||||
@ -801,7 +797,7 @@ mod token_tests {
|
|||||||
mod parse_tests {
|
mod parse_tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::func::{Function, Scope};
|
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 Node::{Space as S, Newline as N, Func as F};
|
||||||
use funcs::*;
|
use funcs::*;
|
||||||
|
|
||||||
@ -818,13 +814,14 @@ mod parse_tests {
|
|||||||
fn parse(_: &FuncHeader, body: Option<&str>, ctx: &ParseContext)
|
fn parse(_: &FuncHeader, body: Option<&str>, ctx: &ParseContext)
|
||||||
-> ParseResult<Self> where Self: Sized {
|
-> ParseResult<Self> where Self: Sized {
|
||||||
if let Some(src) = body {
|
if let Some(src) = body {
|
||||||
parse(src, ctx.scope).map(|tree| TreeFn(tree))
|
parse(src, ctx).map(|tree| TreeFn(tree))
|
||||||
} else {
|
} else {
|
||||||
Err(ParseError::new("expected body for tree fn"))
|
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.
|
/// 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.
|
/// Test if the source code parses into the syntax tree.
|
||||||
fn test(src: &str, tree: SyntaxTree) {
|
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.
|
/// Test with a scope containing function definitions.
|
||||||
fn test_scoped(scope: &Scope, src: &str, tree: SyntaxTree) {
|
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.
|
/// Test if the source parses into the error.
|
||||||
fn test_err(src: &str, err: &str) {
|
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.
|
/// Test with a scope if the source parses into the error.
|
||||||
fn test_err_scoped(scope: &Scope, src: &str, err: &str) {
|
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.
|
/// Create a text node.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user