Document underline, strikethrough, and overline

This commit is contained in:
Laurenz 2022-12-20 18:19:01 +01:00
parent 08daade59f
commit 38a0404050
5 changed files with 249 additions and 42 deletions

View File

@ -45,7 +45,7 @@ impl ContentExt for Content {
}
fn underlined(self) -> Self {
crate::text::DecoNode::<{ crate::text::UNDERLINE }>(self).pack()
crate::text::UnderlineNode(self).pack()
}
fn linked(self, dest: Destination) -> Self {

View File

@ -5,43 +5,71 @@ use super::TextNode;
use crate::prelude::*;
/// # Underline
/// Typeset underline, stricken-through or overlined text.
/// Underline text.
///
/// ## Example
/// ```
/// This is #underline[important].
/// ```
///
/// ## Parameters
/// - body: Content (positional, required)
/// The content to decorate.
/// The content to underline.
///
/// ## Category
/// text
#[func]
#[capable(Show)]
#[derive(Debug, Hash)]
pub struct DecoNode<const L: DecoLine>(pub Content);
/// Typeset underlined text.
pub type UnderlineNode = DecoNode<UNDERLINE>;
/// Typeset stricken-through text.
pub type StrikeNode = DecoNode<STRIKETHROUGH>;
/// Typeset overlined text.
pub type OverlineNode = DecoNode<OVERLINE>;
pub struct UnderlineNode(pub Content);
#[node]
impl<const L: DecoLine> DecoNode<L> {
impl UnderlineNode {
/// How to stroke the line. The text color and thickness are read from the
/// font tables if `auto`.
/// font tables if `{auto}`.
///
/// # Example
/// ```
/// Take #underline(
/// stroke: 1.5pt + red,
/// offset: 2pt,
/// [care],
/// )
/// ```
#[property(shorthand, resolve, fold)]
pub const STROKE: Smart<PartialStroke> = Smart::Auto;
/// Position of the line relative to the baseline, read from the font tables
/// if `auto`.
/// if `{auto}`.
///
/// # Example
/// ```
/// #underline(offset: 5pt)[
/// The Tale Of A Faraway Line I
/// ]
/// ```
#[property(resolve)]
pub const OFFSET: Smart<Length> = Smart::Auto;
/// Amount that the line will be longer or shorter than its associated text.
///
/// # Example
/// ```
/// #align(center,
/// underline(extent: 2pt)[Chapter 1]
/// )
/// ```
#[property(resolve)]
pub const EXTENT: Length = Length::zero();
/// Whether the line skips sections in which it would collide
/// with the glyphs. Does not apply to strikethrough.
/// 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].
/// ```
pub const EVADE: bool = true;
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
@ -56,12 +84,12 @@ impl<const L: DecoLine> DecoNode<L> {
}
}
impl<const L: DecoLine> Show for DecoNode<L> {
impl Show for UnderlineNode {
fn show(&self, _: &mut Vt, _: &Content, styles: StyleChain) -> SourceResult<Content> {
Ok(self.0.clone().styled(
TextNode::DECO,
Decoration {
line: L,
line: DecoLine::Underline,
stroke: styles.get(Self::STROKE).unwrap_or_default(),
offset: styles.get(Self::OFFSET),
extent: styles.get(Self::EXTENT),
@ -71,6 +99,190 @@ impl<const L: DecoLine> Show for DecoNode<L> {
}
}
/// # Overline
/// Add a line over text.
///
/// ## Example
/// ```
/// #overline[A line over text.]
/// ```
///
/// ## Parameters
/// - body: Content (positional, required)
/// The content to add a line over.
///
/// ## Category
/// text
#[func]
#[capable(Show)]
#[derive(Debug, Hash)]
pub struct OverlineNode(pub Content);
#[node]
impl OverlineNode {
/// How to stroke the line. The text color and thickness are read from the
/// font tables if `{auto}`.
///
/// # Example
/// ```
/// #set text(fill: olive)
/// #overline(
/// stroke: green.darken(20%),
/// offset: -12pt,
/// [The Forest Theme],
/// )
/// ```
#[property(shorthand, resolve, fold)]
pub const STROKE: Smart<PartialStroke> = Smart::Auto;
/// 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
/// ]
/// ```
#[property(resolve)]
pub const OFFSET: Smart<Length> = Smart::Auto;
/// Amount that the line will be longer or shorter than its associated text.
///
/// # Example
/// ```
/// #set overline(extent: 4pt)
/// #set underline(extent: 4pt)
/// #overline(underline[Typography Today])
/// ```
#[property(resolve)]
pub const EXTENT: Length = Length::zero();
/// 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],
/// )
/// ```
pub const EVADE: bool = true;
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
Ok(Self(args.expect("body")?).pack())
}
fn field(&self, name: &str) -> Option<Value> {
match name {
"body" => Some(Value::Content(self.0.clone())),
_ => None,
}
}
}
impl Show for OverlineNode {
fn show(&self, _: &mut Vt, _: &Content, styles: StyleChain) -> SourceResult<Content> {
Ok(self.0.clone().styled(
TextNode::DECO,
Decoration {
line: DecoLine::Overline,
stroke: styles.get(Self::STROKE).unwrap_or_default(),
offset: styles.get(Self::OFFSET),
extent: styles.get(Self::EXTENT),
evade: styles.get(Self::EVADE),
},
))
}
}
/// # Strikethrough
/// Strike through text.
///
/// ## Example
/// ```
/// This is #strike[not] relevant.
/// ```
///
/// ## Parameters
/// - body: Content (positional, required)
/// The content to strike through.
///
/// ## Category
/// text
#[func]
#[capable(Show)]
#[derive(Debug, Hash)]
pub struct StrikeNode(pub Content);
#[node]
impl StrikeNode {
/// How to stroke the line. The text color and thickness are read from the
/// font tables if `{auto}`.
///
/// # Example
/// ```
/// This is #strike(stroke: 1.5pt + red)[very stricken through]. \
/// This is #strike(stroke: 10pt)[redacted].
/// ```
#[property(shorthand, resolve, fold)]
pub const STROKE: Smart<PartialStroke> = Smart::Auto;
/// 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(family: "Inria Serif")
/// This is #strike(offset: auto)[low-ish]. \
/// This is #strike(offset: -3.5pt)[on-top].
/// ```
#[property(resolve)]
pub const OFFSET: Smart<Length> = Smart::Auto;
/// Amount that the line will be longer or shorter than its associated text.
///
/// # Example
/// ```
/// This #strike(extent: -2pt)[skips] parts of the word.
/// This #strike(extent: 2pt)[extends] beyond the word.
/// ```
#[property(resolve)]
pub const EXTENT: Length = Length::zero();
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
Ok(Self(args.expect("body")?).pack())
}
fn field(&self, name: &str) -> Option<Value> {
match name {
"body" => Some(Value::Content(self.0.clone())),
_ => None,
}
}
}
impl Show for StrikeNode {
fn show(&self, _: &mut Vt, _: &Content, styles: StyleChain) -> SourceResult<Content> {
Ok(self.0.clone().styled(
TextNode::DECO,
Decoration {
line: DecoLine::Strikethrough,
stroke: styles.get(Self::STROKE).unwrap_or_default(),
offset: styles.get(Self::OFFSET),
extent: styles.get(Self::EXTENT),
evade: false,
},
))
}
}
/// Defines a line that is positioned over, under or on top of text.
///
/// For more details, see [`DecoNode`].
@ -93,16 +305,12 @@ impl Fold for Decoration {
}
/// A kind of decorative line.
pub type DecoLine = usize;
/// A line under text.
pub const UNDERLINE: DecoLine = 0;
/// A line through text.
pub const STRIKETHROUGH: DecoLine = 1;
/// A line over text.
pub const OVERLINE: DecoLine = 2;
#[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(
@ -115,12 +323,11 @@ pub(super) fn decorate(
) {
let font_metrics = text.font.metrics();
let metrics = match deco.line {
STRIKETHROUGH => font_metrics.strikethrough,
OVERLINE => font_metrics.overline,
UNDERLINE | _ => font_metrics.underline,
DecoLine::Strikethrough => font_metrics.strikethrough,
DecoLine::Overline => font_metrics.overline,
DecoLine::Underline => font_metrics.underline,
};
let evade = deco.evade && deco.line != STRIKETHROUGH;
let offset = deco.offset.unwrap_or(-metrics.position.at(text.size)) - shift;
let stroke = deco.stroke.unwrap_or(Stroke {
paint: text.fill,
@ -137,13 +344,13 @@ pub(super) fn decorate(
let origin = Point::new(from, pos.y + offset);
let target = Point::new(to - from, Abs::zero());
if target.x >= min_width || !evade {
if target.x >= min_width || !deco.evade {
let shape = Geometry::Line(target).stroked(stroke);
frame.push(origin, Element::Shape(shape));
}
};
if !evade {
if !deco.evade {
push_segment(start, end);
return;
}

View File

@ -1,7 +1,7 @@
// Test headings.
---
#show heading: it => text(blue, it.body)
#show heading: it => text(blue, it.title)
=
No heading
@ -44,7 +44,7 @@ multiline.
---
// Test styling.
#show heading.where(level: 5): it => block(
text(family: "Roboto", fill: eastern, it.body + [!])
text(family: "Roboto", fill: eastern, it.title + [!])
)
= Heading

View File

@ -31,9 +31,9 @@ my heading?
move(dy: -1pt)[📖]
h(5pt)
if it.level == 1 {
underline(text(1.25em, blue, it.body))
underline(text(1.25em, blue, it.title))
} else {
text(red, it.body)
text(red, it.title)
}
})
@ -51,7 +51,7 @@ Another text.
#show heading: it => {
set text(red)
show "ding": [🛎]
it.body
it.title
}
= Heading

View File

@ -39,7 +39,7 @@
#test(abs(-25%), 25%)
---
// Error: 6-17 expected integer, float, angle, ratio, or fraction, found string
// Error: 6-17 expected integer, float, length, angle, ratio, or fraction, found string
#abs("no number")
---