mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
Basic multiline support 📜
This commit is contained in:
parent
107450ee5c
commit
0c87c0c5a5
102
src/doc.rs
102
src/doc.rs
@ -1,6 +1,5 @@
|
||||
//! Abstract representation of a typesetted document.
|
||||
|
||||
use std::ops;
|
||||
use crate::font::Font;
|
||||
|
||||
|
||||
@ -23,8 +22,10 @@ pub struct Style {
|
||||
|
||||
/// A fallback list of font families to use.
|
||||
pub font_families: Vec<String>,
|
||||
/// The default font size.
|
||||
/// The font size.
|
||||
pub font_size: f32,
|
||||
/// The line spacing (as a multiple of the font size).
|
||||
pub line_spacing: f32,
|
||||
}
|
||||
|
||||
impl Default for Style {
|
||||
@ -39,6 +40,7 @@ impl Default for Style {
|
||||
"NotoSans", "NotoSansMath"
|
||||
]).iter().map(ToString::to_string).collect(),
|
||||
font_size: 12.0,
|
||||
line_spacing: 1.25,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -78,6 +80,10 @@ pub struct Size {
|
||||
}
|
||||
|
||||
impl Size {
|
||||
/// Create an zeroed size.
|
||||
#[inline]
|
||||
pub fn zero() -> Size { Size { points: 0.0 } }
|
||||
|
||||
/// Create a size from a number of points.
|
||||
#[inline]
|
||||
pub fn from_points(points: f32) -> Size { Size { points } }
|
||||
@ -111,18 +117,98 @@ impl Size {
|
||||
pub fn to_cm(&self) -> f32 { self.points * 0.0352778 }
|
||||
}
|
||||
|
||||
impl ops::Add for Size {
|
||||
mod size {
|
||||
use super::Size;
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt;
|
||||
use std::iter::Sum;
|
||||
use std::ops::*;
|
||||
|
||||
impl fmt::Display for Size {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}pt", self.points)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_reflexive {
|
||||
($trait:ident, $func:ident, $assign_trait:ident, $assign_func:ident) => {
|
||||
impl $trait for Size {
|
||||
type Output = Size;
|
||||
|
||||
fn add(self, other: Size) -> Size {
|
||||
Size { points: self.points + other.points }
|
||||
#[inline]
|
||||
fn $func(self, other: Size) -> Size {
|
||||
Size { points: $trait::$func(self.points, other.points) }
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Sub for Size {
|
||||
impl $assign_trait for Size {
|
||||
#[inline]
|
||||
fn $assign_func(&mut self, other: Size) {
|
||||
$assign_trait::$assign_func(&mut self.points, other.points);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! impl_num_back {
|
||||
($trait:ident, $func:ident, $assign_trait:ident, $assign_func:ident, $ty:ty) => {
|
||||
impl $trait<$ty> for Size {
|
||||
type Output = Size;
|
||||
|
||||
fn sub(self, other: Size) -> Size {
|
||||
Size { points: self.points - other.points }
|
||||
#[inline]
|
||||
fn $func(self, other: $ty) -> Size {
|
||||
Size { points: $trait::$func(self.points, other as f32) }
|
||||
}
|
||||
}
|
||||
|
||||
impl $assign_trait<$ty> for Size {
|
||||
#[inline]
|
||||
fn $assign_func(&mut self, other: $ty) {
|
||||
$assign_trait::$assign_func(&mut self.points, other as f32);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! impl_num_both {
|
||||
($trait:ident, $func:ident, $assign_trait:ident, $assign_func:ident, $ty:ty) => {
|
||||
impl_num_back!($trait, $func, $assign_trait, $assign_func, $ty);
|
||||
|
||||
impl $trait<Size> for $ty {
|
||||
type Output = Size;
|
||||
|
||||
#[inline]
|
||||
fn $func(self, other: Size) -> Size {
|
||||
Size { points: $trait::$func(self as f32, other.points) }
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl Neg for Size {
|
||||
type Output = Size;
|
||||
|
||||
fn neg(self) -> Size {
|
||||
Size { points: -self.points }
|
||||
}
|
||||
}
|
||||
|
||||
impl_reflexive!(Add, add, AddAssign, add_assign);
|
||||
impl_reflexive!(Sub, sub, SubAssign, sub_assign);
|
||||
impl_num_both!(Mul, mul, MulAssign, mul_assign, f32);
|
||||
impl_num_both!(Mul, mul, MulAssign, mul_assign, i32);
|
||||
impl_num_back!(Div, div, DivAssign, div_assign, f32);
|
||||
impl_num_back!(Div, div, DivAssign, div_assign, i32);
|
||||
|
||||
impl PartialOrd for Size {
|
||||
fn partial_cmp(&self, other: &Size) -> Option<Ordering> {
|
||||
self.points.partial_cmp(&other.points)
|
||||
}
|
||||
}
|
||||
|
||||
impl Sum for Size {
|
||||
fn sum<I>(iter: I) -> Size where I: Iterator<Item=Size> {
|
||||
iter.fold(Size::zero(), Add::add)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,59 +3,114 @@
|
||||
use std::error;
|
||||
use std::fmt;
|
||||
use crate::syntax::{SyntaxTree, Node};
|
||||
use crate::doc::{Document, Style, Page, Text, TextCommand};
|
||||
use crate::doc::{Document, Style, Size, Page, Text, TextCommand};
|
||||
use crate::font::Font;
|
||||
|
||||
|
||||
/// The core typesetting engine, transforming an abstract syntax tree into a document.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Engine<'s> {
|
||||
tree: SyntaxTree<'s>,
|
||||
// Immutable
|
||||
tree: &'s SyntaxTree<'s>,
|
||||
style: Style,
|
||||
|
||||
// Mutable
|
||||
fonts: Vec<Font>,
|
||||
active_font: usize,
|
||||
text_commands: Vec<TextCommand>,
|
||||
current_line: String,
|
||||
current_width: Size,
|
||||
}
|
||||
|
||||
impl<'s> Engine<'s> {
|
||||
/// Create a new generator from a syntax tree.
|
||||
pub fn new(tree: SyntaxTree<'s>) -> Engine<'s> {
|
||||
Engine { tree }
|
||||
pub fn new(tree: &'s SyntaxTree<'s>) -> Engine<'s> {
|
||||
Engine {
|
||||
style: Style::default(),
|
||||
tree,
|
||||
fonts: Vec::new(),
|
||||
active_font: 0,
|
||||
text_commands: Vec::new(),
|
||||
current_line: String::new(),
|
||||
current_width: Size::zero(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate the abstract document.
|
||||
pub fn typeset(&mut self) -> TypeResult<Document> {
|
||||
let style = Style::default();
|
||||
|
||||
pub fn typeset(mut self) -> TypeResult<Document> {
|
||||
// Load font defined by style
|
||||
let font_family = style.font_families.first().unwrap();
|
||||
let font_family = self.style.font_families.first().unwrap();
|
||||
let program = std::fs::read(format!("../fonts/{}-Regular.ttf", font_family)).unwrap();
|
||||
let font = Font::new(program).unwrap();
|
||||
|
||||
let mut text = String::new();
|
||||
self.fonts.push(font);
|
||||
self.active_font = 0;
|
||||
|
||||
// Move cursor to top-left position
|
||||
self.text_commands.push(TextCommand::Move(
|
||||
self.style.margins[0],
|
||||
self.style.paper_size[1] - self.style.margins[1])
|
||||
);
|
||||
|
||||
// Set the current font
|
||||
self.text_commands.push(TextCommand::SetFont(0, self.style.font_size));
|
||||
|
||||
// Iterate through the documents nodes.
|
||||
for node in &self.tree.nodes {
|
||||
match node {
|
||||
Node::Space if !text.is_empty() => text.push(' '),
|
||||
Node::Space | Node::Newline => (),
|
||||
Node::Word(word) => text.push_str(word),
|
||||
Node::Word(word) => self.write_word(word),
|
||||
|
||||
Node::Space => self.write_space(),
|
||||
Node::Newline => (),
|
||||
|
||||
Node::ToggleItalics | Node::ToggleBold | Node::ToggleMath => unimplemented!(),
|
||||
Node::Func(_) => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
// Create a page from the contents.
|
||||
let page = Page {
|
||||
size: style.paper_size,
|
||||
size: self.style.paper_size,
|
||||
text: vec![Text {
|
||||
commands: vec![
|
||||
TextCommand::Move(style.margins[0], style.paper_size[1] - style.margins[1]),
|
||||
TextCommand::SetFont(0, style.font_size),
|
||||
TextCommand::Text(text)
|
||||
]
|
||||
commands: self.text_commands,
|
||||
}],
|
||||
};
|
||||
|
||||
Ok(Document {
|
||||
pages: vec![page],
|
||||
fonts: vec![font],
|
||||
fonts: self.fonts,
|
||||
})
|
||||
}
|
||||
|
||||
fn write_word(&mut self, word: &str) {
|
||||
let max_width = self.style.paper_size[0] - 2 * self.style.margins[0];
|
||||
|
||||
let font = &self.fonts[self.active_font];
|
||||
let width = word.chars()
|
||||
.map(|c| font.widths[font.map(c) as usize] * self.style.font_size)
|
||||
.sum();
|
||||
|
||||
|
||||
if self.current_width + width > max_width {
|
||||
let vertical_move = - self.style.font_size
|
||||
* self.style.line_spacing
|
||||
* font.metrics.ascender;
|
||||
self.text_commands.push(TextCommand::Move(Size::zero(), vertical_move));
|
||||
|
||||
self.current_line.clear();
|
||||
self.current_width = Size::zero();
|
||||
}
|
||||
|
||||
self.text_commands.push(TextCommand::Text(word.to_owned()));
|
||||
self.current_line.push_str(word);
|
||||
self.current_width += width;
|
||||
}
|
||||
|
||||
fn write_space(&mut self) {
|
||||
if !self.current_line.is_empty() {
|
||||
self.write_word(" ");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Result type used for parsing.
|
||||
|
16
src/font.rs
16
src/font.rs
@ -23,6 +23,15 @@ pub struct Font {
|
||||
pub widths: Vec<Size>,
|
||||
/// The fallback glyph.
|
||||
pub default_glyph: u16,
|
||||
/// The relevant metrics of this font.
|
||||
pub metrics: FontMetrics,
|
||||
}
|
||||
|
||||
/// Font metrics relevant to the typesetting engine.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct FontMetrics {
|
||||
/// The typographics ascender relevant for line spacing.
|
||||
pub ascender: Size,
|
||||
}
|
||||
|
||||
impl Font {
|
||||
@ -44,12 +53,17 @@ impl Font {
|
||||
let font_name = base_font.unwrap_or_else(|| "unknown".to_owned());
|
||||
let widths = hmtx.metrics.iter().map(|m| convert(m.advance_width)).collect();
|
||||
|
||||
let metrics = FontMetrics {
|
||||
ascender: convert(os2.s_typo_ascender),
|
||||
};
|
||||
|
||||
Ok(Font {
|
||||
name: font_name,
|
||||
program,
|
||||
mapping: charmap.mapping,
|
||||
widths,
|
||||
default_glyph: os2.us_default_char.unwrap_or(0),
|
||||
metrics,
|
||||
})
|
||||
}
|
||||
|
||||
@ -62,7 +76,6 @@ impl Font {
|
||||
/// Encode the given text for this font (into glyph ids).
|
||||
#[inline]
|
||||
pub fn encode(&self, text: &str) -> Vec<u8> {
|
||||
println!("encoding {} with {:?}", text, self.mapping);
|
||||
let mut bytes = Vec::with_capacity(2 * text.len());
|
||||
for glyph in text.chars().map(|c| self.map(c)) {
|
||||
bytes.push((glyph >> 8) as u8);
|
||||
@ -181,6 +194,7 @@ impl<'p> Subsetter<'p> {
|
||||
mapping,
|
||||
widths,
|
||||
default_glyph: self.font.default_glyph,
|
||||
metrics: self.font.metrics.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
|
61
src/lib.rs
61
src/lib.rs
@ -68,7 +68,8 @@ impl<'s> Compiler<'s> {
|
||||
/// Return the abstract typesetted representation of the document.
|
||||
#[inline]
|
||||
pub fn typeset(&self) -> Result<Document, Error> {
|
||||
Engine::new(self.parse()?).typeset().map_err(Into::into)
|
||||
let tree = self.parse()?;
|
||||
Engine::new(&tree).typeset().map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Write the document as a _PDF_, returning how many bytes were written.
|
||||
@ -145,15 +146,63 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pdf() {
|
||||
fn pdfs() {
|
||||
test("unicode", "∑mbe∂∂ed font with Unicode!");
|
||||
test("parentheses", "Text with ) and ( or (enclosed) works.");
|
||||
test("composite-glyph", "Composite character‼");
|
||||
test("multiline","
|
||||
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed
|
||||
diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed
|
||||
diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum.
|
||||
Stet clita kasd gubergren, no sea takimata sanctus est.
|
||||
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy
|
||||
eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam
|
||||
voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet
|
||||
clita kasd gubergren, no sea takimata sanctus est.
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long() {
|
||||
test("wikipedia", r#"
|
||||
Typesetting is the composition of text by means of arranging physical types or the
|
||||
digital equivalents. Stored letters and other symbols (called sorts in mechanical
|
||||
systems and glyphs in digital systems) are retrieved and ordered according to a
|
||||
language's orthography for visual display. Typesetting requires one or more fonts
|
||||
(which are widely but erroneously confused with and substituted for typefaces). One
|
||||
significant effect of typesetting was that authorship of works could be spotted more
|
||||
easily, making it difficult for copiers who have not gained permission.
|
||||
|
||||
During much of the letterpress era, movable type was composed by hand for each page.
|
||||
Cast metal sorts were composed into words, then lines, then paragraphs, then pages of
|
||||
text and tightly bound together to make up a form, with all letter faces exactly the
|
||||
same "height to paper", creating an even surface of type. The form was placed in a
|
||||
press, inked, and an impression made on paper.
|
||||
|
||||
During typesetting, individual sorts are picked from a type case with the right hand,
|
||||
and set into a composing stick held in the left hand from left to right, and as viewed
|
||||
by the setter upside down. As seen in the photo of the composing stick, a lower case
|
||||
'q' looks like a 'd', a lower case 'b' looks like a 'p', a lower case 'p' looks like a
|
||||
'b' and a lower case 'd' looks like a 'q'. This is reputed to be the origin of the
|
||||
expression "mind your p's and q's". It might just as easily have been "mind your b's
|
||||
and d's".
|
||||
|
||||
The diagram at right illustrates a cast metal sort: a face, b body or shank, c point
|
||||
size, 1 shoulder, 2 nick, 3 groove, 4 foot. Wooden printing sorts were in use for
|
||||
centuries in combination with metal type. Not shown, and more the concern of the
|
||||
casterman, is the “set”, or width of each sort. Set width, like body size, is measured
|
||||
in points.
|
||||
|
||||
In order to extend the working life of type, and to account for the finite sorts in a
|
||||
case of type, copies of forms were cast when anticipating subsequent printings of a
|
||||
text, freeing the costly type for other work. This was particularly prevalent in book
|
||||
and newspaper work where rotary presses required type forms to wrap an impression
|
||||
cylinder rather than set in the bed of a press. In this process, called stereotyping,
|
||||
the entire form is pressed into a fine matrix such as plaster of Paris or papier mâché
|
||||
called a flong to create a positive, from which the stereotype form was electrotyped,
|
||||
cast of type metal.
|
||||
|
||||
Advances such as the typewriter and computer would push the state of the art even
|
||||
farther ahead. Still, hand composition and letterpress printing have not fallen
|
||||
completely out of use, and since the introduction of digital typesetting, it has seen a
|
||||
revival as an artisanal pursuit. However, it is a very small niche within the larger
|
||||
typesetting market.
|
||||
"#);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user