├── src ├── document │ ├── document.rs │ ├── counters.rs │ ├── configuration.rs │ └── mod.rs ├── parser │ ├── tests │ │ ├── mod.rs │ │ ├── successes.rs │ │ └── errors.rs │ ├── mod.rs │ ├── utils.rs │ ├── warning.rs │ ├── error.rs │ ├── ast.rs │ └── combinators.rs ├── layout │ ├── paragraphs │ │ ├── utils │ │ │ ├── mod.rs │ │ │ ├── paragraphs.rs │ │ │ ├── ast.rs │ │ │ └── linebreak.rs │ │ ├── mod.rs │ │ ├── graph.rs │ │ ├── items.rs │ │ ├── ligatures.rs │ │ ├── justification.rs │ │ └── engine.rs │ ├── mod.rs │ └── constants.rs ├── fonts │ ├── configuration.rs │ ├── styles.rs │ ├── mod.rs │ └── manager.rs ├── lib.rs └── main.rs ├── assets ├── tests │ ├── successes │ │ ├── test-title-1.dex │ │ ├── test-title-2.dex │ │ └── test-titles.dex │ └── errors │ │ ├── test-unmatched-slash.dex │ │ ├── test-unmatched-star.dex │ │ ├── test-unmatched-dollar.dex │ │ ├── test-mixed-star-slash.dex │ │ ├── test-title-no-new-line.dex │ │ └── test-accent.dex ├── fonts │ ├── cmunbi.ttf │ ├── cmunbl.ttf │ ├── cmunbx.ttf │ ├── cmunci.ttf │ ├── cmunit.ttf │ ├── cmunrm.ttf │ ├── cmunsi.ttf │ ├── cmunsl.ttf │ ├── cmunso.ttf │ ├── cmunss.ttf │ ├── cmunsx.ttf │ ├── cmuntb.ttf │ ├── cmunti.ttf │ ├── cmuntt.ttf │ ├── cmuntx.ttf │ ├── cmunui.ttf │ ├── cmunvi.ttf │ ├── cmunvt.ttf │ ├── cmunbmo.ttf │ ├── cmunbmr.ttf │ ├── cmunbso.ttf │ ├── cmunbsr.ttf │ ├── cmunbtl.ttf │ ├── cmunbto.ttf │ ├── cmunobi.ttf │ ├── cmunobx.ttf │ ├── cmunorm.ttf │ ├── cmunoti.ttf │ ├── cmunssdc.ttf │ └── SIL Open Font License.txt └── example.dex ├── .gitignore ├── examples ├── dex │ ├── spandex.toml │ └── main.dex └── text │ └── spandex.toml ├── Cargo.toml ├── .github └── workflows │ ├── docs.yml │ └── ci.yml ├── README.md ├── LICENSE └── Cargo.lock /src/document/document.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/tests/successes/test-title-1.dex: -------------------------------------------------------------------------------- 1 | # A title 2 | -------------------------------------------------------------------------------- /assets/tests/successes/test-title-2.dex: -------------------------------------------------------------------------------- 1 | ## A subtitle 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | **/*.rs.bk 4 | *.pdf 5 | -------------------------------------------------------------------------------- /assets/tests/errors/test-unmatched-slash.dex: -------------------------------------------------------------------------------- 1 | Hello /you 2 | -------------------------------------------------------------------------------- /assets/tests/errors/test-unmatched-star.dex: -------------------------------------------------------------------------------- 1 | Hello to *you 2 | -------------------------------------------------------------------------------- /assets/tests/errors/test-unmatched-dollar.dex: -------------------------------------------------------------------------------- 1 | What a nice $formula 2 | -------------------------------------------------------------------------------- /assets/tests/errors/test-mixed-star-slash.dex: -------------------------------------------------------------------------------- 1 | A /sentence with an *error/ 2 | -------------------------------------------------------------------------------- /assets/tests/successes/test-titles.dex: -------------------------------------------------------------------------------- 1 | # A title 2 | 3 | ## With its subtitle 4 | -------------------------------------------------------------------------------- /assets/tests/errors/test-title-no-new-line.dex: -------------------------------------------------------------------------------- 1 | # This is a title 2 | with a new line 3 | -------------------------------------------------------------------------------- /assets/tests/errors/test-accent.dex: -------------------------------------------------------------------------------- 1 | Here is an accent é and an error in the same *line 2 | -------------------------------------------------------------------------------- /assets/fonts/cmunbi.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-spandex/spandex/HEAD/assets/fonts/cmunbi.ttf -------------------------------------------------------------------------------- /assets/fonts/cmunbl.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-spandex/spandex/HEAD/assets/fonts/cmunbl.ttf -------------------------------------------------------------------------------- /assets/fonts/cmunbx.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-spandex/spandex/HEAD/assets/fonts/cmunbx.ttf -------------------------------------------------------------------------------- /assets/fonts/cmunci.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-spandex/spandex/HEAD/assets/fonts/cmunci.ttf -------------------------------------------------------------------------------- /assets/fonts/cmunit.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-spandex/spandex/HEAD/assets/fonts/cmunit.ttf -------------------------------------------------------------------------------- /assets/fonts/cmunrm.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-spandex/spandex/HEAD/assets/fonts/cmunrm.ttf -------------------------------------------------------------------------------- /assets/fonts/cmunsi.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-spandex/spandex/HEAD/assets/fonts/cmunsi.ttf -------------------------------------------------------------------------------- /assets/fonts/cmunsl.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-spandex/spandex/HEAD/assets/fonts/cmunsl.ttf -------------------------------------------------------------------------------- /assets/fonts/cmunso.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-spandex/spandex/HEAD/assets/fonts/cmunso.ttf -------------------------------------------------------------------------------- /assets/fonts/cmunss.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-spandex/spandex/HEAD/assets/fonts/cmunss.ttf -------------------------------------------------------------------------------- /assets/fonts/cmunsx.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-spandex/spandex/HEAD/assets/fonts/cmunsx.ttf -------------------------------------------------------------------------------- /assets/fonts/cmuntb.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-spandex/spandex/HEAD/assets/fonts/cmuntb.ttf -------------------------------------------------------------------------------- /assets/fonts/cmunti.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-spandex/spandex/HEAD/assets/fonts/cmunti.ttf -------------------------------------------------------------------------------- /assets/fonts/cmuntt.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-spandex/spandex/HEAD/assets/fonts/cmuntt.ttf -------------------------------------------------------------------------------- /assets/fonts/cmuntx.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-spandex/spandex/HEAD/assets/fonts/cmuntx.ttf -------------------------------------------------------------------------------- /assets/fonts/cmunui.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-spandex/spandex/HEAD/assets/fonts/cmunui.ttf -------------------------------------------------------------------------------- /assets/fonts/cmunvi.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-spandex/spandex/HEAD/assets/fonts/cmunvi.ttf -------------------------------------------------------------------------------- /assets/fonts/cmunvt.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-spandex/spandex/HEAD/assets/fonts/cmunvt.ttf -------------------------------------------------------------------------------- /assets/fonts/cmunbmo.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-spandex/spandex/HEAD/assets/fonts/cmunbmo.ttf -------------------------------------------------------------------------------- /assets/fonts/cmunbmr.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-spandex/spandex/HEAD/assets/fonts/cmunbmr.ttf -------------------------------------------------------------------------------- /assets/fonts/cmunbso.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-spandex/spandex/HEAD/assets/fonts/cmunbso.ttf -------------------------------------------------------------------------------- /assets/fonts/cmunbsr.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-spandex/spandex/HEAD/assets/fonts/cmunbsr.ttf -------------------------------------------------------------------------------- /assets/fonts/cmunbtl.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-spandex/spandex/HEAD/assets/fonts/cmunbtl.ttf -------------------------------------------------------------------------------- /assets/fonts/cmunbto.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-spandex/spandex/HEAD/assets/fonts/cmunbto.ttf -------------------------------------------------------------------------------- /assets/fonts/cmunobi.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-spandex/spandex/HEAD/assets/fonts/cmunobi.ttf -------------------------------------------------------------------------------- /assets/fonts/cmunobx.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-spandex/spandex/HEAD/assets/fonts/cmunobx.ttf -------------------------------------------------------------------------------- /assets/fonts/cmunorm.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-spandex/spandex/HEAD/assets/fonts/cmunorm.ttf -------------------------------------------------------------------------------- /assets/fonts/cmunoti.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-spandex/spandex/HEAD/assets/fonts/cmunoti.ttf -------------------------------------------------------------------------------- /assets/fonts/cmunssdc.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-spandex/spandex/HEAD/assets/fonts/cmunssdc.ttf -------------------------------------------------------------------------------- /src/parser/tests/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module will test that the parser works correctly. 2 | 3 | #[cfg(test)] 4 | mod errors; 5 | 6 | #[cfg(test)] 7 | mod successes; 8 | -------------------------------------------------------------------------------- /src/layout/paragraphs/utils/mod.rs: -------------------------------------------------------------------------------- 1 | //! Set of utility functions for manipulating ASTs, the line breaking algorithm, 2 | //! and the `Paragraph` structure. 3 | 4 | pub mod ast; 5 | pub mod linebreak; 6 | pub mod paragraphs; 7 | -------------------------------------------------------------------------------- /examples/dex/spandex.toml: -------------------------------------------------------------------------------- 1 | title = "dex" 2 | page_width = 595.27566 3 | page_height = 841.889862 4 | top_margin = 85.03938 5 | left_margin = 85.03938 6 | text_width = 425.1969 7 | text_height = 671.811102 8 | input = "main.dex" 9 | -------------------------------------------------------------------------------- /assets/example.dex: -------------------------------------------------------------------------------- 1 | ## He/l*lo*/ *baby* 2 | 3 | ## A title on 4 | two lines 5 | 6 | How *are* you ? 7 | 8 | Im fine i *love* my life 9 | 10 | Im fine i /love my life 11 | because its the b*est/. 12 | 13 | I am *the best 14 | -------------------------------------------------------------------------------- /examples/text/spandex.toml: -------------------------------------------------------------------------------- 1 | title = "example" 2 | page_width = 595.27566 3 | page_height = 841.889862 4 | top_margin = 85.03938 5 | left_margin = 85.03938 6 | text_width = 425.1969 7 | text_height = 671.811102 8 | input = "main.dex" 9 | -------------------------------------------------------------------------------- /src/layout/mod.rs: -------------------------------------------------------------------------------- 1 | //! Logic for laying out the various pieces that make up a document. 2 | 3 | pub mod constants; 4 | pub mod paragraphs; 5 | 6 | use crate::fonts::Font; 7 | use printpdf::Pt; 8 | 9 | /// A glyph with its font style. 10 | #[derive(Debug, Clone)] 11 | pub struct Glyph<'a> { 12 | /// The content of the word. 13 | pub glyph: char, 14 | 15 | /// The font style of the word. 16 | pub font: &'a Font, 17 | 18 | /// The size of the font. 19 | pub scale: Pt, 20 | } 21 | 22 | impl<'a> Glyph<'a> { 23 | /// Creates a new word from a string and a font style. 24 | pub fn new(glyph: char, font: &'a Font, scale: Pt) -> Glyph<'a> { 25 | Glyph { glyph, font, scale } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/fonts/configuration.rs: -------------------------------------------------------------------------------- 1 | //! Configuration for a `Font` used to typeset a document. 2 | 3 | use crate::fonts::styles::FontStyle; 4 | use crate::fonts::Font; 5 | 6 | /// A font configuration for a document. 7 | pub struct FontConfig<'a> { 8 | /// The regular font. 9 | pub regular: &'a Font, 10 | 11 | /// The bold font. 12 | pub bold: &'a Font, 13 | 14 | /// The italic font. 15 | pub italic: &'a Font, 16 | 17 | /// The bold italic font. 18 | pub bold_italic: &'a Font, 19 | } 20 | 21 | impl<'a> FontConfig<'a> { 22 | /// Returns the font corresponding to the style. 23 | pub fn for_style(&self, style: FontStyle) -> &Font { 24 | match (style.bold, style.italic) { 25 | (false, false) => self.regular, 26 | (true, false) => self.bold, 27 | (false, true) => self.italic, 28 | (true, true) => self.bold_italic, 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/layout/paragraphs/mod.rs: -------------------------------------------------------------------------------- 1 | //! Logic for laying out a paragraph. 2 | 3 | pub mod engine; 4 | pub mod graph; 5 | pub mod items; 6 | pub mod justification; 7 | pub mod ligatures; 8 | pub mod utils; 9 | 10 | use std::slice::Iter; 11 | 12 | use crate::layout::paragraphs::items::Item; 13 | 14 | /// Holds a list of items describing a paragraph. 15 | #[derive(Debug, Default)] 16 | pub struct Paragraph<'a> { 17 | /// Sequence of items representing the structure of the paragraph. 18 | pub items: Vec>, 19 | } 20 | 21 | impl<'a> Paragraph<'a> { 22 | /// Instantiates a new paragraph. 23 | pub fn new() -> Paragraph<'a> { 24 | Paragraph { items: Vec::new() } 25 | } 26 | 27 | /// Pushes an item at the end of the paragraph. 28 | pub fn push(&mut self, item: Item<'a>) { 29 | self.items.push(item) 30 | } 31 | 32 | /// Returns an iterator to the items of the paragraph. 33 | pub fn iter(&self) -> Iter { 34 | self.items.iter() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "spandex" 3 | version = "0.0.4" 4 | description = "A modern LaTeX alternative" 5 | authors = ["Thomas Forgione ", "Matthieu Le Boucher "] 6 | edition = "2018" 7 | license = "MPL-2.0" 8 | homepage = "https://rust-spandex.github.io" 9 | documentation = "https://rust-spandex.github.io/spandex" 10 | repository = "https://github.com/rust-spandex/spandex" 11 | readme = "README.md" 12 | keywords = ["tex", "latex", "typesetting"] 13 | categories = ["template-engine", "visualization", "text-processing"] 14 | 15 | [badges] 16 | travis-ci = { repository = "rust-spandex/spandex", branch = "master" } 17 | 18 | [dependencies] 19 | serde = { version = "1.0", features = ["derive"] } 20 | spandex-hyphenation = { version = "0.7.4", features = ["embed_all"] } 21 | nom = "7.1.0" 22 | nom_locate = "4.0.0" 23 | printpdf = { version = "0.4.1", default-features = false } 24 | freetype-rs = "0.28.0" 25 | toml = "0.5.8" 26 | petgraph = "0.6.0" 27 | colored = "2.0.0" 28 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | 3 | on: 4 | push: 5 | branches: [master, main] 6 | 7 | jobs: 8 | build: 9 | name: Docs 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions-rs/toolchain@v1 14 | with: 15 | toolchain: stable 16 | - name: Generate docs 17 | run: | 18 | cargo doc --no-deps --lib 19 | echo "Redirect" > target/doc/index.html 20 | cd target/doc 21 | git init 22 | git config user.email "thomas@forgione.fr" 23 | git config user.name "Thomas Forgione" 24 | git remote add origin https://tforgione:$TOKEN@github.com/rust-spandex/spandex 25 | git checkout -b gh-pages 26 | git add . 27 | git commit -m "Deployment from github actions" 28 | git push -f --set-upstream origin gh-pages 29 | env: 30 | TOKEN: ${{ secrets.TOKEN }} 31 | 32 | 33 | -------------------------------------------------------------------------------- /examples/dex/main.dex: -------------------------------------------------------------------------------- 1 | # Hello world 2 | 3 | Lorem ipsum dolor sit amet, *consectetur* adipisicing elit, sed do eiusmod 4 | tempor incididunt ut /labore et dolore magna aliqua/. Ut enim ad minim veniam, 5 | quis nostrud exercitation ullamco || This is another comment right in the middle 6 | laboris nisi ut aliquip ex ea commodo consequat. /*Duis aute irure dolor*/ in 7 | reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 8 | *Excepteur sint occaecat* cupidatat non proident, sunt in culpa qui officia 9 | deserunt mollit anim id est laborum. 10 | || Since there is not empty line, the following will still be in the same 11 | || paragraph. 12 | Lorem ipsum dolor sit amet, *consectetur* adipisicing elit. 13 | 14 | 15 | ## Hello to you || A comment in a title 16 | 17 | Lorem ipsum dolor sit amet, *consectetur* adipisicing elit, sed do eiusmod 18 | tempor incididunt ut /labore et dolore magna aliqua/. Ut enim ad minim veniam, 19 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 20 | consequat. 21 | 22 | Voici un exemple d'affichage avec ligature. | Ceci *n'est pas* un commentaire. 23 | -------------------------------------------------------------------------------- /src/layout/constants.rs: -------------------------------------------------------------------------------- 1 | //! Various constants used for laying out the items of a document. 2 | 3 | use printpdf::Pt; 4 | use std::f64; 5 | 6 | // Linebreaking constants. 7 | /// The glyph that represents a char. 8 | // FIXME: replace this with an instance of `Glyph`. 9 | pub const DASH_GLYPH: char = '-'; 10 | 11 | /// The width a whitespace. 12 | pub const SPACE_WIDTH: Pt = Pt(5.0); 13 | 14 | /// The default length of a line if no desired length is specified. 15 | pub const DEFAULT_LINE_LENGTH: Pt = Pt(680.0); 16 | 17 | /// The minimal cost of a penalty to count as a legal breakpoint. 18 | pub const MIN_COST: f64 = -1000.0; 19 | 20 | /// The maximal cost of a penalty to count as a legal breakpoint. 21 | pub const MAX_COST: f64 = 1000.0; 22 | 23 | /// The additional cost that should be added to a penalty when the engine 24 | /// picks up to adjacent hyphens. 25 | pub const ADJACENT_LOOSE_TIGHT_PENALTY: f64 = 50.0; 26 | 27 | /// Minimum adjustment ratio to consider a breakpoint is legal. 28 | pub const MIN_ADJUSTMENT_RATIO: f64 = -1.0; 29 | 30 | /// Maximal adjustment ratio to consider a breakpoint is legal. 31 | pub const MAX_ADJUSTMENT_RATIO: f64 = 10.0; 32 | 33 | /// An infinite length in points. 34 | pub const PLUS_INFINITY: Pt = Pt(f64::INFINITY); 35 | 36 | /// The ideal spacing between two words. 37 | pub const IDEAL_SPACING: Pt = Pt(5.0); 38 | -------------------------------------------------------------------------------- /src/parser/mod.rs: -------------------------------------------------------------------------------- 1 | //! This crate contains the parser for spandex. 2 | 3 | pub mod ast; 4 | pub mod combinators; 5 | pub mod error; 6 | pub mod utils; 7 | pub mod warning; 8 | 9 | #[cfg(test)] 10 | mod tests; 11 | 12 | use nom_locate::LocatedSpan; 13 | 14 | use crate::parser::ast::Ast; 15 | use crate::parser::warning::Warnings; 16 | use crate::Error; 17 | 18 | /// This type will allow us to know where we are while we're parsing the content. 19 | pub type Span<'a> = LocatedSpan<&'a str>; 20 | 21 | /// A position is a span but without the reference to the complete str. 22 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 23 | pub struct Position { 24 | /// The line number of the position. 25 | pub line: u32, 26 | 27 | /// The column number of the position. 28 | pub column: usize, 29 | 30 | /// The offset from the beginning of the string. 31 | pub offset: usize, 32 | } 33 | 34 | /// Returns the position of a span. 35 | pub fn position(span: &Span) -> Position { 36 | Position { 37 | line: span.location_line(), 38 | column: span.get_utf8_column(), 39 | offset: span.location_offset(), 40 | } 41 | } 42 | 43 | /// An ast that was successfully parsed. 44 | #[derive(Debug)] 45 | pub struct Parsed { 46 | /// The parsed ast. 47 | pub ast: Ast, 48 | 49 | /// The warnings that were produced. 50 | pub warnings: Warnings, 51 | } 52 | 53 | pub use combinators::parse; 54 | -------------------------------------------------------------------------------- /src/fonts/styles.rs: -------------------------------------------------------------------------------- 1 | //! Different style variants of a `Font`. 2 | 3 | /// A style for a font. It can be bold, italic, both or none. 4 | #[derive(Copy, Clone, Debug)] 5 | pub struct FontStyle { 6 | /// Whether the bold is activated or not. 7 | pub bold: bool, 8 | 9 | /// Whether the italic is activated or not. 10 | pub italic: bool, 11 | } 12 | 13 | impl FontStyle { 14 | /// Creates a new regular font style. 15 | pub fn regular() -> FontStyle { 16 | FontStyle { 17 | bold: false, 18 | italic: false, 19 | } 20 | } 21 | 22 | /// Adds the bold style to the font. 23 | pub fn bold(self) -> FontStyle { 24 | FontStyle { 25 | bold: true, 26 | italic: self.italic, 27 | } 28 | } 29 | 30 | /// Adds the italic style to the font. 31 | pub fn italic(self) -> FontStyle { 32 | FontStyle { 33 | bold: self.bold, 34 | italic: true, 35 | } 36 | } 37 | 38 | /// Removes the bold style from the font. 39 | pub fn unbold(self) -> FontStyle { 40 | FontStyle { 41 | bold: false, 42 | italic: self.italic, 43 | } 44 | } 45 | 46 | /// Removes the italic style from the font. 47 | pub fn unitalic(self) -> FontStyle { 48 | FontStyle { 49 | bold: self.bold, 50 | italic: false, 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/parser/utils.rs: -------------------------------------------------------------------------------- 1 | //! This module contains some functions that will help us managing strings. 2 | 3 | /// Replicates a char n times. 4 | pub fn replicate(c: char, n: usize) -> String { 5 | let mut string = String::new(); 6 | for _ in 0..n { 7 | string.push(c); 8 | } 9 | string 10 | } 11 | 12 | /// Returns true if the char at specified byte is a \n. 13 | pub fn is_new_line(content: &str, byte: usize) -> bool { 14 | content.is_char_boundary(byte) 15 | && content.is_char_boundary(byte + 1) 16 | && &content[byte..=byte] == "\n" 17 | } 18 | 19 | /// Computes the column of a specified byte depending on its line and offset. 20 | pub fn compute_column(content: &str, start: usize, current: usize) -> usize { 21 | let mut column = 0; 22 | let mut pointer = start; 23 | 24 | for c in content[start..].chars() { 25 | if pointer == current { 26 | break; 27 | } 28 | 29 | column += 1; 30 | pointer += c.len_utf8(); 31 | } 32 | 33 | column 34 | } 35 | 36 | /// Finds the previous \n char. 37 | /// 38 | /// Returns 0 is no \n was found. 39 | pub fn previous_new_line(content: &str, byte: usize) -> usize { 40 | let mut i = byte; 41 | 42 | while i != 0 && !is_new_line(content, i) { 43 | i -= 1; 44 | } 45 | 46 | if &content[i..=i] == "\n" { 47 | i + 1 48 | } else { 49 | i 50 | } 51 | } 52 | 53 | /// Finds the next \n char. 54 | /// 55 | /// Returns the length of the string if no \n was found. 56 | pub fn next_new_line(content: &str, byte: usize) -> usize { 57 | let mut i = byte; 58 | 59 | while i != content.len() && !is_new_line(content, i) { 60 | i += 1; 61 | } 62 | 63 | i 64 | } 65 | -------------------------------------------------------------------------------- /src/layout/paragraphs/graph.rs: -------------------------------------------------------------------------------- 1 | //! Structure used in the graph part of the line breaking algorithm. It allows 2 | //! to store the accumulated statistics of line breaks and allows a 3 | //! dynamic programming approach. 4 | 5 | use printpdf::Pt; 6 | use std::cmp::Ordering; 7 | use std::fmt; 8 | use std::hash::{Hash, Hasher}; 9 | 10 | /// Aggregates various measures up to and from a feasible breakpoint. 11 | #[derive(Copy, Clone)] 12 | pub struct Node { 13 | /// Index of the item represented by the node, within the paragraph. 14 | pub index: usize, 15 | 16 | /// Line at which the item lives within the paragraph. 17 | pub line: usize, 18 | 19 | /// The fitness class of the item represented by the node. 20 | pub fitness: i64, 21 | 22 | /// Total width from the previous breakpoint to this one. 23 | pub total_width: Pt, 24 | 25 | /// Total stretchability from the previous breakpoint to this one. 26 | pub total_stretch: Pt, 27 | 28 | /// Total shrinkability from the previous breakpoint to this one. 29 | pub total_shrink: Pt, 30 | 31 | /// Accumulated demerits from previous breakpoints. 32 | pub total_demerits: f64, 33 | } 34 | 35 | impl fmt::Debug for Node { 36 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 37 | write!(f, "{}", self.index) 38 | } 39 | } 40 | 41 | impl PartialOrd for Node { 42 | fn partial_cmp(&self, other: &Node) -> Option { 43 | Some(self.index.cmp(&other.index)) 44 | } 45 | } 46 | 47 | impl Ord for Node { 48 | fn cmp(&self, other: &Node) -> Ordering { 49 | self.index.cmp(&other.index) 50 | } 51 | } 52 | 53 | impl PartialEq for Node { 54 | fn eq(&self, other: &Node) -> bool { 55 | self.index == other.index 56 | } 57 | } 58 | 59 | impl Eq for Node {} 60 | 61 | impl Hash for Node { 62 | fn hash(&self, state: &mut H) { 63 | self.index.hash(state); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/layout/paragraphs/utils/paragraphs.rs: -------------------------------------------------------------------------------- 1 | //! Utility functions for manipulating and typesetting a `Paragraph`. 2 | 3 | use crate::layout::constants::{DASH_GLYPH, DEFAULT_LINE_LENGTH, SPACE_WIDTH}; 4 | use crate::layout::paragraphs::items::Item; 5 | use crate::layout::paragraphs::Paragraph; 6 | use crate::layout::Glyph; 7 | use printpdf::Pt; 8 | use spandex_hyphenation::*; 9 | 10 | /// Adds a word to a buffer. 11 | pub fn add_word_to_paragraph<'a>( 12 | word: Vec>, 13 | dictionary: &Standard, 14 | buffer: &mut Paragraph<'a>, 15 | ) { 16 | // Reached end of current word, handle hyphenation. 17 | let to_hyphenate = word 18 | .iter() 19 | .map(|x: &Glyph| x.glyph.to_string()) 20 | .collect::>() 21 | .join(""); 22 | 23 | let hyphenated = dictionary.hyphenate(&to_hyphenate); 24 | let break_indices = &hyphenated.breaks; 25 | 26 | for (i, g) in word.iter().enumerate() { 27 | if break_indices.contains(&i) { 28 | buffer.push(Item::penalty(Pt(0.0), 50.0, true)); 29 | } 30 | 31 | buffer.push(Item::from_glyph(g.clone())); 32 | 33 | if g.glyph == DASH_GLYPH { 34 | buffer.push(Item::penalty(Pt(0.0), 50.0, true)); 35 | } 36 | } 37 | } 38 | 39 | /// Returns the glue based on the spatial context of the cursor. 40 | pub fn glue_from_context(_previous_glyph: Option, ideal_spacing: Pt) -> Item { 41 | // Todo: make this glue context dependent. 42 | Item::glue(ideal_spacing, SPACE_WIDTH, SPACE_WIDTH * 0.5) 43 | } 44 | 45 | /// Returns the length of the line of given index, from a list of 46 | /// potential line lengths. If the list is too short, the line 47 | /// length will default to `DEFAULT_LINE_LENGTH`. 48 | pub fn get_line_length(lines_length: &[Pt], index: usize) -> Pt { 49 | if index < lines_length.len() { 50 | lines_length[index] 51 | } else { 52 | *lines_length.first().unwrap_or(&DEFAULT_LINE_LENGTH) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/parser/tests/successes.rs: -------------------------------------------------------------------------------- 1 | //! This module contains the tests that should success and checks that the ast is correct. 2 | 3 | use std::error::Error; 4 | use std::path::PathBuf; 5 | 6 | use crate::parser::{parse, Ast}; 7 | 8 | #[test] 9 | fn test_title_1() -> Result<(), Box> { 10 | let path = "assets/tests/successes/test-title-1.dex"; 11 | let p = parse(path); 12 | assert!(p.is_ok()); 13 | 14 | let ast = p.unwrap().ast; 15 | 16 | let expected_ast = Ast::File( 17 | PathBuf::from(path), 18 | vec![Ast::Title { 19 | level: 0, 20 | children: vec![Ast::Text("A title".into())], 21 | }], 22 | ); 23 | 24 | assert_eq!(expected_ast, ast); 25 | 26 | Ok(()) 27 | } 28 | 29 | #[test] 30 | fn test_title_2() -> Result<(), Box> { 31 | let path = "assets/tests/successes/test-title-2.dex"; 32 | let p = parse(path); 33 | assert!(p.is_ok()); 34 | 35 | let ast = p.unwrap().ast; 36 | 37 | let expected_ast = Ast::File( 38 | PathBuf::from(path), 39 | vec![Ast::Title { 40 | level: 1, 41 | children: vec![Ast::Text("A subtitle".into())], 42 | }], 43 | ); 44 | 45 | assert_eq!(expected_ast, ast); 46 | 47 | Ok(()) 48 | } 49 | 50 | #[test] 51 | fn test_titles() -> Result<(), Box> { 52 | let path = "assets/tests/successes/test-titles.dex"; 53 | let p = parse(path); 54 | assert!(p.is_ok()); 55 | 56 | let ast = p.unwrap().ast; 57 | 58 | let expected_ast = Ast::File( 59 | PathBuf::from(path), 60 | vec![ 61 | Ast::Title { 62 | level: 0, 63 | children: vec![Ast::Text("A title".into())], 64 | }, 65 | Ast::Title { 66 | level: 1, 67 | children: vec![Ast::Text("With its subtitle".into())], 68 | }, 69 | ], 70 | ); 71 | 72 | assert_eq!(expected_ast, ast); 73 | 74 | Ok(()) 75 | } 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SpanDeX 2 | 3 | [![CI](https://github.com/rust-spandex/spandex/actions/workflows/ci.yml/badge.svg)](https://github.com/rust-spandex/spandex/actions/workflows/ci.yml) 4 | [![Crate](https://img.shields.io/crates/v/spandex.svg)](https://crates.io/crates/spandex) 5 | 6 | 7 | ## Install 8 | 9 | As always in rust, it's super simple to install. 10 | 11 | First, install rust 12 | 13 | ``` sh 14 | curl https://sh.rustup.rs -sSf | sh 15 | ``` 16 | 17 | Then, install SpanDeX 18 | 19 | ``` sh 20 | cargo install spandex 21 | ``` 22 | 23 | ## Usage 24 | 25 | For the moment, only two commands are available: 26 | - `spandex init `: creates a directory for a SpanDeX document with a 27 | `spandex.toml` and an initial `main.md` files. If no name is specified, the 28 | name of the current directory will be used instead. 29 | 30 | - `spandex build`: triggers the build of SpanDeX, and generates an 31 | `output.pdf` file. 32 | 33 | ## Build the examples 34 | 35 | To build one of the examples, go to the example directory and run `cargo run -- build`. 36 | 37 | ## Default fonts 38 | - CMU Serif BoldItalic 39 | - CMU Serif Extra BoldSlanted 40 | - CMU Bright Oblique 41 | - CMU Bright Roman 42 | - CMU Bright SemiBoldOblique 43 | - CMU Bright SemiBold 44 | - CMU Typewriter Text Light 45 | - CMU Typewriter Text LightOblique 46 | - CMU Serif Bold 47 | - CMU Classical Serif Italic 48 | - CMU Typewriter Text Italic 49 | - CMU Concrete BoldItalic 50 | - CMU Concrete Bold 51 | - CMU Concrete Roman 52 | - CMU Concrete Italic 53 | - CMU Serif Roman 54 | - CMU Sans Serif Oblique 55 | - CMU Serif Extra RomanSlanted 56 | - CMU Sans Serif BoldOblique 57 | - CMU Sans Serif Demi Condensed DemiCondensed 58 | - CMU Sans Serif Medium 59 | - CMU Sans Serif Bold 60 | - CMU Typewriter Text Bold 61 | - CMU Serif Italic 62 | - CMU Typewriter Text Regular 63 | - CMU Typewriter Text BoldItalic 64 | - CMU Serif Upright Italic UprightItalic 65 | - CMU Typewriter Text Variable Width Italic 66 | - CMU Typewriter Text Variable Width Medium 67 | -------------------------------------------------------------------------------- /src/document/counters.rs: -------------------------------------------------------------------------------- 1 | //! Logic for recursive counters in a document for titles and other 2 | //! counted items. 3 | 4 | use std::fmt; 5 | 6 | /// The struct that manages the counters for the document. 7 | #[derive(Clone, Default)] 8 | pub struct Counters { 9 | /// The counters. 10 | pub counters: Vec, 11 | } 12 | 13 | impl Counters { 14 | /// Creates a new empty counters. 15 | pub fn new() -> Counters { 16 | Counters { counters: vec![0] } 17 | } 18 | 19 | /// Increases the corresponding counter and returns it if it is correct. 20 | /// 21 | /// The counters of the subsections will be reinitialized. 22 | /// 23 | /// # Example 24 | /// 25 | /// ``` 26 | /// # use spandex::document::counters::Counters; 27 | /// let mut counters = Counters::new(); 28 | /// counters.increment(0); 29 | /// assert_eq!(counters.counter(0), 1); 30 | /// assert_eq!(counters.counter(1), 0); 31 | /// assert_eq!(counters.counter(2), 0); 32 | /// counters.increment(1); 33 | /// assert_eq!(counters.counter(1), 1); 34 | /// counters.increment(1); 35 | /// assert_eq!(counters.counter(1), 2); 36 | /// counters.increment(0); 37 | /// assert_eq!(counters.counter(0), 2); 38 | /// assert_eq!(counters.counter(1), 0); 39 | /// println!("{}", counters); 40 | /// ``` 41 | pub fn increment(&mut self, counter_id: usize) -> usize { 42 | self.counters.resize(counter_id + 1, 0); 43 | self.counters[counter_id] += 1; 44 | self.counters[counter_id] 45 | } 46 | 47 | /// Returns a specific value of a counter. 48 | pub fn counter(&self, counter_id: usize) -> usize { 49 | match self.counters.get(counter_id) { 50 | Some(i) => *i, 51 | None => 0, 52 | } 53 | } 54 | } 55 | 56 | impl fmt::Display for Counters { 57 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 58 | write!( 59 | fmt, 60 | "{}", 61 | self.counters 62 | .iter() 63 | .map(std::string::ToString::to_string) 64 | .collect::>() 65 | .join(".") 66 | ) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/parser/tests/errors.rs: -------------------------------------------------------------------------------- 1 | //! This module contains the tests that should fail and checks that the error messages are correct. 2 | 3 | use crate::parser::error::ErrorType; 4 | use crate::parser::parse; 5 | use crate::{Error, Result}; 6 | 7 | macro_rules! to_dex_error { 8 | ($expr: expr) => { 9 | match $expr { 10 | Err(Error::DexError(e)) => e, 11 | _ => panic!("expected an error but received ok"), 12 | } 13 | }; 14 | } 15 | 16 | #[test] 17 | fn test_unmatched_star() -> Result<()> { 18 | let p = parse("assets/tests/errors/test-unmatched-star.dex"); 19 | assert!(p.is_err()); 20 | 21 | let p = to_dex_error!(p); 22 | 23 | let p = &p.errors[0]; 24 | assert_eq!(p.ty, ErrorType::UnmatchedStar); 25 | assert_eq!(p.position.line, 1); 26 | assert_eq!(p.position.column, 10); 27 | Ok(()) 28 | } 29 | 30 | #[test] 31 | fn test_unmatched_slash() -> Result<()> { 32 | let p = parse("assets/tests/errors/test-unmatched-slash.dex"); 33 | assert!(p.is_err()); 34 | 35 | let p = to_dex_error!(p); 36 | assert_eq!(p.errors.len(), 1); 37 | 38 | let p = &p.errors[0]; 39 | assert_eq!(p.ty, ErrorType::UnmatchedSlash); 40 | assert_eq!(p.position.line, 1); 41 | assert_eq!(p.position.column, 7); 42 | Ok(()) 43 | } 44 | 45 | #[test] 46 | fn test_unmatched_dollar() -> Result<()> { 47 | let p = parse("assets/tests/errors/test-unmatched-dollar.dex"); 48 | assert!(p.is_err()); 49 | 50 | let p = to_dex_error!(p); 51 | assert_eq!(p.errors.len(), 1); 52 | 53 | let p = &p.errors[0]; 54 | assert_eq!(p.ty, ErrorType::UnmatchedDollar); 55 | assert_eq!(p.position.line, 1); 56 | assert_eq!(p.position.column, 13); 57 | Ok(()) 58 | } 59 | 60 | #[test] 61 | fn test_mixed_star_slash() -> Result<()> { 62 | let p = parse("assets/tests/errors/test-mixed-star-slash.dex"); 63 | assert!(p.is_err()); 64 | 65 | let p = to_dex_error!(p); 66 | assert_eq!(p.errors.len(), 1); 67 | 68 | let p = &p.errors[0]; 69 | assert_eq!(p.ty, ErrorType::UnmatchedStar); 70 | assert_eq!(p.position.line, 1); 71 | assert_eq!(p.position.column, 21); 72 | 73 | Ok(()) 74 | } 75 | 76 | #[test] 77 | fn test_title_no_new_line() -> Result<()> { 78 | let p = parse("assets/tests/errors/test-title-no-new-line.dex"); 79 | 80 | let p = to_dex_error!(p); 81 | assert_eq!(p.errors.len(), 1); 82 | 83 | println!("{}", p); 84 | 85 | let p = &p.errors[0]; 86 | 87 | assert_eq!(p.ty, ErrorType::MultipleLinesTitle); 88 | assert_eq!(p.position.line, 2); 89 | assert_eq!(p.position.column, 1); 90 | 91 | Ok(()) 92 | } 93 | 94 | #[test] 95 | fn test_accent() -> Result<()> { 96 | let p = parse("assets/tests/errors/test-accent.dex"); 97 | assert!(p.is_err()); 98 | 99 | let p = to_dex_error!(p); 100 | 101 | println!("{}", p); 102 | assert_eq!(p.errors.len(), 1); 103 | 104 | let e = &p.errors[0]; 105 | assert_eq!(e.ty, ErrorType::UnmatchedStar); 106 | assert_eq!(e.position.line, 1); 107 | assert_eq!(e.position.column, 46); 108 | 109 | Ok(()) 110 | } 111 | -------------------------------------------------------------------------------- /src/layout/paragraphs/items.rs: -------------------------------------------------------------------------------- 1 | //! Various blocks holding information and specifications about the structure 2 | //! of a paragraph. 3 | 4 | use printpdf::Pt; 5 | 6 | use crate::layout::Glyph; 7 | 8 | /// Top abstraction of an item, which is a specification for a box, a glue 9 | /// or a penalty. 10 | #[derive(Debug)] 11 | pub struct Item<'a> { 12 | /// The width of the item in scaled units. 13 | pub width: Pt, 14 | 15 | /// The type of the item. 16 | pub content: Content<'a>, 17 | } 18 | 19 | /// Possible available types for an item. 20 | #[derive(Debug)] 21 | pub enum Content<'a> { 22 | /// A bounding box refers to something that is meant to be typeset. 23 | /// 24 | /// Though it holds the glyph it's representing, this item is 25 | /// essentially a black box as the only revelant information 26 | /// about it for splitting a paragraph into lines is its width. 27 | BoundingBox(Glyph<'a>), 28 | /// Glue is a blank space which can see its width altered in specified ways. 29 | /// 30 | /// It can either stretch or shrink up to a certain limit, and is used as 31 | /// mortar to leverage to reach a target column width. 32 | Glue { 33 | /// How inclined the glue is to stretch from its natural width, in scaled points. 34 | stretchability: Pt, 35 | 36 | /// How inclined the glue is to shrink from its natural width, in scaled points. 37 | shrinkability: Pt, 38 | }, 39 | /// Penalty is a potential place to end a line and step to another. It's helpful 40 | /// to cut a line in the middle of a word (hyphenation) or to enforce a break 41 | /// at the end of paragraphs. 42 | Penalty { 43 | /// The "cost" of the penalty. 44 | value: f64, 45 | 46 | /// Whether or not the penalty is considered as flagged. 47 | flagged: bool, 48 | }, 49 | } 50 | 51 | impl<'a> Item<'a> { 52 | /// Creates a box for a particular glyph and font. 53 | pub fn from_glyph(glyph: Glyph<'a>) -> Item<'a> { 54 | Item { 55 | width: glyph.font.char_width(glyph.glyph, glyph.scale), 56 | content: Content::BoundingBox(glyph), 57 | } 58 | } 59 | 60 | /// Creates some glue. 61 | pub fn glue(ideal_spacing: Pt, stretchability: Pt, shrinkability: Pt) -> Item<'a> { 62 | Item { 63 | width: ideal_spacing, 64 | content: Content::Glue { 65 | stretchability, 66 | shrinkability, 67 | }, 68 | } 69 | } 70 | 71 | /// Creates a penalty. 72 | pub fn penalty(width: Pt, value: f64, flagged: bool) -> Item<'a> { 73 | Item { 74 | width, 75 | content: Content::Penalty { value, flagged }, 76 | } 77 | } 78 | } 79 | 80 | /// Holds the information of an item that's ready to be rendered. 81 | #[derive(Debug)] 82 | pub struct PositionedItem<'a> { 83 | /// The index of the item within the list of items that make up 84 | /// the paragraph in which is stands. 85 | pub index: usize, 86 | 87 | /// The index of the line on which this item is to be rendered. 88 | pub line: usize, 89 | 90 | /// The horizontal offset of the item. 91 | pub horizontal_offset: Pt, 92 | 93 | /// The (potentially adjusted) width this item should be rendered with. 94 | pub width: Pt, 95 | 96 | /// The glyph that should be layed out within this item. 97 | pub glyph: Glyph<'a>, 98 | } 99 | -------------------------------------------------------------------------------- /src/layout/paragraphs/ligatures.rs: -------------------------------------------------------------------------------- 1 | //! This module contains the functions related to ligatures. 2 | 3 | /// Ligates a string. 4 | /// 5 | /// From https://en.wikipedia.org/wiki/List_of_precomposed_Latin_characters_in_Unicode#Digraphs_and_ligatures 6 | pub fn ligature(input: &str) -> String { 7 | let mut output = String::new(); 8 | 9 | let mut first = input.chars(); 10 | let mut second = input.chars().skip(1); 11 | let mut third = input.chars().skip(2); 12 | 13 | loop { 14 | let f = first.next(); 15 | let s = second.next(); 16 | let t = third.next(); 17 | 18 | match (f, s, t) { 19 | (Some('f'), Some('f'), Some('i')) => { 20 | output.push('ffi'); 21 | first.next(); 22 | second.next(); 23 | third.next(); 24 | first.next(); 25 | second.next(); 26 | third.next(); 27 | } 28 | (Some('f'), Some('f'), Some('l')) => { 29 | output.push('ffl'); 30 | first.next(); 31 | second.next(); 32 | third.next(); 33 | first.next(); 34 | second.next(); 35 | third.next(); 36 | } 37 | (Some('f'), Some('f'), _) => { 38 | output.push('ff'); 39 | first.next(); 40 | second.next(); 41 | third.next(); 42 | } 43 | (Some('f'), Some('i'), _) => { 44 | output.push('fi'); 45 | first.next(); 46 | second.next(); 47 | third.next(); 48 | } 49 | (Some('f'), Some('l'), _) => { 50 | output.push('fl'); 51 | first.next(); 52 | second.next(); 53 | third.next(); 54 | } 55 | (Some('I'), Some('J'), _) => { 56 | output.push('IJ'); 57 | first.next(); 58 | second.next(); 59 | third.next(); 60 | } 61 | (Some('i'), Some('j'), _) => { 62 | output.push('ij'); 63 | first.next(); 64 | second.next(); 65 | third.next(); 66 | } 67 | (Some('L'), Some('J'), _) => { 68 | output.push('LJ'); 69 | first.next(); 70 | second.next(); 71 | third.next(); 72 | } 73 | (Some('L'), Some('j'), _) => { 74 | output.push('Lj'); 75 | first.next(); 76 | second.next(); 77 | third.next(); 78 | } 79 | (Some('l'), Some('j'), _) => { 80 | output.push('lj'); 81 | first.next(); 82 | second.next(); 83 | third.next(); 84 | } 85 | (Some('N'), Some('J'), _) => { 86 | output.push('NJ'); 87 | first.next(); 88 | second.next(); 89 | third.next(); 90 | } 91 | (Some('N'), Some('j'), _) => { 92 | output.push('Nj'); 93 | first.next(); 94 | second.next(); 95 | third.next(); 96 | } 97 | (Some('n'), Some('j'), _) => { 98 | output.push('nj'); 99 | first.next(); 100 | second.next(); 101 | third.next(); 102 | } 103 | (Some(c), _, _) => output.push(c), 104 | _ => break, 105 | } 106 | } 107 | 108 | output 109 | } 110 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate contains all the tools we need to generate nice pdf documents. 2 | 3 | #![warn(missing_docs)] 4 | #![warn(clippy::cargo)] 5 | #![allow(clippy::multiple_crate_versions)] 6 | 7 | pub mod document; 8 | pub mod fonts; 9 | pub mod layout; 10 | pub mod parser; 11 | 12 | use std::fs::File; 13 | use std::io::Read; 14 | use std::path::PathBuf; 15 | use std::{error, fmt, io, result}; 16 | 17 | use printpdf::Pt; 18 | 19 | use crate::document::configuration::Config; 20 | use crate::parser::error::Errors; 21 | use crate::parser::parse; 22 | 23 | macro_rules! impl_from_error { 24 | ($type: ty, $variant: path, $from: ty) => { 25 | impl From<$from> for $type { 26 | fn from(e: $from) -> $type { 27 | $variant(e) 28 | } 29 | } 30 | }; 31 | } 32 | 33 | /// The error type of the library. 34 | #[derive(Debug)] 35 | pub enum Error { 36 | /// Cannot read current directory. 37 | CannotReadCurrentDir, 38 | 39 | /// No spandex.toml was found. 40 | NoConfigFile, 41 | 42 | /// Error while dealing with freetype. 43 | FreetypeError(freetype::Error), 44 | 45 | /// Error while dealing with printpdf. 46 | PrintpdfError(printpdf::errors::Error), 47 | 48 | /// The specified font was not found. 49 | FontNotFound(PathBuf), 50 | 51 | /// The specified font has no name or no style. 52 | FontWithoutName(PathBuf), 53 | 54 | /// An error occured while loading an hyphenation dictionnary. 55 | HyphenationLoadError(spandex_hyphenation::load::Error), 56 | 57 | /// Another io error occured. 58 | IoError(io::Error), 59 | 60 | /// Some error occured while parsing a dex file. 61 | DexError(Errors), 62 | } 63 | 64 | impl_from_error!(Error, Error::FreetypeError, freetype::Error); 65 | impl_from_error!(Error, Error::PrintpdfError, printpdf::errors::Error); 66 | impl_from_error!(Error, Error::IoError, io::Error); 67 | impl_from_error!( 68 | Error, 69 | Error::HyphenationLoadError, 70 | spandex_hyphenation::load::Error 71 | ); 72 | impl_from_error!(Error, Error::DexError, Errors); 73 | 74 | impl fmt::Display for Error { 75 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 76 | match self { 77 | Error::CannotReadCurrentDir => write!(fmt, "cannot read current directory"), 78 | Error::NoConfigFile => write!(fmt, "no spandex.toml was found"), 79 | Error::FreetypeError(e) => write!(fmt, "freetype error: {}", e), 80 | Error::PrintpdfError(e) => write!(fmt, "printpdf error: {}", e), 81 | Error::FontNotFound(path) => write!(fmt, "couldn't find font \"{}\"", path.display()), 82 | Error::FontWithoutName(path) => { 83 | write!(fmt, "font has no name or style \"{}\"", path.display()) 84 | } 85 | Error::HyphenationLoadError(e) => write!(fmt, "Problem with hyphenation: {}", e), 86 | Error::IoError(e) => write!(fmt, "an io error occured: {}", e), 87 | Error::DexError(e) => write!(fmt, "{}", e), 88 | } 89 | } 90 | } 91 | 92 | impl error::Error for Error {} 93 | 94 | /// The result type of the library. 95 | pub type Result = result::Result; 96 | 97 | /// Compiles a spandex project. 98 | pub fn build(config: &Config) -> Result<()> { 99 | let (mut document, font_manager) = config.init()?; 100 | let font_config = font_manager.default_config(); 101 | 102 | let mut content = String::new(); 103 | let mut file = File::open(&config.input)?; 104 | file.read_to_string(&mut content)?; 105 | 106 | if config.input.ends_with(".dex") { 107 | let parsed = parse(&config.input)?; 108 | println!("{}", parsed.warnings); 109 | println!("{:?}", parsed.ast); 110 | document.render(&parsed.ast, &font_config, Pt(10.0)); 111 | } else { 112 | document.write_content(&content, &font_config, Pt(10.0)); 113 | } 114 | document.save("output.pdf"); 115 | Ok(()) 116 | } 117 | -------------------------------------------------------------------------------- /src/parser/warning.rs: -------------------------------------------------------------------------------- 1 | //! This module contains everything related to parse warnings. 2 | 3 | use std::fmt; 4 | use std::path::PathBuf; 5 | 6 | use colored::*; 7 | 8 | use crate::parser::utils::{next_new_line, previous_new_line, replicate}; 9 | use crate::parser::Position; 10 | 11 | /// The different types of warning that can occur. 12 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 13 | pub enum WarningType { 14 | /// Two consecutive stars only seperated by whitespaces. 15 | ConsecutiveStars, 16 | } 17 | 18 | impl WarningType { 19 | /// Returns the title of the warning. 20 | pub fn title(self) -> &'static str { 21 | match self { 22 | WarningType::ConsecutiveStars => "empty bold section", 23 | } 24 | } 25 | 26 | /// Returns the defail of the warning. 27 | pub fn detail(self) -> &'static str { 28 | match self { 29 | WarningType::ConsecutiveStars => "this will be ignored", 30 | } 31 | } 32 | 33 | /// Returns a potential note. 34 | pub fn note(self) -> Option<&'static str> { 35 | match self { 36 | WarningType::ConsecutiveStars => { 37 | Some("to use bold, you should use single stars, e.g. '*this is bold*'") 38 | } 39 | } 40 | } 41 | } 42 | 43 | /// An warning that occured during the parsing. 44 | #[derive(Debug, Clone, PartialEq, Eq)] 45 | pub struct EmptyWarning { 46 | /// The position of the warning. 47 | pub position: Position, 48 | 49 | /// The type of the warning. 50 | pub ty: WarningType, 51 | } 52 | 53 | /// A struct that contains many warnings that references a file. 54 | #[derive(Debug)] 55 | pub struct Warnings { 56 | /// The path to the corresponding file. 57 | pub path: PathBuf, 58 | 59 | /// The content that produced the warnings. 60 | pub content: String, 61 | 62 | /// The warnings produced. 63 | pub warnings: Vec, 64 | } 65 | 66 | impl fmt::Display for Warnings { 67 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 68 | for warning in &self.warnings { 69 | let start = previous_new_line(&self.content, warning.position.offset); 70 | let end = next_new_line(&self.content, warning.position.offset); 71 | 72 | let line = warning.position.line; 73 | let column = warning.position.column; 74 | 75 | let line_number = format!("{} ", line); 76 | let space = replicate(' ', line_number.len() - 1); 77 | let margin = replicate(' ', column); 78 | let hats = replicate('^', 1); 79 | 80 | writeln!( 81 | fmt, 82 | "{}{}", 83 | "warning: ".bold().yellow(), 84 | warning.ty.title().bold() 85 | )?; 86 | 87 | writeln!( 88 | fmt, 89 | "{}{} {}:{}:{}", 90 | space, 91 | "-->".bold().blue(), 92 | self.path.display(), 93 | line, 94 | column 95 | )?; 96 | 97 | writeln!(fmt, "{} {}", space, "|".blue().bold())?; 98 | writeln!( 99 | fmt, 100 | "{} {}", 101 | &format!("{}|", line_number).blue().bold(), 102 | &self.content[start..end] 103 | )?; 104 | writeln!( 105 | fmt, 106 | "{} {}{}{} {}", 107 | space, 108 | "|".blue().bold(), 109 | margin, 110 | hats.bold().yellow(), 111 | warning.ty.detail().bold().yellow() 112 | )?; 113 | writeln!(fmt, "{} {}", space, "|".blue().bold())?; 114 | 115 | if let Some(note) = warning.ty.note() { 116 | writeln!( 117 | fmt, 118 | "{} {} {}{}", 119 | space, 120 | "=".blue().bold(), 121 | "note: ".bold(), 122 | note 123 | )?; 124 | } 125 | } 126 | 127 | Ok(()) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/fonts/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module contains everything that helps us dealing with fonts. 2 | 3 | pub mod configuration; 4 | pub mod manager; 5 | pub mod styles; 6 | 7 | use std::fs::File; 8 | use std::io::Cursor; 9 | use std::path::{Path, PathBuf}; 10 | 11 | use freetype::{face, Face, Library}; 12 | use printpdf::types::plugins::graphics::two_dimensional::font::IndirectFontRef; 13 | use printpdf::Pt; 14 | 15 | use crate::document::Document; 16 | use crate::{Error, Result}; 17 | 18 | /// A font that contains the printpdf object font needed to render text and the freetype font 19 | /// needed to measure text. 20 | #[derive(Debug)] 21 | pub struct Font { 22 | /// The freetype face. 23 | freetype: Face, 24 | 25 | /// The printpdf font. 26 | printpdf: IndirectFontRef, 27 | } 28 | 29 | impl Font { 30 | /// Creates a font from a path to a file. 31 | pub fn from_file>( 32 | path: P, 33 | library: &Library, 34 | document: &mut Document, 35 | ) -> Result { 36 | let file = File::open(path.as_ref()) 37 | .map_err(|_| Error::FontNotFound(PathBuf::from(path.as_ref())))?; 38 | Ok(Font { 39 | freetype: library.new_face(path.as_ref(), 0)?, 40 | printpdf: document.inner_mut().add_external_font(file)?, 41 | }) 42 | } 43 | 44 | /// Creates a font from a byte array. 45 | pub fn from_bytes(bytes: &[u8], library: &Library, document: &mut Document) -> Result { 46 | let cursor = Cursor::new(bytes); 47 | Ok(Font { 48 | // I don't like this bytes.to_vec() but I'm not sure there's a better way of doing 49 | // this... 50 | freetype: library.new_memory_face(bytes.to_vec(), 0)?, 51 | printpdf: document.inner_mut().add_external_font(cursor)?, 52 | }) 53 | } 54 | 55 | /// Computes the width of a char of the font at a specified size. 56 | pub fn char_width(&self, c: char, scale: Pt) -> Pt { 57 | let scale = scale.0; 58 | 59 | // vertical scale for the space character 60 | let vert_scale = { 61 | if self 62 | .freetype 63 | .load_char(0x0020, face::LoadFlag::NO_SCALE) 64 | .is_ok() 65 | { 66 | self.freetype.glyph().metrics().vertAdvance 67 | } else { 68 | 1000 69 | } 70 | }; 71 | 72 | // calculate the width of the text in unscaled units 73 | let is_ok = self 74 | .freetype 75 | .load_char(c as usize, face::LoadFlag::NO_SCALE) 76 | .is_ok(); 77 | 78 | let width = if is_ok { 79 | self.freetype.glyph().metrics().horiAdvance 80 | } else { 81 | 0 82 | }; 83 | 84 | Pt(width as f64 / (vert_scale as f64 / scale)) 85 | } 86 | 87 | /// Computes the text width of the font at a specified size. 88 | pub fn text_width(&self, text: &str, scale: Pt) -> Pt { 89 | let scale = scale.0; 90 | 91 | // vertical scale for the space character 92 | let vert_scale = { 93 | if self 94 | .freetype 95 | .load_char(0x0020, face::LoadFlag::NO_SCALE) 96 | .is_ok() 97 | { 98 | self.freetype.glyph().metrics().vertAdvance 99 | } else { 100 | 1000 101 | } 102 | }; 103 | 104 | // calculate the width of the text in unscaled units 105 | let sum_width = text.chars().fold(0, |acc, ch| { 106 | let is_ok = self 107 | .freetype 108 | .load_char(ch as usize, face::LoadFlag::NO_SCALE) 109 | .is_ok(); 110 | 111 | if is_ok { 112 | acc + self.freetype.glyph().metrics().horiAdvance 113 | } else { 114 | acc 115 | } 116 | }); 117 | 118 | Pt(sum_width as f64 / (vert_scale as f64 / scale)) 119 | } 120 | 121 | /// Returns a reference to the printpdf font. 122 | pub fn printpdf(&self) -> &IndirectFontRef { 123 | &self.printpdf 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/layout/paragraphs/justification.rs: -------------------------------------------------------------------------------- 1 | //! This module contains the trait and implementation of justification algorithms. 2 | 3 | use printpdf::Pt; 4 | 5 | use crate::layout::constants::IDEAL_SPACING; 6 | use crate::layout::paragraphs::engine::{algorithm, positionate_items}; 7 | use crate::layout::paragraphs::items::Content; 8 | use crate::layout::paragraphs::Paragraph; 9 | use crate::layout::Glyph; 10 | 11 | /// An algorithm that justifies a paragraph. 12 | pub trait Justifier { 13 | /// Justifies the paragraph passed as parameter. 14 | fn justify<'a>(paragraph: &'a Paragraph<'a>, text_width: Pt) -> Vec, Pt)>>; 15 | } 16 | 17 | /// A naive justifier, that goes to the next line once a word overtakes the text width. 18 | pub struct NaiveJustifier; 19 | 20 | impl Justifier for NaiveJustifier { 21 | fn justify<'a>(paragraph: &'a Paragraph<'a>, text_width: Pt) -> Vec, Pt)>> { 22 | let mut ret = vec![]; 23 | let mut current_line = vec![]; 24 | let mut current_word = vec![]; 25 | let mut current_x = Pt(0.0); 26 | 27 | for item in paragraph.iter() { 28 | match item.content { 29 | Content::BoundingBox { .. } => { 30 | current_x += item.width; 31 | current_word.push(item); 32 | } 33 | Content::Glue { .. } => { 34 | current_line.push(current_word); 35 | current_x += item.width; 36 | current_word = vec![]; 37 | } 38 | Content::Penalty { .. } => (), 39 | } 40 | 41 | if current_x > text_width && current_line.len() > 1 { 42 | current_x = Pt(0.0); 43 | 44 | let last_word = current_line.pop().unwrap(); 45 | 46 | let mut occupied_width = Pt(0.0); 47 | for word in ¤t_line { 48 | for glyph in word { 49 | occupied_width += glyph.width; 50 | } 51 | } 52 | 53 | let available_space = text_width - occupied_width; 54 | 55 | let word_space = if current_line.len() > 1 { 56 | available_space / (current_line.len() - 1) as f64 57 | } else { 58 | IDEAL_SPACING 59 | }; 60 | 61 | let mut current_x = Pt(0.0); 62 | let mut final_line = vec![]; 63 | 64 | for word in current_line { 65 | for item in &word { 66 | if let Content::BoundingBox(ref glyph) = item.content { 67 | final_line.push((glyph.clone(), current_x)); 68 | current_x += item.width; 69 | } 70 | } 71 | 72 | // Put a space after the word 73 | current_x += word_space; 74 | } 75 | 76 | ret.push(final_line); 77 | 78 | current_line = vec![last_word]; 79 | } 80 | } 81 | 82 | let mut current_x = Pt(0.0); 83 | let mut final_line = vec![]; 84 | 85 | // There is still content in current_line 86 | for word in current_line { 87 | for item in word { 88 | if let Content::BoundingBox(ref glyph) = item.content { 89 | final_line.push((glyph.clone(), current_x)); 90 | current_x += item.width; 91 | } 92 | } 93 | current_x += IDEAL_SPACING; 94 | } 95 | 96 | ret.push(final_line); 97 | 98 | ret 99 | } 100 | } 101 | 102 | /// The LaTeX style justifier. 103 | pub struct LatexJustifier; 104 | 105 | impl Justifier for LatexJustifier { 106 | fn justify<'a>(paragraph: &Paragraph<'a>, text_width: Pt) -> Vec, Pt)>> { 107 | let lines_length = vec![text_width]; 108 | let breakpoints = algorithm(paragraph, &lines_length); 109 | let positioned_items = positionate_items(¶graph.items, &lines_length, &breakpoints); 110 | 111 | let mut output = vec![]; 112 | 113 | for items in positioned_items { 114 | let mut line = vec![]; 115 | for item in items { 116 | line.push((item.glyph.clone(), item.horizontal_offset)); 117 | } 118 | output.push(line); 119 | } 120 | 121 | output 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/parser/error.rs: -------------------------------------------------------------------------------- 1 | //! This module contains everything related to parsing errors. 2 | 3 | use std::error::Error; 4 | use std::fmt; 5 | use std::path::PathBuf; 6 | 7 | use colored::*; 8 | 9 | use crate::parser::utils::{next_new_line, previous_new_line, replicate}; 10 | use crate::parser::Position; 11 | 12 | /// The different types errors that can occur while parsing. 13 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 14 | pub enum ErrorType { 15 | /// A star for bold content is unmatched. 16 | UnmatchedStar, 17 | 18 | /// A slash for italic content is unmatched. 19 | UnmatchedSlash, 20 | 21 | /// A dollar for a inlinemath is unmatched. 22 | UnmatchedDollar, 23 | 24 | /// A title is on multiple lines. 25 | MultipleLinesTitle, 26 | } 27 | 28 | impl ErrorType { 29 | /// Returns the title of the error. 30 | pub fn title(self) -> &'static str { 31 | match self { 32 | ErrorType::UnmatchedStar => "unmatched *", 33 | ErrorType::UnmatchedSlash => "unmactched /", 34 | ErrorType::UnmatchedDollar => "unmactched $", 35 | ErrorType::MultipleLinesTitle => "titles must be followed by an empty line", 36 | } 37 | } 38 | 39 | /// Returns the detail of the error. 40 | pub fn detail(self) -> &'static str { 41 | match self { 42 | ErrorType::UnmatchedStar => "bold content starts here but never ends", 43 | ErrorType::UnmatchedSlash => "italic content starts here but never ends", 44 | ErrorType::UnmatchedDollar => "inline inlinemath starts here but never ends", 45 | ErrorType::MultipleLinesTitle => "expected empty line here", 46 | } 47 | } 48 | 49 | /// Returns an optional note. 50 | pub fn note(self) -> Option<&'static str> { 51 | match self { 52 | ErrorType::UnmatchedStar => None, 53 | ErrorType::UnmatchedSlash => None, 54 | ErrorType::UnmatchedDollar => None, 55 | ErrorType::MultipleLinesTitle => None, 56 | } 57 | } 58 | } 59 | 60 | /// An error that occured during the parsing. 61 | #[derive(Debug, Clone, PartialEq, Eq)] 62 | pub struct EmptyError { 63 | /// The position of the error. 64 | pub position: Position, 65 | 66 | /// The type of the error. 67 | pub ty: ErrorType, 68 | } 69 | 70 | /// A struct that contains many errors that references a file. 71 | #[derive(Debug)] 72 | pub struct Errors { 73 | /// The path to the corresponding file. 74 | pub path: PathBuf, 75 | 76 | /// The content that produced the errors. 77 | pub content: String, 78 | 79 | /// The errors that were produced. 80 | pub errors: Vec, 81 | } 82 | 83 | impl fmt::Display for Errors { 84 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 85 | for error in &self.errors { 86 | let start = previous_new_line(&self.content, error.position.offset); 87 | let end = next_new_line(&self.content, error.position.offset); 88 | 89 | let line = error.position.line; 90 | let column = error.position.column; 91 | 92 | let line_number = format!("{} ", line); 93 | let space = replicate(' ', line_number.len() - 1); 94 | let margin = replicate(' ', column); 95 | let hats = replicate('^', 1); 96 | 97 | writeln!(fmt, "{}{}", "error: ".bold().red(), error.ty.title().bold())?; 98 | 99 | writeln!( 100 | fmt, 101 | "{}{} {}:{}:{}", 102 | space, 103 | "-->".bold().blue(), 104 | self.path.display(), 105 | line, 106 | column 107 | )?; 108 | 109 | writeln!(fmt, "{} {}", space, "|".blue().bold())?; 110 | writeln!( 111 | fmt, 112 | "{} {}", 113 | &format!("{}|", line_number).blue().bold(), 114 | &self.content[start..end] 115 | )?; 116 | writeln!( 117 | fmt, 118 | "{} {}{}{} {}", 119 | space, 120 | "|".blue().bold(), 121 | margin, 122 | hats.bold().red(), 123 | error.ty.detail().bold().red() 124 | )?; 125 | writeln!(fmt, "{} {}", space, "|".blue().bold())?; 126 | if let Some(note) = error.ty.note() { 127 | writeln!( 128 | fmt, 129 | "{} {} {}{}", 130 | space, 131 | "=".blue().bold(), 132 | "note: ".bold(), 133 | note 134 | )?; 135 | } 136 | } 137 | 138 | Ok(()) 139 | } 140 | } 141 | 142 | impl Error for Errors {} 143 | -------------------------------------------------------------------------------- /assets/fonts/SIL Open Font License.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) Authors of original metafont fonts: 2 | Donald Ervin Knuth (cm, concrete fonts) 3 | 1995, 1996, 1997 J"org Knappen, 1990, 1992 Norbert Schwarz (ec fonts) 4 | 1992-2006 A.Khodulev, O.Lapko, A.Berdnikov, V.Volovich (lh fonts) 5 | 1997-2005 Claudio Beccari (cb greek fonts) 6 | 2002 FUKUI Rei (tipa fonts) 7 | 2003-2005 Han The Thanh (Vietnamese fonts) 8 | 1996-2005 Walter Schmidt (cmbright fonts) 9 | 10 | Copyright (C) 2003-2009, Andrey V. Panov (panov@canopus.iacp.dvo.ru), 11 | with Reserved Font Family Name "Computer Modern Unicode fonts". 12 | 13 | 14 | 15 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 16 | This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL 17 | 18 | ----------------------------------------------------------- 19 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 20 | ----------------------------------------------------------- 21 | 22 | PREAMBLE 23 | The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. 24 | 25 | The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. 26 | 27 | DEFINITIONS 28 | "Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. 29 | 30 | "Reserved Font Name" refers to any names specified as such after the copyright statement(s). 31 | 32 | "Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). 33 | 34 | "Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. 35 | 36 | "Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. 37 | 38 | PERMISSION & CONDITIONS 39 | Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: 40 | 41 | 1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. 42 | 43 | 2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. 44 | 45 | 3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. 46 | 47 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. 48 | 49 | 5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. 50 | 51 | TERMINATION 52 | This license becomes null and void if any of the above conditions are not met. 53 | 54 | DISCLAIMER 55 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. -------------------------------------------------------------------------------- /src/layout/paragraphs/utils/ast.rs: -------------------------------------------------------------------------------- 1 | //! Utility functions for manipulating an abstract syntax tree representing 2 | //! a paragraph. 3 | 4 | use crate::fonts::configuration::FontConfig; 5 | use crate::fonts::styles::FontStyle; 6 | use crate::layout::constants::{IDEAL_SPACING, PLUS_INFINITY}; 7 | use crate::layout::paragraphs::items::Item; 8 | use crate::layout::paragraphs::utils::paragraphs::{add_word_to_paragraph, glue_from_context}; 9 | use crate::layout::paragraphs::Paragraph; 10 | use crate::layout::Glyph; 11 | use crate::parser::ast::Ast; 12 | use printpdf::Pt; 13 | use spandex_hyphenation::*; 14 | use std::f64; 15 | 16 | /// Parses an AST into a sequence of items. 17 | pub fn itemize_ast<'a>( 18 | ast: &Ast, 19 | font_config: &'a FontConfig, 20 | size: Pt, 21 | dictionary: &Standard, 22 | indent: Pt, 23 | ) -> Paragraph<'a> { 24 | let mut p = Paragraph::new(); 25 | let current_style = FontStyle::regular(); 26 | 27 | if indent > Pt(0.0) { 28 | p.push(Item::glue(indent, Pt(0.0), Pt(0.0))); 29 | } 30 | 31 | itemize_ast_aux(ast, font_config, size, dictionary, current_style, &mut p); 32 | p 33 | } 34 | 35 | /// Parses an AST into a sequence of items. 36 | pub fn itemize_ast_aux<'a>( 37 | ast: &Ast, 38 | font_config: &'a FontConfig, 39 | size: Pt, 40 | dictionary: &Standard, 41 | current_style: FontStyle, 42 | buffer: &mut Paragraph<'a>, 43 | ) { 44 | match ast { 45 | Ast::Title { level, children } => { 46 | let size = size + Pt(3.0 * ((4 - *level as isize).max(1)) as f64); 47 | for child in children { 48 | itemize_ast_aux( 49 | child, 50 | font_config, 51 | size, 52 | dictionary, 53 | current_style.bold(), 54 | buffer, 55 | ); 56 | } 57 | buffer.push(Item::glue(Pt(0.0), PLUS_INFINITY, Pt(0.0))); 58 | buffer.push(Item::penalty(Pt(0.0), f64::NEG_INFINITY, false)); 59 | } 60 | 61 | Ast::Bold(children) => { 62 | for child in children { 63 | itemize_ast_aux( 64 | child, 65 | font_config, 66 | size, 67 | dictionary, 68 | current_style.bold(), 69 | buffer, 70 | ); 71 | } 72 | } 73 | 74 | Ast::Italic(children) => { 75 | for child in children { 76 | itemize_ast_aux( 77 | child, 78 | font_config, 79 | size, 80 | dictionary, 81 | current_style.italic(), 82 | buffer, 83 | ); 84 | } 85 | } 86 | 87 | Ast::Text(content) => { 88 | let font = font_config.for_style(current_style); 89 | let ideal_spacing = IDEAL_SPACING; 90 | let mut previous_glyph = None; 91 | let mut current_word = vec![]; 92 | 93 | // Turn each word of the paragraph into a sequence of boxes for the caracters of the 94 | // word. This includes potential punctuation marks. 95 | for c in content.chars() { 96 | if c.is_whitespace() { 97 | add_word_to_paragraph(current_word, dictionary, buffer); 98 | buffer.push(glue_from_context(previous_glyph, ideal_spacing)); 99 | current_word = vec![]; 100 | } else { 101 | current_word.push(Glyph::new(c, font, size)); 102 | } 103 | 104 | previous_glyph = Some(Glyph::new(c, font, size)); 105 | } 106 | 107 | // Current word is empty if content ends with a whitespace. 108 | 109 | if !current_word.is_empty() { 110 | add_word_to_paragraph(current_word, dictionary, buffer); 111 | } 112 | } 113 | 114 | Ast::File(_, children) => { 115 | for child in children { 116 | itemize_ast_aux(child, font_config, size, dictionary, current_style, buffer); 117 | } 118 | } 119 | 120 | Ast::Paragraph(children) => { 121 | for child in children { 122 | itemize_ast_aux(child, font_config, size, dictionary, current_style, buffer); 123 | } 124 | 125 | // Appends two items to ensure the end of any paragraph is treated properly: a glue 126 | // specifying the available space at the right of the last tine, and a penalty item to 127 | // force a line break. 128 | buffer.push(Item::glue(Pt(0.0), PLUS_INFINITY, Pt(0.0))); 129 | buffer.push(Item::penalty(Pt(0.0), f64::NEG_INFINITY, false)); 130 | } 131 | 132 | _ => (), 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/document/configuration.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the basic configuration of a document that is to be 2 | //! typeset. The configuration is parsed from a TOML file located at the 3 | //! root of the SpanDeX project. Mandatory measurements take default values 4 | //! that are also provided by this module. 5 | 6 | use std::{fmt, result}; 7 | 8 | use printpdf::{Mm, Pt}; 9 | use serde::de::{self, Visitor}; 10 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 11 | 12 | use crate::document::{Document, Window}; 13 | use crate::fonts::manager::FontManager; 14 | use crate::Result as CResult; 15 | 16 | /// Serializes a `Pt` structure. 17 | // This is required to use in macro `serialize_with`. 18 | #[allow(clippy::trivially_copy_pass_by_ref)] 19 | pub fn serialize_pt(pt: &Pt, serializer: S) -> result::Result { 20 | serializer.serialize_f64(pt.0) 21 | } 22 | 23 | /// Deserializes a `Pt` structure. 24 | pub fn deserialize_pt<'a, D: Deserializer<'a>>(deserializer: D) -> Result { 25 | deserializer.deserialize_f64(PtVisitor) 26 | } 27 | 28 | macro_rules! visit_from { 29 | ($visit: ident, $ty: ty) => { 30 | fn $visit(self, value: $ty) -> Result 31 | where 32 | E: de::Error, 33 | { 34 | Ok(Pt(f64::from(value))) 35 | } 36 | }; 37 | } 38 | 39 | macro_rules! visit_as { 40 | ($visit: ident, $ty: ty) => { 41 | fn $visit(self, value: $ty) -> Result 42 | where 43 | E: de::Error, 44 | { 45 | Ok(Pt(value as f64)) 46 | } 47 | }; 48 | } 49 | 50 | /// Visitor for the `Pt` structure. 51 | pub struct PtVisitor; 52 | 53 | impl<'a> Visitor<'a> for PtVisitor { 54 | type Value = Pt; 55 | 56 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 57 | formatter.write_str("a floatting point number") 58 | } 59 | 60 | visit_from!(visit_u8, u8); 61 | visit_from!(visit_u16, u16); 62 | visit_from!(visit_u32, u32); 63 | visit_as!(visit_u64, u64); 64 | visit_from!(visit_i8, i8); 65 | visit_from!(visit_i16, i16); 66 | visit_from!(visit_i32, i32); 67 | visit_as!(visit_i64, i64); 68 | visit_from!(visit_f32, f32); 69 | visit_from!(visit_f64, f64); 70 | } 71 | 72 | /// Holds the configuration of a document, including various measurements 73 | /// common to all pages. 74 | #[derive(Clone, Serialize, Deserialize)] 75 | pub struct Config { 76 | /// The title of the document. 77 | pub title: String, 78 | 79 | /// The width of the page of the document. 80 | #[serde(serialize_with = "serialize_pt")] 81 | #[serde(deserialize_with = "deserialize_pt")] 82 | pub page_width: Pt, 83 | 84 | /// The height of the page of the document. 85 | #[serde(serialize_with = "serialize_pt")] 86 | #[serde(deserialize_with = "deserialize_pt")] 87 | pub page_height: Pt, 88 | 89 | /// The top margin of the document. 90 | #[serde(serialize_with = "serialize_pt")] 91 | #[serde(deserialize_with = "deserialize_pt")] 92 | pub top_margin: Pt, 93 | 94 | /// The left margin of the document. 95 | #[serde(serialize_with = "serialize_pt")] 96 | #[serde(deserialize_with = "deserialize_pt")] 97 | pub left_margin: Pt, 98 | 99 | /// The text width of the document. 100 | #[serde(serialize_with = "serialize_pt")] 101 | #[serde(deserialize_with = "deserialize_pt")] 102 | pub text_width: Pt, 103 | 104 | /// The text height of the document. 105 | #[serde(serialize_with = "serialize_pt")] 106 | #[serde(deserialize_with = "deserialize_pt")] 107 | pub text_height: Pt, 108 | 109 | /// The path to the first file of the spandex content. 110 | pub input: String, 111 | } 112 | 113 | impl Config { 114 | /// Creates a default configuration with a title. 115 | pub fn with_title(title: &str) -> Config { 116 | let page_width: Pt = Mm(210.0).into(); 117 | let page_height: Pt = Mm(297.0).into(); 118 | let top_margin: Pt = Mm(30.0).into(); 119 | let left_margin: Pt = Mm(30.0).into(); 120 | let text_width: Pt = Mm(150.0).into(); 121 | let text_height: Pt = Mm(237.0).into(); 122 | 123 | Config { 124 | title: String::from(title), 125 | page_width, 126 | page_height, 127 | top_margin, 128 | left_margin, 129 | text_width, 130 | text_height, 131 | input: String::from("main.dex"), 132 | } 133 | } 134 | 135 | /// Creates a document and a font maanger from the config. 136 | pub fn init(&self) -> CResult<(Document, FontManager)> { 137 | let window = Window { 138 | x: self.left_margin, 139 | y: self.top_margin, 140 | width: self.text_width, 141 | height: self.text_height, 142 | }; 143 | 144 | let mut document = Document::new("Hello", self.page_width, self.page_height, window); 145 | let font_manager = FontManager::init(&mut document)?; 146 | 147 | Ok((document, font_manager)) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::env::{self, current_dir}; 2 | use std::fs::{create_dir_all, File}; 3 | use std::io::{Read, Write}; 4 | use std::process::exit; 5 | 6 | use colored::*; 7 | 8 | use spandex::document::configuration::Config; 9 | use spandex::Error; 10 | 11 | macro_rules! unwrap { 12 | ($e: expr, $error: expr) => { 13 | match $e { 14 | Some(e) => e, 15 | None => return Err($error), 16 | } 17 | }; 18 | } 19 | 20 | fn print_version() { 21 | println!("SpanDeX {}", env!("CARGO_PKG_VERSION")); 22 | } 23 | 24 | fn print_help() { 25 | println!( 26 | r#"{name} {version} 27 | {description} 28 | 29 | {USAGE} 30 | {command} [SUBCOMMAND] 31 | 32 | {FLAGS} 33 | {help_short}, {help_long} Prints help information 34 | {version_short}, {version_long} Prints version information 35 | 36 | {SUBCOMMANDS} 37 | {build} Builds SpanDeX project 38 | {init} Creates new default SpanDeX project"#, 39 | name = "SpanDeX".green(), 40 | version = env!("CARGO_PKG_VERSION"), 41 | description = env!("CARGO_PKG_DESCRIPTION"), 42 | USAGE = "USAGE:".yellow(), 43 | command = env!("CARGO_PKG_NAME"), 44 | FLAGS = "FLAGS:".yellow(), 45 | help_short = "-h".green(), 46 | help_long = "--help".green(), 47 | version_short = "-v".green(), 48 | version_long = "--version".green(), 49 | SUBCOMMANDS = "SUBCOMMANDS:".yellow(), 50 | build = "build".green(), 51 | init = "init [title]".green(), 52 | ); 53 | } 54 | 55 | fn main() { 56 | if let Err(e) = run() { 57 | eprintln!("{}", e); 58 | exit(1); 59 | } 60 | } 61 | 62 | fn init(name: Option<&String>) -> Result<(), Error> { 63 | let mut current_dir = unwrap!(current_dir().ok(), Error::CannotReadCurrentDir); 64 | let current_dir_name = current_dir.clone(); 65 | let current_dir_name = unwrap!(current_dir_name.file_name(), Error::CannotReadCurrentDir); 66 | let current_dir_name = unwrap!(current_dir_name.to_str(), Error::CannotReadCurrentDir); 67 | 68 | // Initialize the project 69 | let title = match name.as_ref() { 70 | // If a title was given, we will create a directory for the project 71 | Some(title) => { 72 | current_dir.push(title); 73 | title 74 | } 75 | 76 | // If no title was given, use current_dir_name 77 | None => current_dir_name, 78 | }; 79 | 80 | // Try to create the directory 81 | create_dir_all(¤t_dir).ok(); 82 | 83 | // Create the default config and save it 84 | let config = Config::with_title(title); 85 | let toml = toml::to_string(&config).expect("Failed to generate toml"); 86 | 87 | current_dir.push("spandex.toml"); 88 | let mut file = File::create(¤t_dir)?; 89 | file.write_all(toml.as_bytes())?; 90 | 91 | // Write an hello world file 92 | current_dir.pop(); 93 | current_dir.push("main.dex"); 94 | 95 | let mut file = File::create(¤t_dir)?; 96 | file.write_all(b"# Hello world")?; 97 | 98 | Ok(()) 99 | } 100 | 101 | fn build() -> Result<(), Error> { 102 | // Look up for spandex config file 103 | let mut current_dir = unwrap!(current_dir().ok(), Error::CannotReadCurrentDir); 104 | let config_path = loop { 105 | current_dir.push("spandex.toml"); 106 | 107 | if current_dir.is_file() { 108 | break current_dir; 109 | } else { 110 | // Remove spandex.toml 111 | current_dir.pop(); 112 | 113 | // Go to the parent directory 114 | if !current_dir.pop() { 115 | return Err(Error::NoConfigFile); 116 | } 117 | } 118 | }; 119 | 120 | // Read config file 121 | let mut file = File::open(&config_path)?; 122 | let mut content = String::new(); 123 | file.read_to_string(&mut content)?; 124 | let config: Config = toml::from_str(&content).expect("Failed to parse toml"); 125 | spandex::build(&config)?; 126 | 127 | Ok(()) 128 | } 129 | 130 | fn run() -> Result<(), Error> { 131 | let args = env::args().collect::>(); 132 | 133 | // The first argument is the name of the binary, the second one is the command 134 | if args.len() < 2 { 135 | eprintln!("{}: toto", "error".red().bold()); 136 | print_help(); 137 | exit(1); 138 | } 139 | 140 | if args.contains(&String::from("-h")) || args.contains(&String::from("--help")) { 141 | print_help(); 142 | exit(0); 143 | } 144 | 145 | if args.contains(&String::from("-v")) || args.contains(&String::from("--version")) { 146 | print_version(); 147 | exit(0); 148 | } 149 | 150 | match args[1].as_ref() { 151 | "init" => init(args.get(2))?, 152 | 153 | "build" => build()?, 154 | 155 | command => { 156 | // Unknwon command 157 | eprintln!( 158 | "{}: command \"{}\" does not exist.", 159 | "error".bold().red(), 160 | command, 161 | ); 162 | print_help(); 163 | } 164 | } 165 | 166 | Ok(()) 167 | } 168 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [ master, main ] 7 | 8 | jobs: 9 | build: 10 | name: Build 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | build: 16 | # - linux-musl 17 | - linux-gnu # temporary because of freetype 18 | - windows 19 | - macos 20 | - macos-arm-64 21 | - linux-arm-64 22 | include: 23 | # - build: linux-musl 24 | # os: ubuntu-latest 25 | # toolchain: stable 26 | # target: x86_64-unknown-linux-musl 27 | # 28 | - build: linux-gnu 29 | os: ubuntu-latest 30 | toolchain: stable 31 | target: x86_64-unknown-linux-gnu 32 | 33 | - build: windows 34 | os: windows-latest 35 | toolchain: stable 36 | target: x86_64-pc-windows-msvc 37 | 38 | - build: macos 39 | os: macos-latest 40 | toolchain: stable 41 | target: x86_64-apple-darwin 42 | 43 | - build: macos-arm-64 44 | os: macos-11 45 | toolchain: nightly 46 | target: aarch64-apple-darwin 47 | cross: true 48 | 49 | - build: linux-arm-64 50 | os: ubuntu-latest 51 | toolchain: nightly 52 | # target: aarch64-unknown-linux-musl 53 | target: aarch64-unknown-linux-gnu # temporary because of freetype 54 | cross: true 55 | 56 | steps: 57 | - name: install musl-tools if musl target 58 | if: ${{ contains(matrix.target, 'musl') }} 59 | run: sudo apt install musl-tools 60 | 61 | - uses: actions/checkout@v2 62 | 63 | - uses: actions-rs/toolchain@v1 64 | with: 65 | profile: minimal 66 | toolchain: ${{ matrix.toolchain }} 67 | target: ${{ matrix.target }} 68 | override: true 69 | 70 | - name: Download cache 71 | uses: actions/cache@v2 72 | with: 73 | path: | 74 | ~/.cargo/ 75 | target/ 76 | key: ${{ matrix.target }}-build-${{ hashFiles('Cargo.lock') }} 77 | restore-keys: | 78 | ${{ matrix.target }}-build- 79 | ${{ matrix.target }}- 80 | 81 | - name: Install cross compiler 82 | if: ${{ matrix.cross }} 83 | uses: actions-rs/cargo@v1 84 | with: 85 | command: install 86 | args: cross 87 | 88 | - name: Use cross instead of cargo for cross compilation 89 | if: ${{ matrix.cross }} 90 | run: cross build --release --target ${{ matrix.target }} 91 | 92 | - uses: actions-rs/cargo@v1 93 | if: ${{ !matrix.cross }} 94 | with: 95 | command: build 96 | args: --release --target ${{ matrix.target }} 97 | 98 | - name: Compress compiled binary 99 | if: ${{ !startsWith(matrix.os, 'windows') }} 100 | run: | 101 | cd target/${{ matrix.target }}/release 102 | tar czf spandex_${{ matrix.target }}.tar.gz spandex 103 | 104 | - uses: actions/upload-artifact@v2 105 | if: ${{ !startsWith(matrix.os, 'windows') }} 106 | with: 107 | name: spandex_${{ matrix.target }}.tar.gz 108 | path: target/${{ matrix.target }}/release/spandex_${{ matrix.target }}.tar.gz 109 | 110 | - uses: actions/upload-artifact@v2 111 | if: ${{ startsWith(matrix.os, 'windows') }} 112 | with: 113 | name: spandex_${{ matrix.target }} 114 | path: target/${{ matrix.target }}/release/spandex.exe 115 | 116 | test: 117 | name: Test 118 | runs-on: ${{ matrix.os }} 119 | strategy: 120 | fail-fast: false 121 | matrix: 122 | os: 123 | - ubuntu-latest 124 | - windows-latest 125 | - macos-latest 126 | toolchain: 127 | - stable 128 | 129 | steps: 130 | - uses: actions/checkout@v2 131 | 132 | - uses: actions-rs/toolchain@v1 133 | with: 134 | profile: minimal 135 | toolchain: ${{ matrix.toolchain }} 136 | override: true 137 | 138 | - name: Download cache 139 | uses: actions/cache@v2 140 | with: 141 | path: | 142 | ~/.cargo/ 143 | target/ 144 | key: test-${{ matrix.os }}-${{ hashFiles('Cargo.lock') }} 145 | restore-keys: | 146 | test-${{ matrix.os }}- 147 | 148 | - uses: actions-rs/cargo@v1 149 | with: 150 | command: test 151 | 152 | check_formatting: 153 | name: Check formatting 154 | runs-on: ubuntu-latest 155 | steps: 156 | - uses: actions/checkout@v2 157 | 158 | - uses: actions-rs/toolchain@v1 159 | with: 160 | profile: minimal 161 | toolchain: stable 162 | components: rustfmt 163 | override: true 164 | 165 | - uses: actions-rs/cargo@v1 166 | with: 167 | command: fmt 168 | args: --all -- --check 169 | 170 | clippy: 171 | name: Check clippy 172 | runs-on: ubuntu-latest 173 | steps: 174 | - uses: actions/checkout@v2 175 | 176 | - uses: actions-rs/toolchain@v1 177 | with: 178 | profile: minimal 179 | toolchain: stable 180 | components: clippy 181 | override: true 182 | 183 | - uses: actions-rs/cargo@v1 184 | env: 185 | RUSTFLAGS: -D warnings 186 | with: 187 | command: clippy 188 | -------------------------------------------------------------------------------- /src/fonts/manager.rs: -------------------------------------------------------------------------------- 1 | //! Font manager that detects, loads and handles the different fonts 2 | //! available on the system. 3 | 4 | use crate::document::Document; 5 | use crate::fonts::configuration::FontConfig; 6 | use crate::fonts::Font; 7 | use crate::{Error, Result}; 8 | use freetype::Library; 9 | use std::collections::HashMap; 10 | use std::path::PathBuf; 11 | 12 | /// This struct holds the different fonts. 13 | pub struct FontManager { 14 | /// The freetype library, needed to be able to measure texts. 15 | library: Library, 16 | 17 | /// The hashmap that associates names of fonts with fonts. 18 | fonts: HashMap, 19 | } 20 | 21 | impl FontManager { 22 | /// Creates a new font manager, with the default fonts. 23 | pub fn init(document: &mut Document) -> Result { 24 | let mut font_manager = FontManager { 25 | library: Library::init()?, 26 | fonts: HashMap::new(), 27 | }; 28 | 29 | // Insert the default fonts 30 | font_manager.add_font(include_bytes!("../../assets/fonts/cmunbi.ttf"), document)?; 31 | font_manager.add_font(include_bytes!("../../assets/fonts/cmunbl.ttf"), document)?; 32 | font_manager.add_font(include_bytes!("../../assets/fonts/cmunbmo.ttf"), document)?; 33 | font_manager.add_font(include_bytes!("../../assets/fonts/cmunbmr.ttf"), document)?; 34 | font_manager.add_font(include_bytes!("../../assets/fonts/cmunbso.ttf"), document)?; 35 | font_manager.add_font(include_bytes!("../../assets/fonts/cmunbsr.ttf"), document)?; 36 | font_manager.add_font(include_bytes!("../../assets/fonts/cmunbtl.ttf"), document)?; 37 | font_manager.add_font(include_bytes!("../../assets/fonts/cmunbto.ttf"), document)?; 38 | font_manager.add_font(include_bytes!("../../assets/fonts/cmunbx.ttf"), document)?; 39 | font_manager.add_font(include_bytes!("../../assets/fonts/cmunci.ttf"), document)?; 40 | font_manager.add_font(include_bytes!("../../assets/fonts/cmunit.ttf"), document)?; 41 | font_manager.add_font(include_bytes!("../../assets/fonts/cmunobi.ttf"), document)?; 42 | font_manager.add_font(include_bytes!("../../assets/fonts/cmunobx.ttf"), document)?; 43 | font_manager.add_font(include_bytes!("../../assets/fonts/cmunorm.ttf"), document)?; 44 | font_manager.add_font(include_bytes!("../../assets/fonts/cmunoti.ttf"), document)?; 45 | font_manager.add_font(include_bytes!("../../assets/fonts/cmunrm.ttf"), document)?; 46 | font_manager.add_font(include_bytes!("../../assets/fonts/cmunsi.ttf"), document)?; 47 | font_manager.add_font(include_bytes!("../../assets/fonts/cmunsl.ttf"), document)?; 48 | font_manager.add_font(include_bytes!("../../assets/fonts/cmunso.ttf"), document)?; 49 | font_manager.add_font(include_bytes!("../../assets/fonts/cmunssdc.ttf"), document)?; 50 | font_manager.add_font(include_bytes!("../../assets/fonts/cmunss.ttf"), document)?; 51 | font_manager.add_font(include_bytes!("../../assets/fonts/cmunsx.ttf"), document)?; 52 | font_manager.add_font(include_bytes!("../../assets/fonts/cmuntb.ttf"), document)?; 53 | font_manager.add_font(include_bytes!("../../assets/fonts/cmunti.ttf"), document)?; 54 | font_manager.add_font(include_bytes!("../../assets/fonts/cmuntt.ttf"), document)?; 55 | font_manager.add_font(include_bytes!("../../assets/fonts/cmuntx.ttf"), document)?; 56 | font_manager.add_font(include_bytes!("../../assets/fonts/cmunui.ttf"), document)?; 57 | font_manager.add_font(include_bytes!("../../assets/fonts/cmunvi.ttf"), document)?; 58 | font_manager.add_font(include_bytes!("../../assets/fonts/cmunvt.ttf"), document)?; 59 | 60 | Ok(font_manager) 61 | } 62 | 63 | /// Adds a new font to the font manager. 64 | pub fn add_font(&mut self, bytes: &[u8], document: &mut Document) -> Result<()> { 65 | let font = Font::from_bytes(bytes, &self.library, document)?; 66 | let name = match (font.freetype.family_name(), font.freetype.style_name()) { 67 | (Some(family), Some(style)) => format!("{} {}", family, style), 68 | _ => { 69 | eprintln!("Failed to create a built in font, this is a implementation error"); 70 | unreachable!(); 71 | } 72 | }; 73 | self.fonts.insert(name, font); 74 | Ok(()) 75 | } 76 | 77 | /// Returns a reference font if it is present in the font manager. 78 | pub fn get(&self, font_name: &str) -> Option<&Font> { 79 | self.fonts.get(font_name) 80 | } 81 | 82 | /// Creates a font config. 83 | pub fn config<'a>( 84 | &'a self, 85 | regular: &str, 86 | bold: &str, 87 | italic: &str, 88 | bold_italic: &str, 89 | ) -> Result> { 90 | Ok(FontConfig { 91 | regular: self 92 | .fonts 93 | .get(regular) 94 | .ok_or_else(|| Error::FontNotFound(PathBuf::from(regular)))?, 95 | bold: self 96 | .fonts 97 | .get(bold) 98 | .ok_or_else(|| Error::FontNotFound(PathBuf::from(bold)))?, 99 | italic: self 100 | .fonts 101 | .get(italic) 102 | .ok_or_else(|| Error::FontNotFound(PathBuf::from(italic)))?, 103 | bold_italic: self 104 | .fonts 105 | .get(bold_italic) 106 | .ok_or_else(|| Error::FontNotFound(PathBuf::from(bold_italic)))?, 107 | }) 108 | } 109 | 110 | /// Returns the default configuration for computer modern fonts. 111 | pub fn default_config(&self) -> FontConfig { 112 | let regular = "CMU Serif Roman"; 113 | let bold = "CMU Serif Bold"; 114 | let italic = "CMU Serif Italic"; 115 | let bold_italic = "CMU Serif BoldItalic"; 116 | 117 | // This should never fail. 118 | match self.config(regular, bold, italic, bold_italic) { 119 | Ok(c) => c, 120 | Err(_) => unreachable!("Default font not found, this should never happen"), 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/parser/ast.rs: -------------------------------------------------------------------------------- 1 | //! This module contains everything related to the ast. 2 | 3 | use std::fmt; 4 | use std::path::PathBuf; 5 | 6 | use colored::*; 7 | 8 | use crate::parser::error::EmptyError; 9 | use crate::parser::warning::EmptyWarning; 10 | 11 | /// The abstract syntax tree representing the parsed file. 12 | #[derive(PartialEq, Eq, Clone)] 13 | pub enum Ast { 14 | /// A title. 15 | Title { 16 | /// The level of the title. 17 | level: u8, 18 | 19 | /// The content of the title. 20 | children: Vec, 21 | }, 22 | 23 | /// Some bold content. 24 | Bold(Vec), 25 | 26 | /// Some italic content. 27 | Italic(Vec), 28 | 29 | /// A math inlinemath. 30 | InlineMath(String), 31 | 32 | /// Some text. 33 | Text(String), 34 | 35 | /// A paragraph. 36 | /// 37 | /// It contains many elements but must be rendered on a single paragraph. 38 | Paragraph(Vec), 39 | 40 | /// Content stored in a specific file. 41 | File(PathBuf, Vec), 42 | 43 | /// An empty line. 44 | Newline, 45 | 46 | /// An error. 47 | /// 48 | /// Error will be stored in the abstract syntax tree so we can keep parsing what's parsable and 49 | /// print many errors instead of crashing immediately. 50 | Error(EmptyError), 51 | 52 | /// A warning. 53 | /// 54 | /// Warning will be stored in the Ast and we will print them in the main function. 55 | Warning(EmptyWarning), 56 | } 57 | 58 | impl Ast { 59 | /// Returns the children of the ast, if any. 60 | pub fn children(&self) -> Option<&Vec> { 61 | match self { 62 | Ast::File(_, children) 63 | | Ast::Paragraph(children) 64 | | Ast::Title { children, .. } 65 | | Ast::Bold(children) 66 | | Ast::Italic(children) => Some(children), 67 | _ => None, 68 | } 69 | } 70 | 71 | /// Returns all the errors contained in the ast. 72 | pub fn errors(&self) -> Vec { 73 | let mut errors = vec![]; 74 | 75 | if let Ast::Error(e) = self { 76 | errors.push(e.clone()); 77 | } 78 | 79 | if let Some(children) = self.children() { 80 | for child in children { 81 | errors.extend(child.errors()); 82 | } 83 | } 84 | 85 | errors 86 | } 87 | 88 | /// Returns all the errors contained in the ast. 89 | pub fn warnings(&self) -> Vec { 90 | let mut warnings = vec![]; 91 | 92 | if let Ast::Warning(e) = self { 93 | warnings.push(e.clone()); 94 | } 95 | 96 | if let Some(children) = self.children() { 97 | for child in children { 98 | warnings.extend(child.warnings()); 99 | } 100 | } 101 | 102 | warnings 103 | } 104 | 105 | /// Pretty prints the ast. 106 | pub fn print_debug( 107 | &self, 108 | fmt: &mut fmt::Formatter, 109 | indent: &str, 110 | last_child: bool, 111 | ) -> fmt::Result { 112 | let delimiter1 = if indent.is_empty() { 113 | "─" 114 | } else if last_child { 115 | "└" 116 | } else { 117 | "├" 118 | }; 119 | 120 | let delimiter2 = match self { 121 | Ast::Error(_) | Ast::Warning(_) | Ast::Text(_) | Ast::Newline | Ast::InlineMath(_) => { 122 | "──" 123 | } 124 | _ => "─┬", 125 | }; 126 | 127 | let new_indent = format!("{}{}{} ", indent, delimiter1, delimiter2); 128 | 129 | let indent = if last_child { 130 | format!("{} ", indent) 131 | } else { 132 | format!("{}│ ", indent) 133 | }; 134 | 135 | match self { 136 | Ast::Error(e) => writeln!(fmt, "{}{}", new_indent, &format!("Error({:?})", e).red())?, 137 | Ast::Warning(e) => writeln!( 138 | fmt, 139 | "{}{}", 140 | new_indent, 141 | &format!("Warning({:?})", e).yellow() 142 | )?, 143 | Ast::Text(t) => writeln!( 144 | fmt, 145 | "{}{}{}{}", 146 | new_indent, 147 | "Text(".green(), 148 | &format!("{:?}", t).dimmed(), 149 | ")".green() 150 | )?, 151 | Ast::Newline => writeln!(fmt, "{}NewLine", new_indent)?, 152 | Ast::InlineMath(math) => writeln!(fmt, "{}Math({:?})", new_indent, math)?, 153 | Ast::File(path, _) => writeln!( 154 | fmt, 155 | "{}{}", 156 | new_indent, 157 | &format!("File(\"{}\")", path.display()).blue().bold() 158 | )?, 159 | Ast::Paragraph(_) => writeln!(fmt, "{}{}", new_indent, "Paragraph".blue().bold())?, 160 | 161 | Ast::Title { level, .. } => writeln!( 162 | fmt, 163 | "{}{}", 164 | new_indent, 165 | &format!("Title(level={})", level).magenta().bold() 166 | )?, 167 | 168 | Ast::Bold(_) => writeln!(fmt, "{}{}", new_indent, "Bold".cyan().bold())?, 169 | 170 | Ast::Italic(_) => writeln!(fmt, "{}{}", new_indent, "Italic".cyan().bold())?, 171 | } 172 | 173 | if let Some(children) = self.children() { 174 | let len = children.len(); 175 | for (index, child) in children.iter().enumerate() { 176 | child.print_debug(fmt, &indent, index == len - 1)?; 177 | } 178 | } 179 | 180 | Ok(()) 181 | } 182 | } 183 | 184 | impl fmt::Display for Ast { 185 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 186 | match self { 187 | Ast::Title { level, .. } => { 188 | for _ in 0..*level { 189 | write!(fmt, "{}", "#".bold())?; 190 | } 191 | } 192 | 193 | Ast::InlineMath(content) => write!(fmt, "${}$", content)?, 194 | Ast::Text(content) => write!(fmt, "{}", content)?, 195 | _ => (), 196 | } 197 | 198 | if let Some(children) = self.children() { 199 | for child in children { 200 | write!(fmt, "{}", child)?; 201 | } 202 | } 203 | 204 | Ok(()) 205 | } 206 | } 207 | 208 | impl fmt::Debug for Ast { 209 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 210 | self.print_debug(fmt, "", true) 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/document/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module allows to create beautiful documents. 2 | 3 | pub mod configuration; 4 | pub mod counters; 5 | 6 | use std::fs::File; 7 | use std::io::BufWriter; 8 | use std::path::Path; 9 | 10 | use printpdf::{PdfDocument, PdfDocumentReference, PdfLayerReference, PdfPageReference, Pt}; 11 | use spandex_hyphenation::load::Load; 12 | use spandex_hyphenation::{Language, Standard}; 13 | 14 | use crate::document::counters::Counters; 15 | use crate::fonts::configuration::FontConfig; 16 | use crate::fonts::Font; 17 | use crate::layout::paragraphs::justification::{Justifier, LatexJustifier}; 18 | use crate::layout::paragraphs::utils::ast::itemize_ast; 19 | use crate::parser::ast::Ast; 20 | 21 | /// The window that is the part of the page on which we're allowed to write. 22 | #[derive(Copy, Clone)] 23 | pub struct Window { 24 | /// The x coordinate of the window, in pt. 25 | pub x: Pt, 26 | 27 | /// The y coordinate of the window, in pt. 28 | pub y: Pt, 29 | 30 | /// The width of the window, in pt. 31 | pub width: Pt, 32 | 33 | /// The height of the window, in pt. 34 | pub height: Pt, 35 | } 36 | 37 | /// This struct contains the pdf document. 38 | pub struct Document { 39 | /// The inner document from printpdf. 40 | document: PdfDocumentReference, 41 | 42 | /// The current page. 43 | page: PdfPageReference, 44 | 45 | /// The current layer. 46 | layer: PdfLayerReference, 47 | 48 | /// The window on which we're allowed to write on the page. 49 | window: Window, 50 | 51 | /// The cursor, the position where we supposed to write next. 52 | cursor: (Pt, Pt), 53 | 54 | /// The current page size, in pt. 55 | page_size: (Pt, Pt), 56 | 57 | /// The counters of the document 58 | counters: Counters, 59 | } 60 | 61 | impl Document { 62 | /// Creates a new pdf document from its name and its size in pt. 63 | pub fn new, U: Into>( 64 | name: &str, 65 | width: T, 66 | height: U, 67 | window: Window, 68 | ) -> Document { 69 | let width: Pt = width.into(); 70 | let height: Pt = height.into(); 71 | 72 | let (document, page, layer) = PdfDocument::new(name, width.into(), height.into(), ""); 73 | 74 | let page = document.get_page(page); 75 | let layer = page.get_layer(layer); 76 | 77 | Document { 78 | document, 79 | page, 80 | layer, 81 | window, 82 | cursor: (window.x, window.height + window.y), 83 | page_size: (width, height), 84 | counters: Counters::new(), 85 | } 86 | } 87 | 88 | /// Returns a reference to the inner pdf document. 89 | pub fn inner(&self) -> &PdfDocumentReference { 90 | &self.document 91 | } 92 | 93 | /// Returns a mutable reference to the inner pdf document. 94 | pub fn inner_mut(&mut self) -> &mut PdfDocumentReference { 95 | &mut self.document 96 | } 97 | 98 | /// Renders an AST to the document. 99 | pub fn render(&mut self, ast: &Ast, font_config: &FontConfig, size: Pt) { 100 | let en = Standard::from_embedded(Language::EnglishUS).unwrap(); 101 | 102 | match ast { 103 | Ast::File(_, children) => { 104 | for child in children { 105 | self.render(child, font_config, size); 106 | } 107 | } 108 | 109 | Ast::Title { level, children } => { 110 | self.counters.increment(*level as usize); 111 | let mut new_children = vec![Ast::Text(format!("{} ", self.counters))]; 112 | new_children.extend_from_slice(children); 113 | let new_ast = Ast::Title { 114 | level: *level, 115 | children: new_children, 116 | }; 117 | self.write_paragraph::(&new_ast, font_config, size, &en); 118 | self.new_line(size); 119 | } 120 | 121 | Ast::Paragraph(_) => { 122 | self.write_paragraph::(ast, font_config, size, &en); 123 | self.new_line(size); 124 | self.new_line(size); 125 | } 126 | 127 | _ => (), 128 | } 129 | } 130 | 131 | /// Writes content on the document. 132 | pub fn write_content(&mut self, content: &str, font_config: &FontConfig, size: Pt) { 133 | let en = Standard::from_embedded(Language::EnglishUS).unwrap(); 134 | 135 | for paragraph in content.split('\n') { 136 | let ast = Ast::Text(paragraph.to_owned()); 137 | self.write_paragraph::(&ast, font_config, size, &en); 138 | self.new_line(size); 139 | } 140 | } 141 | 142 | /// Writes a paragraph on the document. 143 | pub fn write_paragraph( 144 | &mut self, 145 | paragraph: &Ast, 146 | font_config: &FontConfig, 147 | size: Pt, 148 | dict: &Standard, 149 | ) { 150 | let paragraph = itemize_ast(paragraph, font_config, size, dict, Pt(0.0)); 151 | let justified = J::justify(¶graph, self.window.width); 152 | 153 | for line in justified { 154 | for glyph in line { 155 | self.layer.use_text( 156 | glyph.0.glyph.to_string(), 157 | Into::::into(glyph.0.scale).0 as f64, 158 | (self.window.x + glyph.1).into(), 159 | self.cursor.1.into(), 160 | glyph.0.font.printpdf(), 161 | ); 162 | } 163 | 164 | self.new_line(size); 165 | self.cursor.0 = self.window.x; 166 | 167 | if self.cursor.1 <= size + self.window.y { 168 | self.new_page(); 169 | } 170 | } 171 | } 172 | 173 | /// Writes a line in the document. 174 | pub fn write_line(&mut self, words: &[&str], font: &Font, size: Pt, spacing: Pt) { 175 | let size_f64 = Into::::into(size).0 as f64; 176 | let mut current_width = self.window.x; 177 | 178 | for word in words { 179 | let width = current_width; 180 | let height = self.cursor.1; 181 | 182 | self.layer.use_text( 183 | word.to_owned(), 184 | size_f64, 185 | width.into(), 186 | (height - size).into(), 187 | font.printpdf(), 188 | ); 189 | current_width += font.text_width(word, size) + spacing; 190 | } 191 | 192 | self.new_line(size); 193 | } 194 | 195 | /// Goes to the beginning of the next line. 196 | pub fn new_line(&mut self, size: Pt) { 197 | self.cursor.1 -= size; 198 | } 199 | 200 | /// Creates a new page and append it to the document. 201 | pub fn new_page(&mut self) { 202 | let page = self 203 | .document 204 | .add_page(self.page_size.0.into(), self.page_size.1.into(), ""); 205 | self.page = self.document.get_page(page.0); 206 | self.layer = self.page.get_layer(page.1); 207 | self.cursor.1 = self.window.height + self.window.y; 208 | } 209 | 210 | /// Saves the document into a file. 211 | pub fn save>(self, path: P) { 212 | let file = File::create(path.as_ref()).unwrap(); 213 | let mut writer = BufWriter::new(file); 214 | self.document.save(&mut writer).unwrap(); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/layout/paragraphs/utils/linebreak.rs: -------------------------------------------------------------------------------- 1 | //! Utility functions for the various stages of the line breaking algorithm. 2 | 3 | use crate::layout::constants::{ADJACENT_LOOSE_TIGHT_PENALTY, MAX_COST, MIN_COST}; 4 | use crate::layout::paragraphs::graph::Node; 5 | use crate::layout::paragraphs::utils::paragraphs::get_line_length; 6 | 7 | use crate::layout::paragraphs::items::{Content, Item}; 8 | use crate::layout::paragraphs::Paragraph; 9 | use printpdf::Pt; 10 | use std::f64; 11 | 12 | /// Accumulator to hold the three key related measures. 13 | pub struct Measures { 14 | /// Measure of the width accumulated so far. 15 | pub width: Pt, 16 | 17 | /// Measure of the shrinkability accumulated so far. 18 | pub shrinkability: Pt, 19 | 20 | /// Measure of the stretchability accumulated so far. 21 | pub stretchability: Pt, 22 | } 23 | 24 | /// Computes the adjusment ratio of a line of items, based on their combined 25 | /// width, stretchability and shrinkability. This essentially tells how much 26 | /// effort has to be produce to fit the line to the desired width. 27 | pub fn compute_adjustment_ratio( 28 | actual_length: Pt, 29 | desired_length: Pt, 30 | total_stretchability: Pt, 31 | total_shrinkability: Pt, 32 | ) -> f64 { 33 | if actual_length == desired_length { 34 | 0.0 35 | } else if actual_length < desired_length { 36 | if total_stretchability != Pt(0.0) { 37 | (desired_length.0 - actual_length.0) / total_stretchability.0 38 | } else { 39 | f64::INFINITY 40 | } 41 | } else if total_shrinkability != Pt(0.0) { 42 | (desired_length.0 - actual_length.0) / total_shrinkability.0 43 | } else { 44 | f64::INFINITY 45 | } 46 | } 47 | 48 | /// Computes the adjustment ratios of all lines given a set of line lengths and breakpoint indices. 49 | /// This allows to speed up the adaptation of glue items. 50 | pub fn compute_adjustment_ratios_with_breakpoints<'a>( 51 | items: &[Item<'a>], 52 | line_lengths: &[Pt], 53 | breakpoints: &[usize], 54 | ) -> Vec { 55 | let mut adjustment_ratios: Vec = Vec::new(); 56 | 57 | for (breakpoint_line, breakpoint_index) in breakpoints.iter().enumerate() { 58 | let desired_length = get_line_length(line_lengths, breakpoint_line); 59 | let mut actual_length = Pt(0.0); 60 | let mut line_shrink = Pt(0.0); 61 | let mut line_stretch = Pt(0.0); 62 | let next_breakpoint = if breakpoint_line < breakpoints.len() - 1 { 63 | breakpoints[breakpoint_line + 1] 64 | } else { 65 | items.len() - 1 66 | }; 67 | 68 | let beginning = if breakpoint_line == 0 { 69 | *breakpoint_index 70 | } else { 71 | *breakpoint_index + 1 72 | }; 73 | 74 | let range = items 75 | .iter() 76 | .enumerate() 77 | .take(next_breakpoint) 78 | .skip(beginning); 79 | 80 | for (p, item) in range { 81 | match item.content { 82 | Content::BoundingBox { .. } => actual_length += items[p].width, 83 | Content::Glue { 84 | shrinkability, 85 | stretchability, 86 | } => { 87 | if p != beginning && p != next_breakpoint { 88 | actual_length += item.width; 89 | line_shrink += shrinkability; 90 | line_stretch += stretchability; 91 | } 92 | } 93 | Content::Penalty { .. } => { 94 | if p == next_breakpoint { 95 | actual_length += item.width; 96 | } 97 | } 98 | } 99 | } 100 | 101 | adjustment_ratios.push(compute_adjustment_ratio( 102 | actual_length, 103 | desired_length, 104 | line_stretch, 105 | line_shrink, 106 | )); 107 | } 108 | 109 | adjustment_ratios 110 | } 111 | 112 | /// Computes the demerits of a line based on its accumulated penalty 113 | /// and badness. 114 | pub fn compute_demerits(penalty: f64, badness: f64) -> f64 { 115 | if penalty >= 0.0 { 116 | (1.0 + badness + penalty).powi(2) 117 | } else if penalty > MIN_COST { 118 | (1.0 + badness).powi(2) - penalty.powi(2) 119 | } else { 120 | (1.0 + badness).powi(2) 121 | } 122 | } 123 | 124 | /// Computes the fitness class of a line based on its adjustment ratio. 125 | pub fn compute_fitness(adjustment_ratio: f64) -> i64 { 126 | if adjustment_ratio < -0.5 { 127 | 0 128 | } else if adjustment_ratio < 0.5 { 129 | 1 130 | } else if adjustment_ratio < 1.0 { 131 | 2 132 | } else { 133 | 3 134 | } 135 | } 136 | 137 | /// Checks whether or not a given item encodes a forced linebreak. 138 | pub fn is_forced_break<'a>(item: &'a Item<'a>) -> bool { 139 | match item.content { 140 | Content::Penalty { value, .. } => value < MIN_COST, 141 | _ => false, 142 | } 143 | } 144 | 145 | /// Finds all the legal breakpoints within a paragraph. A legal breakpoint 146 | /// is an item index such that this item is either a peanalty which isn't 147 | /// infinite or a glue following a bounding box. 148 | pub fn find_legal_breakpoints(paragraph: &Paragraph) -> Vec { 149 | let mut legal_breakpoints: Vec = vec![0]; 150 | 151 | let mut last_item_was_box = false; 152 | 153 | for (i, item) in paragraph.items.iter().enumerate() { 154 | match item.content { 155 | Content::Penalty { value, .. } => { 156 | if value < f64::INFINITY { 157 | legal_breakpoints.push(i); 158 | } 159 | 160 | last_item_was_box = false; 161 | } 162 | Content::Glue { .. } => { 163 | if last_item_was_box { 164 | legal_breakpoints.push(i) 165 | } 166 | 167 | last_item_was_box = false; 168 | } 169 | Content::BoundingBox { .. } => last_item_was_box = true, 170 | } 171 | } 172 | 173 | legal_breakpoints 174 | } 175 | 176 | /// Handles a feasible breakpoint and adds it to the current graph of 177 | /// feasible breakpoints if it's good enough. 178 | #[inline] 179 | pub fn create_node_for_feasible_breakpoint( 180 | b: usize, 181 | a: &Node, 182 | adjustment_ratio: f64, 183 | item: &Item, 184 | items: &[Item], 185 | measures_sum: &Measures, 186 | ) -> Node { 187 | // This is a feasible breakpoint. 188 | let badness = adjustment_ratio.abs().powi(3); 189 | let penalty = match item.content { 190 | Content::Penalty { value, .. } => value, 191 | _ => 0.0, 192 | }; 193 | 194 | let mut demerits = compute_demerits(penalty, badness); 195 | 196 | // TODO: support double hyphenation penalty. 197 | 198 | // Compute fitness class. 199 | let fitness = compute_fitness(adjustment_ratio); 200 | 201 | if a.index > 0 && (fitness - a.fitness).abs() > 1 { 202 | demerits += ADJACENT_LOOSE_TIGHT_PENALTY; 203 | } 204 | 205 | // TODO: Ignore the width of potential subsequent glue or 206 | // non-breakable penalty item to avoid rendering glue or 207 | // penalties at the beginning of lines. 208 | let measures_to_next_box = get_measures_to_next_box(b, item, items); 209 | 210 | Node { 211 | index: b, 212 | line: a.line + 1, 213 | fitness, 214 | total_width: measures_sum.width + measures_to_next_box.width, 215 | total_shrink: measures_sum.shrinkability + measures_to_next_box.shrinkability, 216 | total_stretch: measures_sum.stretchability + measures_to_next_box.stretchability, 217 | total_demerits: a.total_demerits + demerits, 218 | } 219 | } 220 | 221 | /// Computes the accumulated measures from the current linebreak 222 | /// to the next bounding box in the provided items. 223 | #[inline] 224 | pub fn get_measures_to_next_box(b: usize, item: &Item, items: &[Item]) -> Measures { 225 | let mut width_to_next_box = Pt(0.0); 226 | let mut shrink_to_next_box = Pt(0.0); 227 | let mut stretch_to_next_box = Pt(0.0); 228 | 229 | for next_item in &items[b..] { 230 | width_to_next_box += item.width; 231 | 232 | match next_item.content { 233 | Content::BoundingBox { .. } => break, 234 | Content::Glue { 235 | shrinkability, 236 | stretchability, 237 | } => { 238 | shrink_to_next_box += shrinkability; 239 | stretch_to_next_box += stretchability; 240 | } 241 | Content::Penalty { value, .. } => { 242 | if value >= MAX_COST { 243 | break; 244 | } 245 | } 246 | } 247 | } 248 | 249 | Measures { 250 | width: width_to_next_box, 251 | shrinkability: shrink_to_next_box, 252 | stretchability: stretch_to_next_box, 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/parser/combinators.rs: -------------------------------------------------------------------------------- 1 | //! This module contains all the functions needed for parsing. 2 | 3 | // This module contains marcos that can't be documented, so we'll allow missing docs here. 4 | #![allow(missing_docs)] 5 | // Allow redundant closure because of nom. 6 | #![allow(clippy::redundant_closure)] 7 | 8 | use std::fs::File; 9 | use std::io::Read; 10 | use std::path::{Path, PathBuf}; 11 | 12 | use nom::branch::alt; 13 | use nom::bytes::complete::{tag, take_till1, take_until}; 14 | use nom::character::complete::{char, line_ending, not_line_ending, space0}; 15 | use nom::combinator::{map, map_res, opt, rest, verify}; 16 | use nom::multi::{fold_many0, many0, many1_count}; 17 | use nom::sequence::delimited; 18 | use nom::{IResult, Slice}; 19 | 20 | use crate::layout::paragraphs::ligatures::ligature; 21 | use crate::parser::ast::Ast; 22 | use crate::parser::error::{EmptyError, ErrorType, Errors}; 23 | use crate::parser::warning::{EmptyWarning, WarningType, Warnings}; 24 | use crate::parser::{position, Error, Parsed, Span}; 25 | 26 | /// Returns true if the character passed as parameter changes the type of parsing we're going to do. 27 | pub fn should_stop(c: char) -> bool { 28 | c == '*' || c == '/' || c == '$' || c == '|' 29 | } 30 | 31 | /// Creates an error. 32 | pub fn error(span: Span, ty: ErrorType) -> Ast { 33 | Ast::Error(EmptyError { 34 | position: position(&span), 35 | ty, 36 | }) 37 | } 38 | 39 | /// Creates a warning. 40 | pub fn warning(span: Span, ty: WarningType) -> Ast { 41 | Ast::Warning(EmptyWarning { 42 | position: position(&span), 43 | ty, 44 | }) 45 | } 46 | 47 | /// Parses some bold content. 48 | /// ``` 49 | /// # use spandex::parser::ast::Ast; 50 | /// # use spandex::parser::Span; 51 | /// # use spandex::parser::combinators::parse_bold; 52 | /// let input = Span::new("*Hello*"); 53 | /// let parse = parse_bold(input).unwrap().1; 54 | /// assert_eq!(parse, Ast::Bold(vec![Ast::Text(String::from("Hello"))])); 55 | /// ``` 56 | pub fn parse_bold(input: Span) -> IResult { 57 | let (input, content) = in_between("*", input)?; 58 | let (_, content) = parse_group(content)?; 59 | Ok((input, Ast::Bold(content))) 60 | } 61 | 62 | fn in_between<'a>(pattern: &str, input: Span<'a>) -> IResult, Span<'a>> { 63 | delimited(tag(pattern), take_until(pattern), tag(pattern))(input) 64 | } 65 | 66 | /// Parses some italic content. 67 | /// ``` 68 | /// # use spandex::parser::ast::Ast; 69 | /// # use spandex::parser::Span; 70 | /// # use spandex::parser::combinators::parse_italic; 71 | /// let input = Span::new("/Hello/"); 72 | /// let parse = parse_italic(input).unwrap().1; 73 | /// assert_eq!(parse, Ast::Italic(vec![Ast::Text(String::from("Hello"))])); 74 | /// ``` 75 | pub fn parse_italic(input: Span) -> IResult { 76 | let (input, content) = in_between("/", input)?; 77 | let (_, content) = parse_group(content)?; 78 | Ok((input, Ast::Italic(content))) 79 | } 80 | 81 | /// Parses some math inline math. 82 | /// ``` 83 | /// # use spandex::parser::ast::Ast; 84 | /// # use spandex::parser::Span; 85 | /// # use spandex::parser::combinators::parse_inline_math; 86 | /// let input = Span::new("$x = 9$"); 87 | /// let parse = parse_inline_math(input).unwrap().1; 88 | /// assert_eq!(parse, Ast::InlineMath(String::from("x = 9"))); 89 | /// ``` 90 | pub fn parse_inline_math(input: Span) -> IResult { 91 | let (input, content) = in_between("$", input)?; 92 | Ok((input, Ast::InlineMath(content.fragment().to_string()))) 93 | } 94 | 95 | /// Parses a delimited element. 96 | pub fn parse_delimited(input: Span) -> IResult { 97 | alt((parse_bold, parse_italic, parse_inline_math))(input) 98 | } 99 | 100 | fn parse_delimited_unmatch_error(input: Span) -> IResult { 101 | alt(( 102 | map(tag("*"), |x| error(x, ErrorType::UnmatchedStar)), 103 | map(tag("/"), |x| error(x, ErrorType::UnmatchedSlash)), 104 | map(tag("$"), |x| error(x, ErrorType::UnmatchedDollar)), 105 | ))(input) 106 | } 107 | 108 | /// Parses a comment. 109 | /// ``` 110 | /// # use spandex::parser::ast::Ast; 111 | /// # use spandex::parser::Span; 112 | /// # use spandex::parser::combinators::parse_comment; 113 | /// let input = Span::new("|| comment"); 114 | /// let parse = parse_comment(input).unwrap().1; 115 | /// assert_eq!(parse, Ast::Newline); 116 | /// ``` 117 | pub fn parse_comment(input: Span) -> IResult { 118 | let (input, _) = tag("||")(input)?; 119 | let (input, _) = not_line_ending(input)?; 120 | let (input, _) = opt(line_ending)(input)?; 121 | Ok((input, Ast::Newline)) 122 | } 123 | 124 | /// Parses some multiline inline content. 125 | pub fn parse_any(input: Span) -> IResult { 126 | alt(( 127 | map(tag("**"), |x| warning(x, WarningType::ConsecutiveStars)), 128 | parse_comment, 129 | parse_delimited, 130 | parse_delimited_unmatch_error, 131 | map(tag("|"), |_| Ast::Text(String::from("|"))), 132 | map(take_till1(should_stop), |x: Span| { 133 | Ast::Text(ligature(x.fragment())) 134 | }), 135 | ))(input) 136 | } 137 | 138 | /// Parses some text content. 139 | /// ``` 140 | /// # use spandex::parser::ast::Ast; 141 | /// # use spandex::parser::Span; 142 | /// # use spandex::parser::combinators::parse_group; 143 | /// let input = Span::new("*Hello* to /you/"); 144 | /// let parsed = parse_group(input).unwrap().1; 145 | /// assert_eq!(parsed, vec![ 146 | /// Ast::Bold(vec![Ast::Text(String::from("Hello"))]), 147 | /// Ast::Text(String::from(" to ")), 148 | /// Ast::Italic(vec![Ast::Text(String::from("you"))]), 149 | /// ]); 150 | /// ``` 151 | pub fn parse_group(input: Span) -> IResult> { 152 | many0(parse_any)(input) 153 | } 154 | 155 | /// Parses a paragraph of text content. 156 | /// ``` 157 | /// # use spandex::parser::ast::Ast; 158 | /// # use spandex::parser::Span; 159 | /// # use spandex::parser::combinators::parse_paragraph; 160 | /// let input = Span::new("*Hello* to /you/"); 161 | /// let parsed = parse_paragraph(input).unwrap().1; 162 | /// assert_eq!(parsed, Ast::Paragraph(vec![ 163 | /// Ast::Bold(vec![Ast::Text(String::from("Hello"))]), 164 | /// Ast::Text(String::from(" to ")), 165 | /// Ast::Italic(vec![Ast::Text(String::from("you"))]), 166 | /// ])); 167 | /// ``` 168 | pub fn parse_paragraph(input: Span) -> IResult { 169 | map(parse_group, Ast::Paragraph)(input) 170 | } 171 | 172 | //////////////////////////////////////////////////////////////////////////////// 173 | // For titles 174 | //////////////////////////////////////////////////////////////////////////////// 175 | 176 | /// Parses a title on a single line. 177 | /// ``` 178 | /// # use spandex::parser::ast::Ast; 179 | /// # use spandex::parser::Span; 180 | /// # use spandex::parser::combinators::parse_single_line; 181 | /// let input = Span::new("This is my title"); 182 | /// let parsed = parse_single_line(input).unwrap().1; 183 | /// assert_eq!(parsed, vec![Ast::Text(String::from("This is my title"))]); 184 | /// ``` 185 | pub fn parse_single_line(input: Span) -> IResult> { 186 | alt((parse_two_lines_error, parse_group))(input) 187 | } 188 | 189 | fn parse_two_lines_error(input: Span) -> IResult> { 190 | let (input, _) = not_line_ending(input)?; 191 | let (input, _) = line_ending(input)?; 192 | let (input, span) = not_line_ending(input)?; 193 | Ok((input, vec![error(span, ErrorType::MultipleLinesTitle)])) 194 | } 195 | 196 | /// Parses the hashes from the level of a title. 197 | /// ``` 198 | /// # use spandex::parser::ast::Ast; 199 | /// # use spandex::parser::Span; 200 | /// # use spandex::parser::combinators::parse_title_level; 201 | /// let input = Span::new("# This is my title"); 202 | /// let level = parse_title_level(input).unwrap().1; 203 | /// assert_eq!(level, 0); 204 | /// let input = Span::new("### This is my subtitle"); 205 | /// let level = parse_title_level(input).unwrap().1; 206 | /// assert_eq!(level, 2); 207 | /// ``` 208 | pub fn parse_title_level(input: Span) -> IResult { 209 | map(many1_count(char('#')), |nb_hashes| nb_hashes - 1)(input) 210 | } 211 | 212 | /// Parses a whole title. 213 | /// ``` 214 | /// # use spandex::parser::ast::Ast; 215 | /// # use spandex::parser::Span; 216 | /// # use spandex::parser::combinators::parse_title; 217 | /// let input = Span::new("# This is my title"); 218 | /// let title = parse_title(input).unwrap().1; 219 | /// assert_eq!(title, Ast::Title { level: 0, children: vec![ 220 | /// Ast::Text(String::from("This is my title"))] 221 | /// }); 222 | /// ``` 223 | pub fn parse_title(input: Span) -> IResult { 224 | let (input, level) = parse_title_level(input)?; 225 | let (input, _) = space0(input)?; 226 | let (input, content) = parse_single_line(input)?; 227 | Ok(( 228 | input, 229 | Ast::Title { 230 | level: level as u8, 231 | children: content, 232 | }, 233 | )) 234 | } 235 | 236 | //////////////////////////////////////////////////////////////////////////////// 237 | // For main 238 | //////////////////////////////////////////////////////////////////////////////// 239 | 240 | /// Gets a block of content. 241 | /// ``` 242 | /// # use spandex::parser::ast::Ast; 243 | /// # use spandex::parser::Span; 244 | /// # use spandex::parser::combinators::get_block; 245 | /// let input = Span::new("First paragraph\n\nSecond paragraph"); 246 | /// let (input, block) = get_block(input).unwrap(); 247 | /// assert_eq!(block.fragment(), &"First paragraph"); 248 | /// let (input, block) = get_block(input).unwrap(); 249 | /// assert_eq!(block.fragment(), &"Second paragraph"); 250 | /// ``` 251 | pub fn get_block(input: Span) -> IResult { 252 | let take_until_double_line_ending = |i| { 253 | alt(( 254 | take_until("\r\n\r\n"), 255 | take_until("\r\n\n"), 256 | take_until("\n\n"), 257 | ))(i) 258 | }; 259 | let at_least_1_char = verify(rest, |s: &Span| !s.fragment().is_empty()); 260 | 261 | let (input, span) = alt((take_until_double_line_ending, at_least_1_char))(input)?; 262 | let len_after_trimmed = span.fragment().trim_end().len(); 263 | let (input, _) = many0(line_ending)(input)?; 264 | Ok((input, span.slice(..len_after_trimmed))) 265 | } 266 | 267 | /// Parses a block of content. 268 | /// ``` 269 | /// # use spandex::parser::ast::Ast; 270 | /// # use spandex::parser::Span; 271 | /// # use spandex::parser::combinators::parse_block_content; 272 | /// let input = Span::new("First paragraph"); 273 | /// let (_, block) = parse_block_content(input).unwrap(); 274 | /// assert_eq!(block, Ast::Paragraph(vec![Ast::Text(String::from("First paragraph"))])); 275 | /// ``` 276 | pub fn parse_block_content(input: Span) -> IResult { 277 | alt((parse_title, parse_paragraph))(input) 278 | } 279 | 280 | /// Parses a whole dex file. 281 | pub fn parse_content(input: &str) -> IResult> { 282 | let parse_block = map_res(get_block, parse_block_content); 283 | fold_many0(parse_block, Vec::new, |mut content: Vec<_>, (_, block)| { 284 | content.push(block); 285 | content 286 | })(Span::new(input)) 287 | } 288 | 289 | /// Parses a whole dex file from a name. 290 | pub fn parse>(path: P) -> Result { 291 | let path = path.as_ref(); 292 | let mut file = File::open(&path)?; 293 | let mut content = String::new(); 294 | file.read_to_string(&mut content)?; 295 | 296 | let elements = match parse_content(&content) { 297 | Ok((_, elements)) => elements, 298 | Err(_) => unreachable!(), 299 | }; 300 | 301 | let ast = Ast::File(PathBuf::from(path), elements); 302 | 303 | let errors = ast.errors(); 304 | let warnings = ast.warnings(); 305 | 306 | if errors.is_empty() { 307 | Ok(Parsed { 308 | ast, 309 | warnings: Warnings { 310 | path: PathBuf::from(&path), 311 | warnings, 312 | content, 313 | }, 314 | }) 315 | } else { 316 | Err(Error::DexError(Errors { 317 | path: PathBuf::from(&path), 318 | content, 319 | errors, 320 | })) 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /src/layout/paragraphs/engine.rs: -------------------------------------------------------------------------------- 1 | //! Mathematical logic for typesetting a sequence of words which have a 2 | //! semantics of "paragraph". That is, the logic to split a sequence of 3 | //! words into lines. 4 | 5 | use std::collections::hash_map::{Entry, HashMap}; 6 | use std::f64; 7 | 8 | use petgraph::graph::NodeIndex; 9 | use petgraph::stable_graph::StableGraph; 10 | use petgraph::visit::Dfs; 11 | use petgraph::visit::IntoNodeIdentifiers; 12 | use printpdf::Pt; 13 | 14 | use crate::layout::paragraphs::items::{Content, Item, PositionedItem}; 15 | use crate::layout::paragraphs::Paragraph; 16 | use crate::layout::Glyph; 17 | 18 | use crate::layout::constants::{MAX_ADJUSTMENT_RATIO, MIN_ADJUSTMENT_RATIO}; 19 | use crate::layout::paragraphs::graph::Node; 20 | use crate::layout::paragraphs::utils::linebreak::{ 21 | compute_adjustment_ratio, compute_adjustment_ratios_with_breakpoints, 22 | create_node_for_feasible_breakpoint, is_forced_break, Measures, 23 | }; 24 | use crate::layout::paragraphs::utils::paragraphs::get_line_length; 25 | 26 | /// Finds the optimal sequence of breakpoints that minimize 27 | /// the amount of demerits while breaking a paragraph down 28 | /// into lines. 29 | /// 30 | /// It returns the indexes of items which have been chosen as 31 | /// breakpoints. 32 | pub fn algorithm<'a>(paragraph: &'a Paragraph<'a>, lines_length: &[Pt]) -> Vec { 33 | let mut graph = StableGraph::<_, f64>::new(); 34 | let mut sum_width = Pt(0.0); 35 | let mut sum_stretch = Pt(0.0); 36 | let mut sum_shrink = Pt(0.0); 37 | let mut best_adjustment_ratio_above_threshold = f64::MAX; 38 | let current_maximum_adjustment_ratio = f64::MAX; 39 | 40 | let mut lines_best_node = HashMap::new(); 41 | let mut farthest_line: usize = 0; 42 | 43 | // Add an initial active node for the beginning of the paragraph. 44 | graph.add_node(Node { 45 | index: 0, 46 | line: 0, 47 | fitness: 1, 48 | total_width: Pt(0.0), 49 | total_stretch: Pt(0.0), 50 | total_shrink: Pt(0.0), 51 | total_demerits: 0.0, 52 | }); 53 | 54 | for (b, item) in paragraph.items.iter().enumerate() { 55 | if item.width < Pt(0.0) { 56 | panic!("Item #{} has negative width.", b); 57 | } 58 | 59 | let mut can_break = false; 60 | 61 | match item.content { 62 | Content::BoundingBox { .. } => { 63 | sum_width += item.width; 64 | } 65 | Content::Glue { 66 | stretchability, 67 | shrinkability, 68 | } => { 69 | // We can only break at a glue if it is preceeded by 70 | // a bounding box. 71 | can_break = 72 | b > 0 && matches!(paragraph.items[b - 1].content, Content::BoundingBox { .. }); 73 | 74 | if !can_break { 75 | sum_width += item.width; 76 | sum_shrink += shrinkability; 77 | sum_stretch += stretchability; 78 | } 79 | } 80 | Content::Penalty { value, .. } => { 81 | can_break = value < f64::INFINITY; 82 | } 83 | } 84 | 85 | if !can_break { 86 | continue; 87 | } 88 | 89 | // Update the set of active nodes. 90 | 91 | let mut feasible_breakpoints: Vec<(Node, NodeIndex)> = Vec::new(); 92 | let mut node_to_remove: Vec = Vec::new(); 93 | 94 | for node in graph.node_identifiers() { 95 | if let Some(a) = graph.node_weight(node) { 96 | let line_shrink = sum_shrink - a.total_shrink; 97 | let line_stretch = sum_stretch - a.total_stretch; 98 | let actual_width = sum_width - a.total_width; 99 | 100 | let adjustment_ratio = compute_adjustment_ratio( 101 | actual_width, 102 | get_line_length(lines_length, a.line), 103 | line_stretch, 104 | line_shrink, 105 | ); 106 | 107 | if adjustment_ratio > current_maximum_adjustment_ratio { 108 | best_adjustment_ratio_above_threshold = 109 | adjustment_ratio.min(best_adjustment_ratio_above_threshold) 110 | } 111 | 112 | if adjustment_ratio < MIN_ADJUSTMENT_RATIO || is_forced_break(item) { 113 | // Items from a to b cannot fit on the same line. 114 | node_to_remove.push(node); 115 | } 116 | 117 | if (MIN_ADJUSTMENT_RATIO..=MAX_ADJUSTMENT_RATIO).contains(&adjustment_ratio) { 118 | let measures_sum = Measures { 119 | width: sum_width, 120 | shrinkability: sum_shrink, 121 | stretchability: sum_stretch, 122 | }; 123 | 124 | let new_node = create_node_for_feasible_breakpoint( 125 | b, 126 | a, 127 | adjustment_ratio, 128 | item, 129 | ¶graph.items, 130 | &measures_sum, 131 | ); 132 | 133 | feasible_breakpoints.push((new_node, node)); 134 | } 135 | } 136 | } 137 | 138 | // If there is a feasible break at b, then append the best such break 139 | // as an active node. 140 | if !feasible_breakpoints.is_empty() { 141 | let (mut last_best_node, mut last_node_parent_id) = feasible_breakpoints[0]; 142 | 143 | for (node, parent_id) in feasible_breakpoints.iter() { 144 | if node.total_demerits < last_best_node.total_demerits { 145 | last_best_node = *node; 146 | last_node_parent_id = *parent_id; 147 | } 148 | } 149 | 150 | let inserted_node = graph.add_node(last_best_node); 151 | 152 | // Create a precedence relationship between a and the best node. 153 | graph.add_edge( 154 | inserted_node, 155 | last_node_parent_id, 156 | last_best_node.total_demerits, 157 | ); 158 | 159 | match lines_best_node.entry(last_best_node.line) { 160 | Entry::Vacant(entry) => { 161 | entry.insert((last_best_node, inserted_node)); 162 | farthest_line += 1; 163 | } 164 | 165 | Entry::Occupied(entry) => { 166 | let (best_node_on_current_line, _) = entry.get(); 167 | if last_best_node.total_demerits < best_node_on_current_line.total_demerits { 168 | lines_best_node 169 | .insert(last_best_node.line, (last_best_node, inserted_node)); 170 | } 171 | } 172 | } 173 | } 174 | 175 | if let Content::Glue { 176 | shrinkability, 177 | stretchability, 178 | } = item.content 179 | { 180 | sum_width += item.width; 181 | sum_shrink += shrinkability; 182 | sum_stretch += stretchability; 183 | } 184 | } 185 | 186 | // TODO: handle situation where there's no option to fall within the window of 187 | // accepted adjustment ratios. 188 | 189 | // Follow the edges backwards. 190 | let mut result: Vec = Vec::new(); 191 | 192 | if let Some((_, best_node_on_last_line)) = lines_best_node.get(&farthest_line) { 193 | let mut dfs = Dfs::new(&graph, *best_node_on_last_line); 194 | while let Some(node_index) = dfs.next(&graph) { 195 | // use a detached neighbors walker 196 | if let Some(node) = graph.node_weight(node_index) { 197 | result.push(node.index); 198 | } 199 | } 200 | } 201 | result.reverse(); 202 | result 203 | } 204 | 205 | /// Generates a list of positioned items from a list of items making up a paragraph. 206 | /// The generated list is ready to be rendered. 207 | pub fn positionate_items<'a>( 208 | items: &[Item<'a>], 209 | line_lengths: &[Pt], 210 | breakpoints: &[usize], 211 | ) -> Vec>> { 212 | let adjustment_ratios = 213 | compute_adjustment_ratios_with_breakpoints(items, line_lengths, breakpoints); 214 | let mut lines_breakdown: Vec> = Vec::new(); 215 | 216 | for breakpoint_line in 0..(breakpoints.len() - 1) { 217 | let mut positioned_items: Vec = Vec::new(); 218 | 219 | let breakpoint_index = breakpoints[breakpoint_line]; 220 | let adjustment_ratio = adjustment_ratios[breakpoint_line].max(MIN_ADJUSTMENT_RATIO); 221 | let mut horizontal_offset = Pt(0.0); 222 | let beginning = if breakpoint_line == 0 { 223 | breakpoint_index 224 | } else { 225 | breakpoint_index + 1 226 | }; 227 | 228 | let mut previous_glyph = None; 229 | 230 | let range = items 231 | .iter() 232 | .enumerate() 233 | .take(breakpoints[breakpoint_line + 1]) 234 | .skip(beginning); 235 | 236 | for (p, item) in range { 237 | match items[p].content { 238 | Content::BoundingBox(ref glyph) => { 239 | previous_glyph = Some(glyph.clone()); 240 | positioned_items.push(PositionedItem { 241 | index: p, 242 | line: breakpoint_line, 243 | horizontal_offset, 244 | width: item.width, 245 | glyph: glyph.clone(), 246 | }); 247 | horizontal_offset += item.width; 248 | } 249 | Content::Glue { 250 | shrinkability, 251 | stretchability, 252 | } => { 253 | if p != beginning && p != breakpoints[breakpoint_line + 1] { 254 | let width = item.width; 255 | 256 | let gap = if adjustment_ratio < 0.0 { 257 | width + shrinkability * adjustment_ratio 258 | } else { 259 | width + stretchability * adjustment_ratio 260 | }; 261 | 262 | // TODO: add an option to handle the inclusion of glue. 263 | 264 | horizontal_offset += gap; 265 | } 266 | } 267 | Content::Penalty { .. } => { 268 | if p == breakpoints[breakpoint_line + 1] && items[p].width > Pt(0.0) { 269 | let glyph = previous_glyph.clone().unwrap(); 270 | positioned_items.push(PositionedItem { 271 | index: p, 272 | line: breakpoint_line, 273 | horizontal_offset, 274 | width: item.width, 275 | glyph: Glyph { 276 | glyph: '-', 277 | font: glyph.font, 278 | scale: glyph.scale, 279 | }, 280 | }) 281 | } 282 | } 283 | } 284 | } 285 | 286 | lines_breakdown.push(positioned_items); 287 | } 288 | 289 | lines_breakdown 290 | } 291 | 292 | /// Unit tests for the paragraphs typesetting. 293 | #[cfg(test)] 294 | mod tests { 295 | 296 | use printpdf::Pt; 297 | use spandex_hyphenation::*; 298 | 299 | use crate::document::configuration::Config; 300 | use crate::layout::paragraphs::engine::algorithm; 301 | use crate::layout::paragraphs::items::Content; 302 | use crate::layout::paragraphs::utils::ast::itemize_ast; 303 | use crate::layout::paragraphs::utils::linebreak::{ 304 | compute_adjustment_ratios_with_breakpoints, find_legal_breakpoints, 305 | }; 306 | use crate::parser::ast::Ast; 307 | use crate::Result; 308 | 309 | #[test] 310 | fn test_paragraph_itemization() -> Result<()> { 311 | let words = "Lorem ipsum dolor sit amet."; 312 | let ast = Ast::Paragraph(vec![Ast::Text(words.into())]); 313 | 314 | let en_us = Standard::from_embedded(Language::EnglishUS)?; 315 | 316 | let (_, font_manager) = Config::with_title("Test").init()?; 317 | let config = font_manager.default_config(); 318 | 319 | // No indentation, meaning no leading empty box. 320 | let paragraph = itemize_ast(&ast, &config, Pt(10.0), &en_us, Pt(0.0)); 321 | assert_eq!(paragraph.items.len(), 31); 322 | 323 | // Indentated paragraph, implying the presence of a leading empty box. 324 | let paragraph = itemize_ast(&ast, &config, Pt(10.0), &en_us, Pt(7.5)); 325 | assert_eq!(paragraph.items.len(), 32); 326 | 327 | Ok(()) 328 | } 329 | 330 | #[test] 331 | fn test_legal_breakpoints() -> Result<()> { 332 | let words = "Lorem ipsum dolor sit amet."; 333 | let ast = Ast::Paragraph(vec![Ast::Text(words.into())]); 334 | 335 | let en_us = Standard::from_embedded(Language::EnglishUS)?; 336 | 337 | let (_, font_manager) = Config::with_title("Test").init()?; 338 | let config = font_manager.default_config(); 339 | 340 | // Indentated paragraph, implying the presence of a leading empty box. 341 | let paragraph = itemize_ast(&ast, &config, Pt(10.0), &en_us, Pt(7.5)); 342 | 343 | let legal_breakpoints = find_legal_breakpoints(¶graph); 344 | // [ ] Lorem ip-sum do-lor sit amet. 345 | assert_eq!(legal_breakpoints, [0, 6, 9, 13, 16, 20, 24, 30, 31]); 346 | 347 | Ok(()) 348 | } 349 | 350 | // #[test] 351 | // fn test_adjustment_ratio_computation() -> Result<()> { 352 | // let words = "Lorem ipsum dolor sit amet."; 353 | 354 | // let en_us = Standard::from_embedded(Language::EnglishUS)?; 355 | 356 | // let (_, font_manager) = Config::with_title("Test").init()?; 357 | 358 | // let regular_font_name = "CMU Serif Roman"; 359 | // // let bold_font_name = "CMU Serif Bold"; 360 | 361 | // let font = font_manager 362 | // .get(regular_font_name) 363 | // .ok_or(Error::FontNotFound(PathBuf::from(regular_font_name)))?; 364 | 365 | // // Indentated paragraph, implying the presence of a leading empty box. 366 | // let paragraph = itemize_paragraph(words, Sp(120_000), &font, 12.0, &en_us); 367 | // // assert_eq!(paragraph.items.len(), 26); 368 | 369 | // // TODO: compute the ratio by hand. 370 | 371 | // Ok(()) 372 | // } 373 | 374 | #[test] 375 | fn test_algorithm() -> Result<()> { 376 | // let words = "In olden times when wishing still helped one, \ 377 | // there lived a king whose daughters were all beautiful ; \ 378 | // and the youngest was so beautiful that the sun itself, \ 379 | // which has seen so much, was astonished whenever it shone \ 380 | // in her face."; 381 | 382 | // let words = "The Ministry of Truth, which concerned itself with news, entertainment, education and the fine arts. The Ministry of Peace, which concerned itself with war. The Ministry of Love, which maintained law and order. And the Ministry of Plenty, which was responsible for economic affairs. Their names, in Newspeak: Minitrue, Minipax, Miniluv and Miniplenty."; 383 | 384 | let words = "The hallway smelt of boiled cabbage and old rag mats. At one end of it a coloured poster, too large for indoor display, had been tacked to the wall. It depicted simply an enormous face, more than a metre wide: the face of a man of about forty-five, with a heavy black moustache and ruggedly handsome features. Winston made for the stairs. It was no use trying the lift. Even at the best of times it was seldom working, and at present the electric current was cut off during daylight hours. It was part of the economy drive in preparation for Hate Week. The flat was seven flights up, and Winston, who was thirty-nine and had a varicose ulcer above his right ankle, went slowly, resting several times on the way. On each landing, opposite the lift-shaft, the poster with the enormous face gazed from the wall. It was one of those pictures which are so contrived that the eyes follow you about when you move. BIG BROTHER IS WATCHING YOU, the caption beneath it ran."; 385 | 386 | let ast = Ast::Paragraph(vec![Ast::Text(words.into())]); 387 | 388 | let en_us = Standard::from_embedded(Language::EnglishUS)?; 389 | 390 | let (_, font_manager) = Config::with_title("Test").init()?; 391 | let config = font_manager.default_config(); 392 | 393 | let indentation = Pt(18.0); 394 | 395 | let paragraph = itemize_ast(&ast, &config, Pt(12.0), &en_us, indentation); 396 | 397 | let lines_length = vec![Pt(400.0)]; 398 | let breakpoints = algorithm(¶graph, &lines_length); 399 | // let positions = positionate_items(¶graph.items, &lines_length, &breakpoints); 400 | 401 | let adjustment_ratios = compute_adjustment_ratios_with_breakpoints( 402 | ¶graph.items, 403 | &lines_length, 404 | &breakpoints, 405 | ); 406 | 407 | println!("Line length: {:?}", lines_length[0]); 408 | println!("Breakpoints: {:?}", breakpoints); 409 | println!("There are {:?} lines", breakpoints.len()); 410 | print!("\n\n"); 411 | let mut current_line = 0; 412 | for (c, item) in paragraph.items.iter().enumerate() { 413 | match item.content { 414 | Content::BoundingBox(ref glyph) => print!("{}", glyph.glyph), 415 | Content::Glue { .. } => { 416 | if breakpoints.contains(&c) { 417 | println!(" [{:?}]", adjustment_ratios[current_line]); 418 | current_line += 1; 419 | } else { 420 | print!(" "); 421 | } 422 | } 423 | Content::Penalty { .. } => { 424 | if breakpoints.contains(&c) { 425 | println!("- [{:?}]", adjustment_ratios[current_line]); 426 | current_line += 1; 427 | } 428 | } 429 | } 430 | } 431 | 432 | print!("\n\n"); 433 | 434 | // panic!("Test"); 435 | 436 | Ok(()) 437 | } 438 | } 439 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "adler" 7 | version = "0.2.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" 10 | 11 | [[package]] 12 | name = "atlatl" 13 | version = "0.1.2" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "32bb156841d2e2a888185b5b4f7d93d30efd3a40d1671d9628ab39536adb7ea2" 16 | dependencies = [ 17 | "fnv", 18 | "num-traits", 19 | "serde", 20 | ] 21 | 22 | [[package]] 23 | name = "atty" 24 | version = "0.2.14" 25 | source = "registry+https://github.com/rust-lang/crates.io-index" 26 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 27 | dependencies = [ 28 | "hermit-abi", 29 | "libc", 30 | "winapi", 31 | ] 32 | 33 | [[package]] 34 | name = "autocfg" 35 | version = "1.0.0" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" 38 | 39 | [[package]] 40 | name = "base-x" 41 | version = "0.2.8" 42 | source = "registry+https://github.com/rust-lang/crates.io-index" 43 | checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b" 44 | 45 | [[package]] 46 | name = "bincode" 47 | version = "1.3.1" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "f30d3a39baa26f9651f17b375061f3233dde33424a8b72b0dbe93a68a0bc896d" 50 | dependencies = [ 51 | "byteorder", 52 | "serde", 53 | ] 54 | 55 | [[package]] 56 | name = "bitflags" 57 | version = "1.2.1" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 60 | 61 | [[package]] 62 | name = "bumpalo" 63 | version = "3.5.0" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "f07aa6688c702439a1be0307b6a94dffe1168569e45b9500c1372bc580740d59" 66 | 67 | [[package]] 68 | name = "bytecount" 69 | version = "0.6.2" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "72feb31ffc86498dacdbd0fcebb56138e7177a8cc5cea4516031d15ae85a742e" 72 | 73 | [[package]] 74 | name = "byteorder" 75 | version = "1.3.4" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" 78 | 79 | [[package]] 80 | name = "cc" 81 | version = "1.0.58" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "f9a06fb2e53271d7c279ec1efea6ab691c35a2ae67ec0d91d7acec0caf13b518" 84 | 85 | [[package]] 86 | name = "cfg-if" 87 | version = "0.1.10" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 90 | 91 | [[package]] 92 | name = "cfg-if" 93 | version = "1.0.0" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 96 | 97 | [[package]] 98 | name = "cmake" 99 | version = "0.1.45" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "eb6210b637171dfba4cda12e579ac6dc73f5165ad56133e5d72ef3131f320855" 102 | dependencies = [ 103 | "cc", 104 | ] 105 | 106 | [[package]] 107 | name = "colored" 108 | version = "2.0.0" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" 111 | dependencies = [ 112 | "atty", 113 | "lazy_static", 114 | "winapi", 115 | ] 116 | 117 | [[package]] 118 | name = "const_fn" 119 | version = "0.4.5" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "28b9d6de7f49e22cf97ad17fc4036ece69300032f45f78f30b4a4482cdc3f4a6" 122 | 123 | [[package]] 124 | name = "crc32fast" 125 | version = "1.2.0" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1" 128 | dependencies = [ 129 | "cfg-if 0.1.10", 130 | ] 131 | 132 | [[package]] 133 | name = "discard" 134 | version = "1.0.4" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" 137 | 138 | [[package]] 139 | name = "dtoa" 140 | version = "0.4.6" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "134951f4028bdadb9b84baf4232681efbf277da25144b9b0ad65df75946c422b" 143 | 144 | [[package]] 145 | name = "encoding" 146 | version = "0.2.33" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" 149 | dependencies = [ 150 | "encoding-index-japanese", 151 | "encoding-index-korean", 152 | "encoding-index-simpchinese", 153 | "encoding-index-singlebyte", 154 | "encoding-index-tradchinese", 155 | ] 156 | 157 | [[package]] 158 | name = "encoding-index-japanese" 159 | version = "1.20141219.5" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" 162 | dependencies = [ 163 | "encoding_index_tests", 164 | ] 165 | 166 | [[package]] 167 | name = "encoding-index-korean" 168 | version = "1.20141219.5" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" 171 | dependencies = [ 172 | "encoding_index_tests", 173 | ] 174 | 175 | [[package]] 176 | name = "encoding-index-simpchinese" 177 | version = "1.20141219.5" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" 180 | dependencies = [ 181 | "encoding_index_tests", 182 | ] 183 | 184 | [[package]] 185 | name = "encoding-index-singlebyte" 186 | version = "1.20141219.5" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" 189 | dependencies = [ 190 | "encoding_index_tests", 191 | ] 192 | 193 | [[package]] 194 | name = "encoding-index-tradchinese" 195 | version = "1.20141219.5" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" 198 | dependencies = [ 199 | "encoding_index_tests", 200 | ] 201 | 202 | [[package]] 203 | name = "encoding_index_tests" 204 | version = "0.1.4" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" 207 | 208 | [[package]] 209 | name = "fixedbitset" 210 | version = "0.4.0" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "398ea4fabe40b9b0d885340a2a991a44c8a645624075ad966d21f88688e2b69e" 213 | 214 | [[package]] 215 | name = "flate2" 216 | version = "1.0.16" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "68c90b0fc46cf89d227cc78b40e494ff81287a92dd07631e5af0d06fe3cf885e" 219 | dependencies = [ 220 | "cfg-if 0.1.10", 221 | "crc32fast", 222 | "libc", 223 | "miniz_oxide", 224 | ] 225 | 226 | [[package]] 227 | name = "fnv" 228 | version = "1.0.7" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 231 | 232 | [[package]] 233 | name = "freetype-rs" 234 | version = "0.28.0" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "8a96ca0b8ae3b1e198988c7609a264b355796356e6898a3b928af10e4ac3bc54" 237 | dependencies = [ 238 | "bitflags", 239 | "freetype-sys", 240 | "libc", 241 | ] 242 | 243 | [[package]] 244 | name = "freetype-sys" 245 | version = "0.14.0" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "b67cedeffcf3d18fd38769bb9323b374f09441934a0f9096b9d33efddf920fc7" 248 | dependencies = [ 249 | "cmake", 250 | "libc", 251 | "pkg-config", 252 | ] 253 | 254 | [[package]] 255 | name = "hashbrown" 256 | version = "0.11.2" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" 259 | 260 | [[package]] 261 | name = "hermit-abi" 262 | version = "0.1.15" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "3deed196b6e7f9e44a2ae8d94225d80302d81208b1bb673fd21fe634645c85a9" 265 | dependencies = [ 266 | "libc", 267 | ] 268 | 269 | [[package]] 270 | name = "hyphenation_commons" 271 | version = "0.7.1" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "9e3461ab51107f7beb8e0c46606d6eb7dfa48880014a29c170afad3ce6b25add" 274 | dependencies = [ 275 | "atlatl", 276 | "serde", 277 | ] 278 | 279 | [[package]] 280 | name = "indexmap" 281 | version = "1.7.0" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" 284 | dependencies = [ 285 | "autocfg", 286 | "hashbrown", 287 | ] 288 | 289 | [[package]] 290 | name = "itoa" 291 | version = "0.4.7" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" 294 | 295 | [[package]] 296 | name = "js-sys" 297 | version = "0.3.46" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "cf3d7383929f7c9c7c2d0fa596f325832df98c3704f2c60553080f7127a58175" 300 | dependencies = [ 301 | "wasm-bindgen", 302 | ] 303 | 304 | [[package]] 305 | name = "lazy_static" 306 | version = "1.4.0" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 309 | 310 | [[package]] 311 | name = "libc" 312 | version = "0.2.73" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "bd7d4bd64732af4bf3a67f367c27df8520ad7e230c5817b8ff485864d80242b9" 315 | 316 | [[package]] 317 | name = "linked-hash-map" 318 | version = "0.5.4" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" 321 | 322 | [[package]] 323 | name = "log" 324 | version = "0.4.11" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" 327 | dependencies = [ 328 | "cfg-if 0.1.10", 329 | ] 330 | 331 | [[package]] 332 | name = "lopdf" 333 | version = "0.26.0" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "b49a0272112719d0037ab63d4bb67f73ba659e1e90bc38f235f163a457ac16f3" 336 | dependencies = [ 337 | "dtoa", 338 | "encoding", 339 | "flate2", 340 | "itoa", 341 | "linked-hash-map", 342 | "log", 343 | "lzw", 344 | "time", 345 | ] 346 | 347 | [[package]] 348 | name = "lzw" 349 | version = "0.10.0" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "7d947cbb889ed21c2a84be6ffbaebf5b4e0f4340638cba0444907e38b56be084" 352 | 353 | [[package]] 354 | name = "memchr" 355 | version = "2.3.3" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" 358 | 359 | [[package]] 360 | name = "minimal-lexical" 361 | version = "0.2.1" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 364 | 365 | [[package]] 366 | name = "miniz_oxide" 367 | version = "0.4.0" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "be0f75932c1f6cfae3c04000e40114adf955636e19040f9c0a2c380702aa1c7f" 370 | dependencies = [ 371 | "adler", 372 | ] 373 | 374 | [[package]] 375 | name = "nom" 376 | version = "7.1.0" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "1b1d11e1ef389c76fe5b81bcaf2ea32cf88b62bc494e19f493d0b30e7a930109" 379 | dependencies = [ 380 | "memchr", 381 | "minimal-lexical", 382 | "version_check", 383 | ] 384 | 385 | [[package]] 386 | name = "nom_locate" 387 | version = "4.0.0" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "37794436ca3029a3089e0b95d42da1f0b565ad271e4d3bb4bad0c7bb70b10605" 390 | dependencies = [ 391 | "bytecount", 392 | "memchr", 393 | "nom", 394 | ] 395 | 396 | [[package]] 397 | name = "num-traits" 398 | version = "0.2.12" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" 401 | dependencies = [ 402 | "autocfg", 403 | ] 404 | 405 | [[package]] 406 | name = "owned_ttf_parser" 407 | version = "0.12.1" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "60ac8dda2e5cc09bf6480e3b3feff9783db251710c922ae9369a429c51efdeb0" 410 | dependencies = [ 411 | "ttf-parser", 412 | ] 413 | 414 | [[package]] 415 | name = "petgraph" 416 | version = "0.6.0" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "4a13a2fa9d0b63e5f22328828741e523766fff0ee9e779316902290dff3f824f" 419 | dependencies = [ 420 | "fixedbitset", 421 | "indexmap", 422 | ] 423 | 424 | [[package]] 425 | name = "pkg-config" 426 | version = "0.3.18" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "d36492546b6af1463394d46f0c834346f31548646f6ba10849802c9c9a27ac33" 429 | 430 | [[package]] 431 | name = "pocket-resources" 432 | version = "0.3.2" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "c135f38778ad324d9e9ee68690bac2c1a51f340fdf96ca13e2ab3914eb2e51d8" 435 | 436 | [[package]] 437 | name = "printpdf" 438 | version = "0.4.1" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "995380029b3d8e5dbfa152360ce617e4fb54f0c8a2882a8671604e4a9251c098" 441 | dependencies = [ 442 | "js-sys", 443 | "lopdf", 444 | "owned_ttf_parser", 445 | "time", 446 | ] 447 | 448 | [[package]] 449 | name = "proc-macro-hack" 450 | version = "0.5.19" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" 453 | 454 | [[package]] 455 | name = "proc-macro2" 456 | version = "1.0.19" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "04f5f085b5d71e2188cb8271e5da0161ad52c3f227a661a3c135fdf28e258b12" 459 | dependencies = [ 460 | "unicode-xid", 461 | ] 462 | 463 | [[package]] 464 | name = "quote" 465 | version = "1.0.7" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" 468 | dependencies = [ 469 | "proc-macro2", 470 | ] 471 | 472 | [[package]] 473 | name = "rustc_version" 474 | version = "0.2.3" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" 477 | dependencies = [ 478 | "semver", 479 | ] 480 | 481 | [[package]] 482 | name = "ryu" 483 | version = "1.0.5" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" 486 | 487 | [[package]] 488 | name = "semver" 489 | version = "0.9.0" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" 492 | dependencies = [ 493 | "semver-parser", 494 | ] 495 | 496 | [[package]] 497 | name = "semver-parser" 498 | version = "0.7.0" 499 | source = "registry+https://github.com/rust-lang/crates.io-index" 500 | checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" 501 | 502 | [[package]] 503 | name = "serde" 504 | version = "1.0.114" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "5317f7588f0a5078ee60ef675ef96735a1442132dc645eb1d12c018620ed8cd3" 507 | dependencies = [ 508 | "serde_derive", 509 | ] 510 | 511 | [[package]] 512 | name = "serde_derive" 513 | version = "1.0.114" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "2a0be94b04690fbaed37cddffc5c134bf537c8e3329d53e982fe04c374978f8e" 516 | dependencies = [ 517 | "proc-macro2", 518 | "quote", 519 | "syn", 520 | ] 521 | 522 | [[package]] 523 | name = "serde_json" 524 | version = "1.0.61" 525 | source = "registry+https://github.com/rust-lang/crates.io-index" 526 | checksum = "4fceb2595057b6891a4ee808f70054bd2d12f0e97f1cbb78689b59f676df325a" 527 | dependencies = [ 528 | "itoa", 529 | "ryu", 530 | "serde", 531 | ] 532 | 533 | [[package]] 534 | name = "sha1" 535 | version = "0.6.0" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" 538 | 539 | [[package]] 540 | name = "spandex" 541 | version = "0.0.4" 542 | dependencies = [ 543 | "colored", 544 | "freetype-rs", 545 | "nom", 546 | "nom_locate", 547 | "petgraph", 548 | "printpdf", 549 | "serde", 550 | "spandex-hyphenation", 551 | "toml", 552 | ] 553 | 554 | [[package]] 555 | name = "spandex-hyphenation" 556 | version = "0.7.4" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "d8d8a670879a9e1d5004c56b7b90413dca09db847f30122cd939a866cef1d067" 559 | dependencies = [ 560 | "atlatl", 561 | "bincode", 562 | "hyphenation_commons", 563 | "pocket-resources", 564 | "serde", 565 | ] 566 | 567 | [[package]] 568 | name = "standback" 569 | version = "0.2.14" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | checksum = "c66a8cff4fa24853fdf6b51f75c6d7f8206d7c75cab4e467bcd7f25c2b1febe0" 572 | dependencies = [ 573 | "version_check", 574 | ] 575 | 576 | [[package]] 577 | name = "stdweb" 578 | version = "0.4.20" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" 581 | dependencies = [ 582 | "discard", 583 | "rustc_version", 584 | "stdweb-derive", 585 | "stdweb-internal-macros", 586 | "stdweb-internal-runtime", 587 | "wasm-bindgen", 588 | ] 589 | 590 | [[package]] 591 | name = "stdweb-derive" 592 | version = "0.5.3" 593 | source = "registry+https://github.com/rust-lang/crates.io-index" 594 | checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" 595 | dependencies = [ 596 | "proc-macro2", 597 | "quote", 598 | "serde", 599 | "serde_derive", 600 | "syn", 601 | ] 602 | 603 | [[package]] 604 | name = "stdweb-internal-macros" 605 | version = "0.2.9" 606 | source = "registry+https://github.com/rust-lang/crates.io-index" 607 | checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" 608 | dependencies = [ 609 | "base-x", 610 | "proc-macro2", 611 | "quote", 612 | "serde", 613 | "serde_derive", 614 | "serde_json", 615 | "sha1", 616 | "syn", 617 | ] 618 | 619 | [[package]] 620 | name = "stdweb-internal-runtime" 621 | version = "0.1.5" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" 624 | 625 | [[package]] 626 | name = "syn" 627 | version = "1.0.35" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "fb7f4c519df8c117855e19dd8cc851e89eb746fe7a73f0157e0d95fdec5369b0" 630 | dependencies = [ 631 | "proc-macro2", 632 | "quote", 633 | "unicode-xid", 634 | ] 635 | 636 | [[package]] 637 | name = "time" 638 | version = "0.2.24" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "273d3ed44dca264b0d6b3665e8d48fb515042d42466fad93d2a45b90ec4058f7" 641 | dependencies = [ 642 | "const_fn", 643 | "libc", 644 | "standback", 645 | "stdweb", 646 | "time-macros", 647 | "version_check", 648 | "winapi", 649 | ] 650 | 651 | [[package]] 652 | name = "time-macros" 653 | version = "0.1.1" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" 656 | dependencies = [ 657 | "proc-macro-hack", 658 | "time-macros-impl", 659 | ] 660 | 661 | [[package]] 662 | name = "time-macros-impl" 663 | version = "0.1.1" 664 | source = "registry+https://github.com/rust-lang/crates.io-index" 665 | checksum = "e5c3be1edfad6027c69f5491cf4cb310d1a71ecd6af742788c6ff8bced86b8fa" 666 | dependencies = [ 667 | "proc-macro-hack", 668 | "proc-macro2", 669 | "quote", 670 | "standback", 671 | "syn", 672 | ] 673 | 674 | [[package]] 675 | name = "toml" 676 | version = "0.5.8" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" 679 | dependencies = [ 680 | "serde", 681 | ] 682 | 683 | [[package]] 684 | name = "ttf-parser" 685 | version = "0.12.3" 686 | source = "registry+https://github.com/rust-lang/crates.io-index" 687 | checksum = "7ae2f58a822f08abdaf668897e96a5656fe72f5a9ce66422423e8849384872e6" 688 | 689 | [[package]] 690 | name = "unicode-xid" 691 | version = "0.2.1" 692 | source = "registry+https://github.com/rust-lang/crates.io-index" 693 | checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" 694 | 695 | [[package]] 696 | name = "version_check" 697 | version = "0.9.2" 698 | source = "registry+https://github.com/rust-lang/crates.io-index" 699 | checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" 700 | 701 | [[package]] 702 | name = "wasm-bindgen" 703 | version = "0.2.69" 704 | source = "registry+https://github.com/rust-lang/crates.io-index" 705 | checksum = "3cd364751395ca0f68cafb17666eee36b63077fb5ecd972bbcd74c90c4bf736e" 706 | dependencies = [ 707 | "cfg-if 1.0.0", 708 | "wasm-bindgen-macro", 709 | ] 710 | 711 | [[package]] 712 | name = "wasm-bindgen-backend" 713 | version = "0.2.69" 714 | source = "registry+https://github.com/rust-lang/crates.io-index" 715 | checksum = "1114f89ab1f4106e5b55e688b828c0ab0ea593a1ea7c094b141b14cbaaec2d62" 716 | dependencies = [ 717 | "bumpalo", 718 | "lazy_static", 719 | "log", 720 | "proc-macro2", 721 | "quote", 722 | "syn", 723 | "wasm-bindgen-shared", 724 | ] 725 | 726 | [[package]] 727 | name = "wasm-bindgen-macro" 728 | version = "0.2.69" 729 | source = "registry+https://github.com/rust-lang/crates.io-index" 730 | checksum = "7a6ac8995ead1f084a8dea1e65f194d0973800c7f571f6edd70adf06ecf77084" 731 | dependencies = [ 732 | "quote", 733 | "wasm-bindgen-macro-support", 734 | ] 735 | 736 | [[package]] 737 | name = "wasm-bindgen-macro-support" 738 | version = "0.2.69" 739 | source = "registry+https://github.com/rust-lang/crates.io-index" 740 | checksum = "b5a48c72f299d80557c7c62e37e7225369ecc0c963964059509fbafe917c7549" 741 | dependencies = [ 742 | "proc-macro2", 743 | "quote", 744 | "syn", 745 | "wasm-bindgen-backend", 746 | "wasm-bindgen-shared", 747 | ] 748 | 749 | [[package]] 750 | name = "wasm-bindgen-shared" 751 | version = "0.2.69" 752 | source = "registry+https://github.com/rust-lang/crates.io-index" 753 | checksum = "7e7811dd7f9398f14cc76efd356f98f03aa30419dea46aa810d71e819fc97158" 754 | 755 | [[package]] 756 | name = "winapi" 757 | version = "0.3.9" 758 | source = "registry+https://github.com/rust-lang/crates.io-index" 759 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 760 | dependencies = [ 761 | "winapi-i686-pc-windows-gnu", 762 | "winapi-x86_64-pc-windows-gnu", 763 | ] 764 | 765 | [[package]] 766 | name = "winapi-i686-pc-windows-gnu" 767 | version = "0.4.0" 768 | source = "registry+https://github.com/rust-lang/crates.io-index" 769 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 770 | 771 | [[package]] 772 | name = "winapi-x86_64-pc-windows-gnu" 773 | version = "0.4.0" 774 | source = "registry+https://github.com/rust-lang/crates.io-index" 775 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 776 | --------------------------------------------------------------------------------