mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
Document underline, strikethrough, and overline
This commit is contained in:
parent
08daade59f
commit
38a0404050
@ -45,7 +45,7 @@ impl ContentExt for Content {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn underlined(self) -> Self {
|
fn underlined(self) -> Self {
|
||||||
crate::text::DecoNode::<{ crate::text::UNDERLINE }>(self).pack()
|
crate::text::UnderlineNode(self).pack()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn linked(self, dest: Destination) -> Self {
|
fn linked(self, dest: Destination) -> Self {
|
||||||
|
@ -5,43 +5,71 @@ use super::TextNode;
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
/// # Underline
|
/// # Underline
|
||||||
/// Typeset underline, stricken-through or overlined text.
|
/// Underline text.
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
/// ```
|
||||||
|
/// This is #underline[important].
|
||||||
|
/// ```
|
||||||
///
|
///
|
||||||
/// ## Parameters
|
/// ## Parameters
|
||||||
/// - body: Content (positional, required)
|
/// - body: Content (positional, required)
|
||||||
/// The content to decorate.
|
/// The content to underline.
|
||||||
///
|
///
|
||||||
/// ## Category
|
/// ## Category
|
||||||
/// text
|
/// text
|
||||||
#[func]
|
#[func]
|
||||||
#[capable(Show)]
|
#[capable(Show)]
|
||||||
#[derive(Debug, Hash)]
|
#[derive(Debug, Hash)]
|
||||||
pub struct DecoNode<const L: DecoLine>(pub Content);
|
pub struct UnderlineNode(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>;
|
|
||||||
|
|
||||||
#[node]
|
#[node]
|
||||||
impl<const L: DecoLine> DecoNode<L> {
|
impl UnderlineNode {
|
||||||
/// How to stroke the line. The text color and thickness are read from the
|
/// 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)]
|
#[property(shorthand, resolve, fold)]
|
||||||
pub const STROKE: Smart<PartialStroke> = Smart::Auto;
|
pub const STROKE: Smart<PartialStroke> = Smart::Auto;
|
||||||
|
|
||||||
/// Position of the line relative to the baseline, read from the font tables
|
/// 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)]
|
#[property(resolve)]
|
||||||
pub const OFFSET: Smart<Length> = Smart::Auto;
|
pub const OFFSET: Smart<Length> = Smart::Auto;
|
||||||
|
|
||||||
/// Amount that the line will be longer or shorter than its associated text.
|
/// Amount that the line will be longer or shorter than its associated text.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// #align(center,
|
||||||
|
/// underline(extent: 2pt)[Chapter 1]
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
#[property(resolve)]
|
#[property(resolve)]
|
||||||
pub const EXTENT: Length = Length::zero();
|
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;
|
pub const EVADE: bool = true;
|
||||||
|
|
||||||
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
|
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> {
|
fn show(&self, _: &mut Vt, _: &Content, styles: StyleChain) -> SourceResult<Content> {
|
||||||
Ok(self.0.clone().styled(
|
Ok(self.0.clone().styled(
|
||||||
TextNode::DECO,
|
TextNode::DECO,
|
||||||
Decoration {
|
Decoration {
|
||||||
line: L,
|
line: DecoLine::Underline,
|
||||||
stroke: styles.get(Self::STROKE).unwrap_or_default(),
|
stroke: styles.get(Self::STROKE).unwrap_or_default(),
|
||||||
offset: styles.get(Self::OFFSET),
|
offset: styles.get(Self::OFFSET),
|
||||||
extent: styles.get(Self::EXTENT),
|
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.
|
/// Defines a line that is positioned over, under or on top of text.
|
||||||
///
|
///
|
||||||
/// For more details, see [`DecoNode`].
|
/// For more details, see [`DecoNode`].
|
||||||
@ -93,16 +305,12 @@ impl Fold for Decoration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A kind of decorative line.
|
/// A kind of decorative line.
|
||||||
pub type DecoLine = usize;
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||||
|
pub enum DecoLine {
|
||||||
/// A line under text.
|
Underline,
|
||||||
pub const UNDERLINE: DecoLine = 0;
|
Strikethrough,
|
||||||
|
Overline,
|
||||||
/// A line through text.
|
}
|
||||||
pub const STRIKETHROUGH: DecoLine = 1;
|
|
||||||
|
|
||||||
/// A line over text.
|
|
||||||
pub const OVERLINE: DecoLine = 2;
|
|
||||||
|
|
||||||
/// Add line decorations to a single run of shaped text.
|
/// Add line decorations to a single run of shaped text.
|
||||||
pub(super) fn decorate(
|
pub(super) fn decorate(
|
||||||
@ -115,12 +323,11 @@ pub(super) fn decorate(
|
|||||||
) {
|
) {
|
||||||
let font_metrics = text.font.metrics();
|
let font_metrics = text.font.metrics();
|
||||||
let metrics = match deco.line {
|
let metrics = match deco.line {
|
||||||
STRIKETHROUGH => font_metrics.strikethrough,
|
DecoLine::Strikethrough => font_metrics.strikethrough,
|
||||||
OVERLINE => font_metrics.overline,
|
DecoLine::Overline => font_metrics.overline,
|
||||||
UNDERLINE | _ => font_metrics.underline,
|
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 offset = deco.offset.unwrap_or(-metrics.position.at(text.size)) - shift;
|
||||||
let stroke = deco.stroke.unwrap_or(Stroke {
|
let stroke = deco.stroke.unwrap_or(Stroke {
|
||||||
paint: text.fill,
|
paint: text.fill,
|
||||||
@ -137,13 +344,13 @@ pub(super) fn decorate(
|
|||||||
let origin = Point::new(from, pos.y + offset);
|
let origin = Point::new(from, pos.y + offset);
|
||||||
let target = Point::new(to - from, Abs::zero());
|
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);
|
let shape = Geometry::Line(target).stroked(stroke);
|
||||||
frame.push(origin, Element::Shape(shape));
|
frame.push(origin, Element::Shape(shape));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if !evade {
|
if !deco.evade {
|
||||||
push_segment(start, end);
|
push_segment(start, end);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// Test headings.
|
// Test headings.
|
||||||
|
|
||||||
---
|
---
|
||||||
#show heading: it => text(blue, it.body)
|
#show heading: it => text(blue, it.title)
|
||||||
|
|
||||||
=
|
=
|
||||||
No heading
|
No heading
|
||||||
@ -44,7 +44,7 @@ multiline.
|
|||||||
---
|
---
|
||||||
// Test styling.
|
// Test styling.
|
||||||
#show heading.where(level: 5): it => block(
|
#show heading.where(level: 5): it => block(
|
||||||
text(family: "Roboto", fill: eastern, it.body + [!])
|
text(family: "Roboto", fill: eastern, it.title + [!])
|
||||||
)
|
)
|
||||||
|
|
||||||
= Heading
|
= Heading
|
||||||
|
@ -31,9 +31,9 @@ my heading?
|
|||||||
move(dy: -1pt)[📖]
|
move(dy: -1pt)[📖]
|
||||||
h(5pt)
|
h(5pt)
|
||||||
if it.level == 1 {
|
if it.level == 1 {
|
||||||
underline(text(1.25em, blue, it.body))
|
underline(text(1.25em, blue, it.title))
|
||||||
} else {
|
} else {
|
||||||
text(red, it.body)
|
text(red, it.title)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -51,7 +51,7 @@ Another text.
|
|||||||
#show heading: it => {
|
#show heading: it => {
|
||||||
set text(red)
|
set text(red)
|
||||||
show "ding": [🛎]
|
show "ding": [🛎]
|
||||||
it.body
|
it.title
|
||||||
}
|
}
|
||||||
|
|
||||||
= Heading
|
= Heading
|
||||||
|
@ -39,7 +39,7 @@
|
|||||||
#test(abs(-25%), 25%)
|
#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")
|
#abs("no number")
|
||||||
|
|
||||||
---
|
---
|
||||||
|
Loading…
x
Reference in New Issue
Block a user