Compare commits

...

8 Commits

Author SHA1 Message Date
Tobias Schmitz
2242b7f598
Merge 9649def1081720d351def471cceb735fb594ea9e into b790c6d59ceaf7a809cc24b60c1f1509807470e2 2025-07-18 16:47:12 +02:00
Tobias Schmitz
9649def108
feat: add alt parameter to math.equation 2025-07-18 16:38:54 +02:00
Tobias Schmitz
d2105dcc35
feat: report spans for missing alt text and unknown/duplicate tag ids 2025-07-18 16:38:54 +02:00
Erik
b790c6d59c
Add rust-analyzer to flake devShell (#6618) 2025-07-18 14:36:10 +00:00
Malo
b1c79b50d4
Fix documentation oneliners (#6608) 2025-07-18 13:25:17 +00:00
Patrick Massot
4629ede020
Mention Tinymist in README.md (#6601) 2025-07-18 13:21:36 +00:00
Lachlan Kermode
627f5b9d4f
Add show rule for smallcaps in HTML (#6600) 2025-07-17 16:09:13 +00:00
Robin
5661c20580
Slightly improve selector docs (#6544) 2025-07-16 16:15:49 +00:00
18 changed files with 166 additions and 55 deletions

4
Cargo.lock generated
View File

@ -1373,7 +1373,7 @@ dependencies = [
[[package]] [[package]]
name = "krilla" name = "krilla"
version = "0.4.0" version = "0.4.0"
source = "git+https://github.com/LaurenzV/krilla?branch=main#d40f81a01ca8f8654510a76effeef12518437800" source = "git+https://github.com/LaurenzV/krilla?branch=main#32d070e737cd8ae4c3aa4ff901d15cb22bd052f3"
dependencies = [ dependencies = [
"base64", "base64",
"bumpalo", "bumpalo",
@ -1402,7 +1402,7 @@ dependencies = [
[[package]] [[package]]
name = "krilla-svg" name = "krilla-svg"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/LaurenzV/krilla?branch=main#d40f81a01ca8f8654510a76effeef12518437800" source = "git+https://github.com/LaurenzV/krilla?branch=main#32d070e737cd8ae4c3aa4ff901d15cb22bd052f3"
dependencies = [ dependencies = [
"flate2", "flate2",
"fontdb", "fontdb",

View File

@ -173,8 +173,11 @@ typst help
typst help watch typst help watch
``` ```
If you prefer an integrated IDE-like experience with autocompletion and instant If you prefer an integrated IDE-like experience with autocompletion and instant
preview, you can also check out [Typst's free web app][app]. preview, you can also check out our [free web app][app]. Alternatively, there is
a community-created language server called
[Tinymist](https://myriad-dreamin.github.io/tinymist/) which is integrated into
various editor extensions.
## Community ## Community
The main places where the community gathers are our [Forum][forum] and our The main places where the community gathers are our [Forum][forum] and our

View File

@ -14,8 +14,8 @@ use typst_library::model::{
RefElem, StrongElem, TableCell, TableElem, TermsElem, RefElem, StrongElem, TableCell, TableElem, TermsElem,
}; };
use typst_library::text::{ use typst_library::text::{
HighlightElem, LinebreakElem, OverlineElem, RawElem, RawLine, SpaceElem, StrikeElem, HighlightElem, LinebreakElem, OverlineElem, RawElem, RawLine, SmallcapsElem,
SubElem, SuperElem, UnderlineElem, SpaceElem, StrikeElem, SubElem, SuperElem, UnderlineElem,
}; };
use typst_library::visualize::ImageElem; use typst_library::visualize::ImageElem;
@ -47,6 +47,7 @@ pub fn register(rules: &mut NativeRuleMap) {
rules.register(Html, OVERLINE_RULE); rules.register(Html, OVERLINE_RULE);
rules.register(Html, STRIKE_RULE); rules.register(Html, STRIKE_RULE);
rules.register(Html, HIGHLIGHT_RULE); rules.register(Html, HIGHLIGHT_RULE);
rules.register(Html, SMALLCAPS_RULE);
rules.register(Html, RAW_RULE); rules.register(Html, RAW_RULE);
rules.register(Html, RAW_LINE_RULE); rules.register(Html, RAW_LINE_RULE);
@ -390,6 +391,20 @@ const STRIKE_RULE: ShowFn<StrikeElem> =
const HIGHLIGHT_RULE: ShowFn<HighlightElem> = const HIGHLIGHT_RULE: ShowFn<HighlightElem> =
|elem, _, _| Ok(HtmlElem::new(tag::mark).with_body(Some(elem.body.clone())).pack()); |elem, _, _| Ok(HtmlElem::new(tag::mark).with_body(Some(elem.body.clone())).pack());
const SMALLCAPS_RULE: ShowFn<SmallcapsElem> = |elem, _, styles| {
Ok(HtmlElem::new(tag::span)
.with_attr(
attr::style,
if elem.all.get(styles) {
"font-variant-caps: all-small-caps"
} else {
"font-variant-caps: small-caps"
},
)
.with_body(Some(elem.body.clone()))
.pack())
};
const RAW_RULE: ShowFn<RawElem> = |elem, _, styles| { const RAW_RULE: ShowFn<RawElem> = |elem, _, styles| {
let lines = elem.lines.as_deref().unwrap_or_default(); let lines = elem.lines.as_deref().unwrap_or_default();

View File

@ -23,10 +23,10 @@ use serde::{Serialize, Serializer};
use typst_syntax::Span; use typst_syntax::Span;
use typst_utils::singleton; use typst_utils::singleton;
use crate::diag::{SourceResult, StrResult}; use crate::diag::{bail, SourceResult, StrResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
func, repr, scope, ty, Context, Dict, IntoValue, Label, Property, Recipe, func, repr, scope, ty, Args, Context, Dict, IntoValue, Label, Property, Recipe,
RecipeIndex, Repr, Selector, Str, Style, StyleChain, Styles, Value, RecipeIndex, Repr, Selector, Str, Style, StyleChain, Styles, Value,
}; };
use crate::introspection::{Locatable, Location}; use crate::introspection::{Locatable, Location};
@ -479,7 +479,7 @@ impl Content {
/// Link the content somewhere. /// Link the content somewhere.
pub fn linked(self, dest: Destination, alt: Option<EcoString>) -> Self { pub fn linked(self, dest: Destination, alt: Option<EcoString>) -> Self {
let span = self.span(); let span = self.span();
LinkMarker::new(self, dest.clone(), alt) LinkMarker::new(self, dest.clone(), alt, span)
.pack() .pack()
.spanned(span) .spanned(span)
.set(LinkElem::current, Some(dest)) .set(LinkElem::current, Some(dest))
@ -785,15 +785,27 @@ impl Repr for StyledElem {
} }
/// An element that associates the body of a link with the destination. /// An element that associates the body of a link with the destination.
#[elem(Locatable)] #[elem(Locatable, Construct)]
pub struct LinkMarker { pub struct LinkMarker {
/// The content. /// The content.
#[internal]
#[required] #[required]
pub body: Content, pub body: Content,
#[internal]
#[required] #[required]
pub dest: Destination, pub dest: Destination,
#[internal]
#[required] #[required]
pub alt: Option<EcoString>, pub alt: Option<EcoString>,
#[internal]
#[required]
pub span: Span,
}
impl Construct for LinkMarker {
fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> {
bail!(args.span, "cannot be constructed manually");
}
} }
impl<T: NativeElement> IntoValue for T { impl<T: NativeElement> IntoValue for T {

View File

@ -37,13 +37,12 @@ pub use crate::__select_where as select_where;
/// A filter for selecting elements within the document. /// A filter for selecting elements within the document.
/// ///
/// You can construct a selector in the following ways: /// To construct a selector you can:
/// - you can use an element [function] /// - use an element [function]
/// - you can filter for an element function with /// - filter for an element function with [specific fields]($function.where)
/// [specific fields]($function.where) /// - use a [string]($str) or [regular expression]($regex)
/// - you can use a [string]($str) or [regular expression]($regex) /// - use a [`{<label>}`]($label)
/// - you can use a [`{<label>}`]($label) /// - use a [`location`]
/// - you can use a [`location`]
/// - call the [`selector`] constructor to convert any of the above types into a /// - call the [`selector`] constructor to convert any of the above types into a
/// selector value and use the methods below to refine it /// selector value and use the methods below to refine it
/// ///
@ -148,7 +147,9 @@ impl Selector {
impl Selector { impl Selector {
/// Turns a value into a selector. The following values are accepted: /// Turns a value into a selector. The following values are accepted:
/// - An element function like a `heading` or `figure`. /// - An element function like a `heading` or `figure`.
/// - A [string]($str) or [regular expression]($regex).
/// - A `{<label>}`. /// - A `{<label>}`.
/// - A [`location`].
/// - A more complex selector like `{heading.where(level: 1)}`. /// - A more complex selector like `{heading.where(level: 1)}`.
#[func(constructor)] #[func(constructor)]
pub fn construct( pub fn construct(

View File

@ -1,6 +1,7 @@
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use codex::styling::MathVariant; use codex::styling::MathVariant;
use ecow::EcoString;
use typst_utils::NonZeroExt; use typst_utils::NonZeroExt;
use unicode_math_class::MathClass; use unicode_math_class::MathClass;
@ -47,6 +48,9 @@ use crate::text::{FontFamily, FontList, FontWeight, LocalName, TextElem};
/// [main math page]($category/math). /// [main math page]($category/math).
#[elem(Locatable, Synthesize, ShowSet, Count, LocalName, Refable, Outlinable)] #[elem(Locatable, Synthesize, ShowSet, Count, LocalName, Refable, Outlinable)]
pub struct EquationElem { pub struct EquationElem {
/// An alternative description of the mathematical equation.
pub alt: Option<EcoString>,
/// Whether the equation is displayed as a separate block. /// Whether the equation is displayed as a separate block.
#[default(false)] #[default(false)]
pub block: bool, pub block: bool,

View File

@ -88,7 +88,7 @@ use crate::text::{LocalName, TextElem};
/// generated. /// generated.
#[elem(Locatable)] #[elem(Locatable)]
pub struct LinkElem { pub struct LinkElem {
/// A text describing the link. /// An alternative description of the link.
pub alt: Option<EcoString>, pub alt: Option<EcoString>,
/// The destination the link points to. /// The destination the link points to.

View File

@ -797,7 +797,9 @@ impl Color {
components components
} }
/// Returns the constructor function for this color's space: /// Returns the constructor function for this color's space.
///
/// Returns one of:
/// - [`luma`]($color.luma) /// - [`luma`]($color.luma)
/// - [`oklab`]($color.oklab) /// - [`oklab`]($color.oklab)
/// - [`oklch`]($color.oklch) /// - [`oklch`]($color.oklch)

View File

@ -124,7 +124,7 @@ pub struct ImageElem {
/// The height of the image. /// The height of the image.
pub height: Sizing, pub height: Sizing,
/// A text describing the image. /// An alternative description of the image.
pub alt: Option<EcoString>, pub alt: Option<EcoString>,
/// How the image should adjust itself to a given area (the area is defined /// How the image should adjust itself to a given area (the area is defined

View File

@ -9,6 +9,7 @@ use krilla::error::KrillaError;
use krilla::geom::PathBuilder; use krilla::geom::PathBuilder;
use krilla::page::{PageLabel, PageSettings}; use krilla::page::{PageLabel, PageSettings};
use krilla::surface::Surface; use krilla::surface::Surface;
use krilla::tagging::TagId;
use krilla::{Document, SerializeSettings}; use krilla::{Document, SerializeSettings};
use krilla_svg::render_svg_glyph; use krilla_svg::render_svg_glyph;
use typst_library::diag::{bail, error, SourceDiagnostic, SourceResult}; use typst_library::diag::{bail, error, SourceDiagnostic, SourceResult};
@ -373,11 +374,21 @@ fn finish(
.collect::<EcoVec<_>>(); .collect::<EcoVec<_>>();
Err(errors) Err(errors)
} }
KrillaError::DuplicateTagId(_, _) => { KrillaError::DuplicateTagId(id, loc) => {
unreachable!("duplicate IDs shouldn't be generated") let span = to_span(loc);
let id = display_tag_id(&id);
bail!(
span, "duplicate tag id `{id}`";
hint: "please report this as a bug"
)
} }
KrillaError::UnknownTagId(_, _) => { KrillaError::UnknownTagId(id, loc) => {
unreachable!("all referenced IDs should be present in the tag tree") let span = to_span(loc);
let id = display_tag_id(&id);
bail!(
span, "unknown tag id `{id}`";
hint: "please report this as a bug"
)
} }
KrillaError::Image(_, loc) => { KrillaError::Image(_, loc) => {
let span = to_span(loc); let span = to_span(loc);
@ -394,6 +405,20 @@ fn finish(
} }
} }
fn display_tag_id(id: &TagId) -> impl std::fmt::Display + use<'_> {
typst_utils::display(|f| {
if let Ok(str) = std::str::from_utf8(id.as_bytes()) {
f.write_str(str)
} else {
f.write_str("0x")?;
for b in id.as_bytes() {
write!(f, "{b:x}")?;
}
Ok(())
}
})
}
/// Converts a krilla error into a Typst error. /// Converts a krilla error into a Typst error.
fn convert_error( fn convert_error(
gc: &GlobalContext, gc: &GlobalContext,
@ -562,16 +587,20 @@ fn convert_error(
} }
// The below errors cannot occur yet, only once Typst supports full PDF/A // The below errors cannot occur yet, only once Typst supports full PDF/A
// and PDF/UA. But let's still add a message just to be on the safe side. // and PDF/UA. But let's still add a message just to be on the safe side.
ValidationError::MissingAnnotationAltText => error!( ValidationError::MissingAnnotationAltText(loc) => {
Span::detached(), let span = to_span(*loc);
"{prefix} missing annotation alt text"; error!(
hint: "please report this as a bug" span, "{prefix} missing annotation alt text";
), hint: "please report this as a bug"
ValidationError::MissingAltText => error!( )
Span::detached(), }
"{prefix} missing alt text"; ValidationError::MissingAltText(loc) => {
hint: "make sure your images and equations have alt text" let span = to_span(*loc);
), error!(
span, "{prefix} missing alt text";
hint: "make sure your images and equations have alt text"
)
}
ValidationError::NoDocumentLanguage => error!( ValidationError::NoDocumentLanguage => error!(
Span::detached(), Span::detached(),
"{prefix} missing document language"; "{prefix} missing document language";

View File

@ -6,6 +6,7 @@ use krilla::destination::XyzDestination;
use krilla::geom as kg; use krilla::geom as kg;
use typst_library::layout::{Point, Position, Size}; use typst_library::layout::{Point, Position, Size};
use typst_library::model::Destination; use typst_library::model::Destination;
use typst_syntax::Span;
use crate::convert::{FrameContext, GlobalContext}; use crate::convert::{FrameContext, GlobalContext};
use crate::tags::{self, Placeholder, TagNode}; use crate::tags::{self, Placeholder, TagNode};
@ -17,6 +18,7 @@ pub(crate) struct LinkAnnotation {
pub(crate) alt: Option<String>, pub(crate) alt: Option<String>,
pub(crate) quad_points: Vec<kg::Quadrilateral>, pub(crate) quad_points: Vec<kg::Quadrilateral>,
pub(crate) target: Target, pub(crate) target: Target,
pub(crate) span: Span,
} }
pub(crate) fn handle_link( pub(crate) fn handle_link(
@ -70,6 +72,7 @@ pub(crate) fn handle_link(
quad_points: vec![quad], quad_points: vec![quad],
alt, alt,
target, target,
span: link.span,
}); });
} }
} }

View File

@ -123,9 +123,9 @@ pub(crate) fn handle_start(
} else { } else {
TagKind::Figure.with_alt_text(alt) TagKind::Figure.with_alt_text(alt)
} }
} else if let Some(_) = elem.to_packed::<EquationElem>() { } else if let Some(equation) = elem.to_packed::<EquationElem>() {
// TODO: alt text let alt = equation.alt.get_as_ref().map(|s| s.to_string());
TagKind::Formula.into() TagKind::Formula.with_alt_text(alt)
} else if let Some(table) = elem.to_packed::<TableElem>() { } else if let Some(table) = elem.to_packed::<TableElem>() {
let table_id = gc.tags.next_table_id(); let table_id = gc.tags.next_table_id();
let summary = table.summary.get_as_ref().map(|s| s.to_string()); let summary = table.summary.get_as_ref().map(|s| s.to_string());
@ -176,6 +176,7 @@ pub(crate) fn handle_start(
return Ok(()); return Ok(());
}; };
let tag = tag.with_location(Some(elem.span().into_raw().get()));
push_stack(gc, loc, StackEntryKind::Standard(tag))?; push_stack(gc, loc, StackEntryKind::Standard(tag))?;
Ok(()) Ok(())
@ -202,7 +203,8 @@ pub(crate) fn handle_end(gc: &mut GlobalContext, surface: &mut Surface, loc: Loc
// PDF/UA compliance of the structure hierarchy is checked // PDF/UA compliance of the structure hierarchy is checked
// elsewhere. While this doesn't make a lot of sense, just // elsewhere. While this doesn't make a lot of sense, just
// avoid crashing here. // avoid crashing here.
let tag = TagKind::TOCI.into(); let tag = TagKind::TOCI
.with_location(Some(outline_entry.span().into_raw().get()));
gc.tags.push(TagNode::Group(tag, entry.nodes)); gc.tags.push(TagNode::Group(tag, entry.nodes));
return; return;
}; };
@ -216,7 +218,8 @@ pub(crate) fn handle_end(gc: &mut GlobalContext, surface: &mut Surface, loc: Loc
// PDF/UA compliance of the structure hierarchy is checked // PDF/UA compliance of the structure hierarchy is checked
// elsewhere. While this doesn't make a lot of sense, just // elsewhere. While this doesn't make a lot of sense, just
// avoid crashing here. // avoid crashing here.
let tag = TagKind::TD(TableDataCell::new()).into(); let tag = TagKind::TD(TableDataCell::new())
.with_location(Some(cell.span().into_raw().get()));
gc.tags.push(TagNode::Group(tag, entry.nodes)); gc.tags.push(TagNode::Group(tag, entry.nodes));
return; return;
}; };
@ -324,11 +327,13 @@ pub(crate) fn add_annotations(
annotations: Vec<LinkAnnotation>, annotations: Vec<LinkAnnotation>,
) { ) {
for annotation in annotations.into_iter() { for annotation in annotations.into_iter() {
let LinkAnnotation { id: _, placeholder, alt, quad_points, target } = annotation; let LinkAnnotation { id: _, placeholder, alt, quad_points, target, span } =
annotation;
let annot = krilla::annotation::Annotation::new_link( let annot = krilla::annotation::Annotation::new_link(
krilla::annotation::LinkAnnotation::new_with_quad_points(quad_points, target), krilla::annotation::LinkAnnotation::new_with_quad_points(quad_points, target),
alt, alt,
); )
.with_location(Some(span.into_raw().get()));
let annot_id = page.add_tagged_annotation(annot); let annot_id = page.add_tagged_annotation(annot);
gc.tags.placeholders.init(placeholder, Node::Leaf(annot_id)); gc.tags.placeholders.init(placeholder, Node::Leaf(annot_id));
} }

View File

@ -9,6 +9,7 @@ use smallvec::SmallVec;
use typst_library::foundations::{Packed, Smart, StyleChain}; use typst_library::foundations::{Packed, Smart, StyleChain};
use typst_library::model::TableCell; use typst_library::model::TableCell;
use typst_library::pdf::{TableCellKind, TableHeaderScope}; use typst_library::pdf::{TableCellKind, TableHeaderScope};
use typst_syntax::Span;
use crate::tags::{TableId, TagNode}; use crate::tags::{TableId, TagNode};
@ -57,7 +58,7 @@ impl TableCtx {
} }
} }
pub(crate) fn insert(&mut self, cell: &TableCell, nodes: Vec<TagNode>) { pub(crate) fn insert(&mut self, cell: &Packed<TableCell>, nodes: Vec<TagNode>) {
let x = cell.x.get(StyleChain::default()).unwrap_or_else(|| unreachable!()); let x = cell.x.get(StyleChain::default()).unwrap_or_else(|| unreachable!());
let y = cell.y.get(StyleChain::default()).unwrap_or_else(|| unreachable!()); let y = cell.y.get(StyleChain::default()).unwrap_or_else(|| unreachable!());
let rowspan = cell.rowspan.get(StyleChain::default()); let rowspan = cell.rowspan.get(StyleChain::default());
@ -92,6 +93,7 @@ impl TableCtx {
kind, kind,
headers: SmallVec::new(), headers: SmallVec::new(),
nodes, nodes,
span: cell.span(),
}); });
} }
@ -175,13 +177,14 @@ impl TableCtx {
.with_headers(cell.headers), .with_headers(cell.headers),
) )
.with_id(Some(id)) .with_id(Some(id))
.with_location(Some(cell.span.into_raw().get()))
} }
TableCellKind::Footer | TableCellKind::Data => TagKind::TD( TableCellKind::Footer | TableCellKind::Data => TagKind::TD(
TableDataCell::new() TableDataCell::new()
.with_span(span) .with_span(span)
.with_headers(cell.headers), .with_headers(cell.headers),
) )
.into(), .with_location(Some(cell.span.into_raw().get())),
}; };
Some(TagNode::Group(tag, cell.nodes)) Some(TagNode::Group(tag, cell.nodes))
}) })
@ -296,6 +299,7 @@ struct TableCtxCell {
kind: Smart<TableCellKind>, kind: Smart<TableCellKind>,
headers: SmallVec<[TagId; 1]>, headers: SmallVec<[TagId; 1]>,
nodes: Vec<TagNode>, nodes: Vec<TagNode>,
span: Span,
} }
impl TableCtxCell { impl TableCtxCell {
@ -344,7 +348,7 @@ mod tests {
fn table<const SIZE: usize>(cells: [TableCell; SIZE]) -> TableCtx { fn table<const SIZE: usize>(cells: [TableCell; SIZE]) -> TableCtx {
let mut table = TableCtx::new(TableId(324), Some("summary".into())); let mut table = TableCtx::new(TableId(324), Some("summary".into()));
for cell in cells { for cell in cells {
table.insert(&cell, Vec::new()); table.insert(&Packed::new(cell), Vec::new());
} }
table table
} }
@ -416,7 +420,9 @@ mod tests {
let id = table_cell_id(TableId(324), x, y); let id = table_cell_id(TableId(324), x, y);
let ids = headers.map(|(x, y)| table_cell_id(TableId(324), x, y)); let ids = headers.map(|(x, y)| table_cell_id(TableId(324), x, y));
TagNode::Group( TagNode::Group(
TagKind::TH(TableHeaderCell::new(scope).with_headers(ids)).with_id(Some(id)), TagKind::TH(TableHeaderCell::new(scope).with_headers(ids))
.with_id(Some(id))
.with_location(Some(Span::detached().into_raw().get())),
Vec::new(), Vec::new(),
) )
} }
@ -424,7 +430,8 @@ mod tests {
fn td<const SIZE: usize>(headers: [(u32, u32); SIZE]) -> TagNode { fn td<const SIZE: usize>(headers: [(u32, u32); SIZE]) -> TagNode {
let ids = headers.map(|(x, y)| table_cell_id(TableId(324), x, y)); let ids = headers.map(|(x, y)| table_cell_id(TableId(324), x, y));
TagNode::Group( TagNode::Group(
TagKind::TD(TableDataCell::new().with_headers(ids)).into(), TagKind::TD(TableDataCell::new().with_headers(ids))
.with_location(Some(Span::detached().into_raw().get())),
Vec::new(), Vec::new(),
) )
} }

View File

@ -242,7 +242,7 @@ fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel {
items.push(CategoryItem { items.push(CategoryItem {
name: group.name.clone(), name: group.name.clone(),
route: subpage.route.clone(), route: subpage.route.clone(),
oneliner: oneliner(docs).into(), oneliner: oneliner(docs),
code: true, code: true,
}); });
children.push(subpage); children.push(subpage);
@ -296,7 +296,7 @@ fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel {
items.push(CategoryItem { items.push(CategoryItem {
name: name.into(), name: name.into(),
route: subpage.route.clone(), route: subpage.route.clone(),
oneliner: oneliner(func.docs().unwrap_or_default()).into(), oneliner: oneliner(func.docs().unwrap_or_default()),
code: true, code: true,
}); });
children.push(subpage); children.push(subpage);
@ -306,7 +306,7 @@ fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel {
items.push(CategoryItem { items.push(CategoryItem {
name: ty.short_name().into(), name: ty.short_name().into(),
route: subpage.route.clone(), route: subpage.route.clone(),
oneliner: oneliner(ty.docs()).into(), oneliner: oneliner(ty.docs()),
code: true, code: true,
}); });
children.push(subpage); children.push(subpage);
@ -637,7 +637,7 @@ fn group_page(
let item = CategoryItem { let item = CategoryItem {
name: group.name.clone(), name: group.name.clone(),
route: model.route.clone(), route: model.route.clone(),
oneliner: oneliner(&group.details).into(), oneliner: oneliner(&group.details),
code: false, code: false,
}; };
@ -772,8 +772,24 @@ pub fn urlify(title: &str) -> EcoString {
} }
/// Extract the first line of documentation. /// Extract the first line of documentation.
fn oneliner(docs: &str) -> &str { fn oneliner(docs: &str) -> EcoString {
docs.lines().next().unwrap_or_default() let paragraph = docs.split("\n\n").next().unwrap_or_default();
let mut depth = 0;
let mut period = false;
let mut end = paragraph.len();
for (i, c) in paragraph.char_indices() {
match c {
'(' | '[' | '{' => depth += 1,
')' | ']' | '}' => depth -= 1,
'.' if depth == 0 => period = true,
c if period && c.is_whitespace() && !docs[..i].ends_with("e.g.") => {
end = i;
break;
}
_ => period = false,
}
}
EcoString::from(&docs[..end]).replace("\r\n", " ").replace("\n", " ")
} }
/// The order of types in the documentation. /// The order of types in the documentation.

View File

@ -86,7 +86,7 @@ pub struct FuncModel {
pub name: EcoString, pub name: EcoString,
pub title: &'static str, pub title: &'static str,
pub keywords: &'static [&'static str], pub keywords: &'static [&'static str],
pub oneliner: &'static str, pub oneliner: EcoString,
pub element: bool, pub element: bool,
pub contextual: bool, pub contextual: bool,
pub deprecation: Option<&'static str>, pub deprecation: Option<&'static str>,
@ -139,7 +139,7 @@ pub struct TypeModel {
pub name: &'static str, pub name: &'static str,
pub title: &'static str, pub title: &'static str,
pub keywords: &'static [&'static str], pub keywords: &'static [&'static str],
pub oneliner: &'static str, pub oneliner: EcoString,
pub details: Html, pub details: Html,
pub constructor: Option<FuncModel>, pub constructor: Option<FuncModel>,
pub scope: Vec<FuncModel>, pub scope: Vec<FuncModel>,

View File

@ -127,6 +127,10 @@
checks = self'.checks; checks = self'.checks;
inputsFrom = [ typst ]; inputsFrom = [ typst ];
buildInputs = with pkgs; [
rust-analyzer
];
packages = [ packages = [
# A script for quickly running tests. # A script for quickly running tests.
# See https://github.com/typst/typst/blob/main/tests/README.md#making-an-alias # See https://github.com/typst/typst/blob/main/tests/README.md#making-an-alias

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<p><span style="font-variant-caps: small-caps">Test 012</span><br><span style="font-variant-caps: all-small-caps">Test 012</span></p>
</body>
</html>

View File

@ -11,6 +11,6 @@
#show smallcaps: set text(fill: red) #show smallcaps: set text(fill: red)
#smallcaps[Smallcaps] #smallcaps[Smallcaps]
--- smallcaps-all --- --- smallcaps-all render html ---
#smallcaps(all: false)[Test 012] \ #smallcaps(all: false)[Test 012] \
#smallcaps(all: true)[Test 012] #smallcaps(all: true)[Test 012]