mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
Unify parsing and typesetting functions. 🗳
This commit is contained in:
parent
5c66bac689
commit
e3215fa3b9
@ -46,7 +46,7 @@ fn run() -> Result<(), Box<Error>> {
|
|||||||
]));
|
]));
|
||||||
|
|
||||||
// Compile the source code with the compiler.
|
// Compile the source code with the compiler.
|
||||||
let document = compiler.typeset(&src)?;
|
let document = compiler.compile(&src)?;
|
||||||
|
|
||||||
|
|
||||||
// Export the document into a PDF file.
|
// Export the document into a PDF file.
|
||||||
|
179
src/engine/loader.rs
Normal file
179
src/engine/loader.rs
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
//! 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,25 +1,32 @@
|
|||||||
//! Core typesetting engine.
|
//! Core typesetting engine.
|
||||||
|
|
||||||
use std::cell::{RefCell, Ref};
|
use std::cell::Ref;
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::mem::swap;
|
use std::mem::swap;
|
||||||
|
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
use crate::syntax::{SyntaxTree, Node};
|
|
||||||
use crate::doc::{Document, Page, Text, TextCommand};
|
use crate::doc::{Document, Page, Text, TextCommand};
|
||||||
use crate::font::{Font, FontFamily, FontInfo, FontError};
|
use crate::font::{Font, FontFamily, FontProvider, FontError};
|
||||||
use crate::Context;
|
use crate::syntax::{SyntaxTree, Node};
|
||||||
|
use loader::{FontLoader, FontQuery};
|
||||||
|
|
||||||
mod size;
|
mod size;
|
||||||
|
mod loader;
|
||||||
pub use size::Size;
|
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.
|
/// The core typesetting engine, transforming an abstract syntax tree into a document.
|
||||||
pub struct Engine<'a> {
|
struct Engine<'a> {
|
||||||
// Input
|
// Input
|
||||||
tree: &'a SyntaxTree,
|
tree: &'a SyntaxTree,
|
||||||
ctx: &'a Context<'a>,
|
style: &'a Style,
|
||||||
|
|
||||||
// Internal
|
// Internal
|
||||||
font_loader: FontLoader<'a>,
|
font_loader: FontLoader<'a>,
|
||||||
@ -38,12 +45,15 @@ pub struct Engine<'a> {
|
|||||||
|
|
||||||
impl<'a> Engine<'a> {
|
impl<'a> Engine<'a> {
|
||||||
/// Create a new generator from a syntax tree.
|
/// Create a new generator from a syntax tree.
|
||||||
#[inline]
|
fn new(
|
||||||
pub fn new(tree: &'a SyntaxTree, context: &'a Context<'a>) -> Engine<'a> {
|
tree: &'a SyntaxTree,
|
||||||
|
style: &'a Style,
|
||||||
|
font_providers: &'a [Box<dyn FontProvider + 'a>]
|
||||||
|
) -> Engine<'a> {
|
||||||
Engine {
|
Engine {
|
||||||
tree,
|
tree,
|
||||||
ctx: context,
|
style,
|
||||||
font_loader: FontLoader::new(context),
|
font_loader: FontLoader::new(font_providers),
|
||||||
text_commands: vec![],
|
text_commands: vec![],
|
||||||
active_font: std::usize::MAX,
|
active_font: std::usize::MAX,
|
||||||
current_text: String::new(),
|
current_text: String::new(),
|
||||||
@ -55,7 +65,7 @@ impl<'a> Engine<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Generate the abstract document.
|
/// Generate the abstract document.
|
||||||
pub fn typeset(mut self) -> TypesetResult<Document> {
|
fn typeset(mut self) -> TypesetResult<Document> {
|
||||||
// Start by moving to a suitable position.
|
// Start by moving to a suitable position.
|
||||||
self.move_start();
|
self.move_start();
|
||||||
|
|
||||||
@ -66,7 +76,7 @@ impl<'a> Engine<'a> {
|
|||||||
Node::Space => self.write_space()?,
|
Node::Space => self.write_space()?,
|
||||||
Node::Newline => {
|
Node::Newline => {
|
||||||
self.write_buffered_text();
|
self.write_buffered_text();
|
||||||
self.move_newline(self.ctx.style.paragraph_spacing);
|
self.move_newline(self.style.paragraph_spacing);
|
||||||
},
|
},
|
||||||
|
|
||||||
Node::ToggleItalics => self.italic = !self.italic,
|
Node::ToggleItalics => self.italic = !self.italic,
|
||||||
@ -83,8 +93,8 @@ impl<'a> Engine<'a> {
|
|||||||
// Create a document with one page from the contents.
|
// Create a document with one page from the contents.
|
||||||
Ok(Document {
|
Ok(Document {
|
||||||
pages: vec![Page {
|
pages: vec![Page {
|
||||||
width: self.ctx.style.width,
|
width: self.style.width,
|
||||||
height: self.ctx.style.height,
|
height: self.style.height,
|
||||||
text: vec![Text {
|
text: vec![Text {
|
||||||
commands: self.text_commands,
|
commands: self.text_commands,
|
||||||
}],
|
}],
|
||||||
@ -152,8 +162,8 @@ impl<'a> Engine<'a> {
|
|||||||
fn move_start(&mut self) {
|
fn move_start(&mut self) {
|
||||||
// Move cursor to top-left position
|
// Move cursor to top-left position
|
||||||
self.text_commands.push(TextCommand::Move(
|
self.text_commands.push(TextCommand::Move(
|
||||||
self.ctx.style.margin_left,
|
self.style.margin_left,
|
||||||
self.ctx.style.height - self.ctx.style.margin_top
|
self.style.height - self.style.margin_top
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,8 +176,8 @@ impl<'a> Engine<'a> {
|
|||||||
let vertical_move = if self.current_max_vertical_move == Size::zero() {
|
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
|
// If max vertical move is still zero, the line is empty and we take the
|
||||||
// font size from the previous line.
|
// font size from the previous line.
|
||||||
self.ctx.style.font_size
|
self.style.font_size
|
||||||
* self.ctx.style.line_spacing
|
* self.style.line_spacing
|
||||||
* self.get_font_at(self.active_font).metrics.ascender
|
* self.get_font_at(self.active_font).metrics.ascender
|
||||||
* factor
|
* factor
|
||||||
} else {
|
} else {
|
||||||
@ -181,21 +191,21 @@ impl<'a> Engine<'a> {
|
|||||||
|
|
||||||
/// Set the current font.
|
/// Set the current font.
|
||||||
fn set_font(&mut self, index: usize) {
|
fn set_font(&mut self, index: usize) {
|
||||||
self.text_commands.push(TextCommand::SetFont(index, self.ctx.style.font_size));
|
self.text_commands.push(TextCommand::SetFont(index, self.style.font_size));
|
||||||
self.active_font = index;
|
self.active_font = index;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the current line plus the extra `width` would overflow the line.
|
/// Whether the current line plus the extra `width` would overflow the line.
|
||||||
fn would_overflow(&self, width: Size) -> bool {
|
fn would_overflow(&self, width: Size) -> bool {
|
||||||
let max_width = self.ctx.style.width
|
let max_width = self.style.width
|
||||||
- self.ctx.style.margin_left - self.ctx.style.margin_right;
|
- self.style.margin_left - self.style.margin_right;
|
||||||
self.current_line_width + width > max_width
|
self.current_line_width + width > max_width
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load a font that has the character we need.
|
/// Load a font that has the character we need.
|
||||||
fn get_font_for(&self, character: char) -> TypesetResult<(usize, Ref<Font>)> {
|
fn get_font_for(&self, character: char) -> TypesetResult<(usize, Ref<Font>)> {
|
||||||
self.font_loader.get(FontQuery {
|
self.font_loader.get(FontQuery {
|
||||||
families: &self.ctx.style.font_families,
|
families: &self.style.font_families,
|
||||||
italic: self.italic,
|
italic: self.italic,
|
||||||
bold: self.bold,
|
bold: self.bold,
|
||||||
character,
|
character,
|
||||||
@ -209,167 +219,13 @@ impl<'a> Engine<'a> {
|
|||||||
|
|
||||||
/// The width of a char in a specific font.
|
/// The width of a char in a specific font.
|
||||||
fn char_width(&self, character: char, font: &Font) -> Size {
|
fn char_width(&self, character: char, font: &Font) -> Size {
|
||||||
font.widths[font.map(character) as usize] * self.ctx.style.font_size
|
font.widths[font.map(character) as usize] * self.style.font_size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Serves matching fonts given a query.
|
/// The context for typesetting a function.
|
||||||
struct FontLoader<'a> {
|
#[derive(Debug)]
|
||||||
/// The context containing the used font providers.
|
pub struct TypesetContext {}
|
||||||
context: &'a Context<'a>,
|
|
||||||
/// All available fonts indexed by provider.
|
|
||||||
provider_fonts: Vec<&'a [FontInfo]>,
|
|
||||||
/// The internal state.
|
|
||||||
state: RefCell<FontLoaderState<'a>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Internal state of the font loader (wrapped in a RefCell).
|
|
||||||
struct FontLoaderState<'a> {
|
|
||||||
/// The loaded fonts along with their external indices.
|
|
||||||
fonts: Vec<(Option<usize>, Font)>,
|
|
||||||
/// Allows to retrieve cached results for queries.
|
|
||||||
query_cache: HashMap<FontQuery<'a>, usize>,
|
|
||||||
/// Allows to lookup fonts by their infos.
|
|
||||||
info_cache: HashMap<&'a FontInfo, usize>,
|
|
||||||
/// Indexed by outside and indices maps to internal indices.
|
|
||||||
inner_index: Vec<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> FontLoader<'a> {
|
|
||||||
/// Create a new font loader.
|
|
||||||
pub fn new(context: &'a Context<'a>) -> FontLoader {
|
|
||||||
let provider_fonts = context.font_providers.iter()
|
|
||||||
.map(|prov| prov.available()).collect();
|
|
||||||
|
|
||||||
FontLoader {
|
|
||||||
context,
|
|
||||||
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<'a>) -> 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.context.font_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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A query for a font with specific properties.
|
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
|
||||||
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.
|
|
||||||
families: &'a [FontFamily],
|
|
||||||
/// Whether the font shall be in italics.
|
|
||||||
italic: bool,
|
|
||||||
/// Whether the font shall be in boldface.
|
|
||||||
bold: bool,
|
|
||||||
/// Which character we need.
|
|
||||||
character: char,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Default styles for typesetting.
|
/// Default styles for typesetting.
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
@ -209,7 +209,7 @@ pub struct FontInfo {
|
|||||||
/// A macro to create [FontInfos](crate::font::FontInfo) easily.
|
/// A macro to create [FontInfos](crate::font::FontInfo) easily.
|
||||||
///
|
///
|
||||||
/// Accepts first a bracketed (ordered) list of font families. Allowed are string expressions
|
/// Accepts first a bracketed (ordered) list of font families. Allowed are string expressions
|
||||||
/// aswell as the three base families `SansSerif`, `Serif` and `Monospace`.
|
/// as well as the three base families `SansSerif`, `Serif` and `Monospace`.
|
||||||
///
|
///
|
||||||
/// Then there may follow (separated by commas) the keywords `italic` and/or `bold`.
|
/// Then there may follow (separated by commas) the keywords `italic` and/or `bold`.
|
||||||
///
|
///
|
||||||
|
22
src/func.rs
22
src/func.rs
@ -4,8 +4,9 @@ use std::any::Any;
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt::{self, Debug, Formatter};
|
use std::fmt::{self, Debug, Formatter};
|
||||||
|
|
||||||
use crate::syntax::{FuncHeader, Expression};
|
use crate::syntax::FuncHeader;
|
||||||
use crate::parsing::{FuncContext, ParseResult};
|
use crate::parsing::{ParseContext, ParseResult};
|
||||||
|
use crate::engine::{TypesetContext, TypesetResult};
|
||||||
|
|
||||||
|
|
||||||
/// Types that act as functions.
|
/// Types that act as functions.
|
||||||
@ -17,10 +18,11 @@ use crate::parsing::{FuncContext, ParseResult};
|
|||||||
/// 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 tokens of the context with the given header and scope into self.
|
||||||
fn parse(context: FuncContext) -> ParseResult<Self> where Self: Sized;
|
fn parse(header: &FuncHeader, body: Option<&str>, ctx: &ParseContext)
|
||||||
|
-> ParseResult<Self> where Self: Sized;
|
||||||
|
|
||||||
/// Execute the function and optionally yield a return value.
|
/// Execute the function and optionally yield a return value.
|
||||||
fn typeset(&self, header: &FuncHeader) -> Option<Expression>;
|
fn typeset(&self, ctx: &TypesetContext) -> TypesetResult<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for dyn Function {
|
impl PartialEq for dyn Function {
|
||||||
@ -61,7 +63,8 @@ pub struct Scope {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A function which transforms a parsing context into a boxed function.
|
/// A function which transforms a parsing context into a boxed function.
|
||||||
type ParseFunc = dyn Fn(FuncContext) -> ParseResult<Box<dyn Function>>;
|
type ParseFunc = dyn Fn(&FuncHeader, Option<&str>, &ParseContext)
|
||||||
|
-> ParseResult<Box<dyn Function>>;
|
||||||
|
|
||||||
impl Scope {
|
impl Scope {
|
||||||
/// Create a new empty scope.
|
/// Create a new empty scope.
|
||||||
@ -69,12 +72,17 @@ impl Scope {
|
|||||||
Scope { parsers: HashMap::new() }
|
Scope { parsers: HashMap::new() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a new scope with the standard functions contained.
|
||||||
|
pub fn with_std() -> Scope {
|
||||||
|
Scope::new()
|
||||||
|
}
|
||||||
|
|
||||||
/// Add a function type to the scope with a given name.
|
/// Add a function type to the scope with a given name.
|
||||||
pub fn add<F: Function + 'static>(&mut self, name: &str) {
|
pub fn add<F: Function + 'static>(&mut self, name: &str) {
|
||||||
self.parsers.insert(
|
self.parsers.insert(
|
||||||
name.to_owned(),
|
name.to_owned(),
|
||||||
Box::new(|context| {
|
Box::new(|h, b, c| {
|
||||||
F::parse(context).map(|func| Box::new(func) as Box<dyn Function>)
|
F::parse(h, b, c).map(|func| Box::new(func) as Box<dyn Function>)
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
115
src/lib.rs
115
src/lib.rs
@ -31,8 +31,8 @@
|
|||||||
//! ("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.
|
//! // Compile the source code into a document with the compiler.
|
||||||
//! let document = compiler.typeset(src).unwrap();
|
//! let document = compiler.compile(src).unwrap();
|
||||||
//!
|
//!
|
||||||
//! // Export the document into a PDF file.
|
//! // Export the document into a PDF file.
|
||||||
//! # /*
|
//! # /*
|
||||||
@ -43,10 +43,8 @@
|
|||||||
//! exporter.export(&document, file).unwrap();
|
//! exporter.export(&document, file).unwrap();
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
use std::fmt::{self, Debug, Formatter};
|
|
||||||
|
|
||||||
use crate::doc::Document;
|
use crate::doc::Document;
|
||||||
use crate::engine::{Engine, Style, TypesetError};
|
use crate::engine::{typeset, Style, TypesetResult, TypesetError};
|
||||||
use crate::func::Scope;
|
use crate::func::Scope;
|
||||||
use crate::font::FontProvider;
|
use crate::font::FontProvider;
|
||||||
use crate::parsing::{parse, ParseResult, ParseError};
|
use crate::parsing::{parse, ParseResult, ParseError};
|
||||||
@ -68,7 +66,10 @@ pub mod syntax;
|
|||||||
///
|
///
|
||||||
/// Holds the compilation context, which can be configured through various methods.
|
/// Holds the compilation context, which can be configured through various methods.
|
||||||
pub struct Compiler<'p> {
|
pub struct Compiler<'p> {
|
||||||
context: Context<'p>,
|
/// Style for typesetting.
|
||||||
|
style: Style,
|
||||||
|
/// Font providers.
|
||||||
|
font_providers: Vec<Box<dyn FontProvider + 'p>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'p> Compiler<'p> {
|
impl<'p> Compiler<'p> {
|
||||||
@ -76,55 +77,42 @@ impl<'p> Compiler<'p> {
|
|||||||
#[inline]
|
#[inline]
|
||||||
pub fn new() -> Compiler<'p> {
|
pub fn new() -> Compiler<'p> {
|
||||||
Compiler {
|
Compiler {
|
||||||
context: Context {
|
style: Style::default(),
|
||||||
style: Style::default(),
|
font_providers: vec![],
|
||||||
font_providers: vec![],
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the default style for the document.
|
/// Set the default style for the document.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn set_style(&mut self, style: Style) {
|
pub fn set_style(&mut self, style: Style) {
|
||||||
self.context.style = style;
|
self.style = style;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a font provider to the context of this compiler.
|
/// Add a font provider to the context of this compiler.
|
||||||
#[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.context.font_providers.push(Box::new(provider));
|
self.font_providers.push(Box::new(provider));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse source code into a syntax tree.
|
/// Parse source code into a syntax tree.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn parse(&self, src: &str) -> ParseResult<SyntaxTree> {
|
pub fn parse(&self, src: &str) -> ParseResult<SyntaxTree> {
|
||||||
let scope = Scope::new();
|
let scope = Scope::with_std();
|
||||||
parse(src, &scope)
|
parse(src, &scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Typeset a parsed syntax tree into a document.
|
||||||
|
#[inline]
|
||||||
|
pub fn typeset(&self, tree: &SyntaxTree) -> TypesetResult<Document> {
|
||||||
|
typeset(&tree, &self.style, &self.font_providers).map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
/// Compile a portable typesetted document from source code.
|
/// Compile a portable typesetted document from source code.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn typeset(&self, src: &str) -> CompileResult<Document> {
|
pub fn compile(&self, src: &str) -> Result<Document, CompileError> {
|
||||||
let tree = self.parse(src)?;
|
let tree = self.parse(src)?;
|
||||||
let engine = Engine::new(&tree, &self.context);
|
let document = self.typeset(&tree)?;
|
||||||
engine.typeset().map_err(Into::into)
|
Ok(document)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Holds the compilation context.
|
|
||||||
pub struct Context<'p> {
|
|
||||||
/// Style for typesetting.
|
|
||||||
style: Style,
|
|
||||||
/// Font providers.
|
|
||||||
font_providers: Vec<Box<dyn FontProvider + 'p>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Debug for Context<'_> {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
|
||||||
f.debug_struct("Context")
|
|
||||||
.field("style", &self.style)
|
|
||||||
.field("font_providers", &self.font_providers.len())
|
|
||||||
.finish()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,26 +120,23 @@ impl Debug for Context<'_> {
|
|||||||
pub enum CompileError {
|
pub enum CompileError {
|
||||||
/// 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.
|
||||||
Parse(ParseError),
|
ParseErr(ParseError),
|
||||||
/// An error that occured while typesetting into an abstract document.
|
/// An error that occured while typesetting into an abstract document.
|
||||||
Typeset(TypesetError),
|
TypesetErr(TypesetError),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The result type for compilation.
|
|
||||||
pub type CompileResult<T> = Result<T, CompileError>;
|
|
||||||
|
|
||||||
error_type! {
|
error_type! {
|
||||||
err: CompileError,
|
err: CompileError,
|
||||||
show: f => match err {
|
show: f => match err {
|
||||||
CompileError::Parse(e) => write!(f, "parse error: {}", e),
|
CompileError::ParseErr(e) => write!(f, "parse error: {}", e),
|
||||||
CompileError::Typeset(e) => write!(f, "typeset error: {}", e),
|
CompileError::TypesetErr(e) => write!(f, "typeset error: {}", e),
|
||||||
},
|
},
|
||||||
source: match err {
|
source: match err {
|
||||||
CompileError::Parse(e) => Some(e),
|
CompileError::ParseErr(e) => Some(e),
|
||||||
CompileError::Typeset(e) => Some(e),
|
CompileError::TypesetErr(e) => Some(e),
|
||||||
},
|
},
|
||||||
from: (ParseError, CompileError::Parse(err)),
|
from: (ParseError, CompileError::ParseErr(err)),
|
||||||
from: (TypesetError, CompileError::Typeset(err)),
|
from: (TypesetError, CompileError::TypesetErr(err)),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -177,7 +162,7 @@ mod test {
|
|||||||
]));
|
]));
|
||||||
|
|
||||||
// Compile into document
|
// Compile into document
|
||||||
let document = compiler.typeset(src).unwrap();
|
let document = compiler.compile(src).unwrap();
|
||||||
|
|
||||||
// Write to file
|
// Write to file
|
||||||
let path = format!("../target/typeset-unit-{}.pdf", name);
|
let path = format!("../target/typeset-unit-{}.pdf", name);
|
||||||
@ -187,38 +172,30 @@ mod test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn simple() {
|
fn features() {
|
||||||
test("parentheses", "Text with ) and ( or (enclosed) works.");
|
test("features", r"
|
||||||
test("multiline-lorem","
|
**FEATURES TEST PAGE**
|
||||||
|
|
||||||
|
__Simple multiline:__
|
||||||
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy
|
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy
|
||||||
eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam
|
eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam
|
||||||
voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet
|
voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet
|
||||||
clita kasd gubergren, no sea takimata sanctus est.
|
clita kasd gubergren, no sea takimata sanctus est.
|
||||||
|
|
||||||
|
__Parentheses:__ Text with ) and ( or (enclosed) works.
|
||||||
|
|
||||||
|
__Composite character:__ ‼
|
||||||
|
|
||||||
|
__Unicode:__ ∑mbe∂∂ed font with Unicode!
|
||||||
|
|
||||||
|
__Emoji:__ Hello World 🌍!
|
||||||
|
|
||||||
|
__Styles:__ This is **bold** and that is __great__!
|
||||||
");
|
");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn composite_glyph() {
|
fn wikipedia() {
|
||||||
test("composite-glyph", "Composite character‼");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn unicode() {
|
|
||||||
test("unicode", "∑mbe∂∂ed font with Unicode!");
|
|
||||||
test("mixed-emoji", "Hello World 🌍!")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn styled() {
|
|
||||||
test("styled", "
|
|
||||||
**Hello World**.
|
|
||||||
|
|
||||||
That's __great__!
|
|
||||||
");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn long_wikipedia() {
|
|
||||||
test("wikipedia", r#"
|
test("wikipedia", r#"
|
||||||
Typesetting is the composition of text by means of arranging physical types or the
|
Typesetting is the composition of text by means of arranging physical types or the
|
||||||
digital equivalents. Stored letters and other symbols (called sorts in mechanical
|
digital equivalents. Stored letters and other symbols (called sorts in mechanical
|
||||||
|
@ -457,6 +457,10 @@ impl<'s> Parser<'s> {
|
|||||||
let parser = self.scope.get_parser(&header.name)
|
let parser = self.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.
|
||||||
@ -467,11 +471,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(FuncContext {
|
let body = parser(&header, Some(body_string), &parse_context)?;
|
||||||
header: &header,
|
|
||||||
body: Some(body_string),
|
|
||||||
scope: &self.scope,
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// 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,11 +481,7 @@ impl<'s> Parser<'s> {
|
|||||||
|
|
||||||
body
|
body
|
||||||
} else {
|
} else {
|
||||||
parser(FuncContext {
|
parser(&header, None, &parse_context)?
|
||||||
header: &header,
|
|
||||||
body: None,
|
|
||||||
scope: &self.scope,
|
|
||||||
})?
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -633,12 +629,8 @@ impl<'s> Iterator for PeekableTokens<'s> {
|
|||||||
|
|
||||||
/// The context for parsing a function.
|
/// The context for parsing a function.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct FuncContext<'s> {
|
pub struct ParseContext<'s> {
|
||||||
/// The header of the function to be parsed.
|
/// The scope containing function definitions.
|
||||||
pub header: &'s FuncHeader,
|
|
||||||
/// The body source if the function has a body, otherwise nothing.
|
|
||||||
pub body: Option<&'s str>,
|
|
||||||
/// The current scope containing function definitions.
|
|
||||||
pub scope: &'s Scope,
|
pub scope: &'s Scope,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -668,7 +660,8 @@ pub struct ParseError(String);
|
|||||||
pub type ParseResult<T> = Result<T, ParseError>;
|
pub type ParseResult<T> = Result<T, ParseError>;
|
||||||
|
|
||||||
impl ParseError {
|
impl ParseError {
|
||||||
fn new<S: Into<String>>(message: S) -> ParseError {
|
/// Create a new parse error with a message.
|
||||||
|
pub fn new<S: Into<String>>(message: S) -> ParseError {
|
||||||
ParseError(message.into())
|
ParseError(message.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -807,9 +800,10 @@ mod token_tests {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod parse_tests {
|
mod parse_tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use funcs::*;
|
|
||||||
use crate::func::{Function, Scope};
|
use crate::func::{Function, Scope};
|
||||||
|
use crate::engine::{TypesetContext, TypesetResult};
|
||||||
use Node::{Space as S, Newline as N, Func as F};
|
use Node::{Space as S, Newline as N, Func as F};
|
||||||
|
use funcs::*;
|
||||||
|
|
||||||
/// Two test functions, one which parses it's body as another syntax tree
|
/// Two test functions, one which parses it's body as another syntax tree
|
||||||
/// and another one which does not expect a body.
|
/// and another one which does not expect a body.
|
||||||
@ -821,14 +815,16 @@ mod parse_tests {
|
|||||||
pub struct TreeFn(pub SyntaxTree);
|
pub struct TreeFn(pub SyntaxTree);
|
||||||
|
|
||||||
impl Function for TreeFn {
|
impl Function for TreeFn {
|
||||||
fn parse(context: FuncContext) -> ParseResult<Self> where Self: Sized {
|
fn parse(_: &FuncHeader, body: Option<&str>, ctx: &ParseContext)
|
||||||
if let Some(src) = context.body {
|
-> ParseResult<Self> where Self: Sized {
|
||||||
parse(src, context.scope).map(|tree| TreeFn(tree))
|
if let Some(src) = body {
|
||||||
|
parse(src, ctx.scope).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, _header: &FuncHeader) -> Option<Expression> { None }
|
|
||||||
|
fn typeset(&self, _: &TypesetContext) -> TypesetResult<()> { Ok(()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A testing function without a body.
|
/// A testing function without a body.
|
||||||
@ -836,14 +832,16 @@ mod parse_tests {
|
|||||||
pub struct BodylessFn;
|
pub struct BodylessFn;
|
||||||
|
|
||||||
impl Function for BodylessFn {
|
impl Function for BodylessFn {
|
||||||
fn parse(context: FuncContext) -> ParseResult<Self> where Self: Sized {
|
fn parse(_: &FuncHeader, body: Option<&str>, _: &ParseContext)
|
||||||
if context.body.is_none() {
|
-> ParseResult<Self> where Self: Sized {
|
||||||
|
if body.is_none() {
|
||||||
Ok(BodylessFn)
|
Ok(BodylessFn)
|
||||||
} else {
|
} else {
|
||||||
Err(ParseError::new("unexpected body for bodyless fn"))
|
Err(ParseError::new("unexpected body for bodyless fn"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn typeset(&self, _header: &FuncHeader) -> Option<Expression> { None }
|
|
||||||
|
fn typeset(&self, _: &TypesetContext) -> TypesetResult<()> { Ok(()) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,6 +97,6 @@ pub struct FuncHeader {
|
|||||||
pub kwargs: HashMap<String, Expression>
|
pub kwargs: HashMap<String, Expression>
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A potentially unevaluated expression.
|
/// A value expression.
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum Expression {}
|
pub enum Expression {}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user