diff options
| author | Laurenz <laurmaedje@gmail.com> | 2023-07-02 19:59:52 +0200 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2023-07-02 20:07:43 +0200 |
| commit | ebfdb1dafa430786db10dad2ef7d5467c1bdbed1 (patch) | |
| tree | 2bbc24ddb4124c4bb14dec0e536129d4de37b056 /library/src/text | |
| parent | 3ab19185093d7709f824b95b979060ce125389d8 (diff) | |
Move everything into `crates/` directory
Diffstat (limited to 'library/src/text')
| -rw-r--r-- | library/src/text/deco.rs | 420 | ||||
| -rw-r--r-- | library/src/text/misc.rs | 330 | ||||
| -rw-r--r-- | library/src/text/mod.rs | 769 | ||||
| -rw-r--r-- | library/src/text/quotes.rs | 209 | ||||
| -rw-r--r-- | library/src/text/raw.rs | 398 | ||||
| -rw-r--r-- | library/src/text/shaping.rs | 973 | ||||
| -rw-r--r-- | library/src/text/shift.rs | 229 |
7 files changed, 0 insertions, 3328 deletions
diff --git a/library/src/text/deco.rs b/library/src/text/deco.rs deleted file mode 100644 index 9ec4ca32..00000000 --- a/library/src/text/deco.rs +++ /dev/null @@ -1,420 +0,0 @@ -use kurbo::{BezPath, Line, ParamCurve}; -use ttf_parser::{GlyphId, OutlineBuilder}; - -use super::TextElem; -use crate::prelude::*; - -/// Underlines text. -/// -/// ## Example { #example } -/// ```example -/// This is #underline[important]. -/// ``` -/// -/// Display: Underline -/// Category: text -#[element(Show)] -pub struct UnderlineElem { - /// How to stroke the line. - /// - /// See the [line's documentation]($func/line.stroke) for more details. If - /// set to `{auto}`, takes on the text's color and a thickness defined in - /// the current font. - /// - /// ```example - /// Take #underline( - /// stroke: 1.5pt + red, - /// offset: 2pt, - /// [care], - /// ) - /// ``` - #[resolve] - #[fold] - pub stroke: Smart<PartialStroke>, - - /// The position of the line relative to the baseline, read from the font - /// tables if `{auto}`. - /// - /// ```example - /// #underline(offset: 5pt)[ - /// The Tale Of A Faraway Line I - /// ] - /// ``` - #[resolve] - pub offset: Smart<Length>, - - /// The amount by which to extend the line beyond (or within if negative) - /// the content. - /// - /// ```example - /// #align(center, - /// underline(extent: 2pt)[Chapter 1] - /// ) - /// ``` - #[resolve] - pub extent: Length, - - /// Whether the line skips sections in which it would collide with the - /// glyphs. - /// - /// ```example - /// This #underline(evade: true)[is great]. - /// This #underline(evade: false)[is less great]. - /// ``` - #[default(true)] - pub evade: bool, - - /// The content to underline. - #[required] - pub body: Content, -} - -impl Show for UnderlineElem { - #[tracing::instrument(name = "UnderlineElem::show", skip_all)] - fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { - Ok(self.body().styled(TextElem::set_deco(Decoration { - line: DecoLine::Underline, - stroke: self.stroke(styles).unwrap_or_default(), - offset: self.offset(styles), - extent: self.extent(styles), - evade: self.evade(styles), - }))) - } -} - -/// Adds a line over text. -/// -/// ## Example { #example } -/// ```example -/// #overline[A line over text.] -/// ``` -/// -/// Display: Overline -/// Category: text -#[element(Show)] -pub struct OverlineElem { - /// How to stroke the line. - /// - /// See the [line's documentation]($func/line.stroke) for more details. If - /// set to `{auto}`, takes on the text's color and a thickness defined in - /// the current font. - /// - /// ```example - /// #set text(fill: olive) - /// #overline( - /// stroke: green.darken(20%), - /// offset: -12pt, - /// [The Forest Theme], - /// ) - /// ``` - #[resolve] - #[fold] - pub stroke: Smart<PartialStroke>, - - /// The position of the line relative to the baseline. Read from the font - /// tables if `{auto}`. - /// - /// ```example - /// #overline(offset: -1.2em)[ - /// The Tale Of A Faraway Line II - /// ] - /// ``` - #[resolve] - pub offset: Smart<Length>, - - /// The amount by which to extend the line beyond (or within if negative) - /// the content. - /// - /// ```example - /// #set overline(extent: 4pt) - /// #set underline(extent: 4pt) - /// #overline(underline[Typography Today]) - /// ``` - #[resolve] - pub extent: Length, - - /// Whether the line skips sections in which it would collide with the - /// glyphs. - /// - /// ```example - /// #overline( - /// evade: false, - /// offset: -7.5pt, - /// stroke: 1pt, - /// extent: 3pt, - /// [Temple], - /// ) - /// ``` - #[default(true)] - pub evade: bool, - - /// The content to add a line over. - #[required] - pub body: Content, -} - -impl Show for OverlineElem { - #[tracing::instrument(name = "OverlineElem::show", skip_all)] - fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { - Ok(self.body().styled(TextElem::set_deco(Decoration { - line: DecoLine::Overline, - stroke: self.stroke(styles).unwrap_or_default(), - offset: self.offset(styles), - extent: self.extent(styles), - evade: self.evade(styles), - }))) - } -} - -/// Strikes through text. -/// -/// ## Example { #example } -/// ```example -/// This is #strike[not] relevant. -/// ``` -/// -/// Display: Strikethrough -/// Category: text -#[element(Show)] -pub struct StrikeElem { - /// How to stroke the line. - /// - /// See the [line's documentation]($func/line.stroke) for more details. If - /// set to `{auto}`, takes on the text's color and a thickness defined in - /// the current font. - /// - /// _Note:_ Please don't use this for real redaction as you can still - /// copy paste the text. - /// - /// ```example - /// This is #strike(stroke: 1.5pt + red)[very stricken through]. \ - /// This is #strike(stroke: 10pt)[redacted]. - /// ``` - #[resolve] - #[fold] - pub stroke: Smart<PartialStroke>, - - /// The position of the line relative to the baseline. Read from the font - /// tables if `{auto}`. - /// - /// This is useful if you are unhappy with the offset your font provides. - /// - /// ```example - /// #set text(font: "Inria Serif") - /// This is #strike(offset: auto)[low-ish]. \ - /// This is #strike(offset: -3.5pt)[on-top]. - /// ``` - #[resolve] - pub offset: Smart<Length>, - - /// The amount by which to extend the line beyond (or within if negative) - /// the content. - /// - /// ```example - /// This #strike(extent: -2pt)[skips] parts of the word. - /// This #strike(extent: 2pt)[extends] beyond the word. - /// ``` - #[resolve] - pub extent: Length, - - /// The content to strike through. - #[required] - pub body: Content, -} - -impl Show for StrikeElem { - #[tracing::instrument(name = "StrikeElem::show", skip_all)] - fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { - Ok(self.body().styled(TextElem::set_deco(Decoration { - line: DecoLine::Strikethrough, - stroke: self.stroke(styles).unwrap_or_default(), - offset: self.offset(styles), - extent: self.extent(styles), - evade: false, - }))) - } -} - -/// Defines a line that is positioned over, under or on top of text. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct Decoration { - pub line: DecoLine, - pub stroke: PartialStroke<Abs>, - pub offset: Smart<Abs>, - pub extent: Abs, - pub evade: bool, -} - -impl Fold for Decoration { - type Output = Vec<Self>; - - fn fold(self, mut outer: Self::Output) -> Self::Output { - outer.insert(0, self); - outer - } -} - -cast! { - type Decoration: "decoration", -} - -/// A kind of decorative line. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum DecoLine { - Underline, - Strikethrough, - Overline, -} - -/// Add line decorations to a single run of shaped text. -pub(super) fn decorate( - frame: &mut Frame, - deco: &Decoration, - text: &TextItem, - shift: Abs, - pos: Point, - width: Abs, -) { - let font_metrics = text.font.metrics(); - let metrics = match deco.line { - DecoLine::Strikethrough => font_metrics.strikethrough, - DecoLine::Overline => font_metrics.overline, - DecoLine::Underline => font_metrics.underline, - }; - - let offset = deco.offset.unwrap_or(-metrics.position.at(text.size)) - shift; - let stroke = deco.stroke.clone().unwrap_or(Stroke { - paint: text.fill.clone(), - thickness: metrics.thickness.at(text.size), - ..Stroke::default() - }); - - let gap_padding = 0.08 * text.size; - let min_width = 0.162 * text.size; - - let start = pos.x - deco.extent; - let end = pos.x + (width + 2.0 * deco.extent); - - let mut push_segment = |from: Abs, to: Abs| { - let origin = Point::new(from, pos.y + offset); - let target = Point::new(to - from, Abs::zero()); - - if target.x >= min_width || !deco.evade { - let shape = Geometry::Line(target).stroked(stroke.clone()); - frame.push(origin, FrameItem::Shape(shape, Span::detached())); - } - }; - - if !deco.evade { - push_segment(start, end); - return; - } - - let line = Line::new( - kurbo::Point::new(pos.x.to_raw(), offset.to_raw()), - kurbo::Point::new((pos.x + width).to_raw(), offset.to_raw()), - ); - - let mut x = pos.x; - let mut intersections = vec![]; - - for glyph in text.glyphs.iter() { - let dx = glyph.x_offset.at(text.size) + x; - let mut builder = - BezPathBuilder::new(font_metrics.units_per_em, text.size, dx.to_raw()); - - let bbox = text.font.ttf().outline_glyph(GlyphId(glyph.id), &mut builder); - let path = builder.finish(); - - x += glyph.x_advance.at(text.size); - - // Only do the costly segments intersection test if the line - // intersects the bounding box. - let intersect = bbox.map_or(false, |bbox| { - let y_min = -text.font.to_em(bbox.y_max).at(text.size); - let y_max = -text.font.to_em(bbox.y_min).at(text.size); - offset >= y_min && offset <= y_max - }); - - if intersect { - // Find all intersections of segments with the line. - intersections.extend( - path.segments() - .flat_map(|seg| seg.intersect_line(line)) - .map(|is| Abs::raw(line.eval(is.line_t).x)), - ); - } - } - - // Add start and end points, taking padding into account. - intersections.push(start - gap_padding); - intersections.push(end + gap_padding); - // When emitting the decorative line segments, we move from left to - // right. The intersections are not necessarily in this order, yet. - intersections.sort(); - - for edge in intersections.windows(2) { - let l = edge[0]; - let r = edge[1]; - - // If we are too close, don't draw the segment - if r - l < gap_padding { - continue; - } else { - push_segment(l + gap_padding, r - gap_padding); - } - } -} - -/// Builds a kurbo [`BezPath`] for a glyph. -struct BezPathBuilder { - path: BezPath, - units_per_em: f64, - font_size: Abs, - x_offset: f64, -} - -impl BezPathBuilder { - fn new(units_per_em: f64, font_size: Abs, x_offset: f64) -> Self { - Self { - path: BezPath::new(), - units_per_em, - font_size, - x_offset, - } - } - - fn finish(self) -> BezPath { - self.path - } - - fn p(&self, x: f32, y: f32) -> kurbo::Point { - kurbo::Point::new(self.s(x) + self.x_offset, -self.s(y)) - } - - fn s(&self, v: f32) -> f64 { - Em::from_units(v, self.units_per_em).at(self.font_size).to_raw() - } -} - -impl OutlineBuilder for BezPathBuilder { - fn move_to(&mut self, x: f32, y: f32) { - self.path.move_to(self.p(x, y)); - } - - fn line_to(&mut self, x: f32, y: f32) { - self.path.line_to(self.p(x, y)); - } - - fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) { - self.path.quad_to(self.p(x1, y1), self.p(x, y)); - } - - fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { - self.path.curve_to(self.p(x1, y1), self.p(x2, y2), self.p(x, y)); - } - - fn close(&mut self) { - self.path.close_path(); - } -} diff --git a/library/src/text/misc.rs b/library/src/text/misc.rs deleted file mode 100644 index 811b027e..00000000 --- a/library/src/text/misc.rs +++ /dev/null @@ -1,330 +0,0 @@ -use super::TextElem; -use crate::prelude::*; - -/// A text space. -/// -/// Display: Space -/// Category: text -#[element(Behave, Unlabellable, PlainText)] -pub struct SpaceElem {} - -impl Behave for SpaceElem { - fn behaviour(&self) -> Behaviour { - Behaviour::Weak(2) - } -} - -impl Unlabellable for SpaceElem {} - -impl PlainText for SpaceElem { - fn plain_text(&self, text: &mut EcoString) { - text.push(' '); - } -} - -/// Inserts a line break. -/// -/// Advances the paragraph to the next line. A single trailing line break at the -/// end of a paragraph is ignored, but more than one creates additional empty -/// lines. -/// -/// ## Example { #example } -/// ```example -/// *Date:* 26.12.2022 \ -/// *Topic:* Infrastructure Test \ -/// *Severity:* High \ -/// ``` -/// -/// ## Syntax { #syntax } -/// This function also has dedicated syntax: To insert a line break, simply write -/// a backslash followed by whitespace. This always creates an unjustified -/// break. -/// -/// Display: Line Break -/// Category: text -#[element(Behave)] -pub struct LinebreakElem { - /// Whether to justify the line before the break. - /// - /// This is useful if you found a better line break opportunity in your - /// justified text than Typst did. - /// - /// ```example - /// #set par(justify: true) - /// #let jb = linebreak(justify: true) - /// - /// I have manually tuned the #jb - /// line breaks in this paragraph #jb - /// for an _interesting_ result. #jb - /// ``` - #[default(false)] - pub justify: bool, -} - -impl Behave for LinebreakElem { - fn behaviour(&self) -> Behaviour { - Behaviour::Destructive - } -} - -/// Strongly emphasizes content by increasing the font weight. -/// -/// Increases the current font weight by a given `delta`. -/// -/// ## Example { #example } -/// ```example -/// This is *strong.* \ -/// This is #strong[too.] \ -/// -/// #show strong: set text(red) -/// And this is *evermore.* -/// ``` -/// -/// ## Syntax { #syntax } -/// This function also has dedicated syntax: To strongly emphasize content, -/// simply enclose it in stars/asterisks (`*`). Note that this only works at -/// word boundaries. To strongly emphasize part of a word, you have to use the -/// function. -/// -/// Display: Strong Emphasis -/// Category: text -#[element(Show)] -pub struct StrongElem { - /// The delta to apply on the font weight. - /// - /// ```example - /// #set strong(delta: 0) - /// No *effect!* - /// ``` - #[default(300)] - pub delta: i64, - - /// The content to strongly emphasize. - #[required] - pub body: Content, -} - -impl Show for StrongElem { - #[tracing::instrument(name = "StrongElem::show", skip_all)] - fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { - Ok(self.body().styled(TextElem::set_delta(Delta(self.delta(styles))))) - } -} - -/// A delta that is summed up when folded. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct Delta(pub i64); - -cast! { - Delta, - self => self.0.into_value(), - v: i64 => Self(v), -} - -impl Fold for Delta { - type Output = i64; - - fn fold(self, outer: Self::Output) -> Self::Output { - outer + self.0 - } -} - -/// Emphasizes content by setting it in italics. -/// -/// - If the current [text style]($func/text.style) is `{"normal"}`, -/// this turns it into `{"italic"}`. -/// - If it is already `{"italic"}` or `{"oblique"}`, -/// it turns it back to `{"normal"}`. -/// -/// ## Example { #example } -/// ```example -/// This is _emphasized._ \ -/// This is #emph[too.] -/// -/// #show emph: it => { -/// text(blue, it.body) -/// } -/// -/// This is _emphasized_ differently. -/// ``` -/// -/// ## Syntax { #syntax } -/// This function also has dedicated syntax: To emphasize content, simply -/// enclose it in underscores (`_`). Note that this only works at word -/// boundaries. To emphasize part of a word, you have to use the function. -/// -/// Display: Emphasis -/// Category: text -#[element(Show)] -pub struct EmphElem { - /// The content to emphasize. - #[required] - pub body: Content, -} - -impl Show for EmphElem { - #[tracing::instrument(name = "EmphElem::show", skip(self))] - fn show(&self, _: &mut Vt, _: StyleChain) -> SourceResult<Content> { - Ok(self.body().styled(TextElem::set_emph(Toggle))) - } -} - -/// A toggle that turns on and off alternatingly if folded. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct Toggle; - -cast! { - Toggle, - self => Value::None, - _: Value => Self, -} - -impl Fold for Toggle { - type Output = bool; - - fn fold(self, outer: Self::Output) -> Self::Output { - !outer - } -} - -/// Converts text or content to lowercase. -/// -/// ## Example { #example } -/// ```example -/// #lower("ABC") \ -/// #lower[*My Text*] \ -/// #lower[already low] -/// ``` -/// -/// Display: Lowercase -/// Category: text -#[func] -pub fn lower( - /// The text to convert to lowercase. - text: Caseable, -) -> Caseable { - case(text, Case::Lower) -} - -/// Converts text or content to uppercase. -/// -/// ## Example { #example } -/// ```example -/// #upper("abc") \ -/// #upper[*my text*] \ -/// #upper[ALREADY HIGH] -/// ``` -/// -/// Display: Uppercase -/// Category: text -#[func] -pub fn upper( - /// The text to convert to uppercase. - text: Caseable, -) -> Caseable { - case(text, Case::Upper) -} - -/// Change the case of text. -fn case(text: Caseable, case: Case) -> Caseable { - match text { - Caseable::Str(v) => Caseable::Str(case.apply(&v).into()), - Caseable::Content(v) => { - Caseable::Content(v.styled(TextElem::set_case(Some(case)))) - } - } -} - -/// A value whose case can be changed. -pub enum Caseable { - Str(Str), - Content(Content), -} - -cast! { - Caseable, - self => match self { - Self::Str(v) => v.into_value(), - Self::Content(v) => v.into_value(), - }, - v: Str => Self::Str(v), - v: Content => Self::Content(v), -} - -/// A case transformation on text. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] -pub enum Case { - /// Everything is lowercased. - Lower, - /// Everything is uppercased. - Upper, -} - -impl Case { - /// Apply the case to a string. - pub fn apply(self, text: &str) -> String { - match self { - Self::Lower => text.to_lowercase(), - Self::Upper => text.to_uppercase(), - } - } -} - -/// Displays text in small capitals. -/// -/// _Note:_ This enables the OpenType `smcp` feature for the font. Not all fonts -/// support this feature. Sometimes smallcaps are part of a dedicated font and -/// sometimes they are not available at all. In the future, this function will -/// support selecting a dedicated smallcaps font as well as synthesizing -/// smallcaps from normal letters, but this is not yet implemented. -/// -/// ## Example { #example } -/// ```example -/// #set par(justify: true) -/// #set heading(numbering: "I.") -/// -/// #show heading: it => { -/// set block(below: 10pt) -/// set text(weight: "regular") -/// align(center, smallcaps(it)) -/// } -/// -/// = Introduction -/// #lorem(40) -/// ``` -/// -/// Display: Small Capitals -/// Category: text -#[func] -pub fn smallcaps( - /// The text to display to small capitals. - body: Content, -) -> Content { - body.styled(TextElem::set_smallcaps(true)) -} - -/// Creates blind text. -/// -/// This function yields a Latin-like _Lorem Ipsum_ blind text with the given -/// number of words. The sequence of words generated by the function is always -/// the same but randomly chosen. As usual for blind texts, it does not make any -/// sense. Use it as a placeholder to try layouts. -/// -/// ## Example { #example } -/// ```example -/// = Blind Text -/// #lorem(30) -/// -/// = More Blind Text -/// #lorem(15) -/// ``` -/// -/// Display: Blind Text -/// Category: text -#[func] -pub fn lorem( - /// The length of the blind text in words. - words: usize, -) -> Str { - lipsum::lipsum(words).replace("--", "–").into() -} diff --git a/library/src/text/mod.rs b/library/src/text/mod.rs deleted file mode 100644 index ff8cbad8..00000000 --- a/library/src/text/mod.rs +++ /dev/null @@ -1,769 +0,0 @@ -//! Text handling. - -mod deco; -mod misc; -mod quotes; -mod raw; -mod shaping; -mod shift; - -pub use self::deco::*; -pub use self::misc::*; -pub use self::quotes::*; -pub use self::raw::*; -pub use self::shaping::*; -pub use self::shift::*; - -use rustybuzz::Tag; -use typst::font::{FontMetrics, FontStretch, FontStyle, FontWeight, VerticalFontMetric}; - -use crate::layout::ParElem; -use crate::prelude::*; - -/// Hook up all text definitions. -pub(super) fn define(global: &mut Scope) { - global.define("text", TextElem::func()); - global.define("linebreak", LinebreakElem::func()); - global.define("smartquote", SmartQuoteElem::func()); - global.define("strong", StrongElem::func()); - global.define("emph", EmphElem::func()); - global.define("lower", lower_func()); - global.define("upper", upper_func()); - global.define("smallcaps", smallcaps_func()); - global.define("sub", SubElem::func()); - global.define("super", SuperElem::func()); - global.define("underline", UnderlineElem::func()); - global.define("strike", StrikeElem::func()); - global.define("overline", OverlineElem::func()); - global.define("raw", RawElem::func()); - global.define("lorem", lorem_func()); -} - -/// Customizes the look and layout of text in a variety of ways. -/// -/// This function is used frequently, both with set rules and directly. While -/// the set rule is often the simpler choice, calling the `text` function -/// directly can be useful when passing text as an argument to another function. -/// -/// ## Example { #example } -/// ```example -/// #set text(18pt) -/// With a set rule. -/// -/// #emph(text(blue)[ -/// With a function call. -/// ]) -/// ``` -/// -/// Display: Text -/// Category: text -#[element(Construct, PlainText)] -pub struct TextElem { - /// A prioritized sequence of font families. - /// - /// When processing text, Typst tries all specified font families in order - /// until it finds a font that has the necessary glyphs. In the example - /// below, the font `Inria Serif` is preferred, but since it does not - /// contain Arabic glyphs, the arabic text uses `Noto Sans Arabic` instead. - /// - /// ```example - /// #set text(font: ( - /// "Inria Serif", - /// "Noto Sans Arabic", - /// )) - /// - /// This is Latin. \ - /// هذا عربي. - /// - /// ``` - #[default(FontList(vec![FontFamily::new("Linux Libertine")]))] - pub font: FontList, - - /// Whether to allow last resort font fallback when the primary font list - /// contains no match. This lets Typst search through all available fonts - /// for the most similar one that has the necessary glyphs. - /// - /// _Note:_ Currently, there are no warnings when fallback is disabled and - /// no glyphs are found. Instead, your text shows up in the form of "tofus": - /// Small boxes that indicate the lack of an appropriate glyph. In the - /// future, you will be able to instruct Typst to issue warnings so you know - /// something is up. - /// - /// ```example - /// #set text(font: "Inria Serif") - /// هذا عربي - /// - /// #set text(fallback: false) - /// هذا عربي - /// ``` - #[default(true)] - pub fallback: bool, - - /// The desired font style. - /// - /// When an italic style is requested and only an oblique one is available, - /// it is used. Similarly, the other way around, an italic style can stand - /// in for an oblique one. When neither an italic nor an oblique style is - /// available, Typst selects the normal style. Since most fonts are only - /// available either in an italic or oblique style, the difference between - /// italic and oblique style is rarely observable. - /// - /// If you want to emphasize your text, you should do so using the - /// [emph]($func/emph) function instead. This makes it easy to adapt the - /// style later if you change your mind about how to signify the emphasis. - /// - /// ```example - /// #text(font: "Linux Libertine", style: "italic")[Italic] - /// #text(font: "DejaVu Sans", style: "oblique")[Oblique] - /// ``` - pub style: FontStyle, - - /// The desired thickness of the font's glyphs. Accepts an integer between - /// `{100}` and `{900}` or one of the predefined weight names. When the - /// desired weight is not available, Typst selects the font from the family - /// that is closest in weight. - /// - /// If you want to strongly emphasize your text, you should do so using the - /// [strong]($func/strong) function instead. This makes it easy to adapt the - /// style later if you change your mind about how to signify the strong - /// emphasis. - /// - /// ```example - /// #set text(font: "IBM Plex Sans") - /// - /// #text(weight: "light")[Light] \ - /// #text(weight: "regular")[Regular] \ - /// #text(weight: "medium")[Medium] \ - /// #text(weight: 500)[Medium] \ - /// #text(weight: "bold")[Bold] - /// ``` - pub weight: FontWeight, - - /// The desired width of the glyphs. Accepts a ratio between `{50%}` and - /// `{200%}`. When the desired weight is not available, Typst selects the - /// font from the family that is closest in stretch. This will only stretch - /// the text if a condensed or expanded version of the font is available. - /// - /// If you want to adjust the amount of space between characters instead of - /// stretching the glyphs itself, use the [`tracking`]($func/text.tracking) - /// property instead. - /// - /// ```example - /// #text(stretch: 75%)[Condensed] \ - /// #text(stretch: 100%)[Normal] - /// ``` - pub stretch: FontStretch, - - /// The size of the glyphs. This value forms the basis of the `em` unit: - /// `{1em}` is equivalent to the font size. - /// - /// You can also give the font size itself in `em` units. Then, it is - /// relative to the previous font size. - /// - /// ```example - /// #set text(size: 20pt) - /// very #text(1.5em)[big] text - /// ``` - #[parse(args.named_or_find("size")?)] - #[fold] - #[default(Abs::pt(11.0))] - pub size: TextSize, - - /// The glyph fill color. - /// - /// ```example - /// #set text(fill: red) - /// This text is red. - /// ``` - #[parse(args.named_or_find("fill")?)] - #[default(Color::BLACK.into())] - pub fill: Paint, - - /// The amount of space that should be added between characters. - /// - /// ```example - /// #set text(tracking: 1.5pt) - /// Distant text. - /// ``` - #[resolve] - pub tracking: Length, - - /// The amount of space between words. - /// - /// Can be given as an absolute length, but also relative to the width of - /// the space character in the font. - /// - /// If you want to adjust the amount of space between characters rather than - /// words, use the [`tracking`]($func/text.tracking) property instead. - /// - /// ```example - /// #set text(spacing: 200%) - /// Text with distant words. - /// ``` - #[resolve] - #[default(Rel::one())] - pub spacing: Rel<Length>, - - /// An amount to shift the text baseline by. - /// - /// ```example - /// A #text(baseline: 3pt)[lowered] - /// word. - /// ``` - #[resolve] - pub baseline: Length, - - /// Whether certain glyphs can hang over into the margin in justified text. - /// This can make justification visually more pleasing. - /// - /// ```example - /// #set par(justify: true) - /// This justified text has a hyphen in - /// the paragraph's first line. Hanging - /// the hyphen slightly into the margin - /// results in a clearer paragraph edge. - /// - /// #set text(overhang: false) - /// This justified text has a hyphen in - /// the paragraph's first line. Hanging - /// the hyphen slightly into the margin - /// results in a clearer paragraph edge. - /// ``` - #[default(true)] - pub overhang: bool, - - /// The top end of the conceptual frame around the text used for layout and - /// positioning. This affects the size of containers that hold text. - /// - /// ```example - /// #set rect(inset: 0pt) - /// #set text(size: 20pt) - /// - /// #set text(top-edge: "ascender") - /// #rect(fill: aqua)[Typst] - /// - /// #set text(top-edge: "cap-height") - /// #rect(fill: aqua)[Typst] - /// ``` - #[default(TextEdge::Metric(VerticalFontMetric::CapHeight))] - pub top_edge: TextEdge, - - /// The bottom end of the conceptual frame around the text used for layout - /// and positioning. This affects the size of containers that hold text. - /// - /// ```example - /// #set rect(inset: 0pt) - /// #set text(size: 20pt) - /// - /// #set text(bottom-edge: "baseline") - /// #rect(fill: aqua)[Typst] - /// - /// #set text(bottom-edge: "descender") - /// #rect(fill: aqua)[Typst] - /// ``` - #[default(TextEdge::Metric(VerticalFontMetric::Baseline))] - pub bottom_edge: TextEdge, - - /// An [ISO 639-1/2/3 language code.](https://en.wikipedia.org/wiki/ISO_639) - /// - /// Setting the correct language affects various parts of Typst: - /// - /// - The text processing pipeline can make more informed choices. - /// - Hyphenation will use the correct patterns for the language. - /// - [Smart quotes]($func/smartquote) turns into the correct quotes for the - /// language. - /// - And all other things which are language-aware. - /// - /// ```example - /// #set text(lang: "de") - /// #outline() - /// - /// = Einleitung - /// In diesem Dokument, ... - /// ``` - #[default(Lang::ENGLISH)] - pub lang: Lang, - - /// An [ISO 3166-1 alpha-2 region code.](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) - /// - /// This lets the text processing pipeline make more informed choices. - pub region: Option<Region>, - - /// The dominant direction for text and inline objects. Possible values are: - /// - /// - `{auto}`: Automatically infer the direction from the `lang` property. - /// - `{ltr}`: Layout text from left to right. - /// - `{rtl}`: Layout text from right to left. - /// - /// When writing in right-to-left scripts like Arabic or Hebrew, you should - /// set the [text language]($func/text.lang) or direction. While individual - /// runs of text are automatically layouted in the correct direction, - /// setting the dominant direction gives the bidirectional reordering - /// algorithm the necessary information to correctly place punctuation and - /// inline objects. Furthermore, setting the direction affects the alignment - /// values `start` and `end`, which are equivalent to `left` and `right` in - /// `ltr` text and the other way around in `rtl` text. - /// - /// If you set this to `rtl` and experience bugs or in some way bad looking - /// output, please do get in touch with us through the - /// [contact form](https://typst.app/contact) or our - /// [Discord server]($community/#discord)! - /// - /// ```example - /// #set text(dir: rtl) - /// هذا عربي. - /// ``` - #[resolve] - pub dir: TextDir, - - /// Whether to hyphenate text to improve line breaking. When `{auto}`, text - /// will be hyphenated if and only if justification is enabled. - /// - /// Setting the [text language]($func/text.lang) ensures that the correct - /// hyphenation patterns are used. - /// - /// ```example - /// #set page(width: 200pt) - /// - /// #set par(justify: true) - /// This text illustrates how - /// enabling hyphenation can - /// improve justification. - /// - /// #set text(hyphenate: false) - /// This text illustrates how - /// enabling hyphenation can - /// improve justification. - /// ``` - #[resolve] - pub hyphenate: Hyphenate, - - /// Whether to apply kerning. - /// - /// When enabled, specific letter pairings move closer together or further - /// apart for a more visually pleasing result. The example below - /// demonstrates how decreasing the gap between the "T" and "o" results in a - /// more natural look. Setting this to `{false}` disables kerning by turning - /// off the OpenType `kern` font feature. - /// - /// ```example - /// #set text(size: 25pt) - /// Totally - /// - /// #set text(kerning: false) - /// Totally - /// ``` - #[default(true)] - pub kerning: bool, - - /// Whether to apply stylistic alternates. - /// - /// Sometimes fonts contain alternative glyphs for the same codepoint. - /// Setting this to `{true}` switches to these by enabling the OpenType - /// `salt` font feature. - /// - /// ```example - /// #set text( - /// font: "IBM Plex Sans", - /// size: 20pt, - /// ) - /// - /// 0, a, g, ß - /// - /// #set text(alternates: true) - /// 0, a, g, ß - /// ``` - #[default(false)] - pub alternates: bool, - - /// Which stylistic set to apply. Font designers can categorize alternative - /// glyphs forms into stylistic sets. As this value is highly font-specific, - /// you need to consult your font to know which sets are available. When set - /// to an integer between `{1}` and `{20}`, enables the corresponding - /// OpenType font feature from `ss01`, ..., `ss20`. - pub stylistic_set: Option<StylisticSet>, - - /// Whether standard ligatures are active. - /// - /// Certain letter combinations like "fi" are often displayed as a single - /// merged glyph called a _ligature._ Setting this to `{false}` disables - /// these ligatures by turning off the OpenType `liga` and `clig` font - /// features. - /// - /// ```example - /// #set text(size: 20pt) - /// A fine ligature. - /// - /// #set text(ligatures: false) - /// A fine ligature. - /// ``` - #[default(true)] - pub ligatures: bool, - - /// Whether ligatures that should be used sparingly are active. Setting this - /// to `{true}` enables the OpenType `dlig` font feature. - #[default(false)] - pub discretionary_ligatures: bool, - - /// Whether historical ligatures are active. Setting this to `{true}` - /// enables the OpenType `hlig` font feature. - #[default(false)] - pub historical_ligatures: bool, - - /// Which kind of numbers / figures to select. When set to `{auto}`, the - /// default numbers for the font are used. - /// - /// ```example - /// #set text(font: "Noto Sans", 20pt) - /// #set text(number-type: "lining") - /// Number 9. - /// - /// #set text(number-type: "old-style") - /// Number 9. - /// ``` - pub number_type: Smart<NumberType>, - - /// The width of numbers / figures. When set to `{auto}`, the default - /// numbers for the font are used. - /// - /// ```example - /// #set text(font: "Noto Sans", 20pt) - /// #set text(number-width: "proportional") - /// A 12 B 34. \ - /// A 56 B 78. - /// - /// #set text(number-width: "tabular") - /// A 12 B 34. \ - /// A 56 B 78. - /// ``` - pub number_width: Smart<NumberWidth>, - - /// Whether to have a slash through the zero glyph. Setting this to `{true}` - /// enables the OpenType `zero` font feature. - /// - /// ```example - /// 0, #text(slashed-zero: true)[0] - /// ``` - #[default(false)] - pub slashed_zero: bool, - - /// Whether to turn numbers into fractions. Setting this to `{true}` - /// enables the OpenType `frac` font feature. - /// - /// It is not advisable to enable this property globally as it will mess - /// with all appearances of numbers after a slash (e.g., in URLs). Instead, - /// enable it locally when you want a fraction. - /// - /// ```example - /// 1/2 \ - /// #text(fractions: true)[1/2] - /// ``` - #[default(false)] - pub fractions: bool, - - /// Raw OpenType features to apply. - /// - /// - If given an array of strings, sets the features identified by the - /// strings to `{1}`. - /// - If given a dictionary mapping to numbers, sets the features - /// identified by the keys to the values. - /// - /// ```example - /// // Enable the `frac` feature manually. - /// #set text(features: ("frac",)) - /// 1/2 - /// ``` - #[fold] - pub features: FontFeatures, - - /// Content in which all text is styled according to the other arguments. - #[external] - #[required] - pub body: Content, - - /// The text. - #[internal] - #[required] - pub text: EcoString, - - /// A delta to apply on the font weight. - #[internal] - #[fold] - pub delta: Delta, - - /// Whether the font style should be inverted. - #[internal] - #[fold] - #[default(false)] - pub emph: Toggle, - - /// Decorative lines. - #[internal] - #[fold] - pub deco: Decoration, - - /// A case transformation that should be applied to the text. - #[internal] - pub case: Option<Case>, - - /// Whether small capital glyphs should be used. ("smcp") - #[internal] - #[default(false)] - pub smallcaps: bool, -} - -impl TextElem { - /// Create a new packed text element. - pub fn packed(text: impl Into<EcoString>) -> Content { - Self::new(text.into()).pack() - } -} - -impl Construct for TextElem { - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> { - // The text constructor is special: It doesn't create a text element. - // Instead, it leaves the passed argument structurally unchanged, but - // styles all text in it. - let styles = Self::set(args)?; - let body = args.expect::<Content>("body")?; - Ok(body.styled_with_map(styles)) - } -} - -impl PlainText for TextElem { - fn plain_text(&self, text: &mut EcoString) { - text.push_str(&self.text()); - } -} - -/// A lowercased font family like "arial". -#[derive(Clone, Eq, PartialEq, Hash)] -pub struct FontFamily(EcoString); - -impl FontFamily { - /// Create a named font family variant. - pub fn new(string: &str) -> Self { - Self(string.to_lowercase().into()) - } - - /// The lowercased family name. - pub fn as_str(&self) -> &str { - &self.0 - } -} - -impl Debug for FontFamily { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -cast! { - FontFamily, - self => self.0.into_value(), - string: EcoString => Self::new(&string), -} - -/// Font family fallback list. -#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] -pub struct FontList(pub Vec<FontFamily>); - -impl IntoIterator for FontList { - type IntoIter = std::vec::IntoIter<FontFamily>; - type Item = FontFamily; - - fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() - } -} - -cast! { - FontList, - self => if self.0.len() == 1 { - self.0.into_iter().next().unwrap().0.into_value() - } else { - self.0.into_value() - }, - family: FontFamily => Self(vec![family]), - values: Array => Self(values.into_iter().map(|v| v.cast()).collect::<StrResult<_>>()?), -} - -/// The size of text. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct TextSize(pub Length); - -impl Fold for TextSize { - type Output = Abs; - - fn fold(self, outer: Self::Output) -> Self::Output { - self.0.em.at(outer) + self.0.abs - } -} - -cast! { - TextSize, - self => self.0.into_value(), - v: Length => Self(v), -} - -/// Specifies the bottom or top edge of text. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum TextEdge { - /// An edge specified using one of the well-known font metrics. - Metric(VerticalFontMetric), - /// An edge specified as a length. - Length(Length), -} - -impl TextEdge { - /// Resolve the value of the text edge given a font's metrics. - pub fn resolve(self, styles: StyleChain, metrics: &FontMetrics) -> Abs { - match self { - Self::Metric(metric) => metrics.vertical(metric).resolve(styles), - Self::Length(length) => length.resolve(styles), - } - } -} - -cast! { - TextEdge, - self => match self { - Self::Metric(metric) => metric.into_value(), - Self::Length(length) => length.into_value(), - }, - v: VerticalFontMetric => Self::Metric(v), - v: Length => Self::Length(v), -} - -/// The direction of text and inline objects in their line. -#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] -pub struct TextDir(pub Smart<Dir>); - -cast! { - TextDir, - self => self.0.into_value(), - v: Smart<Dir> => { - if v.map_or(false, |dir| dir.axis() == Axis::Y) { - bail!("text direction must be horizontal"); - } - Self(v) - }, -} - -impl Resolve for TextDir { - type Output = Dir; - - fn resolve(self, styles: StyleChain) -> Self::Output { - match self.0 { - Smart::Auto => TextElem::lang_in(styles).dir(), - Smart::Custom(dir) => dir, - } - } -} - -/// Whether to hyphenate text. -#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] -pub struct Hyphenate(pub Smart<bool>); - -cast! { - Hyphenate, - self => self.0.into_value(), - v: Smart<bool> => Self(v), -} - -impl Resolve for Hyphenate { - type Output = bool; - - fn resolve(self, styles: StyleChain) -> Self::Output { - match self.0 { - Smart::Auto => ParElem::justify_in(styles), - Smart::Custom(v) => v, - } - } -} - -/// A stylistic set in a font. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct StylisticSet(u8); - -impl StylisticSet { - /// Create a new set, clamping to 1-20. - pub fn new(index: u8) -> Self { - Self(index.clamp(1, 20)) - } - - /// Get the value, guaranteed to be 1-20. - pub fn get(self) -> u8 { - self.0 - } -} - -cast! { - StylisticSet, - self => self.0.into_value(), - v: i64 => match v { - 1 ..= 20 => Self::new(v as u8), - _ => bail!("stylistic set must be between 1 and 20"), - }, -} - -/// Which kind of numbers / figures to select. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] -pub enum NumberType { - /// Numbers that fit well with capital text (the OpenType `lnum` - /// font feature). - Lining, - /// Numbers that fit well into a flow of upper- and lowercase text (the - /// OpenType `onum` font feature). - OldStyle, -} - -/// The width of numbers / figures. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] -pub enum NumberWidth { - /// Numbers with glyph-specific widths (the OpenType `pnum` font feature). - Proportional, - /// Numbers of equal width (the OpenType `tnum` font feature). - Tabular, -} - -/// OpenType font features settings. -#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] -pub struct FontFeatures(pub Vec<(Tag, u32)>); - -cast! { - FontFeatures, - self => self.0 - .into_iter() - .map(|(tag, num)| { - let bytes = tag.to_bytes(); - let key = std::str::from_utf8(&bytes).unwrap_or_default(); - (key.into(), num.into_value()) - }) - .collect::<Dict>() - .into_value(), - values: Array => Self(values - .into_iter() - .map(|v| { - let tag = v.cast::<EcoString>()?; - Ok((Tag::from_bytes_lossy(tag.as_bytes()), 1)) - }) - .collect::<StrResult<_>>()?), - values: Dict => Self(values - .into_iter() - .map(|(k, v)| { - let num = v.cast::<u32>()?; - let tag = Tag::from_bytes_lossy(k.as_bytes()); - Ok((tag, num)) - }) - .collect::<StrResult<_>>()?), -} - -impl Fold for FontFeatures { - type Output = Self; - - fn fold(mut self, outer: Self::Output) -> Self::Output { - self.0.extend(outer.0); - self - } -} diff --git a/library/src/text/quotes.rs b/library/src/text/quotes.rs deleted file mode 100644 index d0798064..00000000 --- a/library/src/text/quotes.rs +++ /dev/null @@ -1,209 +0,0 @@ -use typst::syntax::is_newline; - -use crate::prelude::*; - -/// A language-aware quote that reacts to its context. -/// -/// Automatically turns into an appropriate opening or closing quote based on -/// the active [text language]($func/text.lang). -/// -/// ## Example { #example } -/// ```example -/// "This is in quotes." -/// -/// #set text(lang: "de") -/// "Das ist in Anführungszeichen." -/// -/// #set text(lang: "fr") -/// "C'est entre guillemets." -/// ``` -/// -/// ## Syntax { #syntax } -/// This function also has dedicated syntax: The normal quote characters -/// (`'` and `"`). Typst automatically makes your quotes smart. -/// -/// Display: Smart Quote -/// Category: text -#[element] -pub struct SmartQuoteElem { - /// Whether this should be a double quote. - #[default(true)] - pub double: bool, - - /// Whether smart quotes are enabled. - /// - /// To disable smartness for a single quote, you can also escape it with a - /// backslash. - /// - /// ```example - /// #set smartquote(enabled: false) - /// - /// These are "dumb" quotes. - /// ``` - #[default(true)] - pub enabled: bool, -} - -/// State machine for smart quote substitution. -#[derive(Debug, Clone)] -pub struct Quoter { - /// How many quotes have been opened. - quote_depth: usize, - /// Whether an opening quote might follow. - expect_opening: bool, - /// Whether the last character was numeric. - last_num: bool, -} - -impl Quoter { - /// Start quoting. - pub fn new() -> Self { - Self { - quote_depth: 0, - expect_opening: true, - last_num: false, - } - } - - /// Process the last seen character. - pub fn last(&mut self, c: char) { - self.expect_opening = is_ignorable(c) || is_opening_bracket(c); - self.last_num = c.is_numeric(); - } - - /// Process and substitute a quote. - pub fn quote<'a>( - &mut self, - quotes: &Quotes<'a>, - double: bool, - peeked: Option<char>, - ) -> &'a str { - let peeked = peeked.unwrap_or(' '); - if self.expect_opening { - self.quote_depth += 1; - quotes.open(double) - } else if self.quote_depth > 0 - && (peeked.is_ascii_punctuation() || is_ignorable(peeked)) - { - self.quote_depth -= 1; - quotes.close(double) - } else if self.last_num { - quotes.prime(double) - } else { - quotes.fallback(double) - } - } -} - -impl Default for Quoter { - fn default() -> Self { - Self::new() - } -} - -fn is_ignorable(c: char) -> bool { - c.is_whitespace() || is_newline(c) -} - -fn is_opening_bracket(c: char) -> bool { - matches!(c, '(' | '{' | '[') -} - -/// Decides which quotes to substitute smart quotes with. -pub struct Quotes<'s> { - /// The opening single quote. - pub single_open: &'s str, - /// The closing single quote. - pub single_close: &'s str, - /// The opening double quote. - pub double_open: &'s str, - /// The closing double quote. - pub double_close: &'s str, -} - -impl<'s> Quotes<'s> { - /// Create a new `Quotes` struct with the defaults for a language and - /// region. - /// - /// The language should be specified as an all-lowercase ISO 639-1 code, the - /// region as an all-uppercase ISO 3166-alpha2 code. - /// - /// Currently, the supported languages are: English, Czech, Danish, German, - /// Swiss / Liechtensteinian German, Estonian, Icelandic, Lithuanian, - /// Latvian, Slovak, Slovenian, Spanish, Bosnian, Finnish, Swedish, French, - /// Hungarian, Polish, Romanian, Japanese, Traditional Chinese, Russian, and - /// Norwegian. - /// - /// For unknown languages, the English quotes are used. - pub fn from_lang(lang: Lang, region: Option<Region>) -> Self { - let region = region.as_ref().map(Region::as_str); - let (single_open, single_close, double_open, double_close) = match lang.as_str() { - "de" if matches!(region, Some("CH" | "LI")) => ("‹", "›", "«", "»"), - "cs" | "da" | "de" | "et" | "is" | "lt" | "lv" | "sk" | "sl" => { - ("‚", "‘", "„", "“") - } - "fr" => ("‹\u{00A0}", "\u{00A0}›", "«\u{00A0}", "\u{00A0}»"), - "bs" | "fi" | "sv" => ("’", "’", "”", "”"), - "es" if matches!(region, Some("ES") | None) => ("“", "”", "«", "»"), - "hu" | "pl" | "ro" => ("’", "’", "„", "”"), - "ru" | "no" | "nb" | "nn" | "ua" => ("’", "’", "«", "»"), - _ if lang.dir() == Dir::RTL => ("’", "‘", "”", "“"), - _ => return Self::default(), - }; - - Self { - single_open, - single_close, - double_open, - double_close, - } - } - - /// The opening quote. - fn open(&self, double: bool) -> &'s str { - if double { - self.double_open - } else { - self.single_open - } - } - - /// The closing quote. - fn close(&self, double: bool) -> &'s str { - if double { - self.double_close - } else { - self.single_close - } - } - - /// Which character should be used as a prime. - fn prime(&self, double: bool) -> &'static str { - if double { - "″" - } else { - "′" - } - } - - /// Which character should be used as a fallback quote. - fn fallback(&self, double: bool) -> &'static str { - if double { - "\"" - } else { - "’" - } - } -} - -impl Default for Quotes<'_> { - /// Returns the english quotes as default. - fn default() -> Self { - Self { - single_open: "‘", - single_close: "’", - double_open: "“", - double_close: "”", - } - } -} diff --git a/library/src/text/raw.rs b/library/src/text/raw.rs deleted file mode 100644 index 24f39bb6..00000000 --- a/library/src/text/raw.rs +++ /dev/null @@ -1,398 +0,0 @@ -use once_cell::sync::Lazy; -use syntect::highlighting as synt; -use typst::syntax::{self, LinkedNode}; - -use super::{ - FontFamily, FontList, Hyphenate, LinebreakElem, SmartQuoteElem, TextElem, TextSize, -}; -use crate::layout::BlockElem; -use crate::meta::{Figurable, LocalName}; -use crate::prelude::*; - -/// Raw text with optional syntax highlighting. -/// -/// Displays the text verbatim and in a monospace font. This is typically used -/// to embed computer code into your document. -/// -/// ## Example { #example } -/// ````example -/// Adding `rbx` to `rcx` gives -/// the desired result. -/// -/// ```rust -/// fn main() { -/// println!("Hello World!"); -/// } -/// ``` -/// ```` -/// -/// ## Syntax { #syntax } -/// This function also has dedicated syntax. You can enclose text in 1 or 3+ -/// backticks (`` ` ``) to make it raw. Two backticks produce empty raw text. -/// When you use three or more backticks, you can additionally specify a -/// language tag for syntax highlighting directly after the opening backticks. -/// Within raw blocks, everything is rendered as is, in particular, there are no -/// escape sequences. -/// -/// Display: Raw Text / Code -/// Category: text -#[element(Synthesize, Show, Finalize, LocalName, Figurable, PlainText)] -pub struct RawElem { - /// The raw text. - /// - /// You can also use raw blocks creatively to create custom syntaxes for - /// your automations. - /// - /// ````example - /// // Parse numbers in raw blocks with the - /// // `mydsl` tag and sum them up. - /// #show raw.where(lang: "mydsl"): it => { - /// let sum = 0 - /// for part in it.text.split("+") { - /// sum += int(part.trim()) - /// } - /// sum - /// } - /// - /// ```mydsl - /// 1 + 2 + 3 + 4 + 5 - /// ``` - /// ```` - #[required] - pub text: EcoString, - - /// Whether the raw text is displayed as a separate block. - /// - /// In markup mode, using one-backtick notation makes this `{false}`, - /// whereas using three-backtick notation makes it `{true}`. - /// - /// ````example - /// // Display inline code in a small box - /// // that retains the correct baseline. - /// #show raw.where(block: false): box.with( - /// fill: luma(240), - /// inset: (x: 3pt, y: 0pt), - /// outset: (y: 3pt), - /// radius: 2pt, - /// ) - /// - /// // Display block code in a larger block - /// // with more padding. - /// #show raw.where(block: true): block.with( - /// fill: luma(240), - /// inset: 10pt, - /// radius: 4pt, - /// ) - /// - /// With `rg`, you can search through your files quickly. - /// - /// ```bash - /// rg "Hello World" - /// ``` - /// ```` - #[default(false)] - pub block: bool, - - /// The language to syntax-highlight in. - /// - /// Apart from typical language tags known from Markdown, this supports the - /// `{"typ"}` and `{"typc"}` tags for Typst markup and Typst code, - /// respectively. - /// - /// ````example - /// ```typ - /// This is *Typst!* - /// ``` - /// ```` - pub lang: Option<EcoString>, - - /// The horizontal alignment that each line in a raw block should have. - /// This option is ignored if this is not a raw block (if specified - /// `block: false` or single backticks were used in markup mode). - /// - /// By default, this is set to `{start}`, meaning that raw text is - /// aligned towards the start of the text direction inside the block - /// by default, regardless of the current context's alignment (allowing - /// you to center the raw block itself without centering the text inside - /// it, for example). - /// - /// ````example - /// #set raw(align: center) - /// - /// ```typc - /// let f(x) = x - /// code = "centered" - /// ``` - /// ```` - #[default(HorizontalAlign(GenAlign::Start))] - pub align: HorizontalAlign, -} - -impl RawElem { - /// The supported language names and tags. - pub fn languages() -> Vec<(&'static str, Vec<&'static str>)> { - SYNTAXES - .syntaxes() - .iter() - .map(|syntax| { - ( - syntax.name.as_str(), - syntax.file_extensions.iter().map(|s| s.as_str()).collect(), - ) - }) - .chain([("Typst", vec!["typ"]), ("Typst (code)", vec!["typc"])]) - .collect() - } -} - -impl Synthesize for RawElem { - fn synthesize(&mut self, _vt: &mut Vt, styles: StyleChain) -> SourceResult<()> { - self.push_lang(self.lang(styles)); - Ok(()) - } -} - -impl Show for RawElem { - #[tracing::instrument(name = "RawElem::show", skip_all)] - fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { - let text = self.text(); - let lang = self.lang(styles).as_ref().map(|s| s.to_lowercase()); - let foreground = THEME - .settings - .foreground - .map(to_typst) - .map_or(Color::BLACK, Color::from); - - let mut realized = if matches!(lang.as_deref(), Some("typ" | "typst" | "typc")) { - let root = match lang.as_deref() { - Some("typc") => syntax::parse_code(&text), - _ => syntax::parse(&text), - }; - - let mut seq = vec![]; - let highlighter = synt::Highlighter::new(&THEME); - highlight_themed( - &LinkedNode::new(&root), - vec![], - &highlighter, - &mut |node, style| { - seq.push(styled(&text[node.range()], foreground.into(), style)); - }, - ); - - Content::sequence(seq) - } else if let Some(syntax) = - lang.and_then(|token| SYNTAXES.find_syntax_by_token(&token)) - { - let mut seq = vec![]; - let mut highlighter = syntect::easy::HighlightLines::new(syntax, &THEME); - for (i, line) in text.lines().enumerate() { - if i != 0 { - seq.push(LinebreakElem::new().pack()); - } - - for (style, piece) in - highlighter.highlight_line(line, &SYNTAXES).into_iter().flatten() - { - seq.push(styled(piece, foreground.into(), style)); - } - } - - Content::sequence(seq) - } else { - TextElem::packed(text) - }; - - if self.block(styles) { - // Align the text before inserting it into the block. - realized = realized.aligned(Axes::with_x(Some(self.align(styles).into()))); - realized = BlockElem::new().with_body(Some(realized)).pack(); - } - - Ok(realized) - } -} - -impl Finalize for RawElem { - fn finalize(&self, realized: Content, _: StyleChain) -> Content { - let mut styles = Styles::new(); - styles.set(TextElem::set_overhang(false)); - styles.set(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false)))); - styles.set(TextElem::set_size(TextSize(Em::new(0.8).into()))); - styles - .set(TextElem::set_font(FontList(vec![FontFamily::new("DejaVu Sans Mono")]))); - styles.set(SmartQuoteElem::set_enabled(false)); - realized.styled_with_map(styles) - } -} - -impl LocalName for RawElem { - fn local_name(&self, lang: Lang, _: Option<Region>) -> &'static str { - match lang { - Lang::ALBANIAN => "List", - Lang::ARABIC => "قائمة", - Lang::BOKMÅL => "Utskrift", - Lang::CHINESE => "代码", - Lang::CZECH => "Seznam", - Lang::DANISH => "Liste", - Lang::DUTCH => "Listing", - Lang::FILIPINO => "Listahan", - Lang::FRENCH => "Liste", - Lang::GERMAN => "Listing", - Lang::ITALIAN => "Codice", - Lang::NYNORSK => "Utskrift", - Lang::POLISH => "Program", - Lang::RUSSIAN => "Листинг", - Lang::SLOVENIAN => "Program", - Lang::SWEDISH => "Listing", - Lang::TURKISH => "Liste", - Lang::UKRAINIAN => "Лістинг", - Lang::VIETNAMESE => "Chương trình", // TODO: This may be wrong. - Lang::ENGLISH | _ => "Listing", - } - } -} - -impl Figurable for RawElem {} - -impl PlainText for RawElem { - fn plain_text(&self, text: &mut EcoString) { - text.push_str(&self.text()); - } -} - -/// Highlight a syntax node in a theme by calling `f` with ranges and their -/// styles. -fn highlight_themed<F>( - node: &LinkedNode, - scopes: Vec<syntect::parsing::Scope>, - highlighter: &synt::Highlighter, - f: &mut F, -) where - F: FnMut(&LinkedNode, synt::Style), -{ - if node.children().len() == 0 { - let style = highlighter.style_for_stack(&scopes); - f(node, style); - return; - } - - for child in node.children() { - let mut scopes = scopes.clone(); - if let Some(tag) = typst::ide::highlight(&child) { - scopes.push(syntect::parsing::Scope::new(tag.tm_scope()).unwrap()) - } - highlight_themed(&child, scopes, highlighter, f); - } -} - -/// Style a piece of text with a syntect style. -fn styled(piece: &str, foreground: Paint, style: synt::Style) -> Content { - let mut body = TextElem::packed(piece); - - let paint = to_typst(style.foreground).into(); - if paint != foreground { - body = body.styled(TextElem::set_fill(paint)); - } - - if style.font_style.contains(synt::FontStyle::BOLD) { - body = body.strong(); - } - - if style.font_style.contains(synt::FontStyle::ITALIC) { - body = body.emph(); - } - - if style.font_style.contains(synt::FontStyle::UNDERLINE) { - body = body.underlined(); - } - - body -} - -fn to_typst(synt::Color { r, g, b, a }: synt::Color) -> RgbaColor { - RgbaColor { r, g, b, a } -} - -fn to_syn(RgbaColor { r, g, b, a }: RgbaColor) -> synt::Color { - synt::Color { r, g, b, a } -} - -/// The syntect syntax definitions. -/// -/// Code for syntax set generation is below. The `syntaxes` directory is from -/// <https://github.com/sharkdp/bat/tree/master/assets/syntaxes> -/// -/// ```ignore -/// fn main() { -/// let mut builder = syntect::parsing::SyntaxSet::load_defaults_nonewlines().into_builder(); -/// builder.add_from_folder("syntaxes/02_Extra", false).unwrap(); -/// syntect::dumps::dump_to_file(&builder.build(), "syntect.bin").unwrap(); -/// } -/// ``` -/// -/// The following syntaxes are disabled due to compatibility issues: -/// ```text -/// syntaxes/02_Extra/Assembly (ARM).sublime-syntax -/// syntaxes/02_Extra/Elixir/Regular Expressions (Elixir).sublime-syntax -/// syntaxes/02_Extra/JavaScript (Babel).sublime-syntax -/// syntaxes/02_Extra/LiveScript.sublime-syntax -/// syntaxes/02_Extra/PowerShell.sublime-syntax -/// syntaxes/02_Extra/SCSS_Sass/Syntaxes/Sass.sublime-syntax -/// syntaxes/02_Extra/SLS/SLS.sublime-syntax -/// syntaxes/02_Extra/VimHelp.sublime-syntax -/// syntaxes/02_Extra/cmd-help/syntaxes/cmd-help.sublime-syntax -/// ``` -pub static SYNTAXES: Lazy<syntect::parsing::SyntaxSet> = - Lazy::new(|| syntect::dumps::from_binary(include_bytes!("../../assets/syntect.bin"))); - -/// The default theme used for syntax highlighting. -pub static THEME: Lazy<synt::Theme> = Lazy::new(|| synt::Theme { - name: Some("Typst Light".into()), - author: Some("The Typst Project Developers".into()), - settings: synt::ThemeSettings::default(), - scopes: vec![ - item("comment", Some("#8a8a8a"), None), - item("constant.character.escape", Some("#1d6c76"), None), - item("markup.bold", None, Some(synt::FontStyle::BOLD)), - item("markup.italic", None, Some(synt::FontStyle::ITALIC)), - item("markup.underline", None, Some(synt::FontStyle::UNDERLINE)), - item("markup.raw", Some("#818181"), None), - item("string.other.math.typst", None, None), - item("punctuation.definition.math", Some("#298e0d"), None), - item("keyword.operator.math", Some("#1d6c76"), None), - item("markup.heading, entity.name.section", None, Some(synt::FontStyle::BOLD)), - item( - "markup.heading.typst", - None, - Some(synt::FontStyle::BOLD | synt::FontStyle::UNDERLINE), - ), - item("punctuation.definition.list", Some("#8b41b1"), None), - item("markup.list.term", None, Some(synt::FontStyle::BOLD)), - item("entity.name.label, markup.other.reference", Some("#1d6c76"), None), - item("keyword, constant.language, variable.language", Some("#d73a49"), None), - item("storage.type, storage.modifier", Some("#d73a49"), None), - item("constant", Some("#b60157"), None), - item("string", Some("#298e0d"), None), - item("entity.name, variable.function, support", Some("#4b69c6"), None), - item("support.macro", Some("#16718d"), None), - item("meta.annotation", Some("#301414"), None), - item("entity.other, meta.interpolation", Some("#8b41b1"), None), - ], -}); - -/// Create a syntect theme item. -fn item( - scope: &str, - color: Option<&str>, - font_style: Option<synt::FontStyle>, -) -> synt::ThemeItem { - synt::ThemeItem { - scope: scope.parse().unwrap(), - style: synt::StyleModifier { - foreground: color.map(|s| to_syn(s.parse::<RgbaColor>().unwrap())), - background: None, - font_style, - }, - } -} diff --git a/library/src/text/shaping.rs b/library/src/text/shaping.rs deleted file mode 100644 index ec8812fe..00000000 --- a/library/src/text/shaping.rs +++ /dev/null @@ -1,973 +0,0 @@ -use std::borrow::Cow; -use std::ops::Range; -use std::str::FromStr; - -use az::SaturatingAs; -use rustybuzz::{Feature, Tag, UnicodeBuffer}; -use typst::font::{Font, FontStyle, FontVariant}; -use typst::util::SliceExt; -use unicode_script::{Script, UnicodeScript}; - -use super::{decorate, FontFamily, NumberType, NumberWidth, TextElem}; -use crate::layout::SpanMapper; -use crate::prelude::*; - -/// The result of shaping text. -/// -/// This type contains owned or borrowed shaped text runs, which can be -/// measured, used to reshape substrings more quickly and converted into a -/// frame. -pub struct ShapedText<'a> { - /// The start of the text in the full paragraph. - pub base: usize, - /// The text that was shaped. - pub text: &'a str, - /// The text direction. - pub dir: Dir, - /// The text language. - pub lang: Lang, - /// The text region. - pub region: Option<Region>, - /// The text's style properties. - pub styles: StyleChain<'a>, - /// The font variant. - pub variant: FontVariant, - /// The font size. - pub size: Abs, - /// The width of the text's bounding box. - pub width: Abs, - /// The shaped glyphs. - pub glyphs: Cow<'a, [ShapedGlyph]>, -} - -/// A single glyph resulting from shaping. -#[derive(Debug, Clone)] -pub struct ShapedGlyph { - /// The font the glyph is contained in. - pub font: Font, - /// The glyph's index in the font. - pub glyph_id: u16, - /// The advance width of the glyph. - pub x_advance: Em, - /// The horizontal offset of the glyph. - pub x_offset: Em, - /// The vertical offset of the glyph. - pub y_offset: Em, - /// The adjustability of the glyph. - pub adjustability: Adjustability, - /// The byte range of this glyph's cluster in the full paragraph. A cluster - /// is a sequence of one or multiple glyphs that cannot be separated and - /// must always be treated as a union. - /// - /// The range values of the glyphs in a [`ShapedText`] should not - /// overlap with each other, and they should be monotonically - /// increasing (for left-to-right or top-to-bottom text) or - /// monotonically decreasing (for right-to-left or bottom-to-top - /// text). - pub range: Range<usize>, - /// Whether splitting the shaping result before this glyph would yield the - /// same results as shaping the parts to both sides of `text_index` - /// separately. - pub safe_to_break: bool, - /// The first char in this glyph's cluster. - pub c: char, - /// The source code location of the glyph and its byte offset within it. - pub span: (Span, u16), -} - -#[derive(Debug, Clone, Default)] -pub struct Adjustability { - /// The left and right strechability - pub stretchability: (Em, Em), - /// The left and right shrinkability - pub shrinkability: (Em, Em), -} - -impl ShapedGlyph { - /// Whether the glyph is a space. - pub fn is_space(&self) -> bool { - matches!(self.c, ' ' | '\u{00A0}' | ' ') - } - - /// Whether the glyph is justifiable. - pub fn is_justifiable(&self) -> bool { - // GB style is not relevant here. - self.is_space() - || self.is_cjk_script() - || self.is_cjk_left_aligned_punctuation(true) - || self.is_cjk_right_aligned_punctuation() - || self.is_cjk_center_aligned_punctuation(true) - } - - pub fn is_cjk_script(&self) -> bool { - use Script::*; - // U+30FC: Katakana-Hiragana Prolonged Sound Mark - matches!(self.c.script(), Hiragana | Katakana | Han) || self.c == '\u{30FC}' - } - - pub fn is_cjk_punctuation(&self) -> bool { - self.is_cjk_left_aligned_punctuation(true) - || self.is_cjk_right_aligned_punctuation() - || self.is_cjk_center_aligned_punctuation(true) - } - - /// See <https://www.w3.org/TR/clreq/#punctuation_width_adjustment> - pub fn is_cjk_left_aligned_punctuation(&self, gb_style: bool) -> bool { - // CJK quotation marks shares codepoints with latin quotation marks. - // But only the CJK ones have full width. - if matches!(self.c, '”' | '’') - && self.x_advance + self.stretchability().1 == Em::one() - { - return true; - } - - if gb_style && matches!(self.c, ',' | '。' | '、' | ':' | ';') { - return true; - } - - matches!(self.c, '》' | ')' | '』' | '」') - } - - /// See <https://www.w3.org/TR/clreq/#punctuation_width_adjustment> - pub fn is_cjk_right_aligned_punctuation(&self) -> bool { - // CJK quotation marks shares codepoints with latin quotation marks. - // But only the CJK ones have full width. - if matches!(self.c, '“' | '‘') - && self.x_advance + self.stretchability().0 == Em::one() - { - return true; - } - - matches!(self.c, '《' | '(' | '『' | '「') - } - - /// See <https://www.w3.org/TR/clreq/#punctuation_width_adjustment> - pub fn is_cjk_center_aligned_punctuation(&self, gb_style: bool) -> bool { - if !gb_style && matches!(self.c, ',' | '。' | '、' | ':' | ';') { - return true; - } - - // U+30FB: Katakana Middle Dot - matches!(self.c, '\u{30FB}') - } - - pub fn base_adjustability(&self, gb_style: bool) -> Adjustability { - let width = self.x_advance; - if self.is_space() { - Adjustability { - // The number for spaces is from Knuth-Plass' paper - stretchability: (Em::zero(), width / 2.0), - shrinkability: (Em::zero(), width / 3.0), - } - } else if self.is_cjk_left_aligned_punctuation(gb_style) { - Adjustability { - stretchability: (Em::zero(), Em::zero()), - shrinkability: (Em::zero(), width / 2.0), - } - } else if self.is_cjk_right_aligned_punctuation() { - Adjustability { - stretchability: (Em::zero(), Em::zero()), - shrinkability: (width / 2.0, Em::zero()), - } - } else if self.is_cjk_center_aligned_punctuation(gb_style) { - Adjustability { - stretchability: (Em::zero(), Em::zero()), - shrinkability: (width / 4.0, width / 4.0), - } - } else { - Adjustability::default() - } - } - - /// The stretchability of the character. - pub fn stretchability(&self) -> (Em, Em) { - self.adjustability.stretchability - } - - /// The shrinkability of the character. - pub fn shrinkability(&self) -> (Em, Em) { - self.adjustability.shrinkability - } - - /// Shrink the width of glyph on the left side. - pub fn shrink_left(&mut self, amount: Em) { - self.x_offset -= amount; - self.x_advance -= amount; - self.adjustability.shrinkability.0 -= amount; - self.adjustability.stretchability.0 += amount; - } - - /// Shrink the width of glyph on the right side. - pub fn shrink_right(&mut self, amount: Em) { - self.x_advance -= amount; - self.adjustability.shrinkability.1 -= amount; - self.adjustability.stretchability.1 += amount; - } -} - -/// A side you can go toward. -enum Side { - /// To the left-hand side. - Left, - /// To the right-hand side. - Right, -} - -impl<'a> ShapedText<'a> { - /// Build the shaped text's frame. - /// - /// The `justification` defines how much extra advance width each - /// [justifiable glyph](ShapedGlyph::is_justifiable) will get. - pub fn build( - &self, - vt: &Vt, - justification_ratio: f64, - extra_justification: Abs, - ) -> Frame { - let (top, bottom) = self.measure(vt); - let size = Size::new(self.width, top + bottom); - - let mut offset = Abs::zero(); - let mut frame = Frame::new(size); - frame.set_baseline(top); - - let shift = TextElem::baseline_in(self.styles); - let lang = TextElem::lang_in(self.styles); - let decos = TextElem::deco_in(self.styles); - let fill = TextElem::fill_in(self.styles); - - for ((font, y_offset), group) in - self.glyphs.as_ref().group_by_key(|g| (g.font.clone(), g.y_offset)) - { - let mut range = group[0].range.clone(); - for glyph in group { - range.start = range.start.min(glyph.range.start); - range.end = range.end.max(glyph.range.end); - } - - let pos = Point::new(offset, top + shift - y_offset.at(self.size)); - let glyphs = group - .iter() - .map(|glyph| { - let adjustability_left = if justification_ratio < 0.0 { - glyph.shrinkability().0 - } else { - glyph.stretchability().0 - }; - let adjustability_right = if justification_ratio < 0.0 { - glyph.shrinkability().1 - } else { - glyph.stretchability().1 - }; - - let justification_left = adjustability_left * justification_ratio; - let mut justification_right = - adjustability_right * justification_ratio; - if glyph.is_justifiable() { - justification_right += - Em::from_length(extra_justification, self.size) - } - - frame.size_mut().x += justification_left.at(self.size) - + justification_right.at(self.size); - - Glyph { - id: glyph.glyph_id, - x_advance: glyph.x_advance - + justification_left - + justification_right, - x_offset: glyph.x_offset + justification_left, - range: (glyph.range.start - range.start).saturating_as() - ..(glyph.range.end - range.start).saturating_as(), - span: glyph.span, - } - }) - .collect(); - - let item = TextItem { - font, - size: self.size, - lang, - fill: fill.clone(), - text: self.text[range.start - self.base..range.end - self.base].into(), - glyphs, - }; - - let layer = frame.layer(); - let width = item.width(); - - // Apply line decorations. - for deco in &decos { - decorate(&mut frame, deco, &item, shift, pos, width); - } - - frame.insert(layer, pos, FrameItem::Text(item)); - offset += width; - } - - // Apply metadata. - frame.meta(self.styles, false); - - frame - } - - /// Measure the top and bottom extent of this text. - fn measure(&self, vt: &Vt) -> (Abs, Abs) { - let mut top = Abs::zero(); - let mut bottom = Abs::zero(); - - let top_edge = TextElem::top_edge_in(self.styles); - let bottom_edge = TextElem::bottom_edge_in(self.styles); - - // Expand top and bottom by reading the font's vertical metrics. - let mut expand = |font: &Font| { - let metrics = font.metrics(); - top.set_max(top_edge.resolve(self.styles, metrics)); - bottom.set_max(-bottom_edge.resolve(self.styles, metrics)); - }; - - if self.glyphs.is_empty() { - // When there are no glyphs, we just use the vertical metrics of the - // first available font. - let world = vt.world; - for family in families(self.styles) { - if let Some(font) = world - .book() - .select(family.as_str(), self.variant) - .and_then(|id| world.font(id)) - { - expand(&font); - break; - } - } - } else { - for g in self.glyphs.iter() { - expand(&g.font); - } - } - - (top, bottom) - } - - /// How many glyphs are in the text where we can insert additional - /// space when encountering underfull lines. - pub fn justifiables(&self) -> usize { - self.glyphs.iter().filter(|g| g.is_justifiable()).count() - } - - /// Whether the last glyph is a CJK character which should not be justified - /// on line end. - pub fn cjk_justifiable_at_last(&self) -> bool { - self.glyphs - .last() - .map(|g| g.is_cjk_script() || g.is_cjk_punctuation()) - .unwrap_or(false) - } - - /// The stretchability of the text. - pub fn stretchability(&self) -> Abs { - self.glyphs - .iter() - .map(|g| g.stretchability().0 + g.stretchability().1) - .sum::<Em>() - .at(self.size) - } - - /// The shrinkability of the text - pub fn shrinkability(&self) -> Abs { - self.glyphs - .iter() - .map(|g| g.shrinkability().0 + g.shrinkability().1) - .sum::<Em>() - .at(self.size) - } - - /// Reshape a range of the shaped text, reusing information from this - /// shaping process if possible. - /// - /// The text `range` is relative to the whole paragraph. - pub fn reshape( - &'a self, - vt: &Vt, - spans: &SpanMapper, - text_range: Range<usize>, - ) -> ShapedText<'a> { - let text = &self.text[text_range.start - self.base..text_range.end - self.base]; - if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) { - #[cfg(debug_assertions)] - assert_all_glyphs_in_range(glyphs, text, text_range.clone()); - Self { - base: text_range.start, - text, - dir: self.dir, - lang: self.lang, - region: self.region, - styles: self.styles, - size: self.size, - variant: self.variant, - width: glyphs.iter().map(|g| g.x_advance).sum::<Em>().at(self.size), - glyphs: Cow::Borrowed(glyphs), - } - } else { - shape( - vt, - text_range.start, - text, - spans, - self.styles, - self.dir, - self.lang, - self.region, - ) - } - } - - /// Push a hyphen to end of the text. - pub fn push_hyphen(&mut self, vt: &Vt) { - families(self.styles).find_map(|family| { - let world = vt.world; - let font = world - .book() - .select(family.as_str(), self.variant) - .and_then(|id| world.font(id))?; - let ttf = font.ttf(); - let glyph_id = ttf.glyph_index('-')?; - let x_advance = font.to_em(ttf.glyph_hor_advance(glyph_id)?); - let range = self - .glyphs - .last() - .map(|g| g.range.end..g.range.end) - .unwrap_or_default(); - self.width += x_advance.at(self.size); - self.glyphs.to_mut().push(ShapedGlyph { - font, - glyph_id: glyph_id.0, - x_advance, - x_offset: Em::zero(), - y_offset: Em::zero(), - adjustability: Adjustability::default(), - range, - safe_to_break: true, - c: '-', - span: (Span::detached(), 0), - }); - Some(()) - }); - } - - /// Find the subslice of glyphs that represent the given text range if both - /// sides are safe to break. - fn slice_safe_to_break(&self, text_range: Range<usize>) -> Option<&[ShapedGlyph]> { - let Range { mut start, mut end } = text_range; - if !self.dir.is_positive() { - std::mem::swap(&mut start, &mut end); - } - - let left = self.find_safe_to_break(start, Side::Left)?; - let right = self.find_safe_to_break(end, Side::Right)?; - Some(&self.glyphs[left..right]) - } - - /// Find the glyph offset matching the text index that is most towards the - /// given side and safe-to-break. - fn find_safe_to_break(&self, text_index: usize, towards: Side) -> Option<usize> { - let ltr = self.dir.is_positive(); - - // Handle edge cases. - let len = self.glyphs.len(); - if text_index == self.base { - return Some(if ltr { 0 } else { len }); - } else if text_index == self.base + self.text.len() { - return Some(if ltr { len } else { 0 }); - } - - // Find any glyph with the text index. - let mut idx = self - .glyphs - .binary_search_by(|g| { - let ordering = g.range.start.cmp(&text_index); - if ltr { - ordering - } else { - ordering.reverse() - } - }) - .ok()?; - - let next = match towards { - Side::Left => usize::checked_sub, - Side::Right => usize::checked_add, - }; - - // Search for the outermost glyph with the text index. - while let Some(next) = next(idx, 1) { - if self.glyphs.get(next).map_or(true, |g| g.range.start != text_index) { - break; - } - idx = next; - } - - // RTL needs offset one because the left side of the range should be - // exclusive and the right side inclusive, contrary to the normal - // behaviour of ranges. - self.glyphs[idx].safe_to_break.then_some(idx + usize::from(!ltr)) - } -} - -impl Debug for ShapedText<'_> { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - self.text.fmt(f) - } -} - -/// Holds shaping results and metadata common to all shaped segments. -struct ShapingContext<'a, 'v> { - vt: &'a Vt<'v>, - spans: &'a SpanMapper, - glyphs: Vec<ShapedGlyph>, - used: Vec<Font>, - styles: StyleChain<'a>, - size: Abs, - variant: FontVariant, - tags: Vec<rustybuzz::Feature>, - fallback: bool, - dir: Dir, -} - -/// Shape text into [`ShapedText`]. -#[allow(clippy::too_many_arguments)] -pub fn shape<'a>( - vt: &Vt, - base: usize, - text: &'a str, - spans: &SpanMapper, - styles: StyleChain<'a>, - dir: Dir, - lang: Lang, - region: Option<Region>, -) -> ShapedText<'a> { - let size = TextElem::size_in(styles); - let mut ctx = ShapingContext { - vt, - spans, - size, - glyphs: vec![], - used: vec![], - styles, - variant: variant(styles), - tags: tags(styles), - fallback: TextElem::fallback_in(styles), - dir, - }; - - if !text.is_empty() { - shape_segment(&mut ctx, base, text, families(styles)); - } - - track_and_space(&mut ctx); - calculate_adjustability(&mut ctx, lang, region); - - #[cfg(debug_assertions)] - assert_all_glyphs_in_range(&ctx.glyphs, text, base..(base + text.len())); - #[cfg(debug_assertions)] - assert_glyph_ranges_in_order(&ctx.glyphs, dir); - - ShapedText { - base, - text, - dir, - lang, - region, - styles, - variant: ctx.variant, - size, - width: ctx.glyphs.iter().map(|g| g.x_advance).sum::<Em>().at(size), - glyphs: Cow::Owned(ctx.glyphs), - } -} - -/// Shape text with font fallback using the `families` iterator. -fn shape_segment( - ctx: &mut ShapingContext, - base: usize, - text: &str, - mut families: impl Iterator<Item = FontFamily> + Clone, -) { - // Fonts dont have newlines and tabs. - if text.chars().all(|c| c == '\n' || c == '\t') { - return; - } - - // Find the next available family. - let world = ctx.vt.world; - let book = world.book(); - let mut selection = families.find_map(|family| { - book.select(family.as_str(), ctx.variant) - .and_then(|id| world.font(id)) - .filter(|font| !ctx.used.contains(font)) - }); - - // Do font fallback if the families are exhausted and fallback is enabled. - if selection.is_none() && ctx.fallback { - let first = ctx.used.first().map(Font::info); - selection = book - .select_fallback(first, ctx.variant, text) - .and_then(|id| world.font(id)) - .filter(|font| !ctx.used.contains(font)); - } - - // Extract the font id or shape notdef glyphs if we couldn't find any font. - let Some(font) = selection else { - if let Some(font) = ctx.used.first().cloned() { - shape_tofus(ctx, base, text, font); - } - return; - }; - - ctx.used.push(font.clone()); - - // Fill the buffer with our text. - let mut buffer = UnicodeBuffer::new(); - buffer.push_str(text); - buffer.set_language(language(ctx.styles)); - buffer.set_direction(match ctx.dir { - Dir::LTR => rustybuzz::Direction::LeftToRight, - Dir::RTL => rustybuzz::Direction::RightToLeft, - _ => unimplemented!("vertical text layout"), - }); - - // Shape! - let buffer = rustybuzz::shape(font.rusty(), &ctx.tags, buffer); - let infos = buffer.glyph_infos(); - let pos = buffer.glyph_positions(); - let ltr = ctx.dir.is_positive(); - - // Collect the shaped glyphs, doing fallback and shaping parts again with - // the next font if necessary. - let mut i = 0; - while i < infos.len() { - let info = &infos[i]; - let cluster = info.cluster as usize; - - // Add the glyph to the shaped output. - if info.glyph_id != 0 { - // Determine the text range of the glyph. - let start = base + cluster; - let end = base - + if ltr { i.checked_add(1) } else { i.checked_sub(1) } - .and_then(|last| infos.get(last)) - .map_or(text.len(), |info| info.cluster as usize); - - ctx.glyphs.push(ShapedGlyph { - font: font.clone(), - glyph_id: info.glyph_id as u16, - // TODO: Don't ignore y_advance. - x_advance: font.to_em(pos[i].x_advance), - x_offset: font.to_em(pos[i].x_offset), - y_offset: font.to_em(pos[i].y_offset), - adjustability: Adjustability::default(), - range: start..end, - safe_to_break: !info.unsafe_to_break(), - c: text[cluster..].chars().next().unwrap(), - span: ctx.spans.span_at(start), - }); - } else { - // First, search for the end of the tofu sequence. - let k = i; - while infos.get(i + 1).map_or(false, |info| info.glyph_id == 0) { - i += 1; - } - - // Then, determine the start and end text index for the tofu - // sequence. - // - // Examples: - // Everything is shown in visual order. Tofus are written as "_". - // We want to find out that the tofus span the text `2..6`. - // Note that the clusters are longer than 1 char. - // - // Left-to-right: - // Text: h a l i h a l l o - // Glyphs: A _ _ C E - // Clusters: 0 2 4 6 8 - // k=1 i=2 - // - // Right-to-left: - // Text: O L L A H I L A H - // Glyphs: E C _ _ A - // Clusters: 8 6 4 2 0 - // k=2 i=3 - let start = infos[if ltr { k } else { i }].cluster as usize; - let end = if ltr { i.checked_add(1) } else { k.checked_sub(1) } - .and_then(|last| infos.get(last)) - .map_or(text.len(), |info| info.cluster as usize); - - // Trim half-baked cluster. - let remove = base + start..base + end; - while ctx.glyphs.last().map_or(false, |g| remove.contains(&g.range.start)) { - ctx.glyphs.pop(); - } - - // Recursively shape the tofu sequence with the next family. - shape_segment(ctx, base + start, &text[start..end], families.clone()); - } - - i += 1; - } - - ctx.used.pop(); -} - -/// Shape the text with tofus from the given font. -fn shape_tofus(ctx: &mut ShapingContext, base: usize, text: &str, font: Font) { - let x_advance = font.advance(0).unwrap_or_default(); - let add_glyph = |(cluster, c): (usize, char)| { - let start = base + cluster; - let end = start + c.len_utf8(); - ctx.glyphs.push(ShapedGlyph { - font: font.clone(), - glyph_id: 0, - x_advance, - x_offset: Em::zero(), - y_offset: Em::zero(), - adjustability: Adjustability::default(), - range: start..end, - safe_to_break: true, - c, - span: ctx.spans.span_at(start), - }); - }; - if ctx.dir.is_positive() { - text.char_indices().for_each(add_glyph); - } else { - text.char_indices().rev().for_each(add_glyph); - } -} - -/// Apply tracking and spacing to the shaped glyphs. -fn track_and_space(ctx: &mut ShapingContext) { - let tracking = Em::from_length(TextElem::tracking_in(ctx.styles), ctx.size); - let spacing = - TextElem::spacing_in(ctx.styles).map(|abs| Em::from_length(abs, ctx.size)); - - let mut glyphs = ctx.glyphs.iter_mut().peekable(); - while let Some(glyph) = glyphs.next() { - // Make non-breaking space same width as normal space. - if glyph.c == '\u{00A0}' { - glyph.x_advance -= nbsp_delta(&glyph.font).unwrap_or_default(); - } - - if glyph.is_space() { - glyph.x_advance = spacing.relative_to(glyph.x_advance); - } - - if glyphs - .peek() - .map_or(false, |next| glyph.range.start != next.range.start) - { - glyph.x_advance += tracking; - } - } -} - -pub fn is_gb_style(lang: Lang, region: Option<Region>) -> bool { - // Most CJK variants, including zh-CN, ja-JP, zh-SG, zh-MY use GB-style punctuation, - // while zh-HK and zh-TW use alternative style. We default to use GB-style. - !(lang == Lang::CHINESE - && matches!(region.as_ref().map(Region::as_str), Some("TW" | "HK"))) -} - -/// Calculate stretchability and shrinkability of each glyph, -/// and CJK punctuation adjustments according to Chinese Layout Requirements. -fn calculate_adjustability(ctx: &mut ShapingContext, lang: Lang, region: Option<Region>) { - let gb_style = is_gb_style(lang, region); - - for glyph in &mut ctx.glyphs { - glyph.adjustability = glyph.base_adjustability(gb_style); - } - - let mut glyphs = ctx.glyphs.iter_mut().peekable(); - while let Some(glyph) = glyphs.next() { - // Only GB style needs further adjustment. - if glyph.is_cjk_punctuation() && !gb_style { - continue; - } - - // Now we apply consecutive punctuation adjustment, specified in Chinese Layout - // Requirements, section 3.1.6.1 Punctuation Adjustment Space, and Japanese Layout - // Requirements, section 3.1 Line Composition Rules for Punctuation Marks - let Some(next) = glyphs.peek_mut() else { continue }; - let width = glyph.x_advance; - let delta = width / 2.0; - if glyph.is_cjk_punctuation() - && next.is_cjk_punctuation() - && (glyph.shrinkability().1 + next.shrinkability().0) >= delta - { - let left_delta = glyph.shrinkability().1.min(delta); - glyph.shrink_right(left_delta); - next.shrink_left(delta - left_delta); - } - } -} - -/// Difference between non-breaking and normal space. -fn nbsp_delta(font: &Font) -> Option<Em> { - let space = font.ttf().glyph_index(' ')?.0; - let nbsp = font.ttf().glyph_index('\u{00A0}')?.0; - Some(font.advance(nbsp)? - font.advance(space)?) -} - -/// Resolve the font variant. -pub fn variant(styles: StyleChain) -> FontVariant { - let mut variant = FontVariant::new( - TextElem::style_in(styles), - TextElem::weight_in(styles), - TextElem::stretch_in(styles), - ); - - let delta = TextElem::delta_in(styles); - variant.weight = variant - .weight - .thicken(delta.clamp(i16::MIN as i64, i16::MAX as i64) as i16); - - if TextElem::emph_in(styles) { - variant.style = match variant.style { - FontStyle::Normal => FontStyle::Italic, - FontStyle::Italic => FontStyle::Normal, - FontStyle::Oblique => FontStyle::Normal, - } - } - - variant -} - -/// Resolve a prioritized iterator over the font families. -pub fn families(styles: StyleChain) -> impl Iterator<Item = FontFamily> + Clone { - const FALLBACKS: &[&str] = &[ - "linux libertine", - "twitter color emoji", - "noto color emoji", - "apple color emoji", - "segoe ui emoji", - ]; - - let tail = if TextElem::fallback_in(styles) { FALLBACKS } else { &[] }; - TextElem::font_in(styles) - .into_iter() - .chain(tail.iter().copied().map(FontFamily::new)) -} - -/// Collect the tags of the OpenType features to apply. -fn tags(styles: StyleChain) -> Vec<Feature> { - let mut tags = vec![]; - let mut feat = |tag, value| { - tags.push(Feature::new(Tag::from_bytes(tag), value, ..)); - }; - - // Features that are on by default in Harfbuzz are only added if disabled. - if !TextElem::kerning_in(styles) { - feat(b"kern", 0); - } - - // Features that are off by default in Harfbuzz are only added if enabled. - if TextElem::smallcaps_in(styles) { - feat(b"smcp", 1); - } - - if TextElem::alternates_in(styles) { - feat(b"salt", 1); - } - - let storage; - if let Some(set) = TextElem::stylistic_set_in(styles) { - storage = [b's', b's', b'0' + set.get() / 10, b'0' + set.get() % 10]; - feat(&storage, 1); - } - - if !TextElem::ligatures_in(styles) { - feat(b"liga", 0); - feat(b"clig", 0); - } - - if TextElem::discretionary_ligatures_in(styles) { - feat(b"dlig", 1); - } - - if TextElem::historical_ligatures_in(styles) { - feat(b"hilg", 1); - } - - match TextElem::number_type_in(styles) { - Smart::Auto => {} - Smart::Custom(NumberType::Lining) => feat(b"lnum", 1), - Smart::Custom(NumberType::OldStyle) => feat(b"onum", 1), - } - - match TextElem::number_width_in(styles) { - Smart::Auto => {} - Smart::Custom(NumberWidth::Proportional) => feat(b"pnum", 1), - Smart::Custom(NumberWidth::Tabular) => feat(b"tnum", 1), - } - - if TextElem::slashed_zero_in(styles) { - feat(b"zero", 1); - } - - if TextElem::fractions_in(styles) { - feat(b"frac", 1); - } - - for (tag, value) in TextElem::features_in(styles).0 { - tags.push(Feature::new(tag, value, ..)) - } - - tags -} - -/// Process the language and and region of a style chain into a -/// rustybuzz-compatible BCP 47 language. -fn language(styles: StyleChain) -> rustybuzz::Language { - let mut bcp: EcoString = TextElem::lang_in(styles).as_str().into(); - if let Some(region) = TextElem::region_in(styles) { - bcp.push('-'); - bcp.push_str(region.as_str()); - } - rustybuzz::Language::from_str(&bcp).unwrap() -} - -/// Returns true if all glyphs in `glyphs` have ranges within the range `range`. -#[cfg(debug_assertions)] -fn assert_all_glyphs_in_range(glyphs: &[ShapedGlyph], text: &str, range: Range<usize>) { - if glyphs - .iter() - .any(|g| g.range.start < range.start || g.range.end > range.end) - { - panic!("one or more glyphs in {text:?} fell out of range"); - } -} - -/// Asserts that the ranges of `glyphs` is in the proper order according to `dir`. -/// -/// This asserts instead of returning a bool in order to provide a more informative message when the invariant is violated. -#[cfg(debug_assertions)] -fn assert_glyph_ranges_in_order(glyphs: &[ShapedGlyph], dir: Dir) { - if glyphs.is_empty() { - return; - } - - // Iterator::is_sorted and friends are unstable as of Rust 1.70.0 - for i in 0..(glyphs.len() - 1) { - let a = &glyphs[i]; - let b = &glyphs[i + 1]; - let ord = a.range.start.cmp(&b.range.start); - let ord = if dir.is_positive() { ord } else { ord.reverse() }; - if ord == std::cmp::Ordering::Greater { - panic!( - "glyph ranges should be monotonically {}, \ - but found glyphs out of order:\n\n\ - first: {a:#?}\nsecond: {b:#?}", - if dir.is_positive() { "increasing" } else { "decreasing" }, - ); - } - } -} diff --git a/library/src/text/shift.rs b/library/src/text/shift.rs deleted file mode 100644 index 65e309e1..00000000 --- a/library/src/text/shift.rs +++ /dev/null @@ -1,229 +0,0 @@ -use super::{variant, SpaceElem, TextElem, TextSize}; -use crate::prelude::*; - -/// Renders text in subscript. -/// -/// The text is rendered smaller and its baseline is lowered. -/// -/// ## Example { #example } -/// ```example -/// Revenue#sub[yearly] -/// ``` -/// -/// Display: Subscript -/// Category: text -#[element(Show)] -pub struct SubElem { - /// Whether to prefer the dedicated subscript characters of the font. - /// - /// If this is enabled, Typst first tries to transform the text to subscript - /// codepoints. If that fails, it falls back to rendering lowered and shrunk - /// normal letters. - /// - /// ```example - /// N#sub(typographic: true)[1] - /// N#sub(typographic: false)[1] - /// ``` - #[default(true)] - pub typographic: bool, - - /// The baseline shift for synthetic subscripts. Does not apply if - /// `typographic` is true and the font has subscript codepoints for the - /// given `body`. - #[default(Em::new(0.2).into())] - pub baseline: Length, - - /// The font size for synthetic subscripts. Does not apply if - /// `typographic` is true and the font has subscript codepoints for the - /// given `body`. - #[default(TextSize(Em::new(0.6).into()))] - pub size: TextSize, - - /// The text to display in subscript. - #[required] - pub body: Content, -} - -impl Show for SubElem { - #[tracing::instrument(name = "SubElem::show", skip_all)] - fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> { - let body = self.body(); - let mut transformed = None; - if self.typographic(styles) { - if let Some(text) = search_text(&body, true) { - if is_shapable(vt, &text, styles) { - transformed = Some(TextElem::packed(text)); - } - } - }; - - Ok(transformed.unwrap_or_else(|| { - body.styled(TextElem::set_baseline(self.baseline(styles))) - .styled(TextElem::set_size(self.size(styles))) - })) - } -} - -/// Renders text in superscript. -/// -/// The text is rendered smaller and its baseline is raised. -/// -/// ## Example { #example } -/// ```example -/// 1#super[st] try! -/// ``` -/// -/// Display: Superscript -/// Category: text -#[element(Show)] -pub struct SuperElem { - /// Whether to prefer the dedicated superscript characters of the font. - /// - /// If this is enabled, Typst first tries to transform the text to - /// superscript codepoints. If that fails, it falls back to rendering - /// raised and shrunk normal letters. - /// - /// ```example - /// N#super(typographic: true)[1] - /// N#super(typographic: false)[1] - /// ``` - #[default(true)] - pub typographic: bool, - - /// The baseline shift for synthetic superscripts. Does not apply if - /// `typographic` is true and the font has superscript codepoints for the - /// given `body`. - #[default(Em::new(-0.5).into())] - pub baseline: Length, - - /// The font size for synthetic superscripts. Does not apply if - /// `typographic` is true and the font has superscript codepoints for the - /// given `body`. - #[default(TextSize(Em::new(0.6).into()))] - pub size: TextSize, - - /// The text to display in superscript. - #[required] - pub body: Content, -} - -impl Show for SuperElem { - #[tracing::instrument(name = "SuperElem::show", skip_all)] - fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> { - let body = self.body(); - let mut transformed = None; - if self.typographic(styles) { - if let Some(text) = search_text(&body, false) { - if is_shapable(vt, &text, styles) { - transformed = Some(TextElem::packed(text)); - } - } - }; - - Ok(transformed.unwrap_or_else(|| { - body.styled(TextElem::set_baseline(self.baseline(styles))) - .styled(TextElem::set_size(self.size(styles))) - })) - } -} - -/// Find and transform the text contained in `content` to the given script kind -/// if and only if it only consists of `Text`, `Space`, and `Empty` leafs. -fn search_text(content: &Content, sub: bool) -> Option<EcoString> { - if content.is::<SpaceElem>() { - Some(' '.into()) - } else if let Some(elem) = content.to::<TextElem>() { - convert_script(&elem.text(), sub) - } else if let Some(children) = content.to_sequence() { - let mut full = EcoString::new(); - for item in children { - match search_text(item, sub) { - Some(text) => full.push_str(&text), - None => return None, - } - } - Some(full) - } else { - None - } -} - -/// Checks whether the first retrievable family contains all code points of the -/// given string. -fn is_shapable(vt: &Vt, text: &str, styles: StyleChain) -> bool { - let world = vt.world; - for family in TextElem::font_in(styles) { - if let Some(font) = world - .book() - .select(family.as_str(), variant(styles)) - .and_then(|id| world.font(id)) - { - return text.chars().all(|c| font.ttf().glyph_index(c).is_some()); - } - } - - false -} - -/// Convert a string to sub- or superscript codepoints if all characters -/// can be mapped to such a codepoint. -fn convert_script(text: &str, sub: bool) -> Option<EcoString> { - let mut result = EcoString::with_capacity(text.len()); - let converter = if sub { to_subscript_codepoint } else { to_superscript_codepoint }; - - for c in text.chars() { - match converter(c) { - Some(c) => result.push(c), - None => return None, - } - } - - Some(result) -} - -/// Convert a character to its corresponding Unicode superscript. -fn to_superscript_codepoint(c: char) -> Option<char> { - char::from_u32(match c { - '0' => 0x2070, - '1' => 0x00B9, - '2' => 0x00B2, - '3' => 0x00B3, - '4'..='9' => 0x2070 + (c as u32 + 4 - '4' as u32), - '+' => 0x207A, - '-' => 0x207B, - '=' => 0x207C, - '(' => 0x207D, - ')' => 0x207E, - 'n' => 0x207F, - 'i' => 0x2071, - ' ' => 0x0020, - _ => return None, - }) -} - -/// Convert a character to its corresponding Unicode subscript. -fn to_subscript_codepoint(c: char) -> Option<char> { - char::from_u32(match c { - '0' => 0x2080, - '1'..='9' => 0x2080 + (c as u32 - '0' as u32), - '+' => 0x208A, - '-' => 0x208B, - '=' => 0x208C, - '(' => 0x208D, - ')' => 0x208E, - 'a' => 0x2090, - 'e' => 0x2091, - 'o' => 0x2092, - 'x' => 0x2093, - 'h' => 0x2095, - 'k' => 0x2096, - 'l' => 0x2097, - 'm' => 0x2098, - 'n' => 0x2099, - 'p' => 0x209A, - 's' => 0x209B, - 't' => 0x209C, - ' ' => 0x0020, - _ => return None, - }) -} |
