Merge branch 'typst:main' into vendor
@ -325,6 +325,13 @@ fn eval_field_call(
|
|||||||
} else if let Some(callee) = target.ty().scope().get(&field) {
|
} else if let Some(callee) = target.ty().scope().get(&field) {
|
||||||
args.insert(0, target_expr.span(), target);
|
args.insert(0, target_expr.span(), target);
|
||||||
Ok(FieldCall::Normal(callee.clone(), args))
|
Ok(FieldCall::Normal(callee.clone(), args))
|
||||||
|
} else if let Value::Content(content) = &target {
|
||||||
|
if let Some(callee) = content.elem().scope().get(&field) {
|
||||||
|
args.insert(0, target_expr.span(), target);
|
||||||
|
Ok(FieldCall::Normal(callee.clone(), args))
|
||||||
|
} else {
|
||||||
|
bail!(missing_field_call_error(target, field))
|
||||||
|
}
|
||||||
} else if matches!(
|
} else if matches!(
|
||||||
target,
|
target,
|
||||||
Value::Symbol(_) | Value::Func(_) | Value::Type(_) | Value::Module(_)
|
Value::Symbol(_) | Value::Func(_) | Value::Type(_) | Value::Module(_)
|
||||||
@ -341,8 +348,20 @@ fn eval_field_call(
|
|||||||
|
|
||||||
/// Produce an error when we cannot call the field.
|
/// Produce an error when we cannot call the field.
|
||||||
fn missing_field_call_error(target: Value, field: Ident) -> SourceDiagnostic {
|
fn missing_field_call_error(target: Value, field: Ident) -> SourceDiagnostic {
|
||||||
let mut error =
|
let mut error = match &target {
|
||||||
error!(field.span(), "type {} has no method `{}`", target.ty(), field.as_str());
|
Value::Content(content) => error!(
|
||||||
|
field.span(),
|
||||||
|
"element {} has no method `{}`",
|
||||||
|
content.elem().name(),
|
||||||
|
field.as_str(),
|
||||||
|
),
|
||||||
|
_ => error!(
|
||||||
|
field.span(),
|
||||||
|
"type {} has no method `{}`",
|
||||||
|
target.ty(),
|
||||||
|
field.as_str()
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
match target {
|
match target {
|
||||||
Value::Dict(ref dict) if matches!(dict.get(&field), Ok(Value::Func(_))) => {
|
Value::Dict(ref dict) if matches!(dict.get(&field), Ok(Value::Func(_))) => {
|
||||||
@ -360,6 +379,7 @@ fn missing_field_call_error(target: Value, field: Ident) -> SourceDiagnostic {
|
|||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
error
|
error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,7 +120,10 @@ fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
|
|||||||
|
|
||||||
/// Whether the element should be pretty-printed.
|
/// Whether the element should be pretty-printed.
|
||||||
fn is_pretty(element: &HtmlElement) -> bool {
|
fn is_pretty(element: &HtmlElement) -> bool {
|
||||||
tag::is_block_by_default(element.tag) || matches!(element.tag, tag::meta)
|
matches!(
|
||||||
|
element.tag,
|
||||||
|
tag::meta | tag::table | tag::thead | tag::tbody | tag::tfoot | tag::tr
|
||||||
|
) || tag::is_block_by_default(element.tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Escape a character.
|
/// Escape a character.
|
||||||
|
@ -602,7 +602,7 @@ pub enum Entry<'a> {
|
|||||||
|
|
||||||
impl<'a> Entry<'a> {
|
impl<'a> Entry<'a> {
|
||||||
/// Obtains the cell inside this entry, if this is not a merged cell.
|
/// Obtains the cell inside this entry, if this is not a merged cell.
|
||||||
fn as_cell(&self) -> Option<&Cell<'a>> {
|
pub fn as_cell(&self) -> Option<&Cell<'a>> {
|
||||||
match self {
|
match self {
|
||||||
Self::Cell(cell) => Some(cell),
|
Self::Cell(cell) => Some(cell),
|
||||||
Self::Merged { .. } => None,
|
Self::Merged { .. } => None,
|
||||||
|
@ -10,7 +10,7 @@ use crate::layout::{BlockElem, Length};
|
|||||||
/// Space may be inserted between the instances of the body parameter, so be
|
/// Space may be inserted between the instances of the body parameter, so be
|
||||||
/// sure to adjust the [`justify`]($repeat.justify) parameter accordingly.
|
/// sure to adjust the [`justify`]($repeat.justify) parameter accordingly.
|
||||||
///
|
///
|
||||||
/// Errors if there no bounds on the available space, as it would create
|
/// Errors if there are no bounds on the available space, as it would create
|
||||||
/// infinite content.
|
/// infinite content.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
@ -229,35 +229,20 @@ impl Refable for Packed<EquationElem> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Outlinable for Packed<EquationElem> {
|
impl Outlinable for Packed<EquationElem> {
|
||||||
fn outline(
|
fn outlined(&self) -> bool {
|
||||||
&self,
|
self.block(StyleChain::default()) && self.numbering().is_some()
|
||||||
engine: &mut Engine,
|
}
|
||||||
styles: StyleChain,
|
|
||||||
) -> SourceResult<Option<Content>> {
|
|
||||||
if !self.block(StyleChain::default()) {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
let Some(numbering) = self.numbering() else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
// After synthesis, this should always be custom content.
|
|
||||||
let mut supplement = match (**self).supplement(StyleChain::default()) {
|
|
||||||
Smart::Custom(Some(Supplement::Content(content))) => content,
|
|
||||||
_ => Content::empty(),
|
|
||||||
};
|
|
||||||
|
|
||||||
|
fn prefix(&self, numbers: Content) -> Content {
|
||||||
|
let supplement = self.supplement();
|
||||||
if !supplement.is_empty() {
|
if !supplement.is_empty() {
|
||||||
supplement += TextElem::packed("\u{a0}");
|
supplement + TextElem::packed('\u{a0}') + numbers
|
||||||
|
} else {
|
||||||
|
numbers
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let numbers = self.counter().display_at_loc(
|
fn body(&self) -> Content {
|
||||||
engine,
|
Content::empty()
|
||||||
self.location().unwrap(),
|
|
||||||
styles,
|
|
||||||
numbering,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(Some(supplement + numbers))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,9 +17,9 @@ use crate::text::TextElem;
|
|||||||
/// # Predefined Operators { #predefined }
|
/// # Predefined Operators { #predefined }
|
||||||
/// Typst predefines the operators `arccos`, `arcsin`, `arctan`, `arg`, `cos`,
|
/// Typst predefines the operators `arccos`, `arcsin`, `arctan`, `arg`, `cos`,
|
||||||
/// `cosh`, `cot`, `coth`, `csc`, `csch`, `ctg`, `deg`, `det`, `dim`, `exp`,
|
/// `cosh`, `cot`, `coth`, `csc`, `csch`, `ctg`, `deg`, `det`, `dim`, `exp`,
|
||||||
/// `gcd`, `hom`, `id`, `im`, `inf`, `ker`, `lg`, `lim`, `liminf`, `limsup`,
|
/// `gcd`, `lcm`, `hom`, `id`, `im`, `inf`, `ker`, `lg`, `lim`, `liminf`,
|
||||||
/// `ln`, `log`, `max`, `min`, `mod`, `Pr`, `sec`, `sech`, `sin`, `sinc`,
|
/// `limsup`, `ln`, `log`, `max`, `min`, `mod`, `Pr`, `sec`, `sech`, `sin`,
|
||||||
/// `sinh`, `sup`, `tan`, `tanh`, `tg` and `tr`.
|
/// `sinc`, `sinh`, `sup`, `tan`, `tanh`, `tg` and `tr`.
|
||||||
#[elem(title = "Text Operator", Mathy)]
|
#[elem(title = "Text Operator", Mathy)]
|
||||||
pub struct OpElem {
|
pub struct OpElem {
|
||||||
/// The operator's text.
|
/// The operator's text.
|
||||||
@ -75,6 +75,7 @@ ops! {
|
|||||||
dim,
|
dim,
|
||||||
exp,
|
exp,
|
||||||
gcd (limits),
|
gcd (limits),
|
||||||
|
lcm (limits),
|
||||||
hom,
|
hom,
|
||||||
id,
|
id,
|
||||||
im,
|
im,
|
||||||
|
@ -229,19 +229,19 @@ impl Show for Packed<EnumElem> {
|
|||||||
if TargetElem::target_in(styles).is_html() {
|
if TargetElem::target_in(styles).is_html() {
|
||||||
let mut elem = HtmlElem::new(tag::ol);
|
let mut elem = HtmlElem::new(tag::ol);
|
||||||
if self.reversed(styles) {
|
if self.reversed(styles) {
|
||||||
elem =
|
elem = elem.with_attr(HtmlAttr::constant("reversed"), "reversed");
|
||||||
elem.with_attr(const { HtmlAttr::constant("reversed") }, "reversed");
|
|
||||||
}
|
}
|
||||||
return Ok(elem
|
if let Some(n) = self.start(styles).custom() {
|
||||||
.with_body(Some(Content::sequence(self.children.iter().map(|item| {
|
elem = elem.with_attr(HtmlAttr::constant("start"), eco_format!("{n}"));
|
||||||
let mut li = HtmlElem::new(tag::li);
|
}
|
||||||
if let Some(nr) = item.number(styles) {
|
let body = Content::sequence(self.children.iter().map(|item| {
|
||||||
li = li.with_attr(attr::value, eco_format!("{nr}"));
|
let mut li = HtmlElem::new(tag::li);
|
||||||
}
|
if let Some(nr) = item.number(styles) {
|
||||||
li.with_body(Some(item.body.clone())).pack().spanned(item.span())
|
li = li.with_attr(attr::value, eco_format!("{nr}"));
|
||||||
}))))
|
}
|
||||||
.pack()
|
li.with_body(Some(item.body.clone())).pack().spanned(item.span())
|
||||||
.spanned(self.span()));
|
}));
|
||||||
|
return Ok(elem.with_body(Some(body)).pack().spanned(self.span()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut realized =
|
let mut realized =
|
||||||
|
@ -156,6 +156,7 @@ pub struct FigureElem {
|
|||||||
pub scope: PlacementScope,
|
pub scope: PlacementScope,
|
||||||
|
|
||||||
/// The figure's caption.
|
/// The figure's caption.
|
||||||
|
#[borrowed]
|
||||||
pub caption: Option<Packed<FigureCaption>>,
|
pub caption: Option<Packed<FigureCaption>>,
|
||||||
|
|
||||||
/// The kind of figure this is.
|
/// The kind of figure this is.
|
||||||
@ -305,7 +306,7 @@ impl Synthesize for Packed<FigureElem> {
|
|||||||
));
|
));
|
||||||
|
|
||||||
// Fill the figure's caption.
|
// Fill the figure's caption.
|
||||||
let mut caption = elem.caption(styles);
|
let mut caption = elem.caption(styles).clone();
|
||||||
if let Some(caption) = &mut caption {
|
if let Some(caption) = &mut caption {
|
||||||
caption.synthesize(engine, styles)?;
|
caption.synthesize(engine, styles)?;
|
||||||
caption.push_kind(kind.clone());
|
caption.push_kind(kind.clone());
|
||||||
@ -331,7 +332,7 @@ impl Show for Packed<FigureElem> {
|
|||||||
let mut realized = self.body.clone();
|
let mut realized = self.body.clone();
|
||||||
|
|
||||||
// Build the caption, if any.
|
// Build the caption, if any.
|
||||||
if let Some(caption) = self.caption(styles) {
|
if let Some(caption) = self.caption(styles).clone() {
|
||||||
let (first, second) = match caption.position(styles) {
|
let (first, second) = match caption.position(styles) {
|
||||||
OuterVAlignment::Top => (caption.pack(), realized),
|
OuterVAlignment::Top => (caption.pack(), realized),
|
||||||
OuterVAlignment::Bottom => (realized, caption.pack()),
|
OuterVAlignment::Bottom => (realized, caption.pack()),
|
||||||
@ -423,46 +424,26 @@ impl Refable for Packed<FigureElem> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Outlinable for Packed<FigureElem> {
|
impl Outlinable for Packed<FigureElem> {
|
||||||
fn outline(
|
fn outlined(&self) -> bool {
|
||||||
&self,
|
(**self).outlined(StyleChain::default())
|
||||||
engine: &mut Engine,
|
&& (self.caption(StyleChain::default()).is_some()
|
||||||
styles: StyleChain,
|
|| self.numbering().is_some())
|
||||||
) -> SourceResult<Option<Content>> {
|
}
|
||||||
if !self.outlined(StyleChain::default()) {
|
|
||||||
return Ok(None);
|
fn prefix(&self, numbers: Content) -> Content {
|
||||||
|
let supplement = self.supplement();
|
||||||
|
if !supplement.is_empty() {
|
||||||
|
supplement + TextElem::packed('\u{a0}') + numbers
|
||||||
|
} else {
|
||||||
|
numbers
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let Some(caption) = self.caption(StyleChain::default()) else {
|
fn body(&self) -> Content {
|
||||||
return Ok(None);
|
self.caption(StyleChain::default())
|
||||||
};
|
.as_ref()
|
||||||
|
.map(|caption| caption.body.clone())
|
||||||
let mut realized = caption.body.clone();
|
.unwrap_or_default()
|
||||||
if let (
|
|
||||||
Smart::Custom(Some(Supplement::Content(mut supplement))),
|
|
||||||
Some(Some(counter)),
|
|
||||||
Some(numbering),
|
|
||||||
) = (
|
|
||||||
(**self).supplement(StyleChain::default()).clone(),
|
|
||||||
(**self).counter(),
|
|
||||||
self.numbering(),
|
|
||||||
) {
|
|
||||||
let numbers = counter.display_at_loc(
|
|
||||||
engine,
|
|
||||||
self.location().unwrap(),
|
|
||||||
styles,
|
|
||||||
numbering,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
if !supplement.is_empty() {
|
|
||||||
supplement += TextElem::packed('\u{a0}');
|
|
||||||
}
|
|
||||||
|
|
||||||
let separator = caption.get_separator(StyleChain::default());
|
|
||||||
|
|
||||||
realized = supplement + numbers + separator + caption.body.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Some(realized))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use std::num::NonZeroUsize;
|
use std::num::NonZeroUsize;
|
||||||
|
|
||||||
use ecow::eco_format;
|
use ecow::eco_format;
|
||||||
use typst_utils::NonZeroExt;
|
use typst_utils::{Get, NonZeroExt};
|
||||||
|
|
||||||
use crate::diag::{warning, SourceResult};
|
use crate::diag::{warning, SourceResult};
|
||||||
use crate::engine::Engine;
|
use crate::engine::Engine;
|
||||||
@ -13,8 +13,8 @@ use crate::html::{attr, tag, HtmlElem};
|
|||||||
use crate::introspection::{
|
use crate::introspection::{
|
||||||
Count, Counter, CounterUpdate, Locatable, Locator, LocatorLink,
|
Count, Counter, CounterUpdate, Locatable, Locator, LocatorLink,
|
||||||
};
|
};
|
||||||
use crate::layout::{Abs, Axes, BlockBody, BlockElem, Em, HElem, Length, Region};
|
use crate::layout::{Abs, Axes, BlockBody, BlockElem, Em, HElem, Length, Region, Sides};
|
||||||
use crate::model::{Numbering, Outlinable, ParElem, Refable, Supplement};
|
use crate::model::{Numbering, Outlinable, Refable, Supplement};
|
||||||
use crate::text::{FontWeight, LocalName, SpaceElem, TextElem, TextSize};
|
use crate::text::{FontWeight, LocalName, SpaceElem, TextElem, TextSize};
|
||||||
|
|
||||||
/// A section heading.
|
/// A section heading.
|
||||||
@ -264,10 +264,6 @@ impl Show for Packed<HeadingElem> {
|
|||||||
realized = numbering + spacing + realized;
|
realized = numbering + spacing + realized;
|
||||||
}
|
}
|
||||||
|
|
||||||
if indent != Abs::zero() && !html {
|
|
||||||
realized = realized.styled(ParElem::set_hanging_indent(indent.into()));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(if html {
|
Ok(if html {
|
||||||
// HTML's h1 is closer to a title element. There should only be one.
|
// HTML's h1 is closer to a title element. There should only be one.
|
||||||
// Meanwhile, a level 1 Typst heading is a section heading. For this
|
// Meanwhile, a level 1 Typst heading is a section heading. For this
|
||||||
@ -294,8 +290,17 @@ impl Show for Packed<HeadingElem> {
|
|||||||
HtmlElem::new(t).with_body(Some(realized)).pack().spanned(span)
|
HtmlElem::new(t).with_body(Some(realized)).pack().spanned(span)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let realized = BlockBody::Content(realized);
|
let block = if indent != Abs::zero() {
|
||||||
BlockElem::new().with_body(Some(realized)).pack().spanned(span)
|
let body = HElem::new((-indent).into()).pack() + realized;
|
||||||
|
let inset = Sides::default()
|
||||||
|
.with(TextElem::dir_in(styles).start(), Some(indent.into()));
|
||||||
|
BlockElem::new()
|
||||||
|
.with_body(Some(BlockBody::Content(body)))
|
||||||
|
.with_inset(inset)
|
||||||
|
} else {
|
||||||
|
BlockElem::new().with_body(Some(BlockBody::Content(realized)))
|
||||||
|
};
|
||||||
|
block.pack().spanned(span)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -351,32 +356,21 @@ impl Refable for Packed<HeadingElem> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Outlinable for Packed<HeadingElem> {
|
impl Outlinable for Packed<HeadingElem> {
|
||||||
fn outline(
|
fn outlined(&self) -> bool {
|
||||||
&self,
|
(**self).outlined(StyleChain::default())
|
||||||
engine: &mut Engine,
|
|
||||||
styles: StyleChain,
|
|
||||||
) -> SourceResult<Option<Content>> {
|
|
||||||
if !self.outlined(StyleChain::default()) {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut content = self.body.clone();
|
|
||||||
if let Some(numbering) = (**self).numbering(StyleChain::default()).as_ref() {
|
|
||||||
let numbers = Counter::of(HeadingElem::elem()).display_at_loc(
|
|
||||||
engine,
|
|
||||||
self.location().unwrap(),
|
|
||||||
styles,
|
|
||||||
numbering,
|
|
||||||
)?;
|
|
||||||
content = numbers + SpaceElem::shared().clone() + content;
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Some(content))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn level(&self) -> NonZeroUsize {
|
fn level(&self) -> NonZeroUsize {
|
||||||
(**self).resolve_level(StyleChain::default())
|
(**self).resolve_level(StyleChain::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn prefix(&self, numbers: Content) -> Content {
|
||||||
|
numbers
|
||||||
|
}
|
||||||
|
|
||||||
|
fn body(&self) -> Content {
|
||||||
|
self.body.clone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LocalName for Packed<HeadingElem> {
|
impl LocalName for Packed<HeadingElem> {
|
||||||
|
@ -1,50 +1,61 @@
|
|||||||
use std::num::NonZeroUsize;
|
use std::num::NonZeroUsize;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use comemo::Track;
|
use comemo::{Track, Tracked};
|
||||||
|
use smallvec::SmallVec;
|
||||||
use typst_syntax::Span;
|
use typst_syntax::Span;
|
||||||
use typst_utils::NonZeroExt;
|
use typst_utils::{Get, NonZeroExt};
|
||||||
|
|
||||||
use crate::diag::{bail, At, SourceResult};
|
use crate::diag::{bail, error, At, HintedStrResult, SourceResult, StrResult};
|
||||||
use crate::engine::Engine;
|
use crate::engine::Engine;
|
||||||
use crate::foundations::{
|
use crate::foundations::{
|
||||||
cast, elem, scope, select_where, Content, Context, Func, LocatableSelector,
|
cast, elem, func, scope, select_where, Args, Construct, Content, Context, Func,
|
||||||
NativeElement, Packed, Show, ShowSet, Smart, StyleChain, Styles,
|
LocatableSelector, NativeElement, Packed, Resolve, Show, ShowSet, Smart, StyleChain,
|
||||||
|
Styles,
|
||||||
|
};
|
||||||
|
use crate::introspection::{
|
||||||
|
Counter, CounterKey, Introspector, Locatable, Location, Locator, LocatorLink,
|
||||||
};
|
};
|
||||||
use crate::introspection::{Counter, CounterKey, Locatable};
|
|
||||||
use crate::layout::{
|
use crate::layout::{
|
||||||
BoxElem, Dir, Em, Fr, HElem, HideElem, Length, Rel, RepeatElem, Spacing,
|
Abs, Axes, BlockBody, BlockElem, BoxElem, Dir, Em, Fr, HElem, Length, Region, Rel,
|
||||||
|
RepeatElem, Sides,
|
||||||
};
|
};
|
||||||
use crate::model::{
|
use crate::math::EquationElem;
|
||||||
Destination, HeadingElem, NumberingPattern, ParElem, ParbreakElem, Refable,
|
use crate::model::{Destination, HeadingElem, NumberingPattern, ParElem, Refable};
|
||||||
};
|
use crate::text::{LocalName, SpaceElem, TextElem};
|
||||||
use crate::text::{LinebreakElem, LocalName, SpaceElem, TextElem};
|
|
||||||
|
|
||||||
/// A table of contents, figures, or other elements.
|
/// A table of contents, figures, or other elements.
|
||||||
///
|
///
|
||||||
/// This function generates a list of all occurrences of an element in the
|
/// This function generates a list of all occurrences of an element in the
|
||||||
/// document, up to a given depth. The element's numbering and page number will
|
/// document, up to a given [`depth`]($outline.depth). The element's numbering
|
||||||
/// be displayed in the outline alongside its title or caption. By default this
|
/// and page number will be displayed in the outline alongside its title or
|
||||||
/// generates a table of contents.
|
/// caption.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
/// ```example
|
/// ```example
|
||||||
|
/// #set heading(numbering: "1.")
|
||||||
/// #outline()
|
/// #outline()
|
||||||
///
|
///
|
||||||
/// = Introduction
|
/// = Introduction
|
||||||
/// #lorem(5)
|
/// #lorem(5)
|
||||||
///
|
///
|
||||||
/// = Prior work
|
/// = Methods
|
||||||
|
/// == Setup
|
||||||
/// #lorem(10)
|
/// #lorem(10)
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// # Alternative outlines
|
/// # Alternative outlines
|
||||||
|
/// In its default configuration, this function generates a table of contents.
|
||||||
/// By setting the `target` parameter, the outline can be used to generate a
|
/// By setting the `target` parameter, the outline can be used to generate a
|
||||||
/// list of other kinds of elements than headings. In the example below, we list
|
/// list of other kinds of elements than headings.
|
||||||
/// all figures containing images by setting `target` to `{figure.where(kind:
|
///
|
||||||
/// image)}`. We could have also set it to just `figure`, but then the list
|
/// In the example below, we list all figures containing images by setting
|
||||||
/// would also include figures containing tables or other material. For more
|
/// `target` to `{figure.where(kind: image)}`. Just the same, we could have set
|
||||||
/// details on the `where` selector, [see here]($function.where).
|
/// it to `{figure.where(kind: table)}` to generate a list of tables.
|
||||||
|
///
|
||||||
|
/// We could also set it to just `figure`, without using a [`where`]($function.where)
|
||||||
|
/// selector, but then the list would contain _all_ figures, be it ones
|
||||||
|
/// containing images, tables, or other material.
|
||||||
///
|
///
|
||||||
/// ```example
|
/// ```example
|
||||||
/// #outline(
|
/// #outline(
|
||||||
@ -59,16 +70,89 @@ use crate::text::{LinebreakElem, LocalName, SpaceElem, TextElem};
|
|||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// # Styling the outline
|
/// # Styling the outline
|
||||||
/// The outline element has several options for customization, such as its
|
/// At the most basic level, you can style the outline by setting properties on
|
||||||
/// `title` and `indent` parameters. If desired, however, it is possible to have
|
/// it and its entries. This way, you can customize the outline's
|
||||||
/// more control over the outline's look and style through the
|
/// [title]($outline.title), how outline entries are
|
||||||
/// [`outline.entry`]($outline.entry) element.
|
/// [indented]($outline.indent), and how the space between an entry's text and
|
||||||
#[elem(scope, keywords = ["Table of Contents"], Show, ShowSet, LocalName)]
|
/// its page number should be [filled]($outline.entry.fill).
|
||||||
|
///
|
||||||
|
/// Richer customization is possible through configuration of the outline's
|
||||||
|
/// [entries]($outline.entry). The outline generates one entry for each outlined
|
||||||
|
/// element.
|
||||||
|
///
|
||||||
|
/// ## Spacing the entries { #entry-spacing }
|
||||||
|
/// Outline entries are [blocks]($block), so you can adjust the spacing between
|
||||||
|
/// them with normal block-spacing rules:
|
||||||
|
///
|
||||||
|
/// ```example
|
||||||
|
/// #show outline.entry.where(
|
||||||
|
/// level: 1
|
||||||
|
/// ): set block(above: 1.2em)
|
||||||
|
///
|
||||||
|
/// #outline()
|
||||||
|
///
|
||||||
|
/// = About ACME Corp.
|
||||||
|
/// == History
|
||||||
|
/// === Origins
|
||||||
|
/// = Products
|
||||||
|
/// == ACME Tools
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ## Building an outline entry from its parts { #building-an-entry }
|
||||||
|
/// For full control, you can also write a transformational show rule on
|
||||||
|
/// `outline.entry`. However, the logic for properly formatting and indenting
|
||||||
|
/// outline entries is quite complex and the outline entry itself only contains
|
||||||
|
/// two fields: The level and the outlined element.
|
||||||
|
///
|
||||||
|
/// For this reason, various helper functions are provided. You can mix and
|
||||||
|
/// match these to compose an entry from just the parts you like.
|
||||||
|
///
|
||||||
|
/// The default show rule for an outline entry looks like this[^1]:
|
||||||
|
/// ```typ
|
||||||
|
/// #show outline.entry: it => link(
|
||||||
|
/// it.element.location(),
|
||||||
|
/// it.indented(it.prefix(), it.inner()),
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// - The [`indented`]($outline.entry.indented) function takes an optional
|
||||||
|
/// prefix and inner content and automatically applies the proper indentation
|
||||||
|
/// to it, such that different entries align nicely and long headings wrap
|
||||||
|
/// properly.
|
||||||
|
///
|
||||||
|
/// - The [`prefix`]($outline.entry.prefix) function formats the element's
|
||||||
|
/// numbering (if any). It also appends a supplement for certain elements.
|
||||||
|
///
|
||||||
|
/// - The [`inner`]($outline.entry.inner) function combines the element's
|
||||||
|
/// [`body`]($outline.entry.body), the filler, and the
|
||||||
|
/// [`page` number]($outline.entry.page).
|
||||||
|
///
|
||||||
|
/// You can use these individual functions to format the outline entry in
|
||||||
|
/// different ways. Let's say, you'd like to fully remove the filler and page
|
||||||
|
/// numbers. To achieve this, you could write a show rule like this:
|
||||||
|
///
|
||||||
|
/// ```example
|
||||||
|
/// #show outline.entry: it => link(
|
||||||
|
/// it.element.location(),
|
||||||
|
/// // Keep just the body, dropping
|
||||||
|
/// // the fill and the page.
|
||||||
|
/// it.indented(it.prefix(), it.body()),
|
||||||
|
/// )
|
||||||
|
///
|
||||||
|
/// #outline()
|
||||||
|
///
|
||||||
|
/// = About ACME Corp.
|
||||||
|
/// == History
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// [^1]: The outline of equations is the exception to this rule as it does not
|
||||||
|
/// have a body and thus does not use indented layout.
|
||||||
|
#[elem(scope, keywords = ["Table of Contents", "toc"], Show, ShowSet, LocalName, Locatable)]
|
||||||
pub struct OutlineElem {
|
pub struct OutlineElem {
|
||||||
/// The title of the outline.
|
/// The title of the outline.
|
||||||
///
|
///
|
||||||
/// - When set to `{auto}`, an appropriate title for the
|
/// - When set to `{auto}`, an appropriate title for the
|
||||||
/// [text language]($text.lang) will be used. This is the default.
|
/// [text language]($text.lang) will be used.
|
||||||
/// - When set to `{none}`, the outline will not have a title.
|
/// - When set to `{none}`, the outline will not have a title.
|
||||||
/// - A custom title can be set by passing content.
|
/// - A custom title can be set by passing content.
|
||||||
///
|
///
|
||||||
@ -79,8 +163,10 @@ pub struct OutlineElem {
|
|||||||
|
|
||||||
/// The type of element to include in the outline.
|
/// The type of element to include in the outline.
|
||||||
///
|
///
|
||||||
/// To list figures containing a specific kind of element, like a table, you
|
/// To list figures containing a specific kind of element, like an image or
|
||||||
/// can write `{figure.where(kind: table)}`.
|
/// a table, you can specify the desired kind in a [`where`]($function.where)
|
||||||
|
/// selector. See the section on [alternative outlines]($outline/#alternative-outlines)
|
||||||
|
/// for more details.
|
||||||
///
|
///
|
||||||
/// ```example
|
/// ```example
|
||||||
/// #outline(
|
/// #outline(
|
||||||
@ -97,7 +183,7 @@ pub struct OutlineElem {
|
|||||||
/// caption: [Experiment results],
|
/// caption: [Experiment results],
|
||||||
/// )
|
/// )
|
||||||
/// ```
|
/// ```
|
||||||
#[default(LocatableSelector(select_where!(HeadingElem, Outlined => true)))]
|
#[default(LocatableSelector(HeadingElem::elem().select()))]
|
||||||
#[borrowed]
|
#[borrowed]
|
||||||
pub target: LocatableSelector,
|
pub target: LocatableSelector,
|
||||||
|
|
||||||
@ -121,21 +207,22 @@ pub struct OutlineElem {
|
|||||||
|
|
||||||
/// How to indent the outline's entries.
|
/// How to indent the outline's entries.
|
||||||
///
|
///
|
||||||
/// - `{none}`: No indent
|
/// - `{auto}`: Indents the numbering/prefix of a nested entry with the
|
||||||
/// - `{auto}`: Indents the numbering of the nested entry with the title of
|
/// title of its parent entry. If the entries are not numbered (e.g., via
|
||||||
/// its parent entry. This only has an effect if the entries are numbered
|
/// [heading numbering]($heading.numbering)), this instead simply inserts
|
||||||
/// (e.g., via [heading numbering]($heading.numbering)).
|
/// a fixed amount of `{1.2em}` indent per level.
|
||||||
/// - [Relative length]($relative): Indents the item by this length
|
///
|
||||||
/// multiplied by its nesting level. Specifying `{2em}`, for instance,
|
/// - [Relative length]($relative): Indents the entry by the specified
|
||||||
/// would indent top-level headings (not nested) by `{0em}`, second level
|
/// length per nesting level. Specifying `{2em}`, for instance, would
|
||||||
|
/// indent top-level headings by `{0em}` (not nested), second level
|
||||||
/// headings by `{2em}` (nested once), third-level headings by `{4em}`
|
/// headings by `{2em}` (nested once), third-level headings by `{4em}`
|
||||||
/// (nested twice) and so on.
|
/// (nested twice) and so on.
|
||||||
/// - [Function]($function): You can completely customize this setting with
|
///
|
||||||
/// a function. That function receives the nesting level as a parameter
|
/// - [Function]($function): You can further customize this setting with a
|
||||||
/// (starting at 0 for top-level headings/elements) and can return a
|
/// function. That function receives the nesting level as a parameter
|
||||||
/// relative length or content making up the indent. For example,
|
/// (starting at 0 for top-level headings/elements) and should return a
|
||||||
/// `{n => n * 2em}` would be equivalent to just specifying `{2em}`, while
|
/// (relative) length. For example, `{n => n * 2em}` would be equivalent
|
||||||
/// `{n => [→ ] * n}` would indent with one arrow per nesting level.
|
/// to just specifying `{2em}`.
|
||||||
///
|
///
|
||||||
/// ```example
|
/// ```example
|
||||||
/// #set heading(numbering: "1.a.")
|
/// #set heading(numbering: "1.a.")
|
||||||
@ -150,11 +237,6 @@ pub struct OutlineElem {
|
|||||||
/// indent: 2em,
|
/// indent: 2em,
|
||||||
/// )
|
/// )
|
||||||
///
|
///
|
||||||
/// #outline(
|
|
||||||
/// title: [Contents (Function)],
|
|
||||||
/// indent: n => [→ ] * n,
|
|
||||||
/// )
|
|
||||||
///
|
|
||||||
/// = About ACME Corp.
|
/// = About ACME Corp.
|
||||||
/// == History
|
/// == History
|
||||||
/// === Origins
|
/// === Origins
|
||||||
@ -163,20 +245,7 @@ pub struct OutlineElem {
|
|||||||
/// == Products
|
/// == Products
|
||||||
/// #lorem(10)
|
/// #lorem(10)
|
||||||
/// ```
|
/// ```
|
||||||
#[default(None)]
|
pub indent: Smart<OutlineIndent>,
|
||||||
#[borrowed]
|
|
||||||
pub indent: Option<Smart<OutlineIndent>>,
|
|
||||||
|
|
||||||
/// Content to fill the space between the title and the page number. Can be
|
|
||||||
/// set to `{none}` to disable filling.
|
|
||||||
///
|
|
||||||
/// ```example
|
|
||||||
/// #outline(fill: line(length: 100%))
|
|
||||||
///
|
|
||||||
/// = A New Beginning
|
|
||||||
/// ```
|
|
||||||
#[default(Some(RepeatElem::new(TextElem::packed(".")).pack()))]
|
|
||||||
pub fill: Option<Content>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[scope]
|
#[scope]
|
||||||
@ -188,79 +257,52 @@ impl OutlineElem {
|
|||||||
impl Show for Packed<OutlineElem> {
|
impl Show for Packed<OutlineElem> {
|
||||||
#[typst_macros::time(name = "outline", span = self.span())]
|
#[typst_macros::time(name = "outline", span = self.span())]
|
||||||
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
||||||
let mut seq = vec![ParbreakElem::shared().clone()];
|
let span = self.span();
|
||||||
|
|
||||||
// Build the outline title.
|
// Build the outline title.
|
||||||
|
let mut seq = vec![];
|
||||||
if let Some(title) = self.title(styles).unwrap_or_else(|| {
|
if let Some(title) = self.title(styles).unwrap_or_else(|| {
|
||||||
Some(TextElem::packed(Self::local_name_in(styles)).spanned(self.span()))
|
Some(TextElem::packed(Self::local_name_in(styles)).spanned(span))
|
||||||
}) {
|
}) {
|
||||||
seq.push(
|
seq.push(
|
||||||
HeadingElem::new(title)
|
HeadingElem::new(title)
|
||||||
.with_depth(NonZeroUsize::ONE)
|
.with_depth(NonZeroUsize::ONE)
|
||||||
.pack()
|
.pack()
|
||||||
.spanned(self.span()),
|
.spanned(span),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let indent = self.indent(styles);
|
|
||||||
let depth = self.depth(styles).unwrap_or(NonZeroUsize::new(usize::MAX).unwrap());
|
|
||||||
|
|
||||||
let mut ancestors: Vec<&Content> = vec![];
|
|
||||||
let elems = engine.introspector.query(&self.target(styles).0);
|
let elems = engine.introspector.query(&self.target(styles).0);
|
||||||
|
let depth = self.depth(styles).unwrap_or(NonZeroUsize::MAX);
|
||||||
|
|
||||||
for elem in &elems {
|
// Build the outline entries.
|
||||||
let Some(entry) = OutlineEntry::from_outlinable(
|
for elem in elems {
|
||||||
engine,
|
let Some(outlinable) = elem.with::<dyn Outlinable>() else {
|
||||||
self.span(),
|
bail!(span, "cannot outline {}", elem.func().name());
|
||||||
elem.clone(),
|
|
||||||
self.fill(styles),
|
|
||||||
styles,
|
|
||||||
)?
|
|
||||||
else {
|
|
||||||
continue;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if depth < entry.level {
|
let level = outlinable.level();
|
||||||
continue;
|
if outlinable.outlined() && level <= depth {
|
||||||
|
let entry = OutlineEntry::new(level, elem);
|
||||||
|
seq.push(entry.pack().spanned(span));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deals with the ancestors of the current element.
|
|
||||||
// This is only applicable for elements with a hierarchy/level.
|
|
||||||
while ancestors
|
|
||||||
.last()
|
|
||||||
.and_then(|ancestor| ancestor.with::<dyn Outlinable>())
|
|
||||||
.is_some_and(|last| last.level() >= entry.level)
|
|
||||||
{
|
|
||||||
ancestors.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
OutlineIndent::apply(
|
|
||||||
indent,
|
|
||||||
engine,
|
|
||||||
&ancestors,
|
|
||||||
&mut seq,
|
|
||||||
styles,
|
|
||||||
self.span(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Add the overridable outline entry, followed by a line break.
|
|
||||||
seq.push(entry.pack().spanned(self.span()));
|
|
||||||
seq.push(LinebreakElem::shared().clone());
|
|
||||||
|
|
||||||
ancestors.push(elem);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
seq.push(ParbreakElem::shared().clone());
|
|
||||||
|
|
||||||
Ok(Content::sequence(seq))
|
Ok(Content::sequence(seq))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ShowSet for Packed<OutlineElem> {
|
impl ShowSet for Packed<OutlineElem> {
|
||||||
fn show_set(&self, _: StyleChain) -> Styles {
|
fn show_set(&self, styles: StyleChain) -> Styles {
|
||||||
let mut out = Styles::new();
|
let mut out = Styles::new();
|
||||||
out.set(HeadingElem::set_outlined(false));
|
out.set(HeadingElem::set_outlined(false));
|
||||||
out.set(HeadingElem::set_numbering(None));
|
out.set(HeadingElem::set_numbering(None));
|
||||||
out.set(ParElem::set_first_line_indent(Em::new(0.0).into()));
|
out.set(ParElem::set_first_line_indent(Em::new(0.0).into()));
|
||||||
|
out.set(ParElem::set_justify(false));
|
||||||
|
out.set(BlockElem::set_above(Smart::Custom(ParElem::leading_in(styles).into())));
|
||||||
|
// Makes the outline itself available to its entries. Should be
|
||||||
|
// superseded by a proper ancestry mechanism in the future.
|
||||||
|
out.set(OutlineEntry::set_parent(Some(self.clone())));
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -269,93 +311,29 @@ impl LocalName for Packed<OutlineElem> {
|
|||||||
const KEY: &'static str = "outline";
|
const KEY: &'static str = "outline";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Marks an element as being able to be outlined. This is used to implement the
|
|
||||||
/// `#outline()` element.
|
|
||||||
pub trait Outlinable: Refable {
|
|
||||||
/// Produce an outline item for this element.
|
|
||||||
fn outline(
|
|
||||||
&self,
|
|
||||||
engine: &mut Engine,
|
|
||||||
|
|
||||||
styles: StyleChain,
|
|
||||||
) -> SourceResult<Option<Content>>;
|
|
||||||
|
|
||||||
/// Returns the nesting level of this element.
|
|
||||||
fn level(&self) -> NonZeroUsize {
|
|
||||||
NonZeroUsize::ONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Defines how an outline is indented.
|
/// Defines how an outline is indented.
|
||||||
#[derive(Debug, Clone, PartialEq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Hash)]
|
||||||
pub enum OutlineIndent {
|
pub enum OutlineIndent {
|
||||||
Rel(Rel<Length>),
|
/// Indents by the specified length per level.
|
||||||
|
Rel(Rel),
|
||||||
|
/// Resolve the indent for a specific level through the given function.
|
||||||
Func(Func),
|
Func(Func),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OutlineIndent {
|
impl OutlineIndent {
|
||||||
fn apply(
|
/// Resolve the indent for an entry with the given level.
|
||||||
indent: &Option<Smart<Self>>,
|
fn resolve(
|
||||||
|
&self,
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
ancestors: &Vec<&Content>,
|
context: Tracked<Context>,
|
||||||
seq: &mut Vec<Content>,
|
level: NonZeroUsize,
|
||||||
styles: StyleChain,
|
|
||||||
span: Span,
|
span: Span,
|
||||||
) -> SourceResult<()> {
|
) -> SourceResult<Rel> {
|
||||||
match indent {
|
let depth = level.get() - 1;
|
||||||
// 'none' | 'false' => no indenting
|
match self {
|
||||||
None => {}
|
Self::Rel(length) => Ok(*length * depth as f64),
|
||||||
|
Self::Func(func) => func.call(engine, context, [depth])?.cast().at(span),
|
||||||
// 'auto' | 'true' => use numbering alignment for indenting
|
}
|
||||||
Some(Smart::Auto) => {
|
|
||||||
// Add hidden ancestors numberings to realize the indent.
|
|
||||||
let mut hidden = Content::empty();
|
|
||||||
for ancestor in ancestors {
|
|
||||||
let ancestor_outlinable = ancestor.with::<dyn Outlinable>().unwrap();
|
|
||||||
|
|
||||||
if let Some(numbering) = ancestor_outlinable.numbering() {
|
|
||||||
let numbers = ancestor_outlinable.counter().display_at_loc(
|
|
||||||
engine,
|
|
||||||
ancestor.location().unwrap(),
|
|
||||||
styles,
|
|
||||||
numbering,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
hidden += numbers + SpaceElem::shared().clone();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ancestors.is_empty() {
|
|
||||||
seq.push(HideElem::new(hidden).pack().spanned(span));
|
|
||||||
seq.push(SpaceElem::shared().clone().spanned(span));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Length => indent with some fixed spacing per level
|
|
||||||
Some(Smart::Custom(OutlineIndent::Rel(length))) => {
|
|
||||||
seq.push(
|
|
||||||
HElem::new(Spacing::Rel(*length))
|
|
||||||
.pack()
|
|
||||||
.spanned(span)
|
|
||||||
.repeat(ancestors.len()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function => call function with the current depth and take
|
|
||||||
// the returned content
|
|
||||||
Some(Smart::Custom(OutlineIndent::Func(func))) => {
|
|
||||||
let depth = ancestors.len();
|
|
||||||
let LengthOrContent(content) = func
|
|
||||||
.call(engine, Context::new(None, Some(styles)).track(), [depth])?
|
|
||||||
.cast()
|
|
||||||
.at(span)?;
|
|
||||||
if !content.is_empty() {
|
|
||||||
seq.push(content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -365,46 +343,33 @@ cast! {
|
|||||||
Self::Rel(v) => v.into_value(),
|
Self::Rel(v) => v.into_value(),
|
||||||
Self::Func(v) => v.into_value()
|
Self::Func(v) => v.into_value()
|
||||||
},
|
},
|
||||||
v: Rel<Length> => OutlineIndent::Rel(v),
|
v: Rel<Length> => Self::Rel(v),
|
||||||
v: Func => OutlineIndent::Func(v),
|
v: Func => Self::Func(v),
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LengthOrContent(Content);
|
/// Marks an element as being able to be outlined.
|
||||||
|
pub trait Outlinable: Refable {
|
||||||
|
/// Whether this element should be included in the outline.
|
||||||
|
fn outlined(&self) -> bool;
|
||||||
|
|
||||||
cast! {
|
/// The nesting level of this element.
|
||||||
LengthOrContent,
|
fn level(&self) -> NonZeroUsize {
|
||||||
v: Rel<Length> => Self(HElem::new(Spacing::Rel(v)).pack()),
|
NonZeroUsize::ONE
|
||||||
v: Content => Self(v),
|
}
|
||||||
|
|
||||||
|
/// Constructs the default prefix given the formatted numbering.
|
||||||
|
fn prefix(&self, numbers: Content) -> Content;
|
||||||
|
|
||||||
|
/// The body of the entry.
|
||||||
|
fn body(&self) -> Content;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents each entry line in an outline, including the reference to the
|
/// Represents an entry line in an outline.
|
||||||
/// outlined element, its page number, and the filler content between both.
|
|
||||||
///
|
///
|
||||||
/// This element is intended for use with show rules to control the appearance
|
/// With show-set and show rules on outline entries, you can richly customize
|
||||||
/// of outlines. To customize an entry's line, you can build it from scratch by
|
/// the outline's appearance. See the
|
||||||
/// accessing the `level`, `element`, `body`, `fill` and `page` fields on the
|
/// [section on styling the outline]($outline/#styling-the-outline) for details.
|
||||||
/// entry.
|
#[elem(scope, name = "entry", title = "Outline Entry", Show)]
|
||||||
///
|
|
||||||
/// ```example
|
|
||||||
/// #set heading(numbering: "1.")
|
|
||||||
///
|
|
||||||
/// #show outline.entry.where(
|
|
||||||
/// level: 1
|
|
||||||
/// ): it => {
|
|
||||||
/// v(12pt, weak: true)
|
|
||||||
/// strong(it)
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// #outline(indent: auto)
|
|
||||||
///
|
|
||||||
/// = Introduction
|
|
||||||
/// = Background
|
|
||||||
/// == History
|
|
||||||
/// == State of the Art
|
|
||||||
/// = Analysis
|
|
||||||
/// == Setup
|
|
||||||
/// ```
|
|
||||||
#[elem(name = "entry", title = "Outline Entry", Show)]
|
|
||||||
pub struct OutlineEntry {
|
pub struct OutlineEntry {
|
||||||
/// The nesting level of this outline entry. Starts at `{1}` for top-level
|
/// The nesting level of this outline entry. Starts at `{1}` for top-level
|
||||||
/// entries.
|
/// entries.
|
||||||
@ -412,90 +377,206 @@ pub struct OutlineEntry {
|
|||||||
pub level: NonZeroUsize,
|
pub level: NonZeroUsize,
|
||||||
|
|
||||||
/// The element this entry refers to. Its location will be available
|
/// The element this entry refers to. Its location will be available
|
||||||
/// through the [`location`]($content.location) method on content
|
/// through the [`location`]($content.location) method on the content
|
||||||
/// and can be [linked]($link) to.
|
/// and can be [linked]($link) to.
|
||||||
#[required]
|
#[required]
|
||||||
pub element: Content,
|
pub element: Content,
|
||||||
|
|
||||||
/// The content which is displayed in place of the referred element at its
|
/// Content to fill the space between the title and the page number. Can be
|
||||||
/// entry in the outline. For a heading, this would be its number followed
|
/// set to `{none}` to disable filling.
|
||||||
/// by the heading's title, for example.
|
|
||||||
#[required]
|
|
||||||
pub body: Content,
|
|
||||||
|
|
||||||
/// The content used to fill the space between the element's outline and
|
|
||||||
/// its page number, as defined by the outline element this entry is
|
|
||||||
/// located in. When `{none}`, empty space is inserted in that gap instead.
|
|
||||||
///
|
///
|
||||||
/// Note that, when using show rules to override outline entries, it is
|
/// The `fill` will be placed into a fractionally sized box that spans the
|
||||||
/// recommended to wrap the filling content in a [`box`] with fractional
|
/// space between the entry's body and the page number. When using show
|
||||||
/// width. For example, `{box(width: 1fr, repeat[-])}` would show precisely
|
/// rules to override outline entries, it is thus recommended to wrap the
|
||||||
/// as many `-` characters as necessary to fill a particular gap.
|
/// fill in a [`box`] with fractional width, i.e.
|
||||||
#[required]
|
/// `{box(width: 1fr, it.fill}`.
|
||||||
|
///
|
||||||
|
/// When using [`repeat`], the [`gap`]($repeat.gap) property can be useful
|
||||||
|
/// to tweak the visual weight of the fill.
|
||||||
|
///
|
||||||
|
/// ```example
|
||||||
|
/// #set outline.entry(fill: line(length: 100%))
|
||||||
|
/// #outline()
|
||||||
|
///
|
||||||
|
/// = A New Beginning
|
||||||
|
/// ```
|
||||||
|
#[borrowed]
|
||||||
|
#[default(Some(
|
||||||
|
RepeatElem::new(TextElem::packed("."))
|
||||||
|
.with_gap(Em::new(0.15).into())
|
||||||
|
.pack()
|
||||||
|
))]
|
||||||
pub fill: Option<Content>,
|
pub fill: Option<Content>,
|
||||||
|
|
||||||
/// The page number of the element this entry links to, formatted with the
|
/// Lets outline entries access the outline they are part of. This is a bit
|
||||||
/// numbering set for the referenced page.
|
/// of a hack and should be superseded by a proper ancestry mechanism.
|
||||||
#[required]
|
#[ghost]
|
||||||
pub page: Content,
|
#[internal]
|
||||||
}
|
pub parent: Option<Packed<OutlineElem>>,
|
||||||
|
|
||||||
impl OutlineEntry {
|
|
||||||
/// Generates an OutlineEntry from the given element, if possible (errors if
|
|
||||||
/// the element does not implement `Outlinable`). If the element should not
|
|
||||||
/// be outlined (e.g. heading with 'outlined: false'), does not generate an
|
|
||||||
/// entry instance (returns `Ok(None)`).
|
|
||||||
fn from_outlinable(
|
|
||||||
engine: &mut Engine,
|
|
||||||
span: Span,
|
|
||||||
elem: Content,
|
|
||||||
fill: Option<Content>,
|
|
||||||
styles: StyleChain,
|
|
||||||
) -> SourceResult<Option<Self>> {
|
|
||||||
let Some(outlinable) = elem.with::<dyn Outlinable>() else {
|
|
||||||
bail!(span, "cannot outline {}", elem.func().name());
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(body) = outlinable.outline(engine, styles)? else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
let location = elem.location().unwrap();
|
|
||||||
let page_numbering = engine
|
|
||||||
.introspector
|
|
||||||
.page_numbering(location)
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_else(|| NumberingPattern::from_str("1").unwrap().into());
|
|
||||||
|
|
||||||
let page = Counter::new(CounterKey::Page).display_at_loc(
|
|
||||||
engine,
|
|
||||||
location,
|
|
||||||
styles,
|
|
||||||
&page_numbering,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(Some(Self::new(outlinable.level(), elem, body, fill, page)))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Show for Packed<OutlineEntry> {
|
impl Show for Packed<OutlineEntry> {
|
||||||
#[typst_macros::time(name = "outline.entry", span = self.span())]
|
#[typst_macros::time(name = "outline.entry", span = self.span())]
|
||||||
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
||||||
let mut seq = vec![];
|
let span = self.span();
|
||||||
let elem = &self.element;
|
let context = Context::new(None, Some(styles));
|
||||||
|
let context = context.track();
|
||||||
|
|
||||||
// In case a user constructs an outline entry with an arbitrary element.
|
let prefix = self.prefix(engine, context, span)?;
|
||||||
let Some(location) = elem.location() else {
|
let inner = self.inner(engine, context, span)?;
|
||||||
if elem.can::<dyn Locatable>() && elem.can::<dyn Outlinable>() {
|
let block = if self.element.is::<EquationElem>() {
|
||||||
bail!(
|
let body = prefix.unwrap_or_default() + inner;
|
||||||
self.span(), "{} must have a location", elem.func().name();
|
BlockElem::new()
|
||||||
hint: "try using a query or a show rule to customize the outline.entry instead",
|
.with_body(Some(BlockBody::Content(body)))
|
||||||
)
|
.pack()
|
||||||
} else {
|
.spanned(span)
|
||||||
bail!(self.span(), "cannot outline {}", elem.func().name())
|
} else {
|
||||||
|
self.indented(engine, context, span, prefix, inner, Em::new(0.5).into())?
|
||||||
|
};
|
||||||
|
|
||||||
|
let loc = self.element_location().at(span)?;
|
||||||
|
Ok(block.linked(Destination::Location(loc)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[scope]
|
||||||
|
impl OutlineEntry {
|
||||||
|
/// A helper function for producing an indented entry layout: Lays out a
|
||||||
|
/// prefix and the rest of the entry in an indent-aware way.
|
||||||
|
///
|
||||||
|
/// If the parent outline's [`indent`]($outline.indent) is `{auto}`, the
|
||||||
|
/// inner content of all entries at level `N` is aligned with the prefix of
|
||||||
|
/// all entries at with level `N + 1`, leaving at least `gap` space between
|
||||||
|
/// the prefix and inner parts. Furthermore, the `inner` contents of all
|
||||||
|
/// entries at the same level are aligned.
|
||||||
|
///
|
||||||
|
/// If the outline's indent is a fixed value or a function, the prefixes are
|
||||||
|
/// indented, but the inner contents are simply inset from the prefix by the
|
||||||
|
/// specified `gap`, rather than aligning outline-wide.
|
||||||
|
#[func(contextual)]
|
||||||
|
pub fn indented(
|
||||||
|
&self,
|
||||||
|
engine: &mut Engine,
|
||||||
|
context: Tracked<Context>,
|
||||||
|
span: Span,
|
||||||
|
/// The `prefix` is aligned with the `inner` content of entries that
|
||||||
|
/// have level one less.
|
||||||
|
///
|
||||||
|
/// In the default show rule, this is just to `it.prefix()`, but it can
|
||||||
|
/// be freely customized.
|
||||||
|
prefix: Option<Content>,
|
||||||
|
/// The formatted inner content of the entry.
|
||||||
|
///
|
||||||
|
/// In the default show rule, this is just to `it.inner()`, but it can
|
||||||
|
/// be freely customized.
|
||||||
|
inner: Content,
|
||||||
|
/// The gap between the prefix and the inner content.
|
||||||
|
#[named]
|
||||||
|
#[default(Em::new(0.5).into())]
|
||||||
|
gap: Length,
|
||||||
|
) -> SourceResult<Content> {
|
||||||
|
let styles = context.styles().at(span)?;
|
||||||
|
let outline = Self::parent_in(styles)
|
||||||
|
.ok_or("must be called within the context of an outline")
|
||||||
|
.at(span)?;
|
||||||
|
let outline_loc = outline.location().unwrap();
|
||||||
|
|
||||||
|
let prefix_width = prefix
|
||||||
|
.as_ref()
|
||||||
|
.map(|prefix| measure_prefix(engine, prefix, outline_loc, styles))
|
||||||
|
.transpose()?;
|
||||||
|
let prefix_inset = prefix_width.map(|w| w + gap.resolve(styles));
|
||||||
|
|
||||||
|
let indent = outline.indent(styles);
|
||||||
|
let (base_indent, hanging_indent) = match &indent {
|
||||||
|
Smart::Auto => compute_auto_indents(
|
||||||
|
engine.introspector,
|
||||||
|
outline_loc,
|
||||||
|
styles,
|
||||||
|
self.level,
|
||||||
|
prefix_inset,
|
||||||
|
),
|
||||||
|
Smart::Custom(amount) => {
|
||||||
|
let base = amount.resolve(engine, context, self.level, span)?;
|
||||||
|
(base, prefix_inset)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let body = if let (
|
||||||
|
Some(prefix),
|
||||||
|
Some(prefix_width),
|
||||||
|
Some(prefix_inset),
|
||||||
|
Some(hanging_indent),
|
||||||
|
) = (prefix, prefix_width, prefix_inset, hanging_indent)
|
||||||
|
{
|
||||||
|
// Save information about our prefix that other outline entries
|
||||||
|
// can query for (within `compute_auto_indent`) to align
|
||||||
|
// themselves).
|
||||||
|
let mut seq = Vec::with_capacity(5);
|
||||||
|
if indent.is_auto() {
|
||||||
|
seq.push(PrefixInfo::new(outline_loc, self.level, prefix_inset).pack());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dedent the prefix by the amount of hanging indent and then skip
|
||||||
|
// ahead so that the inner contents are aligned.
|
||||||
|
seq.extend([
|
||||||
|
HElem::new((-hanging_indent).into()).pack(),
|
||||||
|
prefix,
|
||||||
|
HElem::new((hanging_indent - prefix_width).into()).pack(),
|
||||||
|
inner,
|
||||||
|
]);
|
||||||
|
Content::sequence(seq)
|
||||||
|
} else {
|
||||||
|
inner
|
||||||
|
};
|
||||||
|
|
||||||
|
let inset = Sides::default().with(
|
||||||
|
TextElem::dir_in(styles).start(),
|
||||||
|
Some(base_indent + Rel::from(hanging_indent.unwrap_or_default())),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(BlockElem::new()
|
||||||
|
.with_inset(inset)
|
||||||
|
.with_body(Some(BlockBody::Content(body)))
|
||||||
|
.pack()
|
||||||
|
.spanned(span))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formats the element's numbering (if any).
|
||||||
|
///
|
||||||
|
/// This also appends the element's supplement in case of figures or
|
||||||
|
/// equations. For instance, it would output `1.1` for a heading, but
|
||||||
|
/// `Figure 1` for a figure, as is usual for outlines.
|
||||||
|
#[func(contextual)]
|
||||||
|
pub fn prefix(
|
||||||
|
&self,
|
||||||
|
engine: &mut Engine,
|
||||||
|
context: Tracked<Context>,
|
||||||
|
span: Span,
|
||||||
|
) -> SourceResult<Option<Content>> {
|
||||||
|
let outlinable = self.outlinable().at(span)?;
|
||||||
|
let Some(numbering) = outlinable.numbering() else { return Ok(None) };
|
||||||
|
let loc = self.element_location().at(span)?;
|
||||||
|
let styles = context.styles().at(span)?;
|
||||||
|
let numbers =
|
||||||
|
outlinable.counter().display_at_loc(engine, loc, styles, numbering)?;
|
||||||
|
Ok(Some(outlinable.prefix(numbers)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates the default inner content of the entry.
|
||||||
|
///
|
||||||
|
/// This includes the body, the fill, and page number.
|
||||||
|
#[func(contextual)]
|
||||||
|
pub fn inner(
|
||||||
|
&self,
|
||||||
|
engine: &mut Engine,
|
||||||
|
context: Tracked<Context>,
|
||||||
|
span: Span,
|
||||||
|
) -> SourceResult<Content> {
|
||||||
|
let styles = context.styles().at(span)?;
|
||||||
|
|
||||||
|
let mut seq = vec![];
|
||||||
|
|
||||||
// Isolate the entry body in RTL because the page number is typically
|
// Isolate the entry body in RTL because the page number is typically
|
||||||
// LTR. I'm not sure whether LTR should conceptually also be isolated,
|
// LTR. I'm not sure whether LTR should conceptually also be isolated,
|
||||||
// but in any case we don't do it for now because the text shaping
|
// but in any case we don't do it for now because the text shaping
|
||||||
@ -511,32 +592,174 @@ impl Show for Packed<OutlineEntry> {
|
|||||||
seq.push(TextElem::packed("\u{202B}"));
|
seq.push(TextElem::packed("\u{202B}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
seq.push(self.body.clone().linked(Destination::Location(location)));
|
seq.push(self.body().at(span)?);
|
||||||
|
|
||||||
if rtl {
|
if rtl {
|
||||||
// "Pop Directional Formatting"
|
// "Pop Directional Formatting"
|
||||||
seq.push(TextElem::packed("\u{202C}"));
|
seq.push(TextElem::packed("\u{202C}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add filler symbols between the section name and page number.
|
// Add the filler between the section name and page number.
|
||||||
if let Some(filler) = &self.fill {
|
if let Some(filler) = self.fill(styles) {
|
||||||
seq.push(SpaceElem::shared().clone());
|
seq.push(SpaceElem::shared().clone());
|
||||||
seq.push(
|
seq.push(
|
||||||
BoxElem::new()
|
BoxElem::new()
|
||||||
.with_body(Some(filler.clone()))
|
.with_body(Some(filler.clone()))
|
||||||
.with_width(Fr::one().into())
|
.with_width(Fr::one().into())
|
||||||
.pack()
|
.pack()
|
||||||
.spanned(self.span()),
|
.spanned(span),
|
||||||
);
|
);
|
||||||
seq.push(SpaceElem::shared().clone());
|
seq.push(SpaceElem::shared().clone());
|
||||||
} else {
|
} else {
|
||||||
seq.push(HElem::new(Fr::one().into()).pack().spanned(self.span()));
|
seq.push(HElem::new(Fr::one().into()).pack().spanned(span));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the page number.
|
// Add the page number. The word joiner in front ensures that the page
|
||||||
let page = self.page.clone().linked(Destination::Location(location));
|
// number doesn't stand alone in its line.
|
||||||
seq.push(page);
|
seq.push(TextElem::packed("\u{2060}"));
|
||||||
|
seq.push(self.page(engine, context, span)?);
|
||||||
|
|
||||||
Ok(Content::sequence(seq))
|
Ok(Content::sequence(seq))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The content which is displayed in place of the referred element at its
|
||||||
|
/// entry in the outline. For a heading, this is its
|
||||||
|
/// [`body`]($heading.body), for a figure a caption, and for equations it is
|
||||||
|
/// empty.
|
||||||
|
#[func]
|
||||||
|
pub fn body(&self) -> StrResult<Content> {
|
||||||
|
Ok(self.outlinable()?.body())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The page number of this entry's element, formatted with the numbering
|
||||||
|
/// set for the referenced page.
|
||||||
|
#[func(contextual)]
|
||||||
|
pub fn page(
|
||||||
|
&self,
|
||||||
|
engine: &mut Engine,
|
||||||
|
context: Tracked<Context>,
|
||||||
|
span: Span,
|
||||||
|
) -> SourceResult<Content> {
|
||||||
|
let loc = self.element_location().at(span)?;
|
||||||
|
let styles = context.styles().at(span)?;
|
||||||
|
let numbering = engine
|
||||||
|
.introspector
|
||||||
|
.page_numbering(loc)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| NumberingPattern::from_str("1").unwrap().into());
|
||||||
|
Counter::new(CounterKey::Page).display_at_loc(engine, loc, styles, &numbering)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutlineEntry {
|
||||||
|
fn outlinable(&self) -> StrResult<&dyn Outlinable> {
|
||||||
|
self.element
|
||||||
|
.with::<dyn Outlinable>()
|
||||||
|
.ok_or_else(|| error!("cannot outline {}", self.element.func().name()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn element_location(&self) -> HintedStrResult<Location> {
|
||||||
|
let elem = &self.element;
|
||||||
|
elem.location().ok_or_else(|| {
|
||||||
|
if elem.can::<dyn Locatable>() && elem.can::<dyn Outlinable>() {
|
||||||
|
error!(
|
||||||
|
"{} must have a location", elem.func().name();
|
||||||
|
hint: "try using a show rule to customize the outline.entry instead",
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
error!("cannot outline {}", elem.func().name())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cast! {
|
||||||
|
OutlineEntry,
|
||||||
|
v: Content => v.unpack::<Self>().map_err(|_| "expected outline entry")?
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Measures the width of a prefix.
|
||||||
|
fn measure_prefix(
|
||||||
|
engine: &mut Engine,
|
||||||
|
prefix: &Content,
|
||||||
|
loc: Location,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<Abs> {
|
||||||
|
let pod = Region::new(Axes::splat(Abs::inf()), Axes::splat(false));
|
||||||
|
let link = LocatorLink::measure(loc);
|
||||||
|
Ok((engine.routines.layout_frame)(engine, prefix, Locator::link(&link), styles, pod)?
|
||||||
|
.width())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute the base indent and hanging indent for an auto-indented outline
|
||||||
|
/// entry of the given level, with the given prefix inset.
|
||||||
|
fn compute_auto_indents(
|
||||||
|
introspector: Tracked<Introspector>,
|
||||||
|
outline_loc: Location,
|
||||||
|
styles: StyleChain,
|
||||||
|
level: NonZeroUsize,
|
||||||
|
prefix_inset: Option<Abs>,
|
||||||
|
) -> (Rel, Option<Abs>) {
|
||||||
|
let indents = query_prefix_widths(introspector, outline_loc);
|
||||||
|
|
||||||
|
let fallback = Em::new(1.2).resolve(styles);
|
||||||
|
let get = |i: usize| indents.get(i).copied().flatten().unwrap_or(fallback);
|
||||||
|
|
||||||
|
let last = level.get() - 1;
|
||||||
|
let base: Abs = (0..last).map(get).sum();
|
||||||
|
let hang = prefix_inset.map(|p| p.max(get(last)));
|
||||||
|
|
||||||
|
(base.into(), hang)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determines the maximum prefix inset (prefix width + gap) at each outline
|
||||||
|
/// level, for the outline with the given `loc`. Levels for which there is no
|
||||||
|
/// information available yield `None`.
|
||||||
|
#[comemo::memoize]
|
||||||
|
fn query_prefix_widths(
|
||||||
|
introspector: Tracked<Introspector>,
|
||||||
|
outline_loc: Location,
|
||||||
|
) -> SmallVec<[Option<Abs>; 4]> {
|
||||||
|
let mut widths = SmallVec::<[Option<Abs>; 4]>::new();
|
||||||
|
let elems = introspector.query(&select_where!(PrefixInfo, Key => outline_loc));
|
||||||
|
for elem in &elems {
|
||||||
|
let info = elem.to_packed::<PrefixInfo>().unwrap();
|
||||||
|
let level = info.level.get();
|
||||||
|
if widths.len() < level {
|
||||||
|
widths.resize(level, None);
|
||||||
|
}
|
||||||
|
widths[level - 1].get_or_insert(info.inset).set_max(info.inset);
|
||||||
|
}
|
||||||
|
widths
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper type for introspection-based prefix alignment.
|
||||||
|
#[elem(Construct, Locatable, Show)]
|
||||||
|
struct PrefixInfo {
|
||||||
|
/// The location of the outline this prefix is part of. This is used to
|
||||||
|
/// scope prefix computations to a specific outline.
|
||||||
|
#[required]
|
||||||
|
key: Location,
|
||||||
|
|
||||||
|
/// The level of this prefix's entry.
|
||||||
|
#[required]
|
||||||
|
#[internal]
|
||||||
|
level: NonZeroUsize,
|
||||||
|
|
||||||
|
/// The width of the prefix, including the gap.
|
||||||
|
#[required]
|
||||||
|
#[internal]
|
||||||
|
inset: Abs,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Construct for PrefixInfo {
|
||||||
|
fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> {
|
||||||
|
bail!(args.span, "cannot be constructed manually");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Show for Packed<PrefixInfo> {
|
||||||
|
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
|
||||||
|
Ok(Content::empty())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,13 +2,14 @@ use crate::diag::SourceResult;
|
|||||||
use crate::engine::Engine;
|
use crate::engine::Engine;
|
||||||
use crate::foundations::{
|
use crate::foundations::{
|
||||||
cast, elem, Content, Depth, Label, NativeElement, Packed, Show, ShowSet, Smart,
|
cast, elem, Content, Depth, Label, NativeElement, Packed, Show, ShowSet, Smart,
|
||||||
StyleChain, Styles,
|
StyleChain, Styles, TargetElem,
|
||||||
};
|
};
|
||||||
|
use crate::html::{tag, HtmlAttr, HtmlElem};
|
||||||
use crate::introspection::Locatable;
|
use crate::introspection::Locatable;
|
||||||
use crate::layout::{
|
use crate::layout::{
|
||||||
Alignment, BlockBody, BlockElem, Em, HElem, PadElem, Spacing, VElem,
|
Alignment, BlockBody, BlockElem, Em, HElem, PadElem, Spacing, VElem,
|
||||||
};
|
};
|
||||||
use crate::model::{CitationForm, CiteElem};
|
use crate::model::{CitationForm, CiteElem, Destination, LinkElem, LinkTarget};
|
||||||
use crate::text::{SmartQuoteElem, SmartQuotes, SpaceElem, TextElem};
|
use crate::text::{SmartQuoteElem, SmartQuotes, SpaceElem, TextElem};
|
||||||
|
|
||||||
/// Displays a quote alongside an optional attribution.
|
/// Displays a quote alongside an optional attribution.
|
||||||
@ -158,6 +159,7 @@ impl Show for Packed<QuoteElem> {
|
|||||||
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
||||||
let mut realized = self.body.clone();
|
let mut realized = self.body.clone();
|
||||||
let block = self.block(styles);
|
let block = self.block(styles);
|
||||||
|
let html = TargetElem::target_in(styles).is_html();
|
||||||
|
|
||||||
if self.quotes(styles) == Smart::Custom(true) || !block {
|
if self.quotes(styles) == Smart::Custom(true) || !block {
|
||||||
let quotes = SmartQuotes::get(
|
let quotes = SmartQuotes::get(
|
||||||
@ -171,50 +173,65 @@ impl Show for Packed<QuoteElem> {
|
|||||||
let Depth(depth) = QuoteElem::depth_in(styles);
|
let Depth(depth) = QuoteElem::depth_in(styles);
|
||||||
let double = depth % 2 == 0;
|
let double = depth % 2 == 0;
|
||||||
|
|
||||||
// Add zero-width weak spacing to make the quotes "sticky".
|
if !html {
|
||||||
let hole = HElem::hole().pack();
|
// Add zero-width weak spacing to make the quotes "sticky".
|
||||||
|
let hole = HElem::hole().pack();
|
||||||
|
realized = Content::sequence([hole.clone(), realized, hole]);
|
||||||
|
}
|
||||||
realized = Content::sequence([
|
realized = Content::sequence([
|
||||||
TextElem::packed(quotes.open(double)),
|
TextElem::packed(quotes.open(double)),
|
||||||
hole.clone(),
|
|
||||||
realized,
|
realized,
|
||||||
hole,
|
|
||||||
TextElem::packed(quotes.close(double)),
|
TextElem::packed(quotes.close(double)),
|
||||||
])
|
])
|
||||||
.styled(QuoteElem::set_depth(Depth(1)));
|
.styled(QuoteElem::set_depth(Depth(1)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let attribution = self.attribution(styles);
|
||||||
|
|
||||||
if block {
|
if block {
|
||||||
realized = BlockElem::new()
|
realized = if html {
|
||||||
.with_body(Some(BlockBody::Content(realized)))
|
let mut elem = HtmlElem::new(tag::blockquote).with_body(Some(realized));
|
||||||
.pack()
|
if let Some(Attribution::Content(attribution)) = attribution {
|
||||||
.spanned(self.span());
|
if let Some(link) = attribution.to_packed::<LinkElem>() {
|
||||||
|
if let LinkTarget::Dest(Destination::Url(url)) = &link.dest {
|
||||||
if let Some(attribution) = self.attribution(styles).as_ref() {
|
elem = elem.with_attr(
|
||||||
let mut seq = vec![TextElem::packed('—'), SpaceElem::shared().clone()];
|
HtmlAttr::constant("cite"),
|
||||||
|
url.clone().into_inner(),
|
||||||
match attribution {
|
);
|
||||||
Attribution::Content(content) => {
|
}
|
||||||
seq.push(content.clone());
|
|
||||||
}
|
|
||||||
Attribution::Label(label) => {
|
|
||||||
seq.push(
|
|
||||||
CiteElem::new(*label)
|
|
||||||
.with_form(Some(CitationForm::Prose))
|
|
||||||
.pack()
|
|
||||||
.spanned(self.span()),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
elem.pack()
|
||||||
|
} else {
|
||||||
|
BlockElem::new().with_body(Some(BlockBody::Content(realized))).pack()
|
||||||
|
}
|
||||||
|
.spanned(self.span());
|
||||||
|
|
||||||
// Use v(0.9em, weak: true) bring the attribution closer to the
|
if let Some(attribution) = attribution.as_ref() {
|
||||||
// quote.
|
let attribution = match attribution {
|
||||||
let gap = Spacing::Rel(Em::new(0.9).into());
|
Attribution::Content(content) => content.clone(),
|
||||||
let v = VElem::new(gap).with_weak(true).pack();
|
Attribution::Label(label) => CiteElem::new(*label)
|
||||||
realized += v + Content::sequence(seq).aligned(Alignment::END);
|
.with_form(Some(CitationForm::Prose))
|
||||||
|
.pack()
|
||||||
|
.spanned(self.span()),
|
||||||
|
};
|
||||||
|
let attribution =
|
||||||
|
[TextElem::packed('—'), SpaceElem::shared().clone(), attribution];
|
||||||
|
|
||||||
|
if !html {
|
||||||
|
// Use v(0.9em, weak: true) to bring the attribution closer
|
||||||
|
// to the quote.
|
||||||
|
let gap = Spacing::Rel(Em::new(0.9).into());
|
||||||
|
let v = VElem::new(gap).with_weak(true).pack();
|
||||||
|
realized += v;
|
||||||
|
}
|
||||||
|
realized += Content::sequence(attribution).aligned(Alignment::END);
|
||||||
}
|
}
|
||||||
|
|
||||||
realized = PadElem::new(realized).pack();
|
if !html {
|
||||||
} else if let Some(Attribution::Label(label)) = self.attribution(styles) {
|
realized = PadElem::new(realized).pack();
|
||||||
|
}
|
||||||
|
} else if let Some(Attribution::Label(label)) = attribution {
|
||||||
realized += SpaceElem::shared().clone()
|
realized += SpaceElem::shared().clone()
|
||||||
+ CiteElem::new(*label).pack().spanned(self.span());
|
+ CiteElem::new(*label).pack().spanned(self.span());
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,11 @@ use crate::diag::{bail, HintedStrResult, HintedString, SourceResult};
|
|||||||
use crate::engine::Engine;
|
use crate::engine::Engine;
|
||||||
use crate::foundations::{
|
use crate::foundations::{
|
||||||
cast, elem, scope, Content, NativeElement, Packed, Show, Smart, StyleChain,
|
cast, elem, scope, Content, NativeElement, Packed, Show, Smart, StyleChain,
|
||||||
|
TargetElem,
|
||||||
};
|
};
|
||||||
|
use crate::html::{tag, HtmlAttr, HtmlAttrs, HtmlElem, HtmlTag};
|
||||||
|
use crate::introspection::Locator;
|
||||||
|
use crate::layout::grid::resolve::{table_to_cellgrid, Cell, CellGrid, Entry};
|
||||||
use crate::layout::{
|
use crate::layout::{
|
||||||
show_grid_cell, Abs, Alignment, BlockElem, Celled, GridCell, GridFooter, GridHLine,
|
show_grid_cell, Abs, Alignment, BlockElem, Celled, GridCell, GridFooter, GridHLine,
|
||||||
GridHeader, GridVLine, Length, OuterHAlignment, OuterVAlignment, Rel, Sides,
|
GridHeader, GridVLine, Length, OuterHAlignment, OuterVAlignment, Rel, Sides,
|
||||||
@ -258,11 +262,65 @@ impl TableElem {
|
|||||||
type TableFooter;
|
type TableFooter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn show_cell_html(tag: HtmlTag, cell: &Cell, styles: StyleChain) -> Content {
|
||||||
|
let cell = cell.body.clone();
|
||||||
|
let Some(cell) = cell.to_packed::<TableCell>() else { return cell };
|
||||||
|
let mut attrs = HtmlAttrs::default();
|
||||||
|
let span = |n: NonZeroUsize| (n != NonZeroUsize::MIN).then(|| n.to_string());
|
||||||
|
if let Some(colspan) = span(cell.colspan(styles)) {
|
||||||
|
attrs.push(HtmlAttr::constant("colspan"), colspan);
|
||||||
|
}
|
||||||
|
if let Some(rowspan) = span(cell.rowspan(styles)) {
|
||||||
|
attrs.push(HtmlAttr::constant("rowspan"), rowspan);
|
||||||
|
}
|
||||||
|
HtmlElem::new(tag)
|
||||||
|
.with_body(Some(cell.body.clone()))
|
||||||
|
.with_attrs(attrs)
|
||||||
|
.pack()
|
||||||
|
.spanned(cell.span())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_cellgrid_html(grid: CellGrid, styles: StyleChain) -> Content {
|
||||||
|
let elem = |tag, body| HtmlElem::new(tag).with_body(Some(body)).pack();
|
||||||
|
let mut rows: Vec<_> = grid.entries.chunks(grid.cols.len()).collect();
|
||||||
|
|
||||||
|
let tr = |tag, row: &[Entry]| {
|
||||||
|
let row = row
|
||||||
|
.iter()
|
||||||
|
.flat_map(|entry| entry.as_cell())
|
||||||
|
.map(|cell| show_cell_html(tag, cell, styles));
|
||||||
|
elem(tag::tr, Content::sequence(row))
|
||||||
|
};
|
||||||
|
|
||||||
|
let footer = grid.footer.map(|ft| {
|
||||||
|
let rows = rows.drain(ft.unwrap().start..);
|
||||||
|
elem(tag::tfoot, Content::sequence(rows.map(|row| tr(tag::td, row))))
|
||||||
|
});
|
||||||
|
let header = grid.header.map(|hd| {
|
||||||
|
let rows = rows.drain(..hd.unwrap().end);
|
||||||
|
elem(tag::thead, Content::sequence(rows.map(|row| tr(tag::th, row))))
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut body = Content::sequence(rows.into_iter().map(|row| tr(tag::td, row)));
|
||||||
|
if header.is_some() || footer.is_some() {
|
||||||
|
body = elem(tag::tbody, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = header.into_iter().chain(core::iter::once(body)).chain(footer);
|
||||||
|
elem(tag::table, Content::sequence(content))
|
||||||
|
}
|
||||||
|
|
||||||
impl Show for Packed<TableElem> {
|
impl Show for Packed<TableElem> {
|
||||||
fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> {
|
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
||||||
Ok(BlockElem::multi_layouter(self.clone(), engine.routines.layout_table)
|
Ok(if TargetElem::target_in(styles).is_html() {
|
||||||
.pack()
|
// TODO: This is a hack, it is not clear whether the locator is actually used by HTML.
|
||||||
.spanned(self.span()))
|
// How can we find out whether locator is actually used?
|
||||||
|
let locator = Locator::root();
|
||||||
|
show_cellgrid_html(table_to_cellgrid(self, engine, locator, styles)?, styles)
|
||||||
|
} else {
|
||||||
|
BlockElem::multi_layouter(self.clone(), engine.routines.layout_table).pack()
|
||||||
|
}
|
||||||
|
.spanned(self.span()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use typst_utils::Numeric;
|
use typst_utils::{Get, Numeric};
|
||||||
|
|
||||||
use crate::diag::{bail, SourceResult};
|
use crate::diag::{bail, SourceResult};
|
||||||
use crate::engine::Engine;
|
use crate::engine::Engine;
|
||||||
@ -7,7 +7,7 @@ use crate::foundations::{
|
|||||||
Styles, TargetElem,
|
Styles, TargetElem,
|
||||||
};
|
};
|
||||||
use crate::html::{tag, HtmlElem};
|
use crate::html::{tag, HtmlElem};
|
||||||
use crate::layout::{Dir, Em, HElem, Length, Sides, StackChild, StackElem, VElem};
|
use crate::layout::{Em, HElem, Length, Sides, StackChild, StackElem, VElem};
|
||||||
use crate::model::{ListItemLike, ListLike, ParElem};
|
use crate::model::{ListItemLike, ListLike, ParElem};
|
||||||
use crate::text::TextElem;
|
use crate::text::TextElem;
|
||||||
|
|
||||||
@ -160,12 +160,7 @@ impl Show for Packed<TermsElem> {
|
|||||||
children.push(StackChild::Block(Content::sequence(seq)));
|
children.push(StackChild::Block(Content::sequence(seq)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut padding = Sides::default();
|
let padding = Sides::default().with(TextElem::dir_in(styles).start(), pad.into());
|
||||||
if TextElem::dir_in(styles) == Dir::LTR {
|
|
||||||
padding.left = pad.into();
|
|
||||||
} else {
|
|
||||||
padding.right = pad.into();
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut realized = StackElem::new(children)
|
let mut realized = StackElem::new(children)
|
||||||
.with_spacing(Some(gutter.into()))
|
.with_spacing(Some(gutter.into()))
|
||||||
|
@ -287,6 +287,7 @@ pub fn highlight(node: &LinkedNode) -> Option<Tag> {
|
|||||||
SyntaxKind::Destructuring => None,
|
SyntaxKind::Destructuring => None,
|
||||||
SyntaxKind::DestructAssignment => None,
|
SyntaxKind::DestructAssignment => None,
|
||||||
|
|
||||||
|
SyntaxKind::Shebang => Some(Tag::Comment),
|
||||||
SyntaxKind::LineComment => Some(Tag::Comment),
|
SyntaxKind::LineComment => Some(Tag::Comment),
|
||||||
SyntaxKind::BlockComment => Some(Tag::Comment),
|
SyntaxKind::BlockComment => Some(Tag::Comment),
|
||||||
SyntaxKind::Error => Some(Tag::Error),
|
SyntaxKind::Error => Some(Tag::Error),
|
||||||
|
@ -9,6 +9,8 @@ pub enum SyntaxKind {
|
|||||||
/// An invalid sequence of characters.
|
/// An invalid sequence of characters.
|
||||||
Error,
|
Error,
|
||||||
|
|
||||||
|
/// A shebang: `#! ...`
|
||||||
|
Shebang,
|
||||||
/// A line comment: `// ...`.
|
/// A line comment: `// ...`.
|
||||||
LineComment,
|
LineComment,
|
||||||
/// A block comment: `/* ... */`.
|
/// A block comment: `/* ... */`.
|
||||||
@ -357,7 +359,11 @@ impl SyntaxKind {
|
|||||||
pub fn is_trivia(self) -> bool {
|
pub fn is_trivia(self) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
self,
|
self,
|
||||||
Self::LineComment | Self::BlockComment | Self::Space | Self::Parbreak
|
Self::Shebang
|
||||||
|
| Self::LineComment
|
||||||
|
| Self::BlockComment
|
||||||
|
| Self::Space
|
||||||
|
| Self::Parbreak
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -371,6 +377,7 @@ impl SyntaxKind {
|
|||||||
match self {
|
match self {
|
||||||
Self::End => "end of tokens",
|
Self::End => "end of tokens",
|
||||||
Self::Error => "syntax error",
|
Self::Error => "syntax error",
|
||||||
|
Self::Shebang => "shebang",
|
||||||
Self::LineComment => "line comment",
|
Self::LineComment => "line comment",
|
||||||
Self::BlockComment => "block comment",
|
Self::BlockComment => "block comment",
|
||||||
Self::Markup => "markup",
|
Self::Markup => "markup",
|
||||||
|
@ -103,6 +103,7 @@ impl Lexer<'_> {
|
|||||||
self.newline = false;
|
self.newline = false;
|
||||||
let kind = match self.s.eat() {
|
let kind = match self.s.eat() {
|
||||||
Some(c) if is_space(c, self.mode) => self.whitespace(start, c),
|
Some(c) if is_space(c, self.mode) => self.whitespace(start, c),
|
||||||
|
Some('#') if start == 0 && self.s.eat_if('!') => self.shebang(),
|
||||||
Some('/') if self.s.eat_if('/') => self.line_comment(),
|
Some('/') if self.s.eat_if('/') => self.line_comment(),
|
||||||
Some('/') if self.s.eat_if('*') => self.block_comment(),
|
Some('/') if self.s.eat_if('*') => self.block_comment(),
|
||||||
Some('*') if self.s.eat_if('/') => {
|
Some('*') if self.s.eat_if('/') => {
|
||||||
@ -151,6 +152,11 @@ impl Lexer<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn shebang(&mut self) -> SyntaxKind {
|
||||||
|
self.s.eat_until(is_newline);
|
||||||
|
SyntaxKind::Shebang
|
||||||
|
}
|
||||||
|
|
||||||
fn line_comment(&mut self) -> SyntaxKind {
|
fn line_comment(&mut self) -> SyntaxKind {
|
||||||
self.s.eat_until(is_newline);
|
self.s.eat_until(is_newline);
|
||||||
SyntaxKind::LineComment
|
SyntaxKind::LineComment
|
||||||
|
@ -93,6 +93,8 @@ fn markup_expr(p: &mut Parser, at_start: bool, nesting: &mut usize) {
|
|||||||
p.hint("try using a backslash escape: \\]");
|
p.hint("try using a backslash escape: \\]");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SyntaxKind::Shebang => p.eat(),
|
||||||
|
|
||||||
SyntaxKind::Text
|
SyntaxKind::Text
|
||||||
| SyntaxKind::Linebreak
|
| SyntaxKind::Linebreak
|
||||||
| SyntaxKind::Escape
|
| SyntaxKind::Escape
|
||||||
@ -160,7 +162,7 @@ fn list_item(p: &mut Parser) {
|
|||||||
p.with_nl_mode(AtNewline::RequireColumn(p.current_column()), |p| {
|
p.with_nl_mode(AtNewline::RequireColumn(p.current_column()), |p| {
|
||||||
let m = p.marker();
|
let m = p.marker();
|
||||||
p.assert(SyntaxKind::ListMarker);
|
p.assert(SyntaxKind::ListMarker);
|
||||||
markup(p, false, false, syntax_set!(RightBracket, End));
|
markup(p, true, false, syntax_set!(RightBracket, End));
|
||||||
p.wrap(m, SyntaxKind::ListItem);
|
p.wrap(m, SyntaxKind::ListItem);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -170,7 +172,7 @@ fn enum_item(p: &mut Parser) {
|
|||||||
p.with_nl_mode(AtNewline::RequireColumn(p.current_column()), |p| {
|
p.with_nl_mode(AtNewline::RequireColumn(p.current_column()), |p| {
|
||||||
let m = p.marker();
|
let m = p.marker();
|
||||||
p.assert(SyntaxKind::EnumMarker);
|
p.assert(SyntaxKind::EnumMarker);
|
||||||
markup(p, false, false, syntax_set!(RightBracket, End));
|
markup(p, true, false, syntax_set!(RightBracket, End));
|
||||||
p.wrap(m, SyntaxKind::EnumItem);
|
p.wrap(m, SyntaxKind::EnumItem);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -184,7 +186,7 @@ fn term_item(p: &mut Parser) {
|
|||||||
markup(p, false, false, syntax_set!(Colon, RightBracket, End));
|
markup(p, false, false, syntax_set!(Colon, RightBracket, End));
|
||||||
});
|
});
|
||||||
p.expect(SyntaxKind::Colon);
|
p.expect(SyntaxKind::Colon);
|
||||||
markup(p, false, false, syntax_set!(RightBracket, End));
|
markup(p, true, false, syntax_set!(RightBracket, End));
|
||||||
p.wrap(m, SyntaxKind::TermItem);
|
p.wrap(m, SyntaxKind::TermItem);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -442,10 +444,10 @@ fn math_unparen(p: &mut Parser, m: Marker) {
|
|||||||
if first.text() == "(" && last.text() == ")" {
|
if first.text() == "(" && last.text() == ")" {
|
||||||
first.convert_to_kind(SyntaxKind::LeftParen);
|
first.convert_to_kind(SyntaxKind::LeftParen);
|
||||||
last.convert_to_kind(SyntaxKind::RightParen);
|
last.convert_to_kind(SyntaxKind::RightParen);
|
||||||
|
// Only convert if we did have regular parens.
|
||||||
|
node.convert_to_kind(SyntaxKind::Math);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
node.convert_to_kind(SyntaxKind::Math);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The unicode math class of a string. Only returns `Some` if `text` has
|
/// The unicode math class of a string. Only returns `Some` if `text` has
|
||||||
|
@ -276,6 +276,15 @@ pub trait Get<Index> {
|
|||||||
fn set(&mut self, index: Index, component: Self::Component) {
|
fn set(&mut self, index: Index, component: Self::Component) {
|
||||||
*self.get_mut(index) = component;
|
*self.get_mut(index) = component;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Builder-style method for setting a component.
|
||||||
|
fn with(mut self, index: Index, component: Self::Component) -> Self
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
self.set(index, component);
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A numeric type.
|
/// A numeric type.
|
||||||
|
BIN
tests/ref/heading-hanging-indent-auto.png
Normal file
After Width: | Height: | Size: 849 B |
BIN
tests/ref/heading-hanging-indent-length.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
tests/ref/heading-hanging-indent-zero.png
Normal file
After Width: | Height: | Size: 859 B |
35
tests/ref/html/basic-table.html
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>The</th><th>first</th><th>and</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>the</th><th>second</th><th>row</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Foo</td><td rowspan="2">Baz</td><td>Bar</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>1</td><td>2</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2">3</td><td>4</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td>The</td><td>last</td><td>row</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
12
tests/ref/html/enum-start.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<ol start="3">
|
||||||
|
<li>Skipping</li><li>Ahead</li>
|
||||||
|
</ol>
|
||||||
|
</body>
|
||||||
|
</html>
|
15
tests/ref/html/quote-attribution-link.html
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<blockquote cite="https://typst.app/home">
|
||||||
|
Compose papers faster
|
||||||
|
</blockquote>
|
||||||
|
<p>
|
||||||
|
— <a href="https://typst.app/home">typst.com</a>
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
12
tests/ref/html/quote-nesting-html.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>
|
||||||
|
When you said that “he surely meant that ‘she intended to say “I'm sorry”’”, I was quite confused.
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
21
tests/ref/html/quote-plato.html
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<blockquote>
|
||||||
|
… ἔοικα γοῦν τούτου γε σμικρῷ τινι αὐτῷ τούτῳ σοφώτερος εἶναι, ὅτι ἃ μὴ οἶδα οὐδὲ οἴομαι εἰδέναι.
|
||||||
|
</blockquote>
|
||||||
|
<p>
|
||||||
|
— Plato
|
||||||
|
</p>
|
||||||
|
<blockquote>
|
||||||
|
… I seem, then, in just this little thing to be wiser than this man at any rate, that what I do not know I do not think I know either.
|
||||||
|
</blockquote>
|
||||||
|
<p>
|
||||||
|
— from the Henry Cary literal translation of 1897
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.3 KiB |
BIN
tests/ref/issue-2048-outline-multiline.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
tests/ref/issue-4476-outline-rtl-title-ending-in-ltr-text.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 6.2 KiB |
BIN
tests/ref/issue-4859-outline-entry-show-set.png
Normal file
After Width: | Height: | Size: 749 B |
Before Width: | Height: | Size: 1.2 KiB |
BIN
tests/ref/issue-5176-outline-cjk-title.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 2.0 KiB |
BIN
tests/ref/issue-5719-enum-nested.png
Normal file
After Width: | Height: | Size: 800 B |
BIN
tests/ref/issue-5719-heading-nested.png
Normal file
After Width: | Height: | Size: 217 B |
BIN
tests/ref/issue-5719-list-nested.png
Normal file
After Width: | Height: | Size: 506 B |
BIN
tests/ref/issue-5719-terms-nested.png
Normal file
After Width: | Height: | Size: 921 B |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 9.2 KiB |
BIN
tests/ref/math-lr-unparen.png
Normal file
After Width: | Height: | Size: 493 B |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 474 B |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 8.3 KiB |
BIN
tests/ref/outline-entry-inner.png
Normal file
After Width: | Height: | Size: 462 B |
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 5.4 KiB |
BIN
tests/ref/outline-heading-start-of-page.png
Normal file
After Width: | Height: | Size: 6.8 KiB |
BIN
tests/ref/outline-indent-auto-mixed-prefix-short.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
tests/ref/outline-indent-auto-mixed-prefix.png
Normal file
After Width: | Height: | Size: 5.6 KiB |
BIN
tests/ref/outline-indent-auto-no-prefix.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
tests/ref/outline-indent-auto.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
tests/ref/outline-indent-fixed.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
tests/ref/outline-indent-func.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 6.9 KiB |
BIN
tests/ref/outline-indent-zero.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
tests/ref/outline-spacing.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 8.9 KiB |
32
tests/suite/layout/grid/html.typ
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
--- basic-table html ---
|
||||||
|
#table(
|
||||||
|
columns: 3,
|
||||||
|
rows: 3,
|
||||||
|
|
||||||
|
table.header(
|
||||||
|
[The],
|
||||||
|
[first],
|
||||||
|
[and],
|
||||||
|
[the],
|
||||||
|
[second],
|
||||||
|
[row],
|
||||||
|
table.hline(stroke: red)
|
||||||
|
),
|
||||||
|
|
||||||
|
table.cell(x: 1, rowspan: 2)[Baz],
|
||||||
|
[Foo],
|
||||||
|
[Bar],
|
||||||
|
|
||||||
|
[1],
|
||||||
|
// Baz spans into the next cell
|
||||||
|
[2],
|
||||||
|
|
||||||
|
table.cell(colspan: 2)[3],
|
||||||
|
[4],
|
||||||
|
|
||||||
|
table.footer(
|
||||||
|
[The],
|
||||||
|
[last],
|
||||||
|
[row],
|
||||||
|
),
|
||||||
|
)
|
@ -125,3 +125,11 @@ $ lr(size: #3em, |)_a^b lr(size: #3em, zws|)_a^b
|
|||||||
--- issue-4188-lr-corner-brackets ---
|
--- issue-4188-lr-corner-brackets ---
|
||||||
// Test positioning of U+231C to U+231F
|
// Test positioning of U+231C to U+231F
|
||||||
$⌜a⌟⌞b⌝$ = $⌜$$a$$⌟$$⌞$$b$$⌝$
|
$⌜a⌟⌞b⌝$ = $⌜$$a$$⌟$$⌞$$b$$⌝$
|
||||||
|
|
||||||
|
--- math-lr-unparen ---
|
||||||
|
// Test that unparen with brackets stays as an LrElem.
|
||||||
|
#let item = $limits(sum)_i$
|
||||||
|
$
|
||||||
|
1 / ([item]) quad
|
||||||
|
1 / [item]
|
||||||
|
$
|
||||||
|
@ -101,6 +101,13 @@ a + 0.
|
|||||||
[Red], [Green], [Blue], [Red],
|
[Red], [Green], [Blue], [Red],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
--- enum-start html ---
|
||||||
|
#enum(
|
||||||
|
start: 3,
|
||||||
|
[Skipping],
|
||||||
|
[Ahead],
|
||||||
|
)
|
||||||
|
|
||||||
--- enum-numbering-closure-nested ---
|
--- enum-numbering-closure-nested ---
|
||||||
// Test numbering with closure and nested lists.
|
// Test numbering with closure and nested lists.
|
||||||
#set enum(numbering: n => super[#n])
|
#set enum(numbering: n => super[#n])
|
||||||
@ -192,3 +199,13 @@ a + 0.
|
|||||||
+ f
|
+ f
|
||||||
#align(right)[+ align]
|
#align(right)[+ align]
|
||||||
+ h
|
+ h
|
||||||
|
|
||||||
|
--- issue-5719-enum-nested ---
|
||||||
|
// Enums can be immediately nested.
|
||||||
|
1. A
|
||||||
|
2. 1. B
|
||||||
|
2. C
|
||||||
|
+ + D
|
||||||
|
+ E
|
||||||
|
+ = F
|
||||||
|
G
|
||||||
|
@ -289,9 +289,3 @@ HI#footnote.entry(clearance: 2.5em)[There]
|
|||||||
)
|
)
|
||||||
|
|
||||||
#c
|
#c
|
||||||
|
|
||||||
--- issue-5370-figure-caption-separator-outline ---
|
|
||||||
// Test that language-dependant caption separator is respected in outline.
|
|
||||||
#outline(title: none, target: figure)
|
|
||||||
#set text(lang: "ru")
|
|
||||||
#figure(rect(), caption: [Rectangle])
|
|
||||||
|
@ -97,6 +97,18 @@ comment spans lines
|
|||||||
= Fake level 2
|
= Fake level 2
|
||||||
== Fake level 3
|
== Fake level 3
|
||||||
|
|
||||||
|
--- heading-hanging-indent-auto ---
|
||||||
|
#set heading(numbering: "1.1.a.")
|
||||||
|
= State of the Art
|
||||||
|
|
||||||
|
--- heading-hanging-indent-zero ---
|
||||||
|
#set heading(numbering: "1.1.a.", hanging-indent: 0pt)
|
||||||
|
= State of the Art
|
||||||
|
|
||||||
|
--- heading-hanging-indent-length ---
|
||||||
|
#set heading(numbering: "1.1.a.", hanging-indent: 2em)
|
||||||
|
= State of the Art In Multi-Line
|
||||||
|
|
||||||
--- heading-offset-and-level ---
|
--- heading-offset-and-level ---
|
||||||
// Passing level directly still overrides all other set values
|
// Passing level directly still overrides all other set values
|
||||||
#set heading(numbering: "1.1", offset: 1)
|
#set heading(numbering: "1.1", offset: 1)
|
||||||
@ -136,3 +148,7 @@ Cannot be used as @intro
|
|||||||
// Hint: 1-16 HTML only supports <h1> to <h6>, not <h8>
|
// Hint: 1-16 HTML only supports <h1> to <h6>, not <h8>
|
||||||
// Hint: 1-16 you may want to restructure your document so that it doesn't contain deep headings
|
// Hint: 1-16 you may want to restructure your document so that it doesn't contain deep headings
|
||||||
======= Level 7
|
======= Level 7
|
||||||
|
|
||||||
|
--- issue-5719-heading-nested ---
|
||||||
|
// Headings may not be nested like this.
|
||||||
|
= = A
|
||||||
|
@ -276,3 +276,11 @@ World
|
|||||||
- h
|
- h
|
||||||
#align(right)[- i]
|
#align(right)[- i]
|
||||||
- j
|
- j
|
||||||
|
|
||||||
|
--- issue-5719-list-nested ---
|
||||||
|
// Lists can be immediately nested.
|
||||||
|
- A
|
||||||
|
- - B
|
||||||
|
- C
|
||||||
|
- = D
|
||||||
|
E
|
||||||
|
@ -1,10 +1,195 @@
|
|||||||
--- outline ---
|
--- outline-spacing ---
|
||||||
#set page(height: 200pt, margin: (bottom: 20pt), numbering: "1")
|
#set heading(numbering: "1.a.")
|
||||||
|
#set outline.entry(fill: none)
|
||||||
|
#show outline.entry.where(level: 1): set block(above: 1.2em)
|
||||||
|
|
||||||
|
#outline()
|
||||||
|
|
||||||
|
#show heading: none
|
||||||
|
= A
|
||||||
|
== B
|
||||||
|
== C
|
||||||
|
= D
|
||||||
|
== E
|
||||||
|
|
||||||
|
--- outline-indent-auto ---
|
||||||
|
#set heading(numbering: "I.i.")
|
||||||
|
#set page(width: 150pt)
|
||||||
|
#show heading: none
|
||||||
|
|
||||||
|
#context test(outline.indent, auto)
|
||||||
|
#outline()
|
||||||
|
|
||||||
|
= A
|
||||||
|
== B
|
||||||
|
== C
|
||||||
|
== D
|
||||||
|
=== Title that breaks across lines
|
||||||
|
= E
|
||||||
|
== F
|
||||||
|
=== Aligned
|
||||||
|
|
||||||
|
--- outline-indent-auto-mixed-prefix ---
|
||||||
|
#show heading: none
|
||||||
|
#show outline.entry.where(level: 1): strong
|
||||||
|
|
||||||
|
#outline()
|
||||||
|
|
||||||
|
#set heading(numbering: "I.i.")
|
||||||
|
= A
|
||||||
|
== B
|
||||||
|
=== Title that breaks
|
||||||
|
= C
|
||||||
|
== D
|
||||||
|
= E
|
||||||
|
#[
|
||||||
|
#set heading(numbering: none)
|
||||||
|
= F
|
||||||
|
== Numberless title that breaks
|
||||||
|
=== G
|
||||||
|
]
|
||||||
|
= H
|
||||||
|
|
||||||
|
--- outline-indent-auto-mixed-prefix-short ---
|
||||||
|
#show heading: none
|
||||||
|
|
||||||
|
#outline()
|
||||||
|
|
||||||
|
#set heading(numbering: "I.i.")
|
||||||
|
= A
|
||||||
|
#set heading(numbering: none)
|
||||||
|
= B
|
||||||
|
|
||||||
|
--- outline-indent-auto-no-prefix ---
|
||||||
|
#show heading: none
|
||||||
|
|
||||||
|
#outline()
|
||||||
|
|
||||||
|
= A
|
||||||
|
== B
|
||||||
|
=== Title that breaks across lines
|
||||||
|
= C
|
||||||
|
== D
|
||||||
|
=== E
|
||||||
|
|
||||||
|
--- outline-indent-zero ---
|
||||||
|
#set heading(numbering: "1.a.")
|
||||||
|
#show heading: none
|
||||||
|
|
||||||
|
#outline(indent: 0pt)
|
||||||
|
|
||||||
|
= A
|
||||||
|
== B
|
||||||
|
=== C
|
||||||
|
==== Title that breaks across lines
|
||||||
|
#set heading(numbering: none)
|
||||||
|
== E
|
||||||
|
= F
|
||||||
|
|
||||||
|
--- outline-indent-fixed ---
|
||||||
|
#set heading(numbering: "1.a.")
|
||||||
|
#show heading: none
|
||||||
|
|
||||||
|
#outline(indent: 1em)
|
||||||
|
|
||||||
|
= A
|
||||||
|
== B
|
||||||
|
=== C
|
||||||
|
==== Title that breaks
|
||||||
|
#set heading(numbering: none)
|
||||||
|
== E
|
||||||
|
= F
|
||||||
|
|
||||||
|
--- outline-indent-func ---
|
||||||
|
#set heading(numbering: "1.a.")
|
||||||
|
#show heading: none
|
||||||
|
|
||||||
|
#outline(indent: n => (0pt, 1em, 2.5em, 3em).at(n))
|
||||||
|
|
||||||
|
= A
|
||||||
|
== B
|
||||||
|
=== C
|
||||||
|
==== Title breaks
|
||||||
|
#set heading(numbering: none)
|
||||||
|
== E
|
||||||
|
= F
|
||||||
|
|
||||||
|
--- outline-indent-bad-type ---
|
||||||
|
// Error: 2-35 expected relative length, found dictionary
|
||||||
|
#outline(indent: n => (a: "dict"))
|
||||||
|
|
||||||
|
= Heading
|
||||||
|
|
||||||
|
--- outline-entry ---
|
||||||
|
#set page(width: 150pt)
|
||||||
|
#set heading(numbering: "1.")
|
||||||
|
|
||||||
|
#show outline.entry.where(level: 1): set block(above: 12pt)
|
||||||
|
#show outline.entry.where(level: 1): strong
|
||||||
|
|
||||||
|
#outline(indent: auto)
|
||||||
|
|
||||||
|
#show heading: none
|
||||||
|
= Introduction
|
||||||
|
= Background
|
||||||
|
== History
|
||||||
|
== State of the Art
|
||||||
|
= Analysis
|
||||||
|
== Setup
|
||||||
|
|
||||||
|
--- outline-entry-complex ---
|
||||||
|
#set page(width: 150pt, numbering: "I", margin: (bottom: 20pt))
|
||||||
|
#set heading(numbering: "1.")
|
||||||
|
|
||||||
|
#set outline.entry(fill: repeat[--])
|
||||||
|
#show outline.entry.where(level: 1): it => link(
|
||||||
|
it.element.location(),
|
||||||
|
it.indented(it.prefix(), {
|
||||||
|
emph(it.body())
|
||||||
|
[ ]
|
||||||
|
text(luma(100), box(width: 1fr, repeat[--·--]))
|
||||||
|
[ ]
|
||||||
|
it.page()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
#counter(page).update(3)
|
||||||
|
#outline()
|
||||||
|
|
||||||
|
#show heading: none
|
||||||
|
|
||||||
|
= Top heading
|
||||||
|
== Not top heading
|
||||||
|
=== Lower heading
|
||||||
|
=== Lower too
|
||||||
|
== Also not top
|
||||||
|
|
||||||
|
#pagebreak()
|
||||||
|
#set page(numbering: "1")
|
||||||
|
|
||||||
|
= Another top heading
|
||||||
|
== Middle heading
|
||||||
|
=== Lower heading
|
||||||
|
|
||||||
|
--- outline-entry-inner ---
|
||||||
|
#set heading(numbering: "1.")
|
||||||
|
#show outline.entry: it => block(it.inner())
|
||||||
|
#show heading: none
|
||||||
|
|
||||||
|
#set outline.entry(fill: repeat[ -- ])
|
||||||
|
#outline()
|
||||||
|
|
||||||
|
= A
|
||||||
|
= B
|
||||||
|
|
||||||
|
--- outline-heading-start-of-page ---
|
||||||
|
#set page(width: 140pt, height: 200pt, margin: (bottom: 20pt), numbering: "1")
|
||||||
#set heading(numbering: "(1/a)")
|
#set heading(numbering: "(1/a)")
|
||||||
#show heading.where(level: 1): set text(12pt)
|
#show heading.where(level: 1): set text(12pt)
|
||||||
#show heading.where(level: 2): set text(10pt)
|
#show heading.where(level: 2): set text(10pt)
|
||||||
|
|
||||||
#outline(fill: none)
|
#set outline.entry(fill: none)
|
||||||
|
#outline()
|
||||||
|
|
||||||
= A
|
= A
|
||||||
= B
|
= B
|
||||||
@ -23,66 +208,28 @@ A
|
|||||||
== F
|
== F
|
||||||
==== G
|
==== G
|
||||||
|
|
||||||
|
--- outline-bookmark ---
|
||||||
|
// Ensure that `bookmarked` option doesn't affect the outline
|
||||||
|
#set heading(numbering: "(I)", bookmarked: false)
|
||||||
|
#set outline.entry(fill: none)
|
||||||
|
#show heading: none
|
||||||
|
#outline()
|
||||||
|
|
||||||
|
= A
|
||||||
|
|
||||||
--- outline-styled-text ---
|
--- outline-styled-text ---
|
||||||
#outline(title: none)
|
#outline(title: none)
|
||||||
|
|
||||||
= #text(blue)[He]llo
|
= #text(blue)[He]llo
|
||||||
|
|
||||||
--- outline-bookmark ---
|
|
||||||
#outline(title: none, fill: none)
|
|
||||||
|
|
||||||
// Ensure 'bookmarked' option doesn't affect the outline
|
|
||||||
#set heading(numbering: "(I)", bookmarked: false)
|
|
||||||
= A
|
|
||||||
|
|
||||||
--- outline-indent-numbering ---
|
|
||||||
// With heading numbering
|
|
||||||
#set page(width: 200pt)
|
|
||||||
#set heading(numbering: "1.a.")
|
|
||||||
#show heading: none
|
|
||||||
#set outline(fill: none)
|
|
||||||
|
|
||||||
#context test(outline.indent, none)
|
|
||||||
#outline(indent: none)
|
|
||||||
#outline(indent: auto)
|
|
||||||
#outline(indent: 2em)
|
|
||||||
#outline(indent: n => ([-], [], [==], [====]).at(n))
|
|
||||||
|
|
||||||
= A
|
|
||||||
== B
|
|
||||||
== C
|
|
||||||
=== D
|
|
||||||
==== E
|
|
||||||
|
|
||||||
--- outline-indent-no-numbering ---
|
|
||||||
// Without heading numbering
|
|
||||||
#set page(width: 200pt)
|
|
||||||
#show heading: none
|
|
||||||
#set outline(fill: none)
|
|
||||||
|
|
||||||
#outline(indent: none)
|
|
||||||
#outline(indent: auto)
|
|
||||||
#outline(indent: n => 2em * n)
|
|
||||||
|
|
||||||
= About
|
|
||||||
== History
|
|
||||||
|
|
||||||
--- outline-indent-bad-type ---
|
|
||||||
// Error: 2-35 expected relative length or content, found dictionary
|
|
||||||
#outline(indent: n => (a: "dict"))
|
|
||||||
|
|
||||||
= Heading
|
|
||||||
|
|
||||||
--- outline-first-line-indent ---
|
--- outline-first-line-indent ---
|
||||||
#set par(first-line-indent: 1.5em)
|
#set par(first-line-indent: 1.5em)
|
||||||
#set heading(numbering: "1.1.a.")
|
#set heading(numbering: "1.1.a.")
|
||||||
#show outline.entry.where(level: 1): it => {
|
#show outline.entry.where(level: 1): strong
|
||||||
v(0.5em, weak: true)
|
|
||||||
strong(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
#outline()
|
#outline()
|
||||||
|
|
||||||
|
#show heading: none
|
||||||
= Introduction
|
= Introduction
|
||||||
= Background
|
= Background
|
||||||
== History
|
== History
|
||||||
@ -90,85 +237,54 @@ A
|
|||||||
= Analysis
|
= Analysis
|
||||||
== Setup
|
== Setup
|
||||||
|
|
||||||
--- outline-entry ---
|
|
||||||
#set page(width: 150pt)
|
|
||||||
#set heading(numbering: "1.")
|
|
||||||
|
|
||||||
#show outline.entry.where(
|
|
||||||
level: 1
|
|
||||||
): it => {
|
|
||||||
v(12pt, weak: true)
|
|
||||||
strong(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
#outline(indent: auto)
|
|
||||||
#v(1.2em, weak: true)
|
|
||||||
|
|
||||||
#set text(8pt)
|
|
||||||
#show heading: set block(spacing: 0.65em)
|
|
||||||
|
|
||||||
= Introduction
|
|
||||||
= Background
|
|
||||||
== History
|
|
||||||
== State of the Art
|
|
||||||
= Analysis
|
|
||||||
== Setup
|
|
||||||
|
|
||||||
--- outline-entry-complex ---
|
|
||||||
#set page(width: 150pt, numbering: "I", margin: (bottom: 20pt))
|
|
||||||
#set heading(numbering: "1.")
|
|
||||||
#show outline.entry.where(level: 1): it => [
|
|
||||||
#let loc = it.element.location()
|
|
||||||
#let num = numbering(loc.page-numbering(), ..counter(page).at(loc))
|
|
||||||
#emph(link(loc, it.body))
|
|
||||||
#text(luma(100), box(width: 1fr, repeat[#it.fill.body;·]))
|
|
||||||
#link(loc, num)
|
|
||||||
]
|
|
||||||
|
|
||||||
#counter(page).update(3)
|
|
||||||
#outline(indent: auto, fill: repeat[--])
|
|
||||||
#v(1.2em, weak: true)
|
|
||||||
|
|
||||||
#set text(8pt)
|
|
||||||
#show heading: set block(spacing: 0.65em)
|
|
||||||
|
|
||||||
= Top heading
|
|
||||||
== Not top heading
|
|
||||||
=== Lower heading
|
|
||||||
=== Lower too
|
|
||||||
== Also not top
|
|
||||||
|
|
||||||
#pagebreak()
|
|
||||||
#set page(numbering: "1")
|
|
||||||
|
|
||||||
= Another top heading
|
|
||||||
== Middle heading
|
|
||||||
=== Lower heading
|
|
||||||
|
|
||||||
--- outline-bad-element ---
|
--- outline-bad-element ---
|
||||||
// Error: 2-27 cannot outline metadata
|
// Error: 2-27 cannot outline metadata
|
||||||
#outline(target: metadata)
|
#outline(target: metadata)
|
||||||
#metadata("hello")
|
#metadata("hello")
|
||||||
|
|
||||||
|
|
||||||
|
--- issue-2048-outline-multiline ---
|
||||||
|
// Without the word joiner between the dots and the page number,
|
||||||
|
// the page number would be alone in its line.
|
||||||
|
#set page(width: 125pt)
|
||||||
|
#set heading(numbering: "1.a.")
|
||||||
|
#show heading: none
|
||||||
|
|
||||||
|
#outline()
|
||||||
|
|
||||||
|
= A
|
||||||
|
== This just fits here
|
||||||
|
|
||||||
--- issue-2530-outline-entry-panic-text ---
|
--- issue-2530-outline-entry-panic-text ---
|
||||||
// Outline entry (pre-emptive)
|
// Outline entry (pre-emptive)
|
||||||
// Error: 2-48 cannot outline text
|
// Error: 2-27 cannot outline text
|
||||||
#outline.entry(1, [Hello], [World!], none, [1])
|
#outline.entry(1, [Hello])
|
||||||
|
|
||||||
--- issue-2530-outline-entry-panic-heading ---
|
--- issue-2530-outline-entry-panic-heading ---
|
||||||
// Outline entry (pre-emptive, improved error)
|
// Outline entry (pre-emptive, improved error)
|
||||||
// Error: 2-55 heading must have a location
|
// Error: 2-34 heading must have a location
|
||||||
// Hint: 2-55 try using a query or a show rule to customize the outline.entry instead
|
// Hint: 2-34 try using a show rule to customize the outline.entry instead
|
||||||
#outline.entry(1, heading[Hello], [World!], none, [1])
|
#outline.entry(1, heading[Hello])
|
||||||
|
|
||||||
--- issue-4476-rtl-title-ending-in-ltr-text ---
|
--- issue-4476-outline-rtl-title-ending-in-ltr-text ---
|
||||||
#set text(lang: "he")
|
#set text(lang: "he")
|
||||||
#outline()
|
#outline()
|
||||||
|
|
||||||
|
#show heading: none
|
||||||
= הוקוס Pocus
|
= הוקוס Pocus
|
||||||
= זוהי כותרת שתורגמה על ידי מחשב
|
= זוהי כותרת שתורגמה על ידי מחשב
|
||||||
|
|
||||||
--- issue-5176-cjk-title ---
|
--- issue-4859-outline-entry-show-set ---
|
||||||
|
#set heading(numbering: "1.a.")
|
||||||
|
#show outline.entry.where(level: 1): set outline.entry(fill: none)
|
||||||
|
#show heading: none
|
||||||
|
|
||||||
|
#outline()
|
||||||
|
|
||||||
|
= A
|
||||||
|
== B
|
||||||
|
|
||||||
|
--- issue-5176-outline-cjk-title ---
|
||||||
#set text(font: "Noto Serif CJK SC")
|
#set text(font: "Noto Serif CJK SC")
|
||||||
#show heading: none
|
#show heading: none
|
||||||
|
|
||||||
|
@ -84,3 +84,26 @@ And I quote: #quote(attribution: [René Descartes])[cogito, ergo sum].
|
|||||||
// With custom quotes.
|
// With custom quotes.
|
||||||
#set smartquote(quotes: (single: ("<", ">"), double: ("(", ")")))
|
#set smartquote(quotes: (single: ("<", ">"), double: ("(", ")")))
|
||||||
#quote[A #quote[nested] quote]
|
#quote[A #quote[nested] quote]
|
||||||
|
|
||||||
|
--- quote-plato html ---
|
||||||
|
#set quote(block: true)
|
||||||
|
|
||||||
|
#quote(attribution: [Plato])[
|
||||||
|
... ἔοικα γοῦν τούτου γε σμικρῷ τινι αὐτῷ τούτῳ σοφώτερος εἶναι, ὅτι
|
||||||
|
ἃ μὴ οἶδα οὐδὲ οἴομαι εἰδέναι.
|
||||||
|
]
|
||||||
|
#quote(attribution: [from the Henry Cary literal translation of 1897])[
|
||||||
|
... I seem, then, in just this little thing to be wiser than this man at
|
||||||
|
any rate, that what I do not know I do not think I know either.
|
||||||
|
]
|
||||||
|
|
||||||
|
--- quote-nesting-html html ---
|
||||||
|
When you said that #quote[he surely meant that #quote[she intended to say #quote[I'm sorry]]], I was quite confused.
|
||||||
|
|
||||||
|
--- quote-attribution-link html ---
|
||||||
|
#quote(
|
||||||
|
block: true,
|
||||||
|
attribution: link("https://typst.app/home")[typst.com]
|
||||||
|
)[
|
||||||
|
Compose papers faster
|
||||||
|
]
|
||||||
|
@ -90,3 +90,9 @@ Not in list
|
|||||||
/ h: h
|
/ h: h
|
||||||
#align(right)[/ i: i]
|
#align(right)[/ i: i]
|
||||||
/ j: j
|
/ j: j
|
||||||
|
|
||||||
|
--- issue-5719-terms-nested ---
|
||||||
|
// Term lists can be immediately nested.
|
||||||
|
/ Term A: 1
|
||||||
|
/ Term B: / Term C: 2
|
||||||
|
/ Term D: 3
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
#numbers.fun()
|
#numbers.fun()
|
||||||
|
|
||||||
--- method-unknown-but-field-exists ---
|
--- method-unknown-but-field-exists ---
|
||||||
// Error: 2:4-2:10 type content has no method `stroke`
|
// Error: 2:4-2:10 element line has no method `stroke`
|
||||||
// Hint: 2:4-2:10 did you mean to access the field `stroke`?
|
// Hint: 2:4-2:10 did you mean to access the field `stroke`?
|
||||||
#let l = line(stroke: red)
|
#let l = line(stroke: red)
|
||||||
#l.stroke()
|
#l.stroke()
|
||||||
|
7
tests/suite/syntax/shebang.typ
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// Test shebang support.
|
||||||
|
|
||||||
|
--- shebang ---
|
||||||
|
#!typst compile
|
||||||
|
|
||||||
|
// Error: 2-3 the character `!` is not valid in code
|
||||||
|
#!not-a-shebang
|