From e5e1dcd9c01341d2cd3473ac94a70223d5966086 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 9 Jul 2025 10:16:36 +0200 Subject: [PATCH 01/18] Target-specific native show rules (#6569) --- crates/typst-html/src/lib.rs | 2 + crates/typst-html/src/rules.rs | 411 ++++++++ crates/typst-layout/src/lib.rs | 16 +- crates/typst-layout/src/rules.rs | 890 ++++++++++++++++++ .../src/foundations/content/element.rs | 6 - .../typst-library/src/foundations/context.rs | 17 +- .../typst-library/src/foundations/styles.rs | 129 ++- .../typst-library/src/foundations/target.rs | 2 +- .../src/introspection/counter.rs | 40 +- .../src/introspection/metadata.rs | 12 +- .../typst-library/src/introspection/state.rs | 13 +- crates/typst-library/src/layout/align.rs | 16 +- crates/typst-library/src/layout/columns.rs | 16 +- crates/typst-library/src/layout/grid/mod.rs | 44 +- crates/typst-library/src/layout/hide.rs | 13 +- crates/typst-library/src/layout/layout.rs | 42 +- crates/typst-library/src/layout/pad.rs | 16 +- crates/typst-library/src/layout/repeat.rs | 16 +- crates/typst-library/src/layout/stack.rs | 16 +- crates/typst-library/src/layout/transform.rs | 50 +- crates/typst-library/src/math/equation.rs | 25 +- .../typst-library/src/model/bibliography.rs | 92 +- crates/typst-library/src/model/cite.rs | 10 +- crates/typst-library/src/model/emph.rs | 25 +- crates/typst-library/src/model/enum.rs | 63 +- crates/typst-library/src/model/figure.rs | 140 +-- crates/typst-library/src/model/footnote.rs | 64 +- crates/typst-library/src/model/heading.rs | 109 +-- crates/typst-library/src/model/link.rs | 41 +- crates/typst-library/src/model/list.rs | 49 +- crates/typst-library/src/model/mod.rs | 20 +- crates/typst-library/src/model/outline.rs | 86 +- crates/typst-library/src/model/quote.rs | 155 +-- crates/typst-library/src/model/reference.rs | 20 +- crates/typst-library/src/model/strong.rs | 25 +- crates/typst-library/src/model/table.rs | 133 +-- crates/typst-library/src/model/terms.rs | 104 +- crates/typst-library/src/pdf/embed.rs | 21 +- crates/typst-library/src/routines.rs | 237 +---- crates/typst-library/src/text/deco.rs | 123 +-- crates/typst-library/src/text/raw.rs | 59 +- crates/typst-library/src/text/shift.rs | 83 +- crates/typst-library/src/text/smallcaps.rs | 16 +- crates/typst-library/src/text/smartquote.rs | 13 +- crates/typst-library/src/visualize/curve.rs | 19 +- .../typst-library/src/visualize/image/mod.rs | 20 +- crates/typst-library/src/visualize/line.rs | 16 +- crates/typst-library/src/visualize/path.rs | 20 +- crates/typst-library/src/visualize/polygon.rs | 18 +- crates/typst-library/src/visualize/shape.rs | 56 +- crates/typst-realize/src/lib.rs | 75 +- crates/typst/src/lib.rs | 37 +- 52 files changed, 1778 insertions(+), 1963 deletions(-) create mode 100644 crates/typst-html/src/rules.rs create mode 100644 crates/typst-layout/src/rules.rs diff --git a/crates/typst-html/src/lib.rs b/crates/typst-html/src/lib.rs index 60ffa78ee..19eb14464 100644 --- a/crates/typst-html/src/lib.rs +++ b/crates/typst-html/src/lib.rs @@ -1,8 +1,10 @@ //! Typst's HTML exporter. mod encode; +mod rules; pub use self::encode::html; +pub use self::rules::register; use comemo::{Track, Tracked, TrackedMut}; use typst_library::diag::{bail, warning, At, SourceResult}; diff --git a/crates/typst-html/src/rules.rs b/crates/typst-html/src/rules.rs new file mode 100644 index 000000000..f361bfbb3 --- /dev/null +++ b/crates/typst-html/src/rules.rs @@ -0,0 +1,411 @@ +use std::num::NonZeroUsize; + +use ecow::{eco_format, EcoVec}; +use typst_library::diag::warning; +use typst_library::foundations::{ + Content, NativeElement, NativeRuleMap, ShowFn, StyleChain, Target, +}; +use typst_library::html::{attr, tag, HtmlAttrs, HtmlElem, HtmlTag}; +use typst_library::introspection::{Counter, Locator}; +use typst_library::layout::resolve::{table_to_cellgrid, Cell, CellGrid, Entry}; +use typst_library::layout::OuterVAlignment; +use typst_library::model::{ + Attribution, CiteElem, CiteGroup, Destination, EmphElem, EnumElem, FigureCaption, + FigureElem, HeadingElem, LinkElem, LinkTarget, ListElem, ParbreakElem, QuoteElem, + RefElem, StrongElem, TableCell, TableElem, TermsElem, +}; +use typst_library::text::{ + HighlightElem, LinebreakElem, OverlineElem, RawElem, RawLine, SpaceElem, StrikeElem, + SubElem, SuperElem, UnderlineElem, +}; + +/// Register show rules for the [HTML target](Target::Html). +pub fn register(rules: &mut NativeRuleMap) { + use Target::Html; + + // Model. + rules.register(Html, STRONG_RULE); + rules.register(Html, EMPH_RULE); + rules.register(Html, LIST_RULE); + rules.register(Html, ENUM_RULE); + rules.register(Html, TERMS_RULE); + rules.register(Html, LINK_RULE); + rules.register(Html, HEADING_RULE); + rules.register(Html, FIGURE_RULE); + rules.register(Html, FIGURE_CAPTION_RULE); + rules.register(Html, QUOTE_RULE); + rules.register(Html, REF_RULE); + rules.register(Html, CITE_GROUP_RULE); + rules.register(Html, TABLE_RULE); + + // Text. + rules.register(Html, SUB_RULE); + rules.register(Html, SUPER_RULE); + rules.register(Html, UNDERLINE_RULE); + rules.register(Html, OVERLINE_RULE); + rules.register(Html, STRIKE_RULE); + rules.register(Html, HIGHLIGHT_RULE); + rules.register(Html, RAW_RULE); + rules.register(Html, RAW_LINE_RULE); +} + +const STRONG_RULE: ShowFn = |elem, _, _| { + Ok(HtmlElem::new(tag::strong) + .with_body(Some(elem.body.clone())) + .pack() + .spanned(elem.span())) +}; + +const EMPH_RULE: ShowFn = |elem, _, _| { + Ok(HtmlElem::new(tag::em) + .with_body(Some(elem.body.clone())) + .pack() + .spanned(elem.span())) +}; + +const LIST_RULE: ShowFn = |elem, _, styles| { + Ok(HtmlElem::new(tag::ul) + .with_body(Some(Content::sequence(elem.children.iter().map(|item| { + // Text in wide lists shall always turn into paragraphs. + let mut body = item.body.clone(); + if !elem.tight.get(styles) { + body += ParbreakElem::shared(); + } + HtmlElem::new(tag::li) + .with_body(Some(body)) + .pack() + .spanned(item.span()) + })))) + .pack() + .spanned(elem.span())) +}; + +const ENUM_RULE: ShowFn = |elem, _, styles| { + let mut ol = HtmlElem::new(tag::ol); + + if elem.reversed.get(styles) { + ol = ol.with_attr(attr::reversed, "reversed"); + } + + if let Some(n) = elem.start.get(styles).custom() { + ol = ol.with_attr(attr::start, eco_format!("{n}")); + } + + let body = Content::sequence(elem.children.iter().map(|item| { + let mut li = HtmlElem::new(tag::li); + if let Some(nr) = item.number.get(styles) { + li = li.with_attr(attr::value, eco_format!("{nr}")); + } + // Text in wide enums shall always turn into paragraphs. + let mut body = item.body.clone(); + if !elem.tight.get(styles) { + body += ParbreakElem::shared(); + } + li.with_body(Some(body)).pack().spanned(item.span()) + })); + + Ok(ol.with_body(Some(body)).pack().spanned(elem.span())) +}; + +const TERMS_RULE: ShowFn = |elem, _, styles| { + Ok(HtmlElem::new(tag::dl) + .with_body(Some(Content::sequence(elem.children.iter().flat_map(|item| { + // Text in wide term lists shall always turn into paragraphs. + let mut description = item.description.clone(); + if !elem.tight.get(styles) { + description += ParbreakElem::shared(); + } + + [ + HtmlElem::new(tag::dt) + .with_body(Some(item.term.clone())) + .pack() + .spanned(item.term.span()), + HtmlElem::new(tag::dd) + .with_body(Some(description)) + .pack() + .spanned(item.description.span()), + ] + })))) + .pack()) +}; + +const LINK_RULE: ShowFn = |elem, engine, _| { + let body = elem.body.clone(); + Ok(if let LinkTarget::Dest(Destination::Url(url)) = &elem.dest { + HtmlElem::new(tag::a) + .with_attr(attr::href, url.clone().into_inner()) + .with_body(Some(body)) + .pack() + .spanned(elem.span()) + } else { + engine.sink.warn(warning!( + elem.span(), + "non-URL links are not yet supported by HTML export" + )); + body + }) +}; + +const HEADING_RULE: ShowFn = |elem, engine, styles| { + let span = elem.span(); + + let mut realized = elem.body.clone(); + if let Some(numbering) = elem.numbering.get_ref(styles).as_ref() { + let location = elem.location().unwrap(); + let numbering = Counter::of(HeadingElem::ELEM) + .display_at_loc(engine, location, styles, numbering)? + .spanned(span); + realized = numbering + SpaceElem::shared().clone() + realized; + } + + // 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 + // reason, levels are offset by one: A Typst level 1 heading becomes + // a `

`. + let level = elem.resolve_level(styles).get(); + Ok(if level >= 6 { + engine.sink.warn(warning!( + span, + "heading of level {} was transformed to \ +
, which is not \ + supported by all assistive technology", + level, level + 1; + hint: "HTML only supports

to

, not ", level + 1; + hint: "you may want to restructure your document so that \ + it doesn't contain deep headings" + )); + HtmlElem::new(tag::div) + .with_body(Some(realized)) + .with_attr(attr::role, "heading") + .with_attr(attr::aria_level, eco_format!("{}", level + 1)) + .pack() + .spanned(span) + } else { + let t = [tag::h2, tag::h3, tag::h4, tag::h5, tag::h6][level - 1]; + HtmlElem::new(t).with_body(Some(realized)).pack().spanned(span) + }) +}; + +const FIGURE_RULE: ShowFn = |elem, _, styles| { + let span = elem.span(); + let mut realized = elem.body.clone(); + + // Build the caption, if any. + if let Some(caption) = elem.caption.get_cloned(styles) { + realized = match caption.position.get(styles) { + OuterVAlignment::Top => caption.pack() + realized, + OuterVAlignment::Bottom => realized + caption.pack(), + }; + } + + // Ensure that the body is considered a paragraph. + realized += ParbreakElem::shared().clone().spanned(span); + + Ok(HtmlElem::new(tag::figure) + .with_body(Some(realized)) + .pack() + .spanned(span)) +}; + +const FIGURE_CAPTION_RULE: ShowFn = |elem, engine, styles| { + Ok(HtmlElem::new(tag::figcaption) + .with_body(Some(elem.realize(engine, styles)?)) + .pack() + .spanned(elem.span())) +}; + +const QUOTE_RULE: ShowFn = |elem, _, styles| { + let span = elem.span(); + let block = elem.block.get(styles); + + let mut realized = elem.body.clone(); + + if elem.quotes.get(styles).unwrap_or(!block) { + realized = QuoteElem::quoted(realized, styles); + } + + let attribution = elem.attribution.get_ref(styles); + + if block { + let mut blockquote = HtmlElem::new(tag::blockquote).with_body(Some(realized)); + if let Some(Attribution::Content(attribution)) = attribution { + if let Some(link) = attribution.to_packed::() { + if let LinkTarget::Dest(Destination::Url(url)) = &link.dest { + blockquote = + blockquote.with_attr(attr::cite, url.clone().into_inner()); + } + } + } + + realized = blockquote.pack().spanned(span); + + if let Some(attribution) = attribution.as_ref() { + realized += attribution.realize(span); + } + } else if let Some(Attribution::Label(label)) = attribution { + realized += SpaceElem::shared().clone(); + realized += CiteElem::new(*label).pack().spanned(span); + } + + Ok(realized) +}; + +const REF_RULE: ShowFn = |elem, engine, styles| elem.realize(engine, styles); + +const CITE_GROUP_RULE: ShowFn = |elem, engine, _| elem.realize(engine); + +const TABLE_RULE: ShowFn = |elem, engine, styles| { + // The locator is not used by HTML export, so we can just fabricate one. + let locator = Locator::root(); + Ok(show_cellgrid(table_to_cellgrid(elem, engine, locator, styles)?, styles)) +}; + +fn show_cellgrid(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.non_gutter_column_count()).collect(); + + let tr = |tag, row: &[Entry]| { + let row = row + .iter() + .flat_map(|entry| entry.as_cell()) + .map(|cell| show_cell(tag, cell, styles)); + elem(tag::tr, Content::sequence(row)) + }; + + // TODO(subfooters): similarly to headers, take consecutive footers from + // the end for 'tfoot'. + let footer = grid.footer.map(|ft| { + let rows = rows.drain(ft.start..); + elem(tag::tfoot, Content::sequence(rows.map(|row| tr(tag::td, row)))) + }); + + // Store all consecutive headers at the start in 'thead'. All remaining + // headers are just 'th' rows across the table body. + let mut consecutive_header_end = 0; + let first_mid_table_header = grid + .headers + .iter() + .take_while(|hd| { + let is_consecutive = hd.range.start == consecutive_header_end; + consecutive_header_end = hd.range.end; + is_consecutive + }) + .count(); + + let (y_offset, header) = if first_mid_table_header > 0 { + let removed_header_rows = + grid.headers.get(first_mid_table_header - 1).unwrap().range.end; + let rows = rows.drain(..removed_header_rows); + + ( + removed_header_rows, + Some(elem(tag::thead, Content::sequence(rows.map(|row| tr(tag::th, row))))), + ) + } else { + (0, None) + }; + + // TODO: Consider improving accessibility properties of multi-level headers + // inside tables in the future, e.g. indicating which columns they are + // relative to and so on. See also: + // https://www.w3.org/WAI/tutorials/tables/multi-level/ + let mut next_header = first_mid_table_header; + let mut body = + Content::sequence(rows.into_iter().enumerate().map(|(relative_y, row)| { + let y = relative_y + y_offset; + if let Some(current_header) = + grid.headers.get(next_header).filter(|h| h.range.contains(&y)) + { + if y + 1 == current_header.range.end { + next_header += 1; + } + + tr(tag::th, row) + } else { + 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)) +} + +fn show_cell(tag: HtmlTag, cell: &Cell, styles: StyleChain) -> Content { + let cell = cell.body.clone(); + let Some(cell) = cell.to_packed::() 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.get(styles)) { + attrs.push(attr::colspan, colspan); + } + if let Some(rowspan) = span(cell.rowspan.get(styles)) { + attrs.push(attr::rowspan, rowspan); + } + HtmlElem::new(tag) + .with_body(Some(cell.body.clone())) + .with_attrs(attrs) + .pack() + .spanned(cell.span()) +} + +const SUB_RULE: ShowFn = |elem, _, _| { + Ok(HtmlElem::new(tag::sub) + .with_body(Some(elem.body.clone())) + .pack() + .spanned(elem.span())) +}; + +const SUPER_RULE: ShowFn = |elem, _, _| { + Ok(HtmlElem::new(tag::sup) + .with_body(Some(elem.body.clone())) + .pack() + .spanned(elem.span())) +}; + +const UNDERLINE_RULE: ShowFn = |elem, _, _| { + // Note: In modern HTML, `` is not the underline element, but + // rather an "Unarticulated Annotation" element (see HTML spec + // 4.5.22). Using `text-decoration` instead is recommended by MDN. + Ok(HtmlElem::new(tag::span) + .with_attr(attr::style, "text-decoration: underline") + .with_body(Some(elem.body.clone())) + .pack()) +}; + +const OVERLINE_RULE: ShowFn = |elem, _, _| { + Ok(HtmlElem::new(tag::span) + .with_attr(attr::style, "text-decoration: overline") + .with_body(Some(elem.body.clone())) + .pack()) +}; + +const STRIKE_RULE: ShowFn = + |elem, _, _| Ok(HtmlElem::new(tag::s).with_body(Some(elem.body.clone())).pack()); + +const HIGHLIGHT_RULE: ShowFn = + |elem, _, _| Ok(HtmlElem::new(tag::mark).with_body(Some(elem.body.clone())).pack()); + +const RAW_RULE: ShowFn = |elem, _, styles| { + let lines = elem.lines.as_deref().unwrap_or_default(); + + let mut seq = EcoVec::with_capacity((2 * lines.len()).saturating_sub(1)); + for (i, line) in lines.iter().enumerate() { + if i != 0 { + seq.push(LinebreakElem::shared().clone()); + } + + seq.push(line.clone().pack()); + } + + Ok(HtmlElem::new(if elem.block.get(styles) { tag::pre } else { tag::code }) + .with_body(Some(Content::sequence(seq))) + .pack() + .spanned(elem.span())) +}; + +const RAW_LINE_RULE: ShowFn = |elem, _, _| Ok(elem.body.clone()); diff --git a/crates/typst-layout/src/lib.rs b/crates/typst-layout/src/lib.rs index 443e90d61..361bab463 100644 --- a/crates/typst-layout/src/lib.rs +++ b/crates/typst-layout/src/lib.rs @@ -10,21 +10,11 @@ mod modifiers; mod pad; mod pages; mod repeat; +mod rules; mod shapes; mod stack; mod transforms; -pub use self::flow::{layout_columns, layout_fragment, layout_frame}; -pub use self::grid::{layout_grid, layout_table}; -pub use self::image::layout_image; -pub use self::lists::{layout_enum, layout_list}; -pub use self::math::{layout_equation_block, layout_equation_inline}; -pub use self::pad::layout_pad; +pub use self::flow::{layout_fragment, layout_frame}; pub use self::pages::layout_document; -pub use self::repeat::layout_repeat; -pub use self::shapes::{ - layout_circle, layout_curve, layout_ellipse, layout_line, layout_path, - layout_polygon, layout_rect, layout_square, -}; -pub use self::stack::layout_stack; -pub use self::transforms::{layout_move, layout_rotate, layout_scale, layout_skew}; +pub use self::rules::register; diff --git a/crates/typst-layout/src/rules.rs b/crates/typst-layout/src/rules.rs new file mode 100644 index 000000000..ebccd92e8 --- /dev/null +++ b/crates/typst-layout/src/rules.rs @@ -0,0 +1,890 @@ +use std::num::NonZeroUsize; + +use comemo::Track; +use ecow::{eco_format, EcoVec}; +use smallvec::smallvec; +use typst_library::diag::{bail, At, SourceResult}; +use typst_library::foundations::{ + dict, Content, Context, NativeElement, NativeRuleMap, Packed, Resolve, ShowFn, Smart, + StyleChain, Target, +}; +use typst_library::introspection::{Counter, Locator, LocatorLink}; +use typst_library::layout::{ + Abs, AlignElem, Alignment, Axes, BlockBody, BlockElem, ColumnsElem, Em, GridCell, + GridChild, GridElem, GridItem, HAlignment, HElem, HideElem, InlineElem, LayoutElem, + Length, MoveElem, OuterVAlignment, PadElem, PlaceElem, PlacementScope, Region, Rel, + RepeatElem, RotateElem, ScaleElem, Sides, Size, Sizing, SkewElem, Spacing, + StackChild, StackElem, TrackSizings, VElem, +}; +use typst_library::math::EquationElem; +use typst_library::model::{ + Attribution, BibliographyElem, CiteElem, CiteGroup, CslSource, Destination, EmphElem, + EnumElem, FigureCaption, FigureElem, FootnoteElem, FootnoteEntry, HeadingElem, + LinkElem, LinkTarget, ListElem, Outlinable, OutlineElem, OutlineEntry, ParElem, + ParbreakElem, QuoteElem, RefElem, StrongElem, TableCell, TableElem, TermsElem, Works, +}; +use typst_library::pdf::EmbedElem; +use typst_library::text::{ + DecoLine, Decoration, HighlightElem, ItalicToggle, LinebreakElem, LocalName, + OverlineElem, RawElem, RawLine, ScriptKind, ShiftSettings, Smallcaps, SmallcapsElem, + SpaceElem, StrikeElem, SubElem, SuperElem, TextElem, TextSize, UnderlineElem, + WeightDelta, +}; +use typst_library::visualize::{ + CircleElem, CurveElem, EllipseElem, ImageElem, LineElem, PathElem, PolygonElem, + RectElem, SquareElem, Stroke, +}; +use typst_utils::{Get, NonZeroExt, Numeric}; + +/// Register show rules for the [paged target](Target::Paged). +pub fn register(rules: &mut NativeRuleMap) { + use Target::Paged; + + // Model. + rules.register(Paged, STRONG_RULE); + rules.register(Paged, EMPH_RULE); + rules.register(Paged, LIST_RULE); + rules.register(Paged, ENUM_RULE); + rules.register(Paged, TERMS_RULE); + rules.register(Paged, LINK_RULE); + rules.register(Paged, HEADING_RULE); + rules.register(Paged, FIGURE_RULE); + rules.register(Paged, FIGURE_CAPTION_RULE); + rules.register(Paged, QUOTE_RULE); + rules.register(Paged, FOOTNOTE_RULE); + rules.register(Paged, FOOTNOTE_ENTRY_RULE); + rules.register(Paged, OUTLINE_RULE); + rules.register(Paged, OUTLINE_ENTRY_RULE); + rules.register(Paged, REF_RULE); + rules.register(Paged, CITE_GROUP_RULE); + rules.register(Paged, BIBLIOGRAPHY_RULE); + rules.register(Paged, TABLE_RULE); + rules.register(Paged, TABLE_CELL_RULE); + + // Text. + rules.register(Paged, SUB_RULE); + rules.register(Paged, SUPER_RULE); + rules.register(Paged, UNDERLINE_RULE); + rules.register(Paged, OVERLINE_RULE); + rules.register(Paged, STRIKE_RULE); + rules.register(Paged, HIGHLIGHT_RULE); + rules.register(Paged, SMALLCAPS_RULE); + rules.register(Paged, RAW_RULE); + rules.register(Paged, RAW_LINE_RULE); + + // Layout. + rules.register(Paged, ALIGN_RULE); + rules.register(Paged, PAD_RULE); + rules.register(Paged, COLUMNS_RULE); + rules.register(Paged, STACK_RULE); + rules.register(Paged, GRID_RULE); + rules.register(Paged, GRID_CELL_RULE); + rules.register(Paged, MOVE_RULE); + rules.register(Paged, SCALE_RULE); + rules.register(Paged, ROTATE_RULE); + rules.register(Paged, SKEW_RULE); + rules.register(Paged, REPEAT_RULE); + rules.register(Paged, HIDE_RULE); + rules.register(Paged, LAYOUT_RULE); + + // Visualize. + rules.register(Paged, IMAGE_RULE); + rules.register(Paged, LINE_RULE); + rules.register(Paged, RECT_RULE); + rules.register(Paged, SQUARE_RULE); + rules.register(Paged, ELLIPSE_RULE); + rules.register(Paged, CIRCLE_RULE); + rules.register(Paged, POLYGON_RULE); + rules.register(Paged, CURVE_RULE); + rules.register(Paged, PATH_RULE); + + // Math. + rules.register(Paged, EQUATION_RULE); + + // PDF. + rules.register(Paged, EMBED_RULE); +} + +const STRONG_RULE: ShowFn = |elem, _, styles| { + Ok(elem + .body + .clone() + .set(TextElem::delta, WeightDelta(elem.delta.get(styles)))) +}; + +const EMPH_RULE: ShowFn = + |elem, _, _| Ok(elem.body.clone().set(TextElem::emph, ItalicToggle(true))); + +const LIST_RULE: ShowFn = |elem, _, styles| { + let tight = elem.tight.get(styles); + + let mut realized = BlockElem::multi_layouter(elem.clone(), crate::lists::layout_list) + .pack() + .spanned(elem.span()); + + if tight { + let spacing = elem + .spacing + .get(styles) + .unwrap_or_else(|| styles.get(ParElem::leading)); + let v = VElem::new(spacing.into()).with_weak(true).with_attach(true).pack(); + realized = v + realized; + } + + Ok(realized) +}; + +const ENUM_RULE: ShowFn = |elem, _, styles| { + let tight = elem.tight.get(styles); + + let mut realized = BlockElem::multi_layouter(elem.clone(), crate::lists::layout_enum) + .pack() + .spanned(elem.span()); + + if tight { + let spacing = elem + .spacing + .get(styles) + .unwrap_or_else(|| styles.get(ParElem::leading)); + let v = VElem::new(spacing.into()).with_weak(true).with_attach(true).pack(); + realized = v + realized; + } + + Ok(realized) +}; + +const TERMS_RULE: ShowFn = |elem, _, styles| { + let span = elem.span(); + let tight = elem.tight.get(styles); + + let separator = elem.separator.get_ref(styles); + let indent = elem.indent.get(styles); + let hanging_indent = elem.hanging_indent.get(styles); + let gutter = elem.spacing.get(styles).unwrap_or_else(|| { + if tight { + styles.get(ParElem::leading) + } else { + styles.get(ParElem::spacing) + } + }); + + let pad = hanging_indent + indent; + let unpad = (!hanging_indent.is_zero()) + .then(|| HElem::new((-hanging_indent).into()).pack().spanned(span)); + + let mut children = vec![]; + for child in elem.children.iter() { + let mut seq = vec![]; + seq.extend(unpad.clone()); + seq.push(child.term.clone().strong()); + seq.push(separator.clone()); + seq.push(child.description.clone()); + + // Text in wide term lists shall always turn into paragraphs. + if !tight { + seq.push(ParbreakElem::shared().clone()); + } + + children.push(StackChild::Block(Content::sequence(seq))); + } + + let padding = + Sides::default().with(styles.resolve(TextElem::dir).start(), pad.into()); + + let mut realized = StackElem::new(children) + .with_spacing(Some(gutter.into())) + .pack() + .spanned(span) + .padded(padding) + .set(TermsElem::within, true); + + if tight { + let spacing = elem + .spacing + .get(styles) + .unwrap_or_else(|| styles.get(ParElem::leading)); + let v = VElem::new(spacing.into()) + .with_weak(true) + .with_attach(true) + .pack() + .spanned(span); + realized = v + realized; + } + + Ok(realized) +}; + +const LINK_RULE: ShowFn = |elem, engine, _| { + let body = elem.body.clone(); + Ok(match &elem.dest { + LinkTarget::Dest(dest) => body.linked(dest.clone()), + LinkTarget::Label(label) => { + let elem = engine.introspector.query_label(*label).at(elem.span())?; + let dest = Destination::Location(elem.location().unwrap()); + body.linked(dest) + } + }) +}; + +const HEADING_RULE: ShowFn = |elem, engine, styles| { + const SPACING_TO_NUMBERING: Em = Em::new(0.3); + + let span = elem.span(); + let mut realized = elem.body.clone(); + + let hanging_indent = elem.hanging_indent.get(styles); + let mut indent = match hanging_indent { + Smart::Custom(length) => length.resolve(styles), + Smart::Auto => Abs::zero(), + }; + + if let Some(numbering) = elem.numbering.get_ref(styles).as_ref() { + let location = elem.location().unwrap(); + let numbering = Counter::of(HeadingElem::ELEM) + .display_at_loc(engine, location, styles, numbering)? + .spanned(span); + + if hanging_indent.is_auto() { + let pod = Region::new(Axes::splat(Abs::inf()), Axes::splat(false)); + + // We don't have a locator for the numbering here, so we just + // use the measurement infrastructure for now. + let link = LocatorLink::measure(location); + let size = (engine.routines.layout_frame)( + engine, + &numbering, + Locator::link(&link), + styles, + pod, + )? + .size(); + + indent = size.x + SPACING_TO_NUMBERING.resolve(styles); + } + + let spacing = HElem::new(SPACING_TO_NUMBERING.into()).with_weak(true).pack(); + + realized = numbering + spacing + realized; + } + + let block = if indent != Abs::zero() { + let body = HElem::new((-indent).into()).pack() + realized; + let inset = Sides::default() + .with(styles.resolve(TextElem::dir).start(), Some(indent.into())); + BlockElem::new() + .with_body(Some(BlockBody::Content(body))) + .with_inset(inset) + } else { + BlockElem::new().with_body(Some(BlockBody::Content(realized))) + }; + + Ok(block.pack().spanned(span)) +}; + +const FIGURE_RULE: ShowFn = |elem, _, styles| { + let span = elem.span(); + let mut realized = elem.body.clone(); + + // Build the caption, if any. + if let Some(caption) = elem.caption.get_cloned(styles) { + let (first, second) = match caption.position.get(styles) { + OuterVAlignment::Top => (caption.pack(), realized), + OuterVAlignment::Bottom => (realized, caption.pack()), + }; + realized = Content::sequence(vec![ + first, + VElem::new(elem.gap.get(styles).into()) + .with_weak(true) + .pack() + .spanned(span), + second, + ]); + } + + // Ensure that the body is considered a paragraph. + realized += ParbreakElem::shared().clone().spanned(span); + + // Wrap the contents in a block. + realized = BlockElem::new() + .with_body(Some(BlockBody::Content(realized))) + .pack() + .spanned(span); + + // Wrap in a float. + if let Some(align) = elem.placement.get(styles) { + realized = PlaceElem::new(realized) + .with_alignment(align.map(|align| HAlignment::Center + align)) + .with_scope(elem.scope.get(styles)) + .with_float(true) + .pack() + .spanned(span); + } else if elem.scope.get(styles) == PlacementScope::Parent { + bail!( + span, + "parent-scoped placement is only available for floating figures"; + hint: "you can enable floating placement with `figure(placement: auto, ..)`" + ); + } + + Ok(realized) +}; + +const FIGURE_CAPTION_RULE: ShowFn = |elem, engine, styles| { + Ok(BlockElem::new() + .with_body(Some(BlockBody::Content(elem.realize(engine, styles)?))) + .pack() + .spanned(elem.span())) +}; + +const QUOTE_RULE: ShowFn = |elem, _, styles| { + let span = elem.span(); + let block = elem.block.get(styles); + + let mut realized = elem.body.clone(); + + if elem.quotes.get(styles).unwrap_or(!block) { + // Add zero-width weak spacing to make the quotes "sticky". + let hole = HElem::hole().pack(); + let sticky = Content::sequence([hole.clone(), realized, hole]); + realized = QuoteElem::quoted(sticky, styles); + } + + let attribution = elem.attribution.get_ref(styles); + + if block { + realized = BlockElem::new() + .with_body(Some(BlockBody::Content(realized))) + .pack() + .spanned(span); + + if let Some(attribution) = attribution.as_ref() { + // Bring the attribution a bit 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 += BlockElem::new() + .with_body(Some(BlockBody::Content(attribution.realize(span)))) + .pack() + .aligned(Alignment::END); + } + + realized = PadElem::new(realized).pack(); + } else if let Some(Attribution::Label(label)) = attribution { + realized += SpaceElem::shared().clone(); + realized += CiteElem::new(*label).pack().spanned(span); + } + + Ok(realized) +}; + +const FOOTNOTE_RULE: ShowFn = |elem, engine, styles| { + let span = elem.span(); + let loc = elem.declaration_location(engine).at(span)?; + let numbering = elem.numbering.get_ref(styles); + let counter = Counter::of(FootnoteElem::ELEM); + let num = counter.display_at_loc(engine, loc, styles, numbering)?; + let sup = SuperElem::new(num).pack().spanned(span); + let loc = loc.variant(1); + // Add zero-width weak spacing to make the footnote "sticky". + Ok(HElem::hole().pack() + sup.linked(Destination::Location(loc))) +}; + +const FOOTNOTE_ENTRY_RULE: ShowFn = |elem, engine, styles| { + let span = elem.span(); + let number_gap = Em::new(0.05); + let default = StyleChain::default(); + let numbering = elem.note.numbering.get_ref(default); + let counter = Counter::of(FootnoteElem::ELEM); + let Some(loc) = elem.note.location() else { + bail!( + span, "footnote entry must have a location"; + hint: "try using a query or a show rule to customize the footnote instead" + ); + }; + + let num = counter.display_at_loc(engine, loc, styles, numbering)?; + let sup = SuperElem::new(num) + .pack() + .spanned(span) + .linked(Destination::Location(loc)) + .located(loc.variant(1)); + + Ok(Content::sequence([ + HElem::new(elem.indent.get(styles).into()).pack(), + sup, + HElem::new(number_gap.into()).with_weak(true).pack(), + elem.note.body_content().unwrap().clone(), + ])) +}; + +const OUTLINE_RULE: ShowFn = |elem, engine, styles| { + let span = elem.span(); + + // Build the outline title. + let mut seq = vec![]; + if let Some(title) = elem.title.get_cloned(styles).unwrap_or_else(|| { + Some(TextElem::packed(Packed::::local_name_in(styles)).spanned(span)) + }) { + seq.push( + HeadingElem::new(title) + .with_depth(NonZeroUsize::ONE) + .pack() + .spanned(span), + ); + } + + let elems = engine.introspector.query(&elem.target.get_ref(styles).0); + let depth = elem.depth.get(styles).unwrap_or(NonZeroUsize::MAX); + + // Build the outline entries. + for elem in elems { + let Some(outlinable) = elem.with::() else { + bail!(span, "cannot outline {}", elem.func().name()); + }; + + let level = outlinable.level(); + if outlinable.outlined() && level <= depth { + let entry = OutlineEntry::new(level, elem); + seq.push(entry.pack().spanned(span)); + } + } + + Ok(Content::sequence(seq)) +}; + +const OUTLINE_ENTRY_RULE: ShowFn = |elem, engine, styles| { + let span = elem.span(); + let context = Context::new(None, Some(styles)); + let context = context.track(); + + let prefix = elem.prefix(engine, context, span)?; + let inner = elem.inner(engine, context, span)?; + let block = if elem.element.is::() { + let body = prefix.unwrap_or_default() + inner; + BlockElem::new() + .with_body(Some(BlockBody::Content(body))) + .pack() + .spanned(span) + } else { + elem.indented(engine, context, span, prefix, inner, Em::new(0.5).into())? + }; + + let loc = elem.element_location().at(span)?; + Ok(block.linked(Destination::Location(loc))) +}; + +const REF_RULE: ShowFn = |elem, engine, styles| elem.realize(engine, styles); + +const CITE_GROUP_RULE: ShowFn = |elem, engine, _| elem.realize(engine); + +const BIBLIOGRAPHY_RULE: ShowFn = |elem, engine, styles| { + const COLUMN_GUTTER: Em = Em::new(0.65); + const INDENT: Em = Em::new(1.5); + + let span = elem.span(); + + let mut seq = vec![]; + if let Some(title) = elem.title.get_ref(styles).clone().unwrap_or_else(|| { + Some( + TextElem::packed(Packed::::local_name_in(styles)) + .spanned(span), + ) + }) { + seq.push( + HeadingElem::new(title) + .with_depth(NonZeroUsize::ONE) + .pack() + .spanned(span), + ); + } + + let works = Works::generate(engine).at(span)?; + let references = works + .references + .as_ref() + .ok_or_else(|| match elem.style.get_ref(styles).source { + CslSource::Named(style) => eco_format!( + "CSL style \"{}\" is not suitable for bibliographies", + style.display_name() + ), + CslSource::Normal(..) => { + "CSL style is not suitable for bibliographies".into() + } + }) + .at(span)?; + + if references.iter().any(|(prefix, _)| prefix.is_some()) { + let row_gutter = styles.get(ParElem::spacing); + + let mut cells = vec![]; + for (prefix, reference) in references { + cells.push(GridChild::Item(GridItem::Cell( + Packed::new(GridCell::new(prefix.clone().unwrap_or_default())) + .spanned(span), + ))); + cells.push(GridChild::Item(GridItem::Cell( + Packed::new(GridCell::new(reference.clone())).spanned(span), + ))); + } + seq.push( + GridElem::new(cells) + .with_columns(TrackSizings(smallvec![Sizing::Auto; 2])) + .with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()])) + .with_row_gutter(TrackSizings(smallvec![row_gutter.into()])) + .pack() + .spanned(span), + ); + } else { + for (_, reference) in references { + let realized = reference.clone(); + let block = if works.hanging_indent { + let body = HElem::new((-INDENT).into()).pack() + realized; + let inset = Sides::default() + .with(styles.resolve(TextElem::dir).start(), Some(INDENT.into())); + BlockElem::new() + .with_body(Some(BlockBody::Content(body))) + .with_inset(inset) + } else { + BlockElem::new().with_body(Some(BlockBody::Content(realized))) + }; + + seq.push(block.pack().spanned(span)); + } + } + + Ok(Content::sequence(seq)) +}; + +const TABLE_RULE: ShowFn = |elem, _, _| { + Ok(BlockElem::multi_layouter(elem.clone(), crate::grid::layout_table) + .pack() + .spanned(elem.span())) +}; + +const TABLE_CELL_RULE: ShowFn = |elem, _, styles| { + show_cell(elem.body.clone(), elem.inset.get(styles), elem.align.get(styles)) +}; + +const SUB_RULE: ShowFn = |elem, _, styles| { + show_script( + styles, + elem.body.clone(), + elem.typographic.get(styles), + elem.baseline.get(styles), + elem.size.get(styles), + ScriptKind::Sub, + ) +}; + +const SUPER_RULE: ShowFn = |elem, _, styles| { + show_script( + styles, + elem.body.clone(), + elem.typographic.get(styles), + elem.baseline.get(styles), + elem.size.get(styles), + ScriptKind::Super, + ) +}; + +fn show_script( + styles: StyleChain, + body: Content, + typographic: bool, + baseline: Smart, + size: Smart, + kind: ScriptKind, +) -> SourceResult { + let font_size = styles.resolve(TextElem::size); + Ok(body.set( + TextElem::shift_settings, + Some(ShiftSettings { + typographic, + shift: baseline.map(|l| -Em::from_length(l, font_size)), + size: size.map(|t| Em::from_length(t.0, font_size)), + kind, + }), + )) +} + +const UNDERLINE_RULE: ShowFn = |elem, _, styles| { + Ok(elem.body.clone().set( + TextElem::deco, + smallvec![Decoration { + line: DecoLine::Underline { + stroke: elem.stroke.resolve(styles).unwrap_or_default(), + offset: elem.offset.resolve(styles), + evade: elem.evade.get(styles), + background: elem.background.get(styles), + }, + extent: elem.extent.resolve(styles), + }], + )) +}; + +const OVERLINE_RULE: ShowFn = |elem, _, styles| { + Ok(elem.body.clone().set( + TextElem::deco, + smallvec![Decoration { + line: DecoLine::Overline { + stroke: elem.stroke.resolve(styles).unwrap_or_default(), + offset: elem.offset.resolve(styles), + evade: elem.evade.get(styles), + background: elem.background.get(styles), + }, + extent: elem.extent.resolve(styles), + }], + )) +}; + +const STRIKE_RULE: ShowFn = |elem, _, styles| { + Ok(elem.body.clone().set( + TextElem::deco, + smallvec![Decoration { + // Note that we do not support evade option for strikethrough. + line: DecoLine::Strikethrough { + stroke: elem.stroke.resolve(styles).unwrap_or_default(), + offset: elem.offset.resolve(styles), + background: elem.background.get(styles), + }, + extent: elem.extent.resolve(styles), + }], + )) +}; + +const HIGHLIGHT_RULE: ShowFn = |elem, _, styles| { + Ok(elem.body.clone().set( + TextElem::deco, + smallvec![Decoration { + line: DecoLine::Highlight { + fill: elem.fill.get_cloned(styles), + stroke: elem + .stroke + .resolve(styles) + .unwrap_or_default() + .map(|stroke| stroke.map(Stroke::unwrap_or_default)), + top_edge: elem.top_edge.get(styles), + bottom_edge: elem.bottom_edge.get(styles), + radius: elem.radius.resolve(styles).unwrap_or_default(), + }, + extent: elem.extent.resolve(styles), + }], + )) +}; + +const SMALLCAPS_RULE: ShowFn = |elem, _, styles| { + let sc = if elem.all.get(styles) { Smallcaps::All } else { Smallcaps::Minuscules }; + Ok(elem.body.clone().set(TextElem::smallcaps, Some(sc))) +}; + +const RAW_RULE: ShowFn = |elem, _, styles| { + let lines = elem.lines.as_deref().unwrap_or_default(); + + let mut seq = EcoVec::with_capacity((2 * lines.len()).saturating_sub(1)); + for (i, line) in lines.iter().enumerate() { + if i != 0 { + seq.push(LinebreakElem::shared().clone()); + } + + seq.push(line.clone().pack()); + } + + let mut realized = Content::sequence(seq); + + if elem.block.get(styles) { + // Align the text before inserting it into the block. + realized = realized.aligned(elem.align.get(styles).into()); + realized = BlockElem::new() + .with_body(Some(BlockBody::Content(realized))) + .pack() + .spanned(elem.span()); + } + + Ok(realized) +}; + +const RAW_LINE_RULE: ShowFn = |elem, _, _| Ok(elem.body.clone()); + +const ALIGN_RULE: ShowFn = + |elem, _, styles| Ok(elem.body.clone().aligned(elem.alignment.get(styles))); + +const PAD_RULE: ShowFn = |elem, _, _| { + Ok(BlockElem::multi_layouter(elem.clone(), crate::pad::layout_pad) + .pack() + .spanned(elem.span())) +}; + +const COLUMNS_RULE: ShowFn = |elem, _, _| { + Ok(BlockElem::multi_layouter(elem.clone(), crate::flow::layout_columns) + .pack() + .spanned(elem.span())) +}; + +const STACK_RULE: ShowFn = |elem, _, _| { + Ok(BlockElem::multi_layouter(elem.clone(), crate::stack::layout_stack) + .pack() + .spanned(elem.span())) +}; + +const GRID_RULE: ShowFn = |elem, _, _| { + Ok(BlockElem::multi_layouter(elem.clone(), crate::grid::layout_grid) + .pack() + .spanned(elem.span())) +}; + +const GRID_CELL_RULE: ShowFn = |elem, _, styles| { + show_cell(elem.body.clone(), elem.inset.get(styles), elem.align.get(styles)) +}; + +/// Function with common code to display a grid cell or table cell. +fn show_cell( + mut body: Content, + inset: Smart>>>, + align: Smart, +) -> SourceResult { + let inset = inset.unwrap_or_default().map(Option::unwrap_or_default); + + if inset != Sides::default() { + // Only pad if some inset is not 0pt. + // Avoids a bug where using .padded() in any way inside Show causes + // alignment in align(...) to break. + body = body.padded(inset); + } + + if let Smart::Custom(alignment) = align { + body = body.aligned(alignment); + } + + Ok(body) +} + +const MOVE_RULE: ShowFn = |elem, _, _| { + Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_move) + .pack() + .spanned(elem.span())) +}; + +const SCALE_RULE: ShowFn = |elem, _, _| { + Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_scale) + .pack() + .spanned(elem.span())) +}; + +const ROTATE_RULE: ShowFn = |elem, _, _| { + Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_rotate) + .pack() + .spanned(elem.span())) +}; + +const SKEW_RULE: ShowFn = |elem, _, _| { + Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_skew) + .pack() + .spanned(elem.span())) +}; + +const REPEAT_RULE: ShowFn = |elem, _, _| { + Ok(BlockElem::single_layouter(elem.clone(), crate::repeat::layout_repeat) + .pack() + .spanned(elem.span())) +}; + +const HIDE_RULE: ShowFn = + |elem, _, _| Ok(elem.body.clone().set(HideElem::hidden, true)); + +const LAYOUT_RULE: ShowFn = |elem, _, _| { + Ok(BlockElem::multi_layouter( + elem.clone(), + |elem, engine, locator, styles, regions| { + // Gets the current region's base size, which will be the size of the + // outer container, or of the page if there is no such container. + let Size { x, y } = regions.base(); + let loc = elem.location().unwrap(); + let context = Context::new(Some(loc), Some(styles)); + let result = elem + .func + .call(engine, context.track(), [dict! { "width" => x, "height" => y }])? + .display(); + crate::flow::layout_fragment(engine, &result, locator, styles, regions) + }, + ) + .pack() + .spanned(elem.span())) +}; + +const IMAGE_RULE: ShowFn = |elem, _, styles| { + Ok(BlockElem::single_layouter(elem.clone(), crate::image::layout_image) + .with_width(elem.width.get(styles)) + .with_height(elem.height.get(styles)) + .pack() + .spanned(elem.span())) +}; + +const LINE_RULE: ShowFn = |elem, _, _| { + Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_line) + .pack() + .spanned(elem.span())) +}; + +const RECT_RULE: ShowFn = |elem, _, styles| { + Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_rect) + .with_width(elem.width.get(styles)) + .with_height(elem.height.get(styles)) + .pack() + .spanned(elem.span())) +}; + +const SQUARE_RULE: ShowFn = |elem, _, styles| { + Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_square) + .with_width(elem.width.get(styles)) + .with_height(elem.height.get(styles)) + .pack() + .spanned(elem.span())) +}; + +const ELLIPSE_RULE: ShowFn = |elem, _, styles| { + Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_ellipse) + .with_width(elem.width.get(styles)) + .with_height(elem.height.get(styles)) + .pack() + .spanned(elem.span())) +}; + +const CIRCLE_RULE: ShowFn = |elem, _, styles| { + Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_circle) + .with_width(elem.width.get(styles)) + .with_height(elem.height.get(styles)) + .pack() + .spanned(elem.span())) +}; + +const POLYGON_RULE: ShowFn = |elem, _, _| { + Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_polygon) + .pack() + .spanned(elem.span())) +}; + +const CURVE_RULE: ShowFn = |elem, _, _| { + Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_curve) + .pack() + .spanned(elem.span())) +}; + +const PATH_RULE: ShowFn = |elem, _, _| { + Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_path) + .pack() + .spanned(elem.span())) +}; + +const EQUATION_RULE: ShowFn = |elem, _, styles| { + if elem.block.get(styles) { + Ok(BlockElem::multi_layouter(elem.clone(), crate::math::layout_equation_block) + .pack() + .spanned(elem.span())) + } else { + Ok(InlineElem::layouter(elem.clone(), crate::math::layout_equation_inline) + .pack() + .spanned(elem.span())) + } +}; + +const EMBED_RULE: ShowFn = |_, _, _| Ok(Content::empty()); diff --git a/crates/typst-library/src/foundations/content/element.rs b/crates/typst-library/src/foundations/content/element.rs index 49b0b0f9b..65c2e28b5 100644 --- a/crates/typst-library/src/foundations/content/element.rs +++ b/crates/typst-library/src/foundations/content/element.rs @@ -246,12 +246,6 @@ pub trait Synthesize { -> SourceResult<()>; } -/// Defines a built-in show rule for an element. -pub trait Show { - /// Execute the base recipe for this element. - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult; -} - /// Defines built-in show set rules for an element. /// /// This is a bit more powerful than a user-defined show-set because it can diff --git a/crates/typst-library/src/foundations/context.rs b/crates/typst-library/src/foundations/context.rs index bf4bdcd25..56d87775e 100644 --- a/crates/typst-library/src/foundations/context.rs +++ b/crates/typst-library/src/foundations/context.rs @@ -3,7 +3,7 @@ use comemo::Track; use crate::diag::{bail, Hint, HintedStrResult, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - elem, Args, Construct, Content, Func, Packed, Show, StyleChain, Value, + elem, Args, Construct, Content, Func, ShowFn, StyleChain, Value, }; use crate::introspection::{Locatable, Location}; @@ -61,7 +61,7 @@ fn require(val: Option) -> HintedStrResult { } /// Executes a `context` block. -#[elem(Construct, Locatable, Show)] +#[elem(Construct, Locatable)] pub struct ContextElem { /// The function to call with the context. #[required] @@ -75,11 +75,8 @@ impl Construct for ContextElem { } } -impl Show for Packed { - #[typst_macros::time(name = "context", span = self.span())] - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let loc = self.location().unwrap(); - let context = Context::new(Some(loc), Some(styles)); - Ok(self.func.call::<[Value; 0]>(engine, context.track(), [])?.display()) - } -} +pub const CONTEXT_RULE: ShowFn = |elem, engine, styles| { + let loc = elem.location().unwrap(); + let context = Context::new(Some(loc), Some(styles)); + Ok(elem.func.call::<[Value; 0]>(engine, context.track(), [])?.display()) +}; diff --git a/crates/typst-library/src/foundations/styles.rs b/crates/typst-library/src/foundations/styles.rs index 978b47d5f..0da036f6f 100644 --- a/crates/typst-library/src/foundations/styles.rs +++ b/crates/typst-library/src/foundations/styles.rs @@ -1,4 +1,5 @@ use std::any::{Any, TypeId}; +use std::collections::HashMap; use std::fmt::{self, Debug, Formatter}; use std::hash::{Hash, Hasher}; use std::{mem, ptr}; @@ -13,7 +14,7 @@ use crate::diag::{SourceResult, Trace, Tracepoint}; use crate::engine::Engine; use crate::foundations::{ cast, ty, Content, Context, Element, Field, Func, NativeElement, OneOrMultiple, - RefableProperty, Repr, Selector, SettableProperty, + Packed, RefableProperty, Repr, Selector, SettableProperty, Target, }; use crate::text::{FontFamily, FontList, TextElem}; @@ -938,3 +939,129 @@ fn block_wrong_type(func: Element, id: u8, value: &Block) -> ! { value ) } + +/// Holds native show rules. +pub struct NativeRuleMap { + rules: HashMap<(Element, Target), NativeShowRule>, +} + +/// The signature of a native show rule. +pub type ShowFn = fn( + elem: &Packed, + engine: &mut Engine, + styles: StyleChain, +) -> SourceResult; + +impl NativeRuleMap { + /// Creates a new rule map. + /// + /// Should be populated with rules for all target-element combinations that + /// are supported. + /// + /// Contains built-in rules for a few special elements. + pub fn new() -> Self { + let mut rules = Self { rules: HashMap::new() }; + + // ContextElem is as special as SequenceElem and StyledElem and could, + // in theory, also be special cased in realization. + rules.register_builtin(crate::foundations::CONTEXT_RULE); + + // CounterDisplayElem only exists because the compiler can't currently + // express the equivalent of `context counter(..).display(..)` in native + // code (no native closures). + rules.register_builtin(crate::introspection::COUNTER_DISPLAY_RULE); + + // These are all only for introspection and empty on all targets. + rules.register_empty::(); + rules.register_empty::(); + rules.register_empty::(); + rules.register_empty::(); + + rules + } + + /// Registers a rule for all targets. + fn register_empty(&mut self) { + self.register_builtin::(|_, _, _| Ok(Content::empty())); + } + + /// Registers a rule for all targets. + fn register_builtin(&mut self, f: ShowFn) { + self.register(Target::Paged, f); + self.register(Target::Html, f); + } + + /// Registers a rule for a target. + /// + /// Panics if a rule already exists for this target-element combination. + pub fn register(&mut self, target: Target, f: ShowFn) { + let res = self.rules.insert((T::ELEM, target), NativeShowRule::new(f)); + if res.is_some() { + panic!( + "duplicate native show rule for `{}` on {target:?} target", + T::ELEM.name() + ) + } + } + + /// Retrieves the rule that applies to the `content` on the current + /// `target`. + pub fn get(&self, target: Target, content: &Content) -> Option { + self.rules.get(&(content.func(), target)).copied() + } +} + +impl Default for NativeRuleMap { + fn default() -> Self { + Self::new() + } +} + +pub use rule::NativeShowRule; + +mod rule { + use super::*; + + /// The show rule for a native element. + #[derive(Copy, Clone)] + pub struct NativeShowRule { + /// The element to which this rule applies. + elem: Element, + /// Must only be called with content of the appropriate type. + f: unsafe fn( + elem: &Content, + engine: &mut Engine, + styles: StyleChain, + ) -> SourceResult, + } + + impl NativeShowRule { + /// Create a new type-erased show rule. + pub fn new(f: ShowFn) -> Self { + Self { + elem: T::ELEM, + // Safety: The two function pointer types only differ in the + // first argument, which changes from `&Packed` to + // `&Content`. `Packed` is a transparent wrapper around + // `Content`. The resulting function is unsafe to call because + // content of the correct type must be passed to it. + #[allow(clippy::missing_transmute_annotations)] + f: unsafe { std::mem::transmute(f) }, + } + } + + /// Applies the rule to content. Panics if the content is of the wrong + /// type. + pub fn apply( + &self, + content: &Content, + engine: &mut Engine, + styles: StyleChain, + ) -> SourceResult { + assert_eq!(content.elem(), self.elem); + + // Safety: We just checked that the element is of the correct type. + unsafe { (self.f)(content, engine, styles) } + } + } +} diff --git a/crates/typst-library/src/foundations/target.rs b/crates/typst-library/src/foundations/target.rs index 71e7554e0..ff90f1f7b 100644 --- a/crates/typst-library/src/foundations/target.rs +++ b/crates/typst-library/src/foundations/target.rs @@ -4,7 +4,7 @@ use crate::diag::HintedStrResult; use crate::foundations::{elem, func, Cast, Context}; /// The export target. -#[derive(Debug, Default, Copy, Clone, PartialEq, Hash, Cast)] +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Cast)] pub enum Target { /// The target that is used for paged, fully laid-out content. #[default] diff --git a/crates/typst-library/src/introspection/counter.rs b/crates/typst-library/src/introspection/counter.rs index a7925e13a..b3c52de4e 100644 --- a/crates/typst-library/src/introspection/counter.rs +++ b/crates/typst-library/src/introspection/counter.rs @@ -12,7 +12,7 @@ use crate::engine::{Engine, Route, Sink, Traced}; use crate::foundations::{ cast, elem, func, scope, select_where, ty, Args, Array, Construct, Content, Context, Element, Func, IntoValue, Label, LocatableSelector, NativeElement, Packed, Repr, - Selector, Show, Smart, Str, StyleChain, Value, + Selector, ShowFn, Smart, Str, StyleChain, Value, }; use crate::introspection::{Introspector, Locatable, Location, Tag}; use crate::layout::{Frame, FrameItem, PageElem}; @@ -683,8 +683,8 @@ cast! { } /// Executes an update of a counter. -#[elem(Construct, Locatable, Show, Count)] -struct CounterUpdateElem { +#[elem(Construct, Locatable, Count)] +pub struct CounterUpdateElem { /// The key that identifies the counter. #[required] key: CounterKey, @@ -701,12 +701,6 @@ impl Construct for CounterUpdateElem { } } -impl Show for Packed { - fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { - Ok(Content::empty()) - } -} - impl Count for Packed { fn update(&self) -> Option { Some(self.update.clone()) @@ -714,7 +708,7 @@ impl Count for Packed { } /// Executes a display of a counter. -#[elem(Construct, Locatable, Show)] +#[elem(Construct, Locatable)] pub struct CounterDisplayElem { /// The counter. #[required] @@ -738,20 +732,18 @@ impl Construct for CounterDisplayElem { } } -impl Show for Packed { - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(self - .counter - .display_impl( - engine, - self.location().unwrap(), - self.numbering.clone(), - self.both, - Some(styles), - )? - .display()) - } -} +pub const COUNTER_DISPLAY_RULE: ShowFn = |elem, engine, styles| { + Ok(elem + .counter + .display_impl( + engine, + elem.location().unwrap(), + elem.numbering.clone(), + elem.both, + Some(styles), + )? + .display()) +}; /// An specialized handler of the page counter that tracks both the physical /// and the logical page counter. diff --git a/crates/typst-library/src/introspection/metadata.rs b/crates/typst-library/src/introspection/metadata.rs index 06000174f..8ad74b96b 100644 --- a/crates/typst-library/src/introspection/metadata.rs +++ b/crates/typst-library/src/introspection/metadata.rs @@ -1,6 +1,4 @@ -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::{elem, Content, Packed, Show, StyleChain, Value}; +use crate::foundations::{elem, Value}; use crate::introspection::Locatable; /// Exposes a value to the query system without producing visible content. @@ -24,15 +22,9 @@ use crate::introspection::Locatable; /// query().first().value /// } /// ``` -#[elem(Show, Locatable)] +#[elem(Locatable)] pub struct MetadataElem { /// The value to embed into the document. #[required] pub value: Value, } - -impl Show for Packed { - fn show(&self, _: &mut Engine, _styles: StyleChain) -> SourceResult { - Ok(Content::empty()) - } -} diff --git a/crates/typst-library/src/introspection/state.rs b/crates/typst-library/src/introspection/state.rs index 784f2acb2..2d15a5de0 100644 --- a/crates/typst-library/src/introspection/state.rs +++ b/crates/typst-library/src/introspection/state.rs @@ -6,8 +6,7 @@ use crate::diag::{bail, At, SourceResult}; use crate::engine::{Engine, Route, Sink, Traced}; use crate::foundations::{ cast, elem, func, scope, select_where, ty, Args, Construct, Content, Context, Func, - LocatableSelector, NativeElement, Packed, Repr, Selector, Show, Str, StyleChain, - Value, + LocatableSelector, NativeElement, Repr, Selector, Str, Value, }; use crate::introspection::{Introspector, Locatable, Location}; use crate::routines::Routines; @@ -372,8 +371,8 @@ cast! { } /// Executes a display of a state. -#[elem(Construct, Locatable, Show)] -struct StateUpdateElem { +#[elem(Construct, Locatable)] +pub struct StateUpdateElem { /// The key that identifies the state. #[required] key: Str, @@ -389,9 +388,3 @@ impl Construct for StateUpdateElem { bail!(args.span, "cannot be constructed manually"); } } - -impl Show for Packed { - fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { - Ok(Content::empty()) - } -} diff --git a/crates/typst-library/src/layout/align.rs b/crates/typst-library/src/layout/align.rs index e5ceddf65..447648f01 100644 --- a/crates/typst-library/src/layout/align.rs +++ b/crates/typst-library/src/layout/align.rs @@ -2,11 +2,10 @@ use std::ops::Add; use ecow::{eco_format, EcoString}; -use crate::diag::{bail, HintedStrResult, SourceResult, StrResult}; -use crate::engine::Engine; +use crate::diag::{bail, HintedStrResult, StrResult}; use crate::foundations::{ - cast, elem, func, scope, ty, CastInfo, Content, Fold, FromValue, IntoValue, Packed, - Reflect, Repr, Resolve, Show, StyleChain, Value, + cast, elem, func, scope, ty, CastInfo, Content, Fold, FromValue, IntoValue, Reflect, + Repr, Resolve, StyleChain, Value, }; use crate::layout::{Abs, Axes, Axis, Dir, Side}; use crate::text::TextElem; @@ -73,7 +72,7 @@ use crate::text::TextElem; /// ```example /// Start #h(1fr) End /// ``` -#[elem(Show)] +#[elem] pub struct AlignElem { /// The [alignment] along both axes. /// @@ -97,13 +96,6 @@ pub struct AlignElem { pub body: Content, } -impl Show for Packed { - #[typst_macros::time(name = "align", span = self.span())] - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(self.body.clone().aligned(self.alignment.get(styles))) - } -} - /// Where to align something along an axis. /// /// Possible values are: diff --git a/crates/typst-library/src/layout/columns.rs b/crates/typst-library/src/layout/columns.rs index 1cea52759..e7bce393b 100644 --- a/crates/typst-library/src/layout/columns.rs +++ b/crates/typst-library/src/layout/columns.rs @@ -1,9 +1,7 @@ use std::num::NonZeroUsize; -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::{elem, Content, NativeElement, Packed, Show, StyleChain}; -use crate::layout::{BlockElem, Length, Ratio, Rel}; +use crate::foundations::{elem, Content}; +use crate::layout::{Length, Ratio, Rel}; /// Separates a region into multiple equally sized columns. /// @@ -41,7 +39,7 @@ use crate::layout::{BlockElem, Length, Ratio, Rel}; /// /// #lorem(40) /// ``` -#[elem(Show)] +#[elem] pub struct ColumnsElem { /// The number of columns. #[positional] @@ -57,14 +55,6 @@ pub struct ColumnsElem { pub body: Content, } -impl Show for Packed { - fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::multi_layouter(self.clone(), engine.routines.layout_columns) - .pack() - .spanned(self.span())) - } -} - /// Forces a column break. /// /// The function will behave like a [page break]($pagebreak) when used in a diff --git a/crates/typst-library/src/layout/grid/mod.rs b/crates/typst-library/src/layout/grid/mod.rs index 64e7464be..658523ec6 100644 --- a/crates/typst-library/src/layout/grid/mod.rs +++ b/crates/typst-library/src/layout/grid/mod.rs @@ -11,10 +11,10 @@ use crate::diag::{bail, At, HintedStrResult, HintedString, SourceResult}; use crate::engine::Engine; use crate::foundations::{ cast, elem, scope, Array, CastInfo, Content, Context, Fold, FromValue, Func, - IntoValue, NativeElement, Packed, Reflect, Resolve, Show, Smart, StyleChain, Value, + IntoValue, Packed, Reflect, Resolve, Smart, StyleChain, Value, }; use crate::layout::{ - Alignment, BlockElem, Length, OuterHAlignment, OuterVAlignment, Rel, Sides, Sizing, + Alignment, Length, OuterHAlignment, OuterVAlignment, Rel, Sides, Sizing, }; use crate::model::{TableCell, TableFooter, TableHLine, TableHeader, TableVLine}; use crate::visualize::{Paint, Stroke}; @@ -136,7 +136,7 @@ use crate::visualize::{Paint, Stroke}; /// /// Furthermore, strokes of a repeated grid header or footer will take /// precedence over regular cell strokes. -#[elem(scope, Show)] +#[elem(scope)] pub struct GridElem { /// The column sizes. /// @@ -320,14 +320,6 @@ impl GridElem { type GridFooter; } -impl Show for Packed { - fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::multi_layouter(self.clone(), engine.routines.layout_grid) - .pack() - .spanned(self.span())) - } -} - /// Track sizing definitions. #[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] pub struct TrackSizings(pub SmallVec<[Sizing; 4]>); @@ -648,7 +640,7 @@ pub struct GridVLine { /// which allows you, for example, to apply styles based on a cell's position. /// Refer to the examples of the [`table.cell`]($table.cell) element to learn /// more about this. -#[elem(name = "cell", title = "Grid Cell", Show)] +#[elem(name = "cell", title = "Grid Cell")] pub struct GridCell { /// The cell's body. #[required] @@ -748,12 +740,6 @@ cast! { v: Content => v.into(), } -impl Show for Packed { - fn show(&self, _engine: &mut Engine, styles: StyleChain) -> SourceResult { - show_grid_cell(self.body.clone(), self.inset.get(styles), self.align.get(styles)) - } -} - impl Default for Packed { fn default() -> Self { Packed::new( @@ -774,28 +760,6 @@ impl From for GridCell { } } -/// Function with common code to display a grid cell or table cell. -pub(crate) fn show_grid_cell( - mut body: Content, - inset: Smart>>>, - align: Smart, -) -> SourceResult { - let inset = inset.unwrap_or_default().map(Option::unwrap_or_default); - - if inset != Sides::default() { - // Only pad if some inset is not 0pt. - // Avoids a bug where using .padded() in any way inside Show causes - // alignment in align(...) to break. - body = body.padded(inset); - } - - if let Smart::Custom(alignment) = align { - body = body.aligned(alignment); - } - - Ok(body) -} - /// A value that can be configured per cell. #[derive(Debug, Clone, PartialEq, Hash)] pub enum Celled { diff --git a/crates/typst-library/src/layout/hide.rs b/crates/typst-library/src/layout/hide.rs index 5f3a5a2d3..bb40447d5 100644 --- a/crates/typst-library/src/layout/hide.rs +++ b/crates/typst-library/src/layout/hide.rs @@ -1,6 +1,4 @@ -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::{elem, Content, Packed, Show, StyleChain}; +use crate::foundations::{elem, Content}; /// Hides content without affecting layout. /// @@ -14,7 +12,7 @@ use crate::foundations::{elem, Content, Packed, Show, StyleChain}; /// Hello Jane \ /// #hide[Hello] Joe /// ``` -#[elem(Show)] +#[elem] pub struct HideElem { /// The content to hide. #[required] @@ -25,10 +23,3 @@ pub struct HideElem { #[ghost] pub hidden: bool, } - -impl Show for Packed { - #[typst_macros::time(name = "hide", span = self.span())] - fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { - Ok(self.body.clone().set(HideElem::hidden, true)) - } -} diff --git a/crates/typst-library/src/layout/layout.rs b/crates/typst-library/src/layout/layout.rs index 46271ff22..00897bcfe 100644 --- a/crates/typst-library/src/layout/layout.rs +++ b/crates/typst-library/src/layout/layout.rs @@ -1,13 +1,7 @@ -use comemo::Track; use typst_syntax::Span; -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::{ - dict, elem, func, Content, Context, Func, NativeElement, Packed, Show, StyleChain, -}; +use crate::foundations::{elem, func, Content, Func, NativeElement}; use crate::introspection::Locatable; -use crate::layout::{BlockElem, Size}; /// Provides access to the current outer container's (or page's, if none) /// dimensions (width and height). @@ -86,37 +80,9 @@ pub fn layout( } /// Executes a `layout` call. -#[elem(Locatable, Show)] -struct LayoutElem { +#[elem(Locatable)] +pub struct LayoutElem { /// The function to call with the outer container's (or page's) size. #[required] - func: Func, -} - -impl Show for Packed { - fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::multi_layouter( - self.clone(), - |elem, engine, locator, styles, regions| { - // Gets the current region's base size, which will be the size of the - // outer container, or of the page if there is no such container. - let Size { x, y } = regions.base(); - let loc = elem.location().unwrap(); - let context = Context::new(Some(loc), Some(styles)); - let result = elem - .func - .call( - engine, - context.track(), - [dict! { "width" => x, "height" => y }], - )? - .display(); - (engine.routines.layout_fragment)( - engine, &result, locator, styles, regions, - ) - }, - ) - .pack() - .spanned(self.span())) - } + pub func: Func, } diff --git a/crates/typst-library/src/layout/pad.rs b/crates/typst-library/src/layout/pad.rs index 1dc6d1316..d533df35b 100644 --- a/crates/typst-library/src/layout/pad.rs +++ b/crates/typst-library/src/layout/pad.rs @@ -1,7 +1,5 @@ -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::{elem, Content, NativeElement, Packed, Show, StyleChain}; -use crate::layout::{BlockElem, Length, Rel}; +use crate::foundations::{elem, Content}; +use crate::layout::{Length, Rel}; /// Adds spacing around content. /// @@ -16,7 +14,7 @@ use crate::layout::{BlockElem, Length, Rel}; /// _Typing speeds can be /// measured in words per minute._ /// ``` -#[elem(title = "Padding", Show)] +#[elem(title = "Padding")] pub struct PadElem { /// The padding at the left side. #[parse( @@ -55,11 +53,3 @@ pub struct PadElem { #[required] pub body: Content, } - -impl Show for Packed { - fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::multi_layouter(self.clone(), engine.routines.layout_pad) - .pack() - .spanned(self.span())) - } -} diff --git a/crates/typst-library/src/layout/repeat.rs b/crates/typst-library/src/layout/repeat.rs index 9579f1856..a38d5f896 100644 --- a/crates/typst-library/src/layout/repeat.rs +++ b/crates/typst-library/src/layout/repeat.rs @@ -1,7 +1,5 @@ -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::{elem, Content, NativeElement, Packed, Show, StyleChain}; -use crate::layout::{BlockElem, Length}; +use crate::foundations::{elem, Content}; +use crate::layout::Length; /// Repeats content to the available space. /// @@ -24,7 +22,7 @@ use crate::layout::{BlockElem, Length}; /// Berlin, the 22nd of December, 2022 /// ] /// ``` -#[elem(Show)] +#[elem] pub struct RepeatElem { /// The content to repeat. #[required] @@ -39,11 +37,3 @@ pub struct RepeatElem { #[default(true)] pub justify: bool, } - -impl Show for Packed { - fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_repeat) - .pack() - .spanned(self.span())) - } -} diff --git a/crates/typst-library/src/layout/stack.rs b/crates/typst-library/src/layout/stack.rs index 5fc78480e..fca1ecb86 100644 --- a/crates/typst-library/src/layout/stack.rs +++ b/crates/typst-library/src/layout/stack.rs @@ -1,9 +1,7 @@ use std::fmt::{self, Debug, Formatter}; -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::{cast, elem, Content, NativeElement, Packed, Show, StyleChain}; -use crate::layout::{BlockElem, Dir, Spacing}; +use crate::foundations::{cast, elem, Content}; +use crate::layout::{Dir, Spacing}; /// Arranges content and spacing horizontally or vertically. /// @@ -19,7 +17,7 @@ use crate::layout::{BlockElem, Dir, Spacing}; /// rect(width: 90pt), /// ) /// ``` -#[elem(Show)] +#[elem] pub struct StackElem { /// The direction along which the items are stacked. Possible values are: /// @@ -47,14 +45,6 @@ pub struct StackElem { pub children: Vec, } -impl Show for Packed { - fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::multi_layouter(self.clone(), engine.routines.layout_stack) - .pack() - .spanned(self.span())) - } -} - /// A child of a stack element. #[derive(Clone, PartialEq, Hash)] pub enum StackChild { diff --git a/crates/typst-library/src/layout/transform.rs b/crates/typst-library/src/layout/transform.rs index d153d97db..c2d9a21c7 100644 --- a/crates/typst-library/src/layout/transform.rs +++ b/crates/typst-library/src/layout/transform.rs @@ -1,11 +1,5 @@ -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::{ - cast, elem, Content, NativeElement, Packed, Show, Smart, StyleChain, -}; -use crate::layout::{ - Abs, Alignment, Angle, BlockElem, HAlignment, Length, Ratio, Rel, VAlignment, -}; +use crate::foundations::{cast, elem, Content, Smart}; +use crate::layout::{Abs, Alignment, Angle, HAlignment, Length, Ratio, Rel, VAlignment}; /// Moves content without affecting layout. /// @@ -25,7 +19,7 @@ use crate::layout::{ /// ) /// )) /// ``` -#[elem(Show)] +#[elem] pub struct MoveElem { /// The horizontal displacement of the content. pub dx: Rel, @@ -38,14 +32,6 @@ pub struct MoveElem { pub body: Content, } -impl Show for Packed { - fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_move) - .pack() - .spanned(self.span())) - } -} - /// Rotates content without affecting layout. /// /// Rotates an element by a given angle. The layout will act as if the element @@ -60,7 +46,7 @@ impl Show for Packed { /// .map(i => rotate(24deg * i)[X]), /// ) /// ``` -#[elem(Show)] +#[elem] pub struct RotateElem { /// The amount of rotation. /// @@ -107,14 +93,6 @@ pub struct RotateElem { pub body: Content, } -impl Show for Packed { - fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_rotate) - .pack() - .spanned(self.span())) - } -} - /// Scales content without affecting layout. /// /// Lets you mirror content by specifying a negative scale on a single axis. @@ -125,7 +103,7 @@ impl Show for Packed { /// #scale(x: -100%)[This is mirrored.] /// #scale(x: -100%, reflow: true)[This is mirrored.] /// ``` -#[elem(Show)] +#[elem] pub struct ScaleElem { /// The scaling factor for both axes, as a positional argument. This is just /// an optional shorthand notation for setting `x` and `y` to the same @@ -179,14 +157,6 @@ pub struct ScaleElem { pub body: Content, } -impl Show for Packed { - fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_scale) - .pack() - .spanned(self.span())) - } -} - /// To what size something shall be scaled. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum ScaleAmount { @@ -215,7 +185,7 @@ cast! { /// This is some fake italic text. /// ] /// ``` -#[elem(Show)] +#[elem] pub struct SkewElem { /// The horizontal skewing angle. /// @@ -265,14 +235,6 @@ pub struct SkewElem { pub body: Content, } -impl Show for Packed { - fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_skew) - .pack() - .spanned(self.span())) - } -} - /// A scale-skew-translate transformation. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct Transform { diff --git a/crates/typst-library/src/math/equation.rs b/crates/typst-library/src/math/equation.rs index b97bb18da..0c9ba11df 100644 --- a/crates/typst-library/src/math/equation.rs +++ b/crates/typst-library/src/math/equation.rs @@ -6,13 +6,11 @@ use unicode_math_class::MathClass; use crate::diag::SourceResult; use crate::engine::Engine; use crate::foundations::{ - elem, Content, NativeElement, Packed, Show, ShowSet, Smart, StyleChain, Styles, - Synthesize, + elem, Content, NativeElement, Packed, ShowSet, Smart, StyleChain, Styles, Synthesize, }; use crate::introspection::{Count, Counter, CounterUpdate, Locatable}; use crate::layout::{ - AlignElem, Alignment, BlockElem, InlineElem, OuterHAlignment, SpecificAlignment, - VAlignment, + AlignElem, Alignment, BlockElem, OuterHAlignment, SpecificAlignment, VAlignment, }; use crate::math::{MathSize, MathVariant}; use crate::model::{Numbering, Outlinable, ParLine, Refable, Supplement}; @@ -46,7 +44,7 @@ use crate::text::{FontFamily, FontList, FontWeight, LocalName, TextElem}; /// least one space lifts it into a separate block that is centered /// horizontally. For more details about math syntax, see the /// [main math page]($category/math). -#[elem(Locatable, Synthesize, Show, ShowSet, Count, LocalName, Refable, Outlinable)] +#[elem(Locatable, Synthesize, ShowSet, Count, LocalName, Refable, Outlinable)] pub struct EquationElem { /// Whether the equation is displayed as a separate block. #[default(false)] @@ -165,23 +163,6 @@ impl Synthesize for Packed { } } -impl Show for Packed { - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - if self.block.get(styles) { - Ok(BlockElem::multi_layouter( - self.clone(), - engine.routines.layout_equation_block, - ) - .pack() - .spanned(self.span())) - } else { - Ok(InlineElem::layouter(self.clone(), engine.routines.layout_equation_inline) - .pack() - .spanned(self.span())) - } - } -} - impl ShowSet for Packed { fn show_set(&self, styles: StyleChain) -> Styles { let mut out = Styles::new(); diff --git a/crates/typst-library/src/model/bibliography.rs b/crates/typst-library/src/model/bibliography.rs index c44748a95..188af4da1 100644 --- a/crates/typst-library/src/model/bibliography.rs +++ b/crates/typst-library/src/model/bibliography.rs @@ -2,7 +2,6 @@ use std::any::TypeId; use std::collections::HashMap; use std::ffi::OsStr; use std::fmt::{self, Debug, Formatter}; -use std::num::NonZeroUsize; use std::path::Path; use std::sync::{Arc, LazyLock}; @@ -17,7 +16,7 @@ use hayagriva::{ use indexmap::IndexMap; use smallvec::{smallvec, SmallVec}; use typst_syntax::{Span, Spanned, SyntaxMode}; -use typst_utils::{Get, ManuallyHash, NonZeroExt, PicoStr}; +use typst_utils::{ManuallyHash, PicoStr}; use crate::diag::{ bail, error, At, HintedStrResult, LoadError, LoadResult, LoadedWithin, ReportPos, @@ -26,18 +25,17 @@ use crate::diag::{ use crate::engine::{Engine, Sink}; use crate::foundations::{ elem, Bytes, CastInfo, Content, Derived, FromValue, IntoValue, Label, NativeElement, - OneOrMultiple, Packed, Reflect, Scope, Show, ShowSet, Smart, StyleChain, Styles, + OneOrMultiple, Packed, Reflect, Scope, ShowSet, Smart, StyleChain, Styles, Synthesize, Value, }; use crate::introspection::{Introspector, Locatable, Location}; use crate::layout::{ BlockBody, BlockElem, Em, GridCell, GridChild, GridElem, GridItem, HElem, PadElem, - Sides, Sizing, TrackSizings, + Sizing, TrackSizings, }; use crate::loading::{format_yaml_error, DataSource, Load, LoadSource, Loaded}; use crate::model::{ - CitationForm, CiteGroup, Destination, FootnoteElem, HeadingElem, LinkElem, ParElem, - Url, + CitationForm, CiteGroup, Destination, FootnoteElem, HeadingElem, LinkElem, Url, }; use crate::routines::Routines; use crate::text::{ @@ -88,7 +86,7 @@ use crate::World; /// /// #bibliography("works.bib") /// ``` -#[elem(Locatable, Synthesize, Show, ShowSet, LocalName)] +#[elem(Locatable, Synthesize, ShowSet, LocalName)] pub struct BibliographyElem { /// One or multiple paths to or raw bytes for Hayagriva `.yaml` and/or /// BibLaTeX `.bib` files. @@ -203,84 +201,6 @@ impl Synthesize for Packed { } } -impl Show for Packed { - #[typst_macros::time(name = "bibliography", span = self.span())] - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - const COLUMN_GUTTER: Em = Em::new(0.65); - const INDENT: Em = Em::new(1.5); - - let span = self.span(); - - let mut seq = vec![]; - if let Some(title) = self.title.get_ref(styles).clone().unwrap_or_else(|| { - Some(TextElem::packed(Self::local_name_in(styles)).spanned(span)) - }) { - seq.push( - HeadingElem::new(title) - .with_depth(NonZeroUsize::ONE) - .pack() - .spanned(span), - ); - } - - let works = Works::generate(engine).at(span)?; - let references = works - .references - .as_ref() - .ok_or_else(|| match self.style.get_ref(styles).source { - CslSource::Named(style) => eco_format!( - "CSL style \"{}\" is not suitable for bibliographies", - style.display_name() - ), - CslSource::Normal(..) => { - "CSL style is not suitable for bibliographies".into() - } - }) - .at(span)?; - - if references.iter().any(|(prefix, _)| prefix.is_some()) { - let row_gutter = styles.get(ParElem::spacing); - - let mut cells = vec![]; - for (prefix, reference) in references { - cells.push(GridChild::Item(GridItem::Cell( - Packed::new(GridCell::new(prefix.clone().unwrap_or_default())) - .spanned(span), - ))); - cells.push(GridChild::Item(GridItem::Cell( - Packed::new(GridCell::new(reference.clone())).spanned(span), - ))); - } - seq.push( - GridElem::new(cells) - .with_columns(TrackSizings(smallvec![Sizing::Auto; 2])) - .with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()])) - .with_row_gutter(TrackSizings(smallvec![row_gutter.into()])) - .pack() - .spanned(span), - ); - } else { - for (_, reference) in references { - let realized = reference.clone(); - let block = if works.hanging_indent { - let body = HElem::new((-INDENT).into()).pack() + realized; - let inset = Sides::default() - .with(styles.resolve(TextElem::dir).start(), Some(INDENT.into())); - BlockElem::new() - .with_body(Some(BlockBody::Content(body))) - .with_inset(inset) - } else { - BlockElem::new().with_body(Some(BlockBody::Content(realized))) - }; - - seq.push(block.pack().spanned(span)); - } - } - - Ok(Content::sequence(seq)) - } -} - impl ShowSet for Packed { fn show_set(&self, _: StyleChain) -> Styles { const INDENT: Em = Em::new(1.0); @@ -564,7 +484,7 @@ impl IntoValue for CslSource { /// memoization) for the whole document. This setup is necessary because /// citation formatting is inherently stateful and we need access to all /// citations to do it. -pub(super) struct Works { +pub struct Works { /// Maps from the location of a citation group to its rendered content. pub citations: HashMap>, /// Lists all references in the bibliography, with optional prefix, or diff --git a/crates/typst-library/src/model/cite.rs b/crates/typst-library/src/model/cite.rs index 195139907..b3ae3e522 100644 --- a/crates/typst-library/src/model/cite.rs +++ b/crates/typst-library/src/model/cite.rs @@ -3,8 +3,7 @@ use typst_syntax::Spanned; use crate::diag::{error, At, HintedString, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, Cast, Content, Derived, Label, Packed, Show, Smart, StyleChain, - Synthesize, + cast, elem, Cast, Content, Derived, Label, Packed, Smart, StyleChain, Synthesize, }; use crate::introspection::Locatable; use crate::model::bibliography::Works; @@ -153,16 +152,15 @@ pub enum CitationForm { /// /// This is automatically created from adjacent citations during show rule /// application. -#[elem(Locatable, Show)] +#[elem(Locatable)] pub struct CiteGroup { /// The citations. #[required] pub children: Vec>, } -impl Show for Packed { - #[typst_macros::time(name = "cite", span = self.span())] - fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult { +impl Packed { + pub fn realize(&self, engine: &mut Engine) -> SourceResult { let location = self.location().unwrap(); let span = self.span(); Works::generate(engine) diff --git a/crates/typst-library/src/model/emph.rs b/crates/typst-library/src/model/emph.rs index 2d9cbec12..6267736b3 100644 --- a/crates/typst-library/src/model/emph.rs +++ b/crates/typst-library/src/model/emph.rs @@ -1,10 +1,4 @@ -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::{ - elem, Content, NativeElement, Packed, Show, StyleChain, TargetElem, -}; -use crate::html::{tag, HtmlElem}; -use crate::text::{ItalicToggle, TextElem}; +use crate::foundations::{elem, Content}; /// Emphasizes content by toggling italics. /// @@ -29,24 +23,9 @@ use crate::text::{ItalicToggle, TextElem}; /// This function also has dedicated syntax: To emphasize content, simply /// enclose it in underscores (`_`). Note that this only works at word /// boundaries. To emphasize part of a word, you have to use the function. -#[elem(title = "Emphasis", keywords = ["italic"], Show)] +#[elem(title = "Emphasis", keywords = ["italic"])] pub struct EmphElem { /// The content to emphasize. #[required] pub body: Content, } - -impl Show for Packed { - #[typst_macros::time(name = "emph", span = self.span())] - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - let body = self.body.clone(); - Ok(if styles.get(TargetElem::target).is_html() { - HtmlElem::new(tag::em) - .with_body(Some(body)) - .pack() - .spanned(self.span()) - } else { - body.set(TextElem::emph, ItalicToggle(true)) - }) - } -} diff --git a/crates/typst-library/src/model/enum.rs b/crates/typst-library/src/model/enum.rs index 8c1916581..388fb9eda 100644 --- a/crates/typst-library/src/model/enum.rs +++ b/crates/typst-library/src/model/enum.rs @@ -1,19 +1,11 @@ use std::str::FromStr; -use ecow::eco_format; use smallvec::SmallVec; -use crate::diag::{bail, SourceResult}; -use crate::engine::Engine; -use crate::foundations::{ - cast, elem, scope, Array, Content, NativeElement, Packed, Show, Smart, StyleChain, - Styles, TargetElem, -}; -use crate::html::{attr, tag, HtmlElem}; -use crate::layout::{Alignment, BlockElem, Em, HAlignment, Length, VAlignment, VElem}; -use crate::model::{ - ListItemLike, ListLike, Numbering, NumberingPattern, ParElem, ParbreakElem, -}; +use crate::diag::bail; +use crate::foundations::{cast, elem, scope, Array, Content, Packed, Smart, Styles}; +use crate::layout::{Alignment, Em, HAlignment, Length, VAlignment}; +use crate::model::{ListItemLike, ListLike, Numbering, NumberingPattern}; /// A numbered list. /// @@ -71,7 +63,7 @@ use crate::model::{ /// Enumeration items can contain multiple paragraphs and other block-level /// content. All content that is indented more than an item's marker becomes /// part of that item. -#[elem(scope, title = "Numbered List", Show)] +#[elem(scope, title = "Numbered List")] pub struct EnumElem { /// Defines the default [spacing]($enum.spacing) of the enumeration. If it /// is `{false}`, the items are spaced apart with @@ -223,51 +215,6 @@ impl EnumElem { type EnumItem; } -impl Show for Packed { - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let tight = self.tight.get(styles); - - if styles.get(TargetElem::target).is_html() { - let mut elem = HtmlElem::new(tag::ol); - if self.reversed.get(styles) { - elem = elem.with_attr(attr::reversed, "reversed"); - } - if let Some(n) = self.start.get(styles).custom() { - elem = elem.with_attr(attr::start, eco_format!("{n}")); - } - let body = Content::sequence(self.children.iter().map(|item| { - let mut li = HtmlElem::new(tag::li); - if let Some(nr) = item.number.get(styles) { - li = li.with_attr(attr::value, eco_format!("{nr}")); - } - // Text in wide enums shall always turn into paragraphs. - let mut body = item.body.clone(); - if !tight { - body += ParbreakElem::shared(); - } - li.with_body(Some(body)).pack().spanned(item.span()) - })); - return Ok(elem.with_body(Some(body)).pack().spanned(self.span())); - } - - let mut realized = - BlockElem::multi_layouter(self.clone(), engine.routines.layout_enum) - .pack() - .spanned(self.span()); - - if tight { - let spacing = self - .spacing - .get(styles) - .unwrap_or_else(|| styles.get(ParElem::leading)); - let v = VElem::new(spacing.into()).with_weak(true).with_attach(true).pack(); - realized = v + realized; - } - - Ok(realized) - } -} - /// An enumeration item. #[elem(name = "item", title = "Numbered List Item")] pub struct EnumItem { diff --git a/crates/typst-library/src/model/figure.rs b/crates/typst-library/src/model/figure.rs index 7ac659a93..ac3676eea 100644 --- a/crates/typst-library/src/model/figure.rs +++ b/crates/typst-library/src/model/figure.rs @@ -9,19 +9,16 @@ use crate::diag::{bail, SourceResult}; use crate::engine::Engine; use crate::foundations::{ cast, elem, scope, select_where, Content, Element, NativeElement, Packed, Selector, - Show, ShowSet, Smart, StyleChain, Styles, Synthesize, TargetElem, + ShowSet, Smart, StyleChain, Styles, Synthesize, }; -use crate::html::{tag, HtmlElem}; use crate::introspection::{ Count, Counter, CounterKey, CounterUpdate, Locatable, Location, }; use crate::layout::{ - AlignElem, Alignment, BlockBody, BlockElem, Em, HAlignment, Length, OuterVAlignment, - PlaceElem, PlacementScope, VAlignment, VElem, -}; -use crate::model::{ - Numbering, NumberingPattern, Outlinable, ParbreakElem, Refable, Supplement, + AlignElem, Alignment, BlockElem, Em, Length, OuterVAlignment, PlacementScope, + VAlignment, }; +use crate::model::{Numbering, NumberingPattern, Outlinable, Refable, Supplement}; use crate::text::{Lang, Region, TextElem}; use crate::visualize::ImageElem; @@ -104,7 +101,7 @@ use crate::visualize::ImageElem; /// caption: [I'm up here], /// ) /// ``` -#[elem(scope, Locatable, Synthesize, Count, Show, ShowSet, Refable, Outlinable)] +#[elem(scope, Locatable, Synthesize, Count, ShowSet, Refable, Outlinable)] pub struct FigureElem { /// The content of the figure. Often, an [image]. #[required] @@ -328,65 +325,6 @@ impl Synthesize for Packed { } } -impl Show for Packed { - #[typst_macros::time(name = "figure", span = self.span())] - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - let span = self.span(); - let target = styles.get(TargetElem::target); - let mut realized = self.body.clone(); - - // Build the caption, if any. - if let Some(caption) = self.caption.get_cloned(styles) { - let (first, second) = match caption.position.get(styles) { - OuterVAlignment::Top => (caption.pack(), realized), - OuterVAlignment::Bottom => (realized, caption.pack()), - }; - let mut seq = Vec::with_capacity(3); - seq.push(first); - if !target.is_html() { - let v = VElem::new(self.gap.get(styles).into()).with_weak(true); - seq.push(v.pack().spanned(span)) - } - seq.push(second); - realized = Content::sequence(seq) - } - - // Ensure that the body is considered a paragraph. - realized += ParbreakElem::shared().clone().spanned(span); - - if target.is_html() { - return Ok(HtmlElem::new(tag::figure) - .with_body(Some(realized)) - .pack() - .spanned(span)); - } - - // Wrap the contents in a block. - realized = BlockElem::new() - .with_body(Some(BlockBody::Content(realized))) - .pack() - .spanned(span); - - // Wrap in a float. - if let Some(align) = self.placement.get(styles) { - realized = PlaceElem::new(realized) - .with_alignment(align.map(|align| HAlignment::Center + align)) - .with_scope(self.scope.get(styles)) - .with_float(true) - .pack() - .spanned(span); - } else if self.scope.get(styles) == PlacementScope::Parent { - bail!( - span, - "parent-scoped placement is only available for floating figures"; - hint: "you can enable floating placement with `figure(placement: auto, ..)`" - ); - } - - Ok(realized) - } -} - impl ShowSet for Packed { fn show_set(&self, _: StyleChain) -> Styles { // Still allows breakable figures with @@ -471,7 +409,7 @@ impl Outlinable for Packed { /// caption: [A rectangle], /// ) /// ``` -#[elem(name = "caption", Synthesize, Show)] +#[elem(name = "caption", Synthesize)] pub struct FigureCaption { /// The caption's position in the figure. Either `{top}` or `{bottom}`. /// @@ -559,6 +497,35 @@ pub struct FigureCaption { } impl FigureCaption { + /// Realizes the textual caption content. + pub fn realize( + &self, + engine: &mut Engine, + styles: StyleChain, + ) -> SourceResult { + let mut realized = self.body.clone(); + + if let ( + Some(Some(mut supplement)), + Some(Some(numbering)), + Some(Some(counter)), + Some(Some(location)), + ) = ( + self.supplement.clone(), + &self.numbering, + &self.counter, + &self.figure_location, + ) { + let numbers = counter.display_at_loc(engine, *location, styles, numbering)?; + if !supplement.is_empty() { + supplement += TextElem::packed('\u{a0}'); + } + realized = supplement + numbers + self.get_separator(styles) + realized; + } + + Ok(realized) + } + /// Gets the default separator in the given language and (optionally) /// region. fn local_separator(lang: Lang, _: Option) -> &'static str { @@ -588,43 +555,6 @@ impl Synthesize for Packed { } } -impl Show for Packed { - #[typst_macros::time(name = "figure.caption", span = self.span())] - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let mut realized = self.body.clone(); - - if let ( - Some(Some(mut supplement)), - Some(Some(numbering)), - Some(Some(counter)), - Some(Some(location)), - ) = ( - self.supplement.clone(), - &self.numbering, - &self.counter, - &self.figure_location, - ) { - let numbers = counter.display_at_loc(engine, *location, styles, numbering)?; - if !supplement.is_empty() { - supplement += TextElem::packed('\u{a0}'); - } - realized = supplement + numbers + self.get_separator(styles) + realized; - } - - Ok(if styles.get(TargetElem::target).is_html() { - HtmlElem::new(tag::figcaption) - .with_body(Some(realized)) - .pack() - .spanned(self.span()) - } else { - BlockElem::new() - .with_body(Some(BlockBody::Content(realized))) - .pack() - .spanned(self.span()) - }) - } -} - cast! { FigureCaption, v: Content => v.unpack::().unwrap_or_else(Self::new), diff --git a/crates/typst-library/src/model/footnote.rs b/crates/typst-library/src/model/footnote.rs index 63a461bd6..b920a8ae4 100644 --- a/crates/typst-library/src/model/footnote.rs +++ b/crates/typst-library/src/model/footnote.rs @@ -3,16 +3,16 @@ use std::str::FromStr; use typst_utils::NonZeroExt; -use crate::diag::{bail, At, SourceResult, StrResult}; +use crate::diag::{bail, StrResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, scope, Content, Label, NativeElement, Packed, Show, ShowSet, Smart, - StyleChain, Styles, + cast, elem, scope, Content, Label, NativeElement, Packed, ShowSet, Smart, StyleChain, + Styles, }; -use crate::introspection::{Count, Counter, CounterUpdate, Locatable, Location}; -use crate::layout::{Abs, Em, HElem, Length, Ratio}; -use crate::model::{Destination, Numbering, NumberingPattern, ParElem}; -use crate::text::{SuperElem, TextElem, TextSize}; +use crate::introspection::{Count, CounterUpdate, Locatable, Location}; +use crate::layout::{Abs, Em, Length, Ratio}; +use crate::model::{Numbering, NumberingPattern, ParElem}; +use crate::text::{TextElem, TextSize}; use crate::visualize::{LineElem, Stroke}; /// A footnote. @@ -51,7 +51,7 @@ use crate::visualize::{LineElem, Stroke}; /// apply to the footnote's content. See [here][issue] for more information. /// /// [issue]: https://github.com/typst/typst/issues/1467#issuecomment-1588799440 -#[elem(scope, Locatable, Show, Count)] +#[elem(scope, Locatable, Count)] pub struct FootnoteElem { /// How to number footnotes. /// @@ -135,21 +135,6 @@ impl Packed { } } -impl Show for Packed { - #[typst_macros::time(name = "footnote", span = self.span())] - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let span = self.span(); - let loc = self.declaration_location(engine).at(span)?; - let numbering = self.numbering.get_ref(styles); - let counter = Counter::of(FootnoteElem::ELEM); - let num = counter.display_at_loc(engine, loc, styles, numbering)?; - let sup = SuperElem::new(num).pack().spanned(span); - let loc = loc.variant(1); - // Add zero-width weak spacing to make the footnote "sticky". - Ok(HElem::hole().pack() + sup.linked(Destination::Location(loc))) - } -} - impl Count for Packed { fn update(&self) -> Option { (!self.is_ref()).then(|| CounterUpdate::Step(NonZeroUsize::ONE)) @@ -191,7 +176,7 @@ cast! { /// page run is a sequence of pages without an explicit pagebreak in between). /// For this reason, set and show rules for footnote entries should be defined /// before any page content, typically at the very start of the document. -#[elem(name = "entry", title = "Footnote Entry", Show, ShowSet)] +#[elem(name = "entry", title = "Footnote Entry", ShowSet)] pub struct FootnoteEntry { /// The footnote for this entry. Its location can be used to determine /// the footnote counter state. @@ -274,37 +259,6 @@ pub struct FootnoteEntry { pub indent: Length, } -impl Show for Packed { - #[typst_macros::time(name = "footnote.entry", span = self.span())] - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let span = self.span(); - let number_gap = Em::new(0.05); - let default = StyleChain::default(); - let numbering = self.note.numbering.get_ref(default); - let counter = Counter::of(FootnoteElem::ELEM); - let Some(loc) = self.note.location() else { - bail!( - span, "footnote entry must have a location"; - hint: "try using a query or a show rule to customize the footnote instead" - ); - }; - - let num = counter.display_at_loc(engine, loc, styles, numbering)?; - let sup = SuperElem::new(num) - .pack() - .spanned(span) - .linked(Destination::Location(loc)) - .located(loc.variant(1)); - - Ok(Content::sequence([ - HElem::new(self.indent.get(styles).into()).pack(), - sup, - HElem::new(number_gap.into()).with_weak(true).pack(), - self.note.body_content().unwrap().clone(), - ])) - } -} - impl ShowSet for Packed { fn show_set(&self, _: StyleChain) -> Styles { let mut out = Styles::new(); diff --git a/crates/typst-library/src/model/heading.rs b/crates/typst-library/src/model/heading.rs index d6f6d01f9..0f2a1d338 100644 --- a/crates/typst-library/src/model/heading.rs +++ b/crates/typst-library/src/model/heading.rs @@ -1,21 +1,16 @@ use std::num::NonZeroUsize; -use ecow::eco_format; -use typst_utils::{Get, NonZeroExt}; +use typst_utils::NonZeroExt; -use crate::diag::{warning, SourceResult}; +use crate::diag::SourceResult; use crate::engine::Engine; use crate::foundations::{ - elem, Content, NativeElement, Packed, Resolve, Show, ShowSet, Smart, StyleChain, - Styles, Synthesize, TargetElem, + elem, Content, NativeElement, Packed, ShowSet, Smart, StyleChain, Styles, Synthesize, }; -use crate::html::{attr, tag, HtmlElem}; -use crate::introspection::{ - Count, Counter, CounterUpdate, Locatable, Locator, LocatorLink, -}; -use crate::layout::{Abs, Axes, BlockBody, BlockElem, Em, HElem, Length, Region, Sides}; +use crate::introspection::{Count, Counter, CounterUpdate, Locatable}; +use crate::layout::{BlockElem, Em, Length}; use crate::model::{Numbering, Outlinable, Refable, Supplement}; -use crate::text::{FontWeight, LocalName, SpaceElem, TextElem, TextSize}; +use crate::text::{FontWeight, LocalName, TextElem, TextSize}; /// A section heading. /// @@ -49,7 +44,7 @@ use crate::text::{FontWeight, LocalName, SpaceElem, TextElem, TextSize}; /// one or multiple equals signs, followed by a space. The number of equals /// signs determines the heading's logical nesting depth. The `{offset}` field /// can be set to configure the starting depth. -#[elem(Locatable, Synthesize, Count, Show, ShowSet, LocalName, Refable, Outlinable)] +#[elem(Locatable, Synthesize, Count, ShowSet, LocalName, Refable, Outlinable)] pub struct HeadingElem { /// The absolute nesting depth of the heading, starting from one. If set /// to `{auto}`, it is computed from `{offset + depth}`. @@ -215,96 +210,6 @@ impl Synthesize for Packed { } } -impl Show for Packed { - #[typst_macros::time(name = "heading", span = self.span())] - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let html = styles.get(TargetElem::target).is_html(); - - const SPACING_TO_NUMBERING: Em = Em::new(0.3); - - let span = self.span(); - let mut realized = self.body.clone(); - - let hanging_indent = self.hanging_indent.get(styles); - let mut indent = match hanging_indent { - Smart::Custom(length) => length.resolve(styles), - Smart::Auto => Abs::zero(), - }; - - if let Some(numbering) = self.numbering.get_ref(styles).as_ref() { - let location = self.location().unwrap(); - let numbering = Counter::of(HeadingElem::ELEM) - .display_at_loc(engine, location, styles, numbering)? - .spanned(span); - - if hanging_indent.is_auto() && !html { - let pod = Region::new(Axes::splat(Abs::inf()), Axes::splat(false)); - - // We don't have a locator for the numbering here, so we just - // use the measurement infrastructure for now. - let link = LocatorLink::measure(location); - let size = (engine.routines.layout_frame)( - engine, - &numbering, - Locator::link(&link), - styles, - pod, - )? - .size(); - - indent = size.x + SPACING_TO_NUMBERING.resolve(styles); - } - - let spacing = if html { - SpaceElem::shared().clone() - } else { - HElem::new(SPACING_TO_NUMBERING.into()).with_weak(true).pack() - }; - - realized = numbering + spacing + realized; - } - - Ok(if html { - // 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 - // reason, levels are offset by one: A Typst level 1 heading becomes - // a `

`. - let level = self.resolve_level(styles).get(); - if level >= 6 { - engine.sink.warn(warning!(span, - "heading of level {} was transformed to \ -
, which is not \ - supported by all assistive technology", - level, level + 1; - hint: "HTML only supports

to

, not ", level + 1; - hint: "you may want to restructure your document so that \ - it doesn't contain deep headings")); - HtmlElem::new(tag::div) - .with_body(Some(realized)) - .with_attr(attr::role, "heading") - .with_attr(attr::aria_level, eco_format!("{}", level + 1)) - .pack() - .spanned(span) - } else { - let t = [tag::h2, tag::h3, tag::h4, tag::h5, tag::h6][level - 1]; - HtmlElem::new(t).with_body(Some(realized)).pack().spanned(span) - } - } else { - let block = if indent != Abs::zero() { - let body = HElem::new((-indent).into()).pack() + realized; - let inset = Sides::default() - .with(styles.resolve(TextElem::dir).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) - }) - } -} - impl ShowSet for Packed { fn show_set(&self, styles: StyleChain) -> Styles { let level = self.resolve_level(styles).get(); diff --git a/crates/typst-library/src/model/link.rs b/crates/typst-library/src/model/link.rs index 1e2c708e8..c630835e0 100644 --- a/crates/typst-library/src/model/link.rs +++ b/crates/typst-library/src/model/link.rs @@ -2,13 +2,10 @@ use std::ops::Deref; use ecow::{eco_format, EcoString}; -use crate::diag::{bail, warning, At, SourceResult, StrResult}; -use crate::engine::Engine; +use crate::diag::{bail, StrResult}; use crate::foundations::{ - cast, elem, Content, Label, NativeElement, Packed, Repr, Show, ShowSet, Smart, - StyleChain, Styles, TargetElem, + cast, elem, Content, Label, Packed, Repr, ShowSet, Smart, StyleChain, Styles, }; -use crate::html::{attr, tag, HtmlElem}; use crate::introspection::Location; use crate::layout::Position; use crate::text::TextElem; @@ -38,7 +35,7 @@ use crate::text::TextElem; /// # Syntax /// This function also has dedicated syntax: Text that starts with `http://` or /// `https://` is automatically turned into a link. -#[elem(Show)] +#[elem] pub struct LinkElem { /// The destination the link points to. /// @@ -103,38 +100,6 @@ impl LinkElem { } } -impl Show for Packed { - #[typst_macros::time(name = "link", span = self.span())] - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let body = self.body.clone(); - - Ok(if styles.get(TargetElem::target).is_html() { - if let LinkTarget::Dest(Destination::Url(url)) = &self.dest { - HtmlElem::new(tag::a) - .with_attr(attr::href, url.clone().into_inner()) - .with_body(Some(body)) - .pack() - .spanned(self.span()) - } else { - engine.sink.warn(warning!( - self.span(), - "non-URL links are not yet supported by HTML export" - )); - body - } - } else { - match &self.dest { - LinkTarget::Dest(dest) => body.linked(dest.clone()), - LinkTarget::Label(label) => { - let elem = engine.introspector.query_label(*label).at(self.span())?; - let dest = Destination::Location(elem.location().unwrap()); - body.clone().linked(dest) - } - } - }) - } -} - impl ShowSet for Packed { fn show_set(&self, _: StyleChain) -> Styles { let mut out = Styles::new(); diff --git a/crates/typst-library/src/model/list.rs b/crates/typst-library/src/model/list.rs index 5e6db1faa..660716de7 100644 --- a/crates/typst-library/src/model/list.rs +++ b/crates/typst-library/src/model/list.rs @@ -3,12 +3,10 @@ use comemo::Track; use crate::diag::{bail, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, scope, Array, Content, Context, Depth, Func, NativeElement, Packed, Show, - Smart, StyleChain, Styles, TargetElem, Value, + cast, elem, scope, Array, Content, Context, Depth, Func, NativeElement, Packed, + Smart, StyleChain, Styles, Value, }; -use crate::html::{tag, HtmlElem}; -use crate::layout::{BlockElem, Em, Length, VElem}; -use crate::model::{ParElem, ParbreakElem}; +use crate::layout::{Em, Length}; use crate::text::TextElem; /// A bullet list. @@ -42,7 +40,7 @@ use crate::text::TextElem; /// followed by a space to create a list item. A list item can contain multiple /// paragraphs and other block-level content. All content that is indented /// more than an item's marker becomes part of that item. -#[elem(scope, title = "Bullet List", Show)] +#[elem(scope, title = "Bullet List")] pub struct ListElem { /// Defines the default [spacing]($list.spacing) of the list. If it is /// `{false}`, the items are spaced apart with @@ -136,45 +134,6 @@ impl ListElem { type ListItem; } -impl Show for Packed { - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let tight = self.tight.get(styles); - - if styles.get(TargetElem::target).is_html() { - return Ok(HtmlElem::new(tag::ul) - .with_body(Some(Content::sequence(self.children.iter().map(|item| { - // Text in wide lists shall always turn into paragraphs. - let mut body = item.body.clone(); - if !tight { - body += ParbreakElem::shared(); - } - HtmlElem::new(tag::li) - .with_body(Some(body)) - .pack() - .spanned(item.span()) - })))) - .pack() - .spanned(self.span())); - } - - let mut realized = - BlockElem::multi_layouter(self.clone(), engine.routines.layout_list) - .pack() - .spanned(self.span()); - - if tight { - let spacing = self - .spacing - .get(styles) - .unwrap_or_else(|| styles.get(ParElem::leading)); - let v = VElem::new(spacing.into()).with_weak(true).with_attach(true).pack(); - realized = v + realized; - } - - Ok(realized) - } -} - /// A bullet list item. #[elem(name = "item", title = "Bullet List Item")] pub struct ListItem { diff --git a/crates/typst-library/src/model/mod.rs b/crates/typst-library/src/model/mod.rs index 9bdbf0013..a0f7e11af 100644 --- a/crates/typst-library/src/model/mod.rs +++ b/crates/typst-library/src/model/mod.rs @@ -46,23 +46,23 @@ use crate::foundations::Scope; pub fn define(global: &mut Scope) { global.start_category(crate::Category::Model); global.define_elem::(); - global.define_elem::(); + global.define_elem::(); + global.define_elem::(); + global.define_elem::(); + global.define_elem::(); + global.define_elem::(); + global.define_elem::(); + global.define_elem::(); global.define_elem::(); - global.define_elem::(); global.define_elem::(); global.define_elem::(); - global.define_elem::(); global.define_elem::(); + global.define_elem::(); + global.define_elem::(); + global.define_elem::(); global.define_elem::(); global.define_elem::(); - global.define_elem::(); - global.define_elem::(); - global.define_elem::(); - global.define_elem::(); global.define_elem::(); - global.define_elem::(); - global.define_elem::(); - global.define_elem::(); global.define_func::(); global.reset_category(); } diff --git a/crates/typst-library/src/model/outline.rs b/crates/typst-library/src/model/outline.rs index bb061fb7b..4bda02ba3 100644 --- a/crates/typst-library/src/model/outline.rs +++ b/crates/typst-library/src/model/outline.rs @@ -1,7 +1,7 @@ use std::num::NonZeroUsize; use std::str::FromStr; -use comemo::{Track, Tracked}; +use comemo::Tracked; use smallvec::SmallVec; use typst_syntax::Span; use typst_utils::{Get, NonZeroExt}; @@ -10,7 +10,7 @@ use crate::diag::{bail, error, At, HintedStrResult, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ cast, elem, func, scope, select_where, Args, Construct, Content, Context, Func, - LocatableSelector, NativeElement, Packed, Resolve, Show, ShowSet, Smart, StyleChain, + LocatableSelector, NativeElement, Packed, Resolve, ShowSet, Smart, StyleChain, Styles, }; use crate::introspection::{ @@ -20,8 +20,7 @@ use crate::layout::{ Abs, Axes, BlockBody, BlockElem, BoxElem, Dir, Em, Fr, HElem, Length, Region, Rel, RepeatElem, Sides, }; -use crate::math::EquationElem; -use crate::model::{Destination, HeadingElem, NumberingPattern, ParElem, Refable}; +use crate::model::{HeadingElem, NumberingPattern, ParElem, Refable}; use crate::text::{LocalName, SpaceElem, TextElem}; /// A table of contents, figures, or other elements. @@ -147,7 +146,7 @@ use crate::text::{LocalName, SpaceElem, TextElem}; /// /// [^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)] +#[elem(scope, keywords = ["Table of Contents", "toc"], ShowSet, LocalName, Locatable)] pub struct OutlineElem { /// The title of the outline. /// @@ -249,44 +248,6 @@ impl OutlineElem { type OutlineEntry; } -impl Show for Packed { - #[typst_macros::time(name = "outline", span = self.span())] - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let span = self.span(); - - // Build the outline title. - let mut seq = vec![]; - if let Some(title) = self.title.get_cloned(styles).unwrap_or_else(|| { - Some(TextElem::packed(Self::local_name_in(styles)).spanned(span)) - }) { - seq.push( - HeadingElem::new(title) - .with_depth(NonZeroUsize::ONE) - .pack() - .spanned(span), - ); - } - - let elems = engine.introspector.query(&self.target.get_ref(styles).0); - let depth = self.depth.get(styles).unwrap_or(NonZeroUsize::MAX); - - // Build the outline entries. - for elem in elems { - let Some(outlinable) = elem.with::() else { - bail!(span, "cannot outline {}", elem.func().name()); - }; - - let level = outlinable.level(); - if outlinable.outlined() && level <= depth { - let entry = OutlineEntry::new(level, elem); - seq.push(entry.pack().spanned(span)); - } - } - - Ok(Content::sequence(seq)) - } -} - impl ShowSet for Packed { fn show_set(&self, styles: StyleChain) -> Styles { let mut out = Styles::new(); @@ -363,7 +324,7 @@ pub trait Outlinable: Refable { /// With show-set and show rules on outline entries, you can richly customize /// the outline's appearance. See the /// [section on styling the outline]($outline/#styling-the-outline) for details. -#[elem(scope, name = "entry", title = "Outline Entry", Show)] +#[elem(scope, name = "entry", title = "Outline Entry")] pub struct OutlineEntry { /// The nesting level of this outline entry. Starts at `{1}` for top-level /// entries. @@ -408,30 +369,6 @@ pub struct OutlineEntry { pub parent: Option>, } -impl Show for Packed { - #[typst_macros::time(name = "outline.entry", span = self.span())] - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let span = self.span(); - let context = Context::new(None, Some(styles)); - let context = context.track(); - - let prefix = self.prefix(engine, context, span)?; - let inner = self.inner(engine, context, span)?; - let block = if self.element.is::() { - let body = prefix.unwrap_or_default() + inner; - BlockElem::new() - .with_body(Some(BlockBody::Content(body))) - .pack() - .spanned(span) - } 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 @@ -654,7 +591,8 @@ impl OutlineEntry { .ok_or_else(|| error!("cannot outline {}", self.element.func().name())) } - fn element_location(&self) -> HintedStrResult { + /// Returns the location of the outlined element. + pub fn element_location(&self) -> HintedStrResult { let elem = &self.element; elem.location().ok_or_else(|| { if elem.can::() && elem.can::() { @@ -730,8 +668,8 @@ fn query_prefix_widths( } /// Helper type for introspection-based prefix alignment. -#[elem(Construct, Locatable, Show)] -struct PrefixInfo { +#[elem(Construct, Locatable)] +pub(crate) struct PrefixInfo { /// The location of the outline this prefix is part of. This is used to /// scope prefix computations to a specific outline. #[required] @@ -753,9 +691,3 @@ impl Construct for PrefixInfo { bail!(args.span, "cannot be constructed manually"); } } - -impl Show for Packed { - fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { - Ok(Content::empty()) - } -} diff --git a/crates/typst-library/src/model/quote.rs b/crates/typst-library/src/model/quote.rs index a8cf3eaef..9960b7587 100644 --- a/crates/typst-library/src/model/quote.rs +++ b/crates/typst-library/src/model/quote.rs @@ -1,16 +1,13 @@ -use crate::diag::SourceResult; -use crate::engine::Engine; +use typst_syntax::Span; + use crate::foundations::{ - cast, elem, Content, Depth, Label, NativeElement, Packed, Show, ShowSet, Smart, - StyleChain, Styles, TargetElem, + cast, elem, Content, Depth, Label, NativeElement, Packed, ShowSet, Smart, StyleChain, + Styles, }; -use crate::html::{attr, tag, HtmlElem}; use crate::introspection::Locatable; -use crate::layout::{ - Alignment, BlockBody, BlockElem, Em, HElem, PadElem, Spacing, VElem, -}; -use crate::model::{CitationForm, CiteElem, Destination, LinkElem, LinkTarget}; -use crate::text::{SmartQuoteElem, SmartQuotes, SpaceElem, TextElem}; +use crate::layout::{BlockElem, Em, PadElem}; +use crate::model::{CitationForm, CiteElem}; +use crate::text::{SmartQuotes, SpaceElem, TextElem}; /// Displays a quote alongside an optional attribution. /// @@ -44,7 +41,7 @@ use crate::text::{SmartQuoteElem, SmartQuotes, SpaceElem, TextElem}; /// flame of Udûn. Go back to the Shadow! You cannot pass. /// ] /// ``` -#[elem(Locatable, ShowSet, Show)] +#[elem(Locatable, ShowSet)] pub struct QuoteElem { /// Whether this is a block quote. /// @@ -62,7 +59,7 @@ pub struct QuoteElem { /// Ich bin ein Berliner. /// ] /// ``` - block: bool, + pub block: bool, /// Whether double quotes should be added around this quote. /// @@ -88,7 +85,7 @@ pub struct QuoteElem { /// translate the quote: /// #quote[I am a Berliner.] /// ``` - quotes: Smart, + pub quotes: Smart, /// The attribution of this quote, usually the author or source. Can be a /// label pointing to a bibliography entry or any content. By default only @@ -123,17 +120,36 @@ pub struct QuoteElem { /// /// #bibliography("works.bib", style: "apa") /// ``` - attribution: Option, + pub attribution: Option, /// The quote. #[required] - body: Content, + pub body: Content, /// The nesting depth. #[internal] #[fold] #[ghost] - depth: Depth, + pub depth: Depth, +} + +impl QuoteElem { + /// Quotes the body content with the appropriate quotes based on the current + /// styles and surroundings. + pub fn quoted(body: Content, styles: StyleChain<'_>) -> Content { + let quotes = SmartQuotes::get_in(styles); + + // Alternate between single and double quotes. + let Depth(depth) = styles.get(QuoteElem::depth); + let double = depth % 2 == 0; + + Content::sequence([ + TextElem::packed(quotes.open(double)), + body, + TextElem::packed(quotes.close(double)), + ]) + .set(QuoteElem::depth, Depth(1)) + } } /// Attribution for a [quote](QuoteElem). @@ -143,6 +159,23 @@ pub enum Attribution { Label(Label), } +impl Attribution { + /// Realize as an em dash followed by text or a citation. + pub fn realize(&self, span: Span) -> Content { + Content::sequence([ + TextElem::packed('—'), + SpaceElem::shared().clone(), + match self { + Attribution::Content(content) => content.clone(), + Attribution::Label(label) => CiteElem::new(*label) + .with_form(Some(CitationForm::Prose)) + .pack() + .spanned(span), + }, + ]) + } +} + cast! { Attribution, self => match self { @@ -153,96 +186,6 @@ cast! { label: Label => Self::Label(label), } -impl Show for Packed { - #[typst_macros::time(name = "quote", span = self.span())] - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - let mut realized = self.body.clone(); - let block = self.block.get(styles); - let html = styles.get(TargetElem::target).is_html(); - - if self.quotes.get(styles).unwrap_or(!block) { - let quotes = SmartQuotes::get( - styles.get_ref(SmartQuoteElem::quotes), - styles.get(TextElem::lang), - styles.get(TextElem::region), - styles.get(SmartQuoteElem::alternative), - ); - - // Alternate between single and double quotes. - let Depth(depth) = styles.get(QuoteElem::depth); - let double = depth % 2 == 0; - - if !html { - // 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([ - TextElem::packed(quotes.open(double)), - realized, - TextElem::packed(quotes.close(double)), - ]) - .set(QuoteElem::depth, Depth(1)); - } - - let attribution = self.attribution.get_ref(styles); - - if block { - realized = if html { - let mut elem = HtmlElem::new(tag::blockquote).with_body(Some(realized)); - if let Some(Attribution::Content(attribution)) = attribution { - if let Some(link) = attribution.to_packed::() { - if let LinkTarget::Dest(Destination::Url(url)) = &link.dest { - elem = elem.with_attr(attr::cite, url.clone().into_inner()); - } - } - } - elem.pack() - } else { - BlockElem::new().with_body(Some(BlockBody::Content(realized))).pack() - } - .spanned(self.span()); - - if let Some(attribution) = attribution { - let attribution = match attribution { - Attribution::Content(content) => content.clone(), - Attribution::Label(label) => CiteElem::new(*label) - .with_form(Some(CitationForm::Prose)) - .pack() - .spanned(self.span()), - }; - let attribution = Content::sequence([ - TextElem::packed('—'), - SpaceElem::shared().clone(), - attribution, - ]); - - if html { - realized += attribution; - } else { - // Bring the attribution a bit 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 += BlockElem::new() - .with_body(Some(BlockBody::Content(attribution))) - .pack() - .aligned(Alignment::END); - } - } - - if !html { - realized = PadElem::new(realized).pack(); - } - } else if let Some(Attribution::Label(label)) = attribution { - realized += SpaceElem::shared().clone() - + CiteElem::new(*label).pack().spanned(self.span()); - } - - Ok(realized) - } -} - impl ShowSet for Packed { fn show_set(&self, styles: StyleChain) -> Styles { let mut out = Styles::new(); diff --git a/crates/typst-library/src/model/reference.rs b/crates/typst-library/src/model/reference.rs index 2d04a97a4..4877409fa 100644 --- a/crates/typst-library/src/model/reference.rs +++ b/crates/typst-library/src/model/reference.rs @@ -5,7 +5,7 @@ use crate::diag::{bail, At, Hint, SourceResult}; use crate::engine::Engine; use crate::foundations::{ cast, elem, Cast, Content, Context, Func, IntoValue, Label, NativeElement, Packed, - Repr, Show, Smart, StyleChain, Synthesize, + Repr, Smart, StyleChain, Synthesize, }; use crate::introspection::{Counter, CounterKey, Locatable}; use crate::math::EquationElem; @@ -134,7 +134,7 @@ use crate::text::TextElem; /// In @beginning we prove @pythagoras. /// $ a^2 + b^2 = c^2 $ /// ``` -#[elem(title = "Reference", Synthesize, Locatable, Show)] +#[elem(title = "Reference", Synthesize, Locatable)] pub struct RefElem { /// The target label that should be referenced. /// @@ -220,9 +220,13 @@ impl Synthesize for Packed { } } -impl Show for Packed { - #[typst_macros::time(name = "ref", span = self.span())] - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { +impl Packed { + /// Realize as a linked, textual reference. + pub fn realize( + &self, + engine: &mut Engine, + styles: StyleChain, + ) -> SourceResult { let elem = engine.introspector.query_label(self.target); let span = self.span(); @@ -242,7 +246,7 @@ impl Show for Packed { .at(span)?; let supplement = engine.introspector.page_supplement(loc); - return show_reference( + return realize_reference( self, engine, styles, @@ -306,7 +310,7 @@ impl Show for Packed { )) .at(span)?; - show_reference( + realize_reference( self, engine, styles, @@ -319,7 +323,7 @@ impl Show for Packed { } /// Show a reference. -fn show_reference( +fn realize_reference( reference: &Packed, engine: &mut Engine, styles: StyleChain, diff --git a/crates/typst-library/src/model/strong.rs b/crates/typst-library/src/model/strong.rs index 08cf48391..7751c95bc 100644 --- a/crates/typst-library/src/model/strong.rs +++ b/crates/typst-library/src/model/strong.rs @@ -1,10 +1,4 @@ -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::{ - elem, Content, NativeElement, Packed, Show, StyleChain, TargetElem, -}; -use crate::html::{tag, HtmlElem}; -use crate::text::{TextElem, WeightDelta}; +use crate::foundations::{elem, Content}; /// Strongly emphasizes content by increasing the font weight. /// @@ -24,7 +18,7 @@ use crate::text::{TextElem, WeightDelta}; /// simply enclose it in stars/asterisks (`*`). Note that this only works at /// word boundaries. To strongly emphasize part of a word, you have to use the /// function. -#[elem(title = "Strong Emphasis", keywords = ["bold", "weight"], Show)] +#[elem(title = "Strong Emphasis", keywords = ["bold", "weight"])] pub struct StrongElem { /// The delta to apply on the font weight. /// @@ -39,18 +33,3 @@ pub struct StrongElem { #[required] pub body: Content, } - -impl Show for Packed { - #[typst_macros::time(name = "strong", span = self.span())] - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - let body = self.body.clone(); - Ok(if styles.get(TargetElem::target).is_html() { - HtmlElem::new(tag::strong) - .with_body(Some(body)) - .pack() - .spanned(self.span()) - } else { - body.set(TextElem::delta, WeightDelta(self.delta.get(styles))) - }) - } -} diff --git a/crates/typst-library/src/model/table.rs b/crates/typst-library/src/model/table.rs index 72c5acc5d..e46efc818 100644 --- a/crates/typst-library/src/model/table.rs +++ b/crates/typst-library/src/model/table.rs @@ -3,19 +3,11 @@ use std::sync::Arc; use typst_utils::NonZeroExt; -use crate::diag::{bail, HintedStrResult, HintedString, SourceResult}; -use crate::engine::Engine; -use crate::foundations::{ - cast, elem, scope, Content, NativeElement, Packed, Show, Smart, StyleChain, - TargetElem, -}; -use crate::html::{attr, tag, HtmlAttrs, HtmlElem, HtmlTag}; -use crate::introspection::Locator; -use crate::layout::grid::resolve::{table_to_cellgrid, Cell, CellGrid, Entry}; +use crate::diag::{bail, HintedStrResult, HintedString}; +use crate::foundations::{cast, elem, scope, Content, Packed, Smart}; use crate::layout::{ - show_grid_cell, Abs, Alignment, BlockElem, Celled, GridCell, GridFooter, GridHLine, - GridHeader, GridVLine, Length, OuterHAlignment, OuterVAlignment, Rel, Sides, - TrackSizings, + Abs, Alignment, Celled, GridCell, GridFooter, GridHLine, GridHeader, GridVLine, + Length, OuterHAlignment, OuterVAlignment, Rel, Sides, TrackSizings, }; use crate::model::Figurable; use crate::text::LocalName; @@ -121,7 +113,7 @@ use crate::visualize::{Paint, Stroke}; /// [Robert], b, a, b, /// ) /// ``` -#[elem(scope, Show, LocalName, Figurable)] +#[elem(scope, LocalName, Figurable)] pub struct TableElem { /// The column sizes. See the [grid documentation]($grid) for more /// information on track sizing. @@ -255,113 +247,6 @@ impl TableElem { type TableFooter; } -fn show_cell_html(tag: HtmlTag, cell: &Cell, styles: StyleChain) -> Content { - let cell = cell.body.clone(); - let Some(cell) = cell.to_packed::() 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.get(styles)) { - attrs.push(attr::colspan, colspan); - } - if let Some(rowspan) = span(cell.rowspan.get(styles)) { - attrs.push(attr::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.non_gutter_column_count()).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)) - }; - - // TODO(subfooters): similarly to headers, take consecutive footers from - // the end for 'tfoot'. - let footer = grid.footer.map(|ft| { - let rows = rows.drain(ft.start..); - elem(tag::tfoot, Content::sequence(rows.map(|row| tr(tag::td, row)))) - }); - - // Store all consecutive headers at the start in 'thead'. All remaining - // headers are just 'th' rows across the table body. - let mut consecutive_header_end = 0; - let first_mid_table_header = grid - .headers - .iter() - .take_while(|hd| { - let is_consecutive = hd.range.start == consecutive_header_end; - consecutive_header_end = hd.range.end; - - is_consecutive - }) - .count(); - - let (y_offset, header) = if first_mid_table_header > 0 { - let removed_header_rows = - grid.headers.get(first_mid_table_header - 1).unwrap().range.end; - let rows = rows.drain(..removed_header_rows); - - ( - removed_header_rows, - Some(elem(tag::thead, Content::sequence(rows.map(|row| tr(tag::th, row))))), - ) - } else { - (0, None) - }; - - // TODO: Consider improving accessibility properties of multi-level headers - // inside tables in the future, e.g. indicating which columns they are - // relative to and so on. See also: - // https://www.w3.org/WAI/tutorials/tables/multi-level/ - let mut next_header = first_mid_table_header; - let mut body = - Content::sequence(rows.into_iter().enumerate().map(|(relative_y, row)| { - let y = relative_y + y_offset; - if let Some(current_header) = - grid.headers.get(next_header).filter(|h| h.range.contains(&y)) - { - if y + 1 == current_header.range.end { - next_header += 1; - } - - tr(tag::th, row) - } else { - 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 { - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(if styles.get(TargetElem::target).is_html() { - // TODO: This is a hack, it is not clear whether the locator is actually used by HTML. - // 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())) - } -} - impl LocalName for Packed { const KEY: &'static str = "table"; } @@ -761,7 +646,7 @@ pub struct TableVLine { /// [Vikram], [49], [Perseverance], /// ) /// ``` -#[elem(name = "cell", title = "Table Cell", Show)] +#[elem(name = "cell", title = "Table Cell")] pub struct TableCell { /// The cell's body. #[required] @@ -808,12 +693,6 @@ cast! { v: Content => v.into(), } -impl Show for Packed { - fn show(&self, _engine: &mut Engine, styles: StyleChain) -> SourceResult { - show_grid_cell(self.body.clone(), self.inset.get(styles), self.align.get(styles)) - } -} - impl Default for Packed { fn default() -> Self { Packed::new( diff --git a/crates/typst-library/src/model/terms.rs b/crates/typst-library/src/model/terms.rs index 280c2d67e..71b1bad6d 100644 --- a/crates/typst-library/src/model/terms.rs +++ b/crates/typst-library/src/model/terms.rs @@ -1,15 +1,9 @@ -use typst_utils::{Get, Numeric}; - -use crate::diag::{bail, SourceResult}; -use crate::engine::Engine; +use crate::diag::bail; use crate::foundations::{ - cast, elem, scope, Array, Content, NativeElement, Packed, Show, Smart, StyleChain, - Styles, TargetElem, + cast, elem, scope, Array, Content, NativeElement, Packed, Smart, Styles, }; -use crate::html::{tag, HtmlElem}; -use crate::layout::{Em, HElem, Length, Sides, StackChild, StackElem, VElem}; -use crate::model::{ListItemLike, ListLike, ParElem, ParbreakElem}; -use crate::text::TextElem; +use crate::layout::{Em, HElem, Length}; +use crate::model::{ListItemLike, ListLike}; /// A list of terms and their descriptions. /// @@ -27,7 +21,7 @@ use crate::text::TextElem; /// # Syntax /// This function also has dedicated syntax: Starting a line with a slash, /// followed by a term, a colon and a description creates a term list item. -#[elem(scope, title = "Term List", Show)] +#[elem(scope, title = "Term List")] pub struct TermsElem { /// Defines the default [spacing]($terms.spacing) of the term list. If it is /// `{false}`, the items are spaced apart with @@ -117,94 +111,6 @@ impl TermsElem { type TermItem; } -impl Show for Packed { - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - let span = self.span(); - let tight = self.tight.get(styles); - - if styles.get(TargetElem::target).is_html() { - return Ok(HtmlElem::new(tag::dl) - .with_body(Some(Content::sequence(self.children.iter().flat_map( - |item| { - // Text in wide term lists shall always turn into paragraphs. - let mut description = item.description.clone(); - if !tight { - description += ParbreakElem::shared(); - } - - [ - HtmlElem::new(tag::dt) - .with_body(Some(item.term.clone())) - .pack() - .spanned(item.term.span()), - HtmlElem::new(tag::dd) - .with_body(Some(description)) - .pack() - .spanned(item.description.span()), - ] - }, - )))) - .pack()); - } - - let separator = self.separator.get_ref(styles); - let indent = self.indent.get(styles); - let hanging_indent = self.hanging_indent.get(styles); - let gutter = self.spacing.get(styles).unwrap_or_else(|| { - if tight { - styles.get(ParElem::leading) - } else { - styles.get(ParElem::spacing) - } - }); - - let pad = hanging_indent + indent; - let unpad = (!hanging_indent.is_zero()) - .then(|| HElem::new((-hanging_indent).into()).pack().spanned(span)); - - let mut children = vec![]; - for child in self.children.iter() { - let mut seq = vec![]; - seq.extend(unpad.clone()); - seq.push(child.term.clone().strong()); - seq.push((*separator).clone()); - seq.push(child.description.clone()); - - // Text in wide term lists shall always turn into paragraphs. - if !tight { - seq.push(ParbreakElem::shared().clone()); - } - - children.push(StackChild::Block(Content::sequence(seq))); - } - - let padding = - Sides::default().with(styles.resolve(TextElem::dir).start(), pad.into()); - - let mut realized = StackElem::new(children) - .with_spacing(Some(gutter.into())) - .pack() - .spanned(span) - .padded(padding) - .set(TermsElem::within, true); - - if tight { - let spacing = self - .spacing - .get(styles) - .unwrap_or_else(|| styles.get(ParElem::leading)); - let v = VElem::new(spacing.into()) - .with_weak(true) - .with_attach(true) - .pack() - .spanned(span); - realized = v + realized; - } - - Ok(realized) - } -} - /// A term list item. #[elem(name = "item", title = "Term List Item")] pub struct TermItem { diff --git a/crates/typst-library/src/pdf/embed.rs b/crates/typst-library/src/pdf/embed.rs index 0f93f95af..3aba85623 100644 --- a/crates/typst-library/src/pdf/embed.rs +++ b/crates/typst-library/src/pdf/embed.rs @@ -1,12 +1,8 @@ use ecow::EcoString; -use typst_library::foundations::Target; use typst_syntax::Spanned; -use crate::diag::{warning, At, SourceResult}; -use crate::engine::Engine; -use crate::foundations::{ - elem, Bytes, Cast, Content, Derived, Packed, Show, StyleChain, TargetElem, -}; +use crate::diag::At; +use crate::foundations::{elem, Bytes, Cast, Derived}; use crate::introspection::Locatable; use crate::World; @@ -33,7 +29,7 @@ use crate::World; /// - This element is ignored if exporting to a format other than PDF. /// - File embeddings are not currently supported for PDF/A-2, even if the /// embedded file conforms to PDF/A-1 or PDF/A-2. -#[elem(Show, Locatable)] +#[elem(Locatable)] pub struct EmbedElem { /// The [path]($syntax/#paths) of the file to be embedded. /// @@ -77,17 +73,6 @@ pub struct EmbedElem { pub description: Option, } -impl Show for Packed { - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - if styles.get(TargetElem::target) == Target::Html { - engine - .sink - .warn(warning!(self.span(), "embed was ignored during HTML export")); - } - Ok(Content::empty()) - } -} - /// The relationship of an embedded file with the document. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] pub enum EmbeddedFileRelationship { diff --git a/crates/typst-library/src/routines.rs b/crates/typst-library/src/routines.rs index 59ce83282..4bf8d60c8 100644 --- a/crates/typst-library/src/routines.rs +++ b/crates/typst-library/src/routines.rs @@ -1,7 +1,4 @@ -#![allow(unused)] - use std::hash::{Hash, Hasher}; -use std::num::NonZeroUsize; use comemo::{Tracked, TrackedMut}; use typst_syntax::{Span, SyntaxMode}; @@ -10,20 +7,12 @@ use typst_utils::LazyHash; use crate::diag::SourceResult; use crate::engine::{Engine, Route, Sink, Traced}; use crate::foundations::{ - Args, Cast, Closure, Content, Context, Func, Packed, Scope, StyleChain, Styles, Value, + Args, Closure, Content, Context, Func, NativeRuleMap, Scope, StyleChain, Styles, + Value, }; use crate::introspection::{Introspector, Locator, SplitLocator}; -use crate::layout::{ - Abs, BoxElem, ColumnsElem, Fragment, Frame, GridElem, InlineItem, MoveElem, PadElem, - PagedDocument, Region, Regions, Rel, RepeatElem, RotateElem, ScaleElem, Size, - SkewElem, StackElem, -}; -use crate::math::EquationElem; -use crate::model::{DocumentInfo, EnumElem, ListElem, TableElem}; -use crate::visualize::{ - CircleElem, CurveElem, EllipseElem, ImageElem, LineElem, PathElem, PolygonElem, - RectElem, SquareElem, -}; +use crate::layout::{Frame, Region}; +use crate::model::DocumentInfo; use crate::World; /// Defines the `Routines` struct. @@ -38,6 +27,8 @@ macro_rules! routines { /// This is essentially dynamic linking and done to allow for crate /// splitting. pub struct Routines { + /// Native show rules. + pub rules: NativeRuleMap, $( $(#[$attr])* pub $name: $(for<$($time),*>)? fn ($($args)*) -> $ret @@ -86,15 +77,6 @@ routines! { styles: StyleChain<'a>, ) -> SourceResult>> - /// Lays out content into multiple regions. - fn layout_fragment( - engine: &mut Engine, - content: &Content, - locator: Locator, - styles: StyleChain, - regions: Regions, - ) -> SourceResult - /// Lays out content into a single region, producing a single frame. fn layout_frame( engine: &mut Engine, @@ -103,213 +85,6 @@ routines! { styles: StyleChain, region: Region, ) -> SourceResult - - /// Lays out a [`ListElem`]. - fn layout_list( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - regions: Regions, - ) -> SourceResult - - /// Lays out an [`EnumElem`]. - fn layout_enum( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - regions: Regions, - ) -> SourceResult - - /// Lays out a [`GridElem`]. - fn layout_grid( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - regions: Regions, - ) -> SourceResult - - /// Lays out a [`TableElem`]. - fn layout_table( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - regions: Regions, - ) -> SourceResult - - /// Lays out a [`StackElem`]. - fn layout_stack( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - regions: Regions, - ) -> SourceResult - - /// Lays out a [`ColumnsElem`]. - fn layout_columns( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - regions: Regions, - ) -> SourceResult - - /// Lays out a [`MoveElem`]. - fn layout_move( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Region, - ) -> SourceResult - - /// Lays out a [`RotateElem`]. - fn layout_rotate( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Region, - ) -> SourceResult - - /// Lays out a [`ScaleElem`]. - fn layout_scale( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Region, - ) -> SourceResult - - /// Lays out a [`SkewElem`]. - fn layout_skew( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Region, - ) -> SourceResult - - /// Lays out a [`RepeatElem`]. - fn layout_repeat( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Region, - ) -> SourceResult - - /// Lays out a [`PadElem`]. - fn layout_pad( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - regions: Regions, - ) -> SourceResult - - /// Lays out a [`LineElem`]. - fn layout_line( - elem: &Packed, - _: &mut Engine, - _: Locator, - styles: StyleChain, - region: Region, - ) -> SourceResult - - /// Lays out a [`CurveElem`]. - fn layout_curve( - elem: &Packed, - _: &mut Engine, - _: Locator, - styles: StyleChain, - region: Region, - ) -> SourceResult - - /// Lays out a [`PathElem`]. - fn layout_path( - elem: &Packed, - _: &mut Engine, - _: Locator, - styles: StyleChain, - region: Region, - ) -> SourceResult - - /// Lays out a [`PolygonElem`]. - fn layout_polygon( - elem: &Packed, - _: &mut Engine, - _: Locator, - styles: StyleChain, - region: Region, - ) -> SourceResult - - /// Lays out a [`RectElem`]. - fn layout_rect( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Region, - ) -> SourceResult - - /// Lays out a [`SquareElem`]. - fn layout_square( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Region, - ) -> SourceResult - - /// Lays out a [`EllipseElem`]. - fn layout_ellipse( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Region, - ) -> SourceResult - - /// Lays out a [`CircleElem`]. - fn layout_circle( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Region, - ) -> SourceResult - - /// Lays out an [`ImageElem`]. - fn layout_image( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Region, - ) -> SourceResult - - /// Lays out an [`EquationElem`] in a paragraph. - fn layout_equation_inline( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Size, - ) -> SourceResult> - - /// Lays out an [`EquationElem`] in a flow. - fn layout_equation_block( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - regions: Regions, - ) -> SourceResult } /// Defines what kind of realization we are performing. diff --git a/crates/typst-library/src/text/deco.rs b/crates/typst-library/src/text/deco.rs index 8c1d56345..f7d5c33be 100644 --- a/crates/typst-library/src/text/deco.rs +++ b/crates/typst-library/src/text/deco.rs @@ -1,13 +1,6 @@ -use smallvec::smallvec; - -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::{ - elem, Content, NativeElement, Packed, Show, Smart, StyleChain, TargetElem, -}; -use crate::html::{attr, tag, HtmlElem}; +use crate::foundations::{elem, Content, Smart}; use crate::layout::{Abs, Corners, Length, Rel, Sides}; -use crate::text::{BottomEdge, BottomEdgeMetric, TextElem, TopEdge, TopEdgeMetric}; +use crate::text::{BottomEdge, BottomEdgeMetric, TopEdge, TopEdgeMetric}; use crate::visualize::{Color, FixedStroke, Paint, Stroke}; /// Underlines text. @@ -16,7 +9,7 @@ use crate::visualize::{Color, FixedStroke, Paint, Stroke}; /// ```example /// This is #underline[important]. /// ``` -#[elem(Show)] +#[elem] pub struct UnderlineElem { /// How to [stroke] the line. /// @@ -78,41 +71,13 @@ pub struct UnderlineElem { pub body: Content, } -impl Show for Packed { - #[typst_macros::time(name = "underline", span = self.span())] - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - if styles.get(TargetElem::target).is_html() { - // Note: In modern HTML, `` is not the underline element, but - // rather an "Unarticulated Annotation" element (see HTML spec - // 4.5.22). Using `text-decoration` instead is recommended by MDN. - return Ok(HtmlElem::new(tag::span) - .with_attr(attr::style, "text-decoration: underline") - .with_body(Some(self.body.clone())) - .pack()); - } - - Ok(self.body.clone().set( - TextElem::deco, - smallvec![Decoration { - line: DecoLine::Underline { - stroke: self.stroke.resolve(styles).unwrap_or_default(), - offset: self.offset.resolve(styles), - evade: self.evade.get(styles), - background: self.background.get(styles), - }, - extent: self.extent.resolve(styles), - }], - )) - } -} - /// Adds a line over text. /// /// # Example /// ```example /// #overline[A line over text.] /// ``` -#[elem(Show)] +#[elem] pub struct OverlineElem { /// How to [stroke] the line. /// @@ -180,38 +145,13 @@ pub struct OverlineElem { pub body: Content, } -impl Show for Packed { - #[typst_macros::time(name = "overline", span = self.span())] - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - if styles.get(TargetElem::target).is_html() { - return Ok(HtmlElem::new(tag::span) - .with_attr(attr::style, "text-decoration: overline") - .with_body(Some(self.body.clone())) - .pack()); - } - - Ok(self.body.clone().set( - TextElem::deco, - smallvec![Decoration { - line: DecoLine::Overline { - stroke: self.stroke.resolve(styles).unwrap_or_default(), - offset: self.offset.resolve(styles), - evade: self.evade.get(styles), - background: self.background.get(styles), - }, - extent: self.extent.resolve(styles), - }], - )) - } -} - /// Strikes through text. /// /// # Example /// ```example /// This is #strike[not] relevant. /// ``` -#[elem(title = "Strikethrough", Show)] +#[elem(title = "Strikethrough")] pub struct StrikeElem { /// How to [stroke] the line. /// @@ -264,35 +204,13 @@ pub struct StrikeElem { pub body: Content, } -impl Show for Packed { - #[typst_macros::time(name = "strike", span = self.span())] - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - if styles.get(TargetElem::target).is_html() { - return Ok(HtmlElem::new(tag::s).with_body(Some(self.body.clone())).pack()); - } - - Ok(self.body.clone().set( - TextElem::deco, - smallvec![Decoration { - // Note that we do not support evade option for strikethrough. - line: DecoLine::Strikethrough { - stroke: self.stroke.resolve(styles).unwrap_or_default(), - offset: self.offset.resolve(styles), - background: self.background.get(styles), - }, - extent: self.extent.resolve(styles), - }], - )) - } -} - /// Highlights text with a background color. /// /// # Example /// ```example /// This is #highlight[important]. /// ``` -#[elem(Show)] +#[elem] pub struct HighlightElem { /// The color to highlight the text with. /// @@ -363,35 +281,6 @@ pub struct HighlightElem { pub body: Content, } -impl Show for Packed { - #[typst_macros::time(name = "highlight", span = self.span())] - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - if styles.get(TargetElem::target).is_html() { - return Ok(HtmlElem::new(tag::mark) - .with_body(Some(self.body.clone())) - .pack()); - } - - Ok(self.body.clone().set( - TextElem::deco, - smallvec![Decoration { - line: DecoLine::Highlight { - fill: self.fill.get_cloned(styles), - stroke: self - .stroke - .resolve(styles) - .unwrap_or_default() - .map(|stroke| stroke.map(Stroke::unwrap_or_default)), - top_edge: self.top_edge.get(styles), - bottom_edge: self.bottom_edge.get(styles), - radius: self.radius.resolve(styles).unwrap_or_default(), - }, - extent: self.extent.resolve(styles), - }], - )) - } -} - /// A text decoration. /// /// Can be positioned over, under, or on top of text, or highlight the text with diff --git a/crates/typst-library/src/text/raw.rs b/crates/typst-library/src/text/raw.rs index 67038163d..8cddfbfb5 100644 --- a/crates/typst-library/src/text/raw.rs +++ b/crates/typst-library/src/text/raw.rs @@ -16,14 +16,13 @@ use crate::diag::{ }; use crate::engine::Engine; use crate::foundations::{ - cast, elem, scope, Bytes, Content, Derived, NativeElement, OneOrMultiple, Packed, - PlainText, Show, ShowSet, Smart, StyleChain, Styles, Synthesize, TargetElem, + cast, elem, scope, Bytes, Content, Derived, OneOrMultiple, Packed, PlainText, + ShowSet, Smart, StyleChain, Styles, Synthesize, }; -use crate::html::{tag, HtmlElem}; -use crate::layout::{BlockBody, BlockElem, Em, HAlignment}; +use crate::layout::{Em, HAlignment}; use crate::loading::{DataSource, Load}; use crate::model::{Figurable, ParElem}; -use crate::text::{FontFamily, FontList, LinebreakElem, LocalName, TextElem, TextSize}; +use crate::text::{FontFamily, FontList, LocalName, TextElem, TextSize}; use crate::visualize::Color; use crate::World; @@ -78,7 +77,6 @@ use crate::World; scope, title = "Raw Text / Code", Synthesize, - Show, ShowSet, LocalName, Figurable, @@ -429,46 +427,6 @@ impl Packed { } } -impl Show for Packed { - #[typst_macros::time(name = "raw", span = self.span())] - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - let lines = self.lines.as_deref().unwrap_or_default(); - - let mut seq = EcoVec::with_capacity((2 * lines.len()).saturating_sub(1)); - for (i, line) in lines.iter().enumerate() { - if i != 0 { - seq.push(LinebreakElem::shared().clone()); - } - - seq.push(line.clone().pack()); - } - - let mut realized = Content::sequence(seq); - - if styles.get(TargetElem::target).is_html() { - return Ok(HtmlElem::new(if self.block.get(styles) { - tag::pre - } else { - tag::code - }) - .with_body(Some(realized)) - .pack() - .spanned(self.span())); - } - - if self.block.get(styles) { - // Align the text before inserting it into the block. - realized = realized.aligned(self.align.get(styles).into()); - realized = BlockElem::new() - .with_body(Some(BlockBody::Content(realized))) - .pack() - .spanned(self.span()); - } - - Ok(realized) - } -} - impl ShowSet for Packed { fn show_set(&self, styles: StyleChain) -> Styles { let mut out = Styles::new(); @@ -634,7 +592,7 @@ fn format_theme_error(error: syntect::LoadingError) -> LoadError { /// It allows you to access various properties of the line, such as the line /// number, the raw non-highlighted text, the highlighted text, and whether it /// is the first or last line of the raw block. -#[elem(name = "line", title = "Raw Text / Code Line", Show, PlainText)] +#[elem(name = "line", title = "Raw Text / Code Line", PlainText)] pub struct RawLine { /// The line number of the raw line inside of the raw block, starts at 1. #[required] @@ -653,13 +611,6 @@ pub struct RawLine { pub body: Content, } -impl Show for Packed { - #[typst_macros::time(name = "raw.line", span = self.span())] - fn show(&self, _: &mut Engine, _styles: StyleChain) -> SourceResult { - Ok(self.body.clone()) - } -} - impl PlainText for Packed { fn plain_text(&self, text: &mut EcoString) { text.push_str(&self.text); diff --git a/crates/typst-library/src/text/shift.rs b/crates/typst-library/src/text/shift.rs index 1a05d8f9c..87ccae635 100644 --- a/crates/typst-library/src/text/shift.rs +++ b/crates/typst-library/src/text/shift.rs @@ -1,13 +1,8 @@ -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::{ - elem, Content, NativeElement, Packed, Show, Smart, StyleChain, TargetElem, -}; -use crate::html::{tag, HtmlElem}; -use crate::layout::{Em, Length}; -use crate::text::{FontMetrics, TextElem, TextSize}; use ttf_parser::Tag; -use typst_library::text::ScriptMetrics; + +use crate::foundations::{elem, Content, Smart}; +use crate::layout::{Em, Length}; +use crate::text::{FontMetrics, ScriptMetrics, TextSize}; /// Renders text in subscript. /// @@ -17,7 +12,7 @@ use typst_library::text::ScriptMetrics; /// ```example /// Revenue#sub[yearly] /// ``` -#[elem(title = "Subscript", Show)] +#[elem(title = "Subscript")] pub struct SubElem { /// Whether to create artificial subscripts by lowering and scaling down /// regular glyphs. @@ -64,29 +59,6 @@ pub struct SubElem { pub body: Content, } -impl Show for Packed { - #[typst_macros::time(name = "sub", span = self.span())] - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - let body = self.body.clone(); - - if styles.get(TargetElem::target).is_html() { - return Ok(HtmlElem::new(tag::sub) - .with_body(Some(body)) - .pack() - .spanned(self.span())); - } - - show_script( - styles, - body, - self.typographic.get(styles), - self.baseline.get(styles), - self.size.get(styles), - ScriptKind::Sub, - ) - } -} - /// Renders text in superscript. /// /// The text is rendered smaller and its baseline is raised. @@ -95,7 +67,7 @@ impl Show for Packed { /// ```example /// 1#super[st] try! /// ``` -#[elem(title = "Superscript", Show)] +#[elem(title = "Superscript")] pub struct SuperElem { /// Whether to create artificial superscripts by raising and scaling down /// regular glyphs. @@ -146,49 +118,6 @@ pub struct SuperElem { pub body: Content, } -impl Show for Packed { - #[typst_macros::time(name = "super", span = self.span())] - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - let body = self.body.clone(); - - if styles.get(TargetElem::target).is_html() { - return Ok(HtmlElem::new(tag::sup) - .with_body(Some(body)) - .pack() - .spanned(self.span())); - } - - show_script( - styles, - body, - self.typographic.get(styles), - self.baseline.get(styles), - self.size.get(styles), - ScriptKind::Super, - ) - } -} - -fn show_script( - styles: StyleChain, - body: Content, - typographic: bool, - baseline: Smart, - size: Smart, - kind: ScriptKind, -) -> SourceResult { - let font_size = styles.resolve(TextElem::size); - Ok(body.set( - TextElem::shift_settings, - Some(ShiftSettings { - typographic, - shift: baseline.map(|l| -Em::from_length(l, font_size)), - size: size.map(|t| Em::from_length(t.0, font_size)), - kind, - }), - )) -} - /// Configuration values for sub- or superscript text. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct ShiftSettings { diff --git a/crates/typst-library/src/text/smallcaps.rs b/crates/typst-library/src/text/smallcaps.rs index 1c2838933..199222fed 100644 --- a/crates/typst-library/src/text/smallcaps.rs +++ b/crates/typst-library/src/text/smallcaps.rs @@ -1,7 +1,4 @@ -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::{elem, Content, Packed, Show, StyleChain}; -use crate::text::TextElem; +use crate::foundations::{elem, Content}; /// Displays text in small capitals. /// @@ -43,7 +40,7 @@ use crate::text::TextElem; /// = Introduction /// #lorem(40) /// ``` -#[elem(title = "Small Capitals", Show)] +#[elem(title = "Small Capitals")] pub struct SmallcapsElem { /// Whether to turn uppercase letters into small capitals as well. /// @@ -61,15 +58,6 @@ pub struct SmallcapsElem { pub body: Content, } -impl Show for Packed { - #[typst_macros::time(name = "smallcaps", span = self.span())] - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - let sc = - if self.all.get(styles) { Smallcaps::All } else { Smallcaps::Minuscules }; - Ok(self.body.clone().set(TextElem::smallcaps, Some(sc))) - } -} - /// What becomes small capitals. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum Smallcaps { diff --git a/crates/typst-library/src/text/smartquote.rs b/crates/typst-library/src/text/smartquote.rs index 24787d062..375b1cf09 100644 --- a/crates/typst-library/src/text/smartquote.rs +++ b/crates/typst-library/src/text/smartquote.rs @@ -5,9 +5,10 @@ use unicode_segmentation::UnicodeSegmentation; use crate::diag::{bail, HintedStrResult, StrResult}; use crate::foundations::{ array, cast, dict, elem, Array, Dict, FromValue, Packed, PlainText, Smart, Str, + StyleChain, }; use crate::layout::Dir; -use crate::text::{Lang, Region}; +use crate::text::{Lang, Region, TextElem}; /// A language-aware quote that reacts to its context. /// @@ -200,6 +201,16 @@ pub struct SmartQuotes<'s> { } impl<'s> SmartQuotes<'s> { + /// Retrieve the smart quotes as configured by the current styles. + pub fn get_in(styles: StyleChain<'s>) -> Self { + Self::get( + styles.get_ref(SmartQuoteElem::quotes), + styles.get(TextElem::lang), + styles.get(TextElem::region), + styles.get(SmartQuoteElem::alternative), + ) + } + /// Create a new `Quotes` struct with the given quotes, optionally falling /// back to the defaults for a language and region. /// diff --git a/crates/typst-library/src/visualize/curve.rs b/crates/typst-library/src/visualize/curve.rs index 587f0d4a2..15ae48c61 100644 --- a/crates/typst-library/src/visualize/curve.rs +++ b/crates/typst-library/src/visualize/curve.rs @@ -2,12 +2,9 @@ use kurbo::ParamCurveExtrema; use typst_macros::{scope, Cast}; use typst_utils::Numeric; -use crate::diag::{bail, HintedStrResult, HintedString, SourceResult}; -use crate::engine::Engine; -use crate::foundations::{ - cast, elem, Content, NativeElement, Packed, Show, Smart, StyleChain, -}; -use crate::layout::{Abs, Axes, BlockElem, Length, Point, Rel, Size}; +use crate::diag::{bail, HintedStrResult, HintedString}; +use crate::foundations::{cast, elem, Content, Packed, Smart}; +use crate::layout::{Abs, Axes, Length, Point, Rel, Size}; use crate::visualize::{FillRule, Paint, Stroke}; use super::FixedStroke; @@ -42,7 +39,7 @@ use super::FixedStroke; /// curve.close(), /// ) /// ``` -#[elem(scope, Show)] +#[elem(scope)] pub struct CurveElem { /// How to fill the curve. /// @@ -95,14 +92,6 @@ pub struct CurveElem { pub components: Vec, } -impl Show for Packed { - fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_curve) - .pack() - .spanned(self.span())) - } -} - #[scope] impl CurveElem { #[elem] diff --git a/crates/typst-library/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs index 48a14f0ed..95021b818 100644 --- a/crates/typst-library/src/visualize/image/mod.rs +++ b/crates/typst-library/src/visualize/image/mod.rs @@ -15,13 +15,11 @@ use ecow::EcoString; use typst_syntax::{Span, Spanned}; use typst_utils::LazyHash; -use crate::diag::{SourceResult, StrResult}; -use crate::engine::Engine; +use crate::diag::StrResult; use crate::foundations::{ - cast, elem, func, scope, Bytes, Cast, Content, Derived, NativeElement, Packed, Show, - Smart, StyleChain, + cast, elem, func, scope, Bytes, Cast, Content, Derived, NativeElement, Packed, Smart, }; -use crate::layout::{BlockElem, Length, Rel, Sizing}; +use crate::layout::{Length, Rel, Sizing}; use crate::loading::{DataSource, Load, LoadSource, Loaded, Readable}; use crate::model::Figurable; use crate::text::LocalName; @@ -44,7 +42,7 @@ use crate::text::LocalName; /// ], /// ) /// ``` -#[elem(scope, Show, LocalName, Figurable)] +#[elem(scope, LocalName, Figurable)] pub struct ImageElem { /// A [path]($syntax/#paths) to an image file or raw bytes making up an /// image in one of the supported [formats]($image.format). @@ -219,16 +217,6 @@ impl ImageElem { } } -impl Show for Packed { - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_image) - .with_width(self.width.get(styles)) - .with_height(self.height.get(styles)) - .pack() - .spanned(self.span())) - } -} - impl LocalName for Packed { const KEY: &'static str = "figure"; } diff --git a/crates/typst-library/src/visualize/line.rs b/crates/typst-library/src/visualize/line.rs index d058b926a..7eecfc915 100644 --- a/crates/typst-library/src/visualize/line.rs +++ b/crates/typst-library/src/visualize/line.rs @@ -1,7 +1,5 @@ -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::{elem, Content, NativeElement, Packed, Show, StyleChain}; -use crate::layout::{Abs, Angle, Axes, BlockElem, Length, Rel}; +use crate::foundations::elem; +use crate::layout::{Abs, Angle, Axes, Length, Rel}; use crate::visualize::Stroke; /// A line from one point to another. @@ -17,7 +15,7 @@ use crate::visualize::Stroke; /// stroke: 2pt + maroon, /// ) /// ``` -#[elem(Show)] +#[elem] pub struct LineElem { /// The start point of the line. /// @@ -50,11 +48,3 @@ pub struct LineElem { #[fold] pub stroke: Stroke, } - -impl Show for Packed { - fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_line) - .pack() - .spanned(self.span())) - } -} diff --git a/crates/typst-library/src/visualize/path.rs b/crates/typst-library/src/visualize/path.rs index e19e091df..bd8aea02d 100644 --- a/crates/typst-library/src/visualize/path.rs +++ b/crates/typst-library/src/visualize/path.rs @@ -1,11 +1,7 @@ use self::PathVertex::{AllControlPoints, MirroredControlPoint, Vertex}; -use crate::diag::{bail, SourceResult}; -use crate::engine::Engine; -use crate::foundations::{ - array, cast, elem, Array, Content, NativeElement, Packed, Reflect, Show, Smart, - StyleChain, -}; -use crate::layout::{Axes, BlockElem, Length, Rel}; +use crate::diag::bail; +use crate::foundations::{array, cast, elem, Array, Reflect, Smart}; +use crate::layout::{Axes, Length, Rel}; use crate::visualize::{FillRule, Paint, Stroke}; /// A path through a list of points, connected by Bézier curves. @@ -21,7 +17,7 @@ use crate::visualize::{FillRule, Paint, Stroke}; /// ((50%, 0pt), (40pt, 0pt)), /// ) /// ``` -#[elem(Show)] +#[elem] pub struct PathElem { /// How to fill the path. /// @@ -83,14 +79,6 @@ pub struct PathElem { pub vertices: Vec, } -impl Show for Packed { - fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_path) - .pack() - .spanned(self.span())) - } -} - /// A component used for path creation. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum PathVertex { diff --git a/crates/typst-library/src/visualize/polygon.rs b/crates/typst-library/src/visualize/polygon.rs index d75e1a657..db75a2670 100644 --- a/crates/typst-library/src/visualize/polygon.rs +++ b/crates/typst-library/src/visualize/polygon.rs @@ -2,12 +2,8 @@ use std::f64::consts::PI; use typst_syntax::Span; -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::{ - elem, func, scope, Content, NativeElement, Packed, Show, Smart, StyleChain, -}; -use crate::layout::{Axes, BlockElem, Em, Length, Rel}; +use crate::foundations::{elem, func, scope, Content, NativeElement, Smart}; +use crate::layout::{Axes, Em, Length, Rel}; use crate::visualize::{FillRule, Paint, Stroke}; /// A closed polygon. @@ -25,7 +21,7 @@ use crate::visualize::{FillRule, Paint, Stroke}; /// (0%, 2cm), /// ) /// ``` -#[elem(scope, Show)] +#[elem(scope)] pub struct PolygonElem { /// How to fill the polygon. /// @@ -124,11 +120,3 @@ impl PolygonElem { elem.pack().spanned(span) } } - -impl Show for Packed { - fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_polygon) - .pack() - .spanned(self.span())) - } -} diff --git a/crates/typst-library/src/visualize/shape.rs b/crates/typst-library/src/visualize/shape.rs index f21bf93e9..fc7b8748e 100644 --- a/crates/typst-library/src/visualize/shape.rs +++ b/crates/typst-library/src/visualize/shape.rs @@ -1,9 +1,5 @@ -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::{ - elem, Cast, Content, NativeElement, Packed, Show, Smart, StyleChain, -}; -use crate::layout::{Abs, BlockElem, Corners, Length, Point, Rel, Sides, Size, Sizing}; +use crate::foundations::{elem, Cast, Content, Smart}; +use crate::layout::{Abs, Corners, Length, Point, Rel, Sides, Size, Sizing}; use crate::visualize::{Curve, FixedStroke, Paint, Stroke}; /// A rectangle with optional content. @@ -19,7 +15,7 @@ use crate::visualize::{Curve, FixedStroke, Paint, Stroke}; /// to fit the content. /// ] /// ``` -#[elem(title = "Rectangle", Show)] +#[elem(title = "Rectangle")] pub struct RectElem { /// The rectangle's width, relative to its parent container. pub width: Smart>, @@ -122,16 +118,6 @@ pub struct RectElem { pub body: Option, } -impl Show for Packed { - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_rect) - .with_width(self.width.get(styles)) - .with_height(self.height.get(styles)) - .pack() - .spanned(self.span())) - } -} - /// A square with optional content. /// /// # Example @@ -145,7 +131,7 @@ impl Show for Packed { /// sized to fit. /// ] /// ``` -#[elem(Show)] +#[elem] pub struct SquareElem { /// The square's side length. This is mutually exclusive with `width` and /// `height`. @@ -209,16 +195,6 @@ pub struct SquareElem { pub body: Option, } -impl Show for Packed { - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_square) - .with_width(self.width.get(styles)) - .with_height(self.height.get(styles)) - .pack() - .spanned(self.span())) - } -} - /// An ellipse with optional content. /// /// # Example @@ -233,7 +209,7 @@ impl Show for Packed { /// to fit the content. /// ] /// ``` -#[elem(Show)] +#[elem] pub struct EllipseElem { /// The ellipse's width, relative to its parent container. pub width: Smart>, @@ -269,16 +245,6 @@ pub struct EllipseElem { pub body: Option, } -impl Show for Packed { - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_ellipse) - .with_width(self.width.get(styles)) - .with_height(self.height.get(styles)) - .pack() - .spanned(self.span())) - } -} - /// A circle with optional content. /// /// # Example @@ -293,7 +259,7 @@ impl Show for Packed { /// sized to fit. /// ] /// ``` -#[elem(Show)] +#[elem] pub struct CircleElem { /// The circle's radius. This is mutually exclusive with `width` and /// `height`. @@ -354,16 +320,6 @@ pub struct CircleElem { pub body: Option, } -impl Show for Packed { - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_circle) - .with_width(self.width.get(styles)) - .with_height(self.height.get(styles)) - .pack() - .spanned(self.span())) - } -} - /// A geometric shape with optional fill and stroke. #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct Shape { diff --git a/crates/typst-realize/src/lib.rs b/crates/typst-realize/src/lib.rs index fcfb40667..6af249cc3 100644 --- a/crates/typst-realize/src/lib.rs +++ b/crates/typst-realize/src/lib.rs @@ -14,9 +14,9 @@ use ecow::EcoString; use typst_library::diag::{bail, At, SourceResult}; use typst_library::engine::Engine; use typst_library::foundations::{ - Content, Context, ContextElem, Element, NativeElement, Recipe, RecipeIndex, Selector, - SequenceElem, Show, ShowSet, Style, StyleChain, StyledElem, Styles, SymbolElem, - Synthesize, Transformation, + Content, Context, ContextElem, Element, NativeElement, NativeShowRule, Recipe, + RecipeIndex, Selector, SequenceElem, ShowSet, Style, StyleChain, StyledElem, Styles, + SymbolElem, Synthesize, TargetElem, Transformation, }; use typst_library::html::{tag, FrameElem, HtmlElem}; use typst_library::introspection::{Locatable, SplitLocator, Tag, TagElem}; @@ -160,7 +160,7 @@ enum ShowStep<'a> { /// A user-defined transformational show rule. Recipe(&'a Recipe, RecipeIndex), /// The built-in show rule. - Builtin, + Builtin(NativeShowRule), } /// A match of a regex show rule. @@ -382,9 +382,7 @@ fn visit_show_rules<'a>( } // Apply a built-in show rule. - ShowStep::Builtin => { - output.with::().unwrap().show(s.engine, chained) - } + ShowStep::Builtin(rule) => rule.apply(&output, s.engine, chained), }; // Errors in show rules don't terminate compilation immediately. We just @@ -426,14 +424,14 @@ fn visit_show_rules<'a>( Ok(true) } -/// Inspects a target element and the current styles and determines how to -/// proceed with the styling. +/// Inspects an element and the current styles and determines how to proceed +/// with the styling. fn verdict<'a>( engine: &mut Engine, - target: &'a Content, + elem: &'a Content, styles: StyleChain<'a>, ) -> Option> { - let prepared = target.is_prepared(); + let prepared = elem.is_prepared(); let mut map = Styles::new(); let mut step = None; @@ -441,20 +439,20 @@ fn verdict<'a>( // fields before real synthesis runs (during preparation). It's really // unfortunate that we have to do this, but otherwise // `show figure.where(kind: table)` won't work :( - let mut target = target; + let mut elem = elem; let mut slot; - if !prepared && target.can::() { - slot = target.clone(); + if !prepared && elem.can::() { + slot = elem.clone(); slot.with_mut::() .unwrap() .synthesize(engine, styles) .ok(); - target = &slot; + elem = &slot; } // Lazily computes the total number of recipes in the style chain. We need // it to determine whether a particular show rule was already applied to the - // `target` previously. For this purpose, show rules are indexed from the + // `elem` previously. For this purpose, show rules are indexed from the // top of the chain as the chain might grow to the bottom. let depth = LazyCell::new(|| styles.recipes().count()); @@ -462,7 +460,7 @@ fn verdict<'a>( // We're not interested in recipes that don't match. if !recipe .selector() - .is_some_and(|selector| selector.matches(target, Some(styles))) + .is_some_and(|selector| selector.matches(elem, Some(styles))) { continue; } @@ -480,9 +478,9 @@ fn verdict<'a>( continue; } - // Check whether this show rule was already applied to the target. + // Check whether this show rule was already applied to the element. let index = RecipeIndex(*depth - r); - if target.is_guarded(index) { + if elem.is_guarded(index) { continue; } @@ -498,19 +496,22 @@ fn verdict<'a>( } // If we found no user-defined rule, also consider the built-in show rule. - if step.is_none() && target.can::() { - step = Some(ShowStep::Builtin); + if step.is_none() { + let target = styles.get(TargetElem::target); + if let Some(rule) = engine.routines.rules.get(target, elem) { + step = Some(ShowStep::Builtin(rule)); + } } // If there's no nothing to do, there is also no verdict. if step.is_none() && map.is_empty() && (prepared || { - target.label().is_none() - && target.location().is_none() - && !target.can::() - && !target.can::() - && !target.can::() + elem.label().is_none() + && elem.location().is_none() + && !elem.can::() + && !elem.can::() + && !elem.can::() }) { return None; @@ -523,7 +524,7 @@ fn verdict<'a>( fn prepare( engine: &mut Engine, locator: &mut SplitLocator, - target: &mut Content, + elem: &mut Content, map: &mut Styles, styles: StyleChain, ) -> SourceResult> { @@ -533,43 +534,43 @@ fn prepare( // // The element could already have a location even if it is not prepared // when it stems from a query. - let key = typst_utils::hash128(&target); - if target.location().is_none() - && (target.can::() || target.label().is_some()) + let key = typst_utils::hash128(&elem); + if elem.location().is_none() + && (elem.can::() || elem.label().is_some()) { let loc = locator.next_location(engine.introspector, key); - target.set_location(loc); + elem.set_location(loc); } // Apply built-in show-set rules. User-defined show-set rules are already // considered in the map built while determining the verdict. - if let Some(show_settable) = target.with::() { + if let Some(show_settable) = elem.with::() { map.apply(show_settable.show_set(styles)); } // If necessary, generated "synthesized" fields (which are derived from // other fields or queries). Do this after show-set so that show-set styles // are respected. - if let Some(synthesizable) = target.with_mut::() { + if let Some(synthesizable) = elem.with_mut::() { synthesizable.synthesize(engine, styles.chain(map))?; } // Copy style chain fields into the element itself, so that they are // available in rules. - target.materialize(styles.chain(map)); + elem.materialize(styles.chain(map)); // If the element is locatable, create start and end tags to be able to find // the element in the frames after layout. Do this after synthesis and // materialization, so that it includes the synthesized fields. Do it before // marking as prepared so that show-set rules will apply to this element // when queried. - let tags = target + let tags = elem .location() - .map(|loc| (Tag::Start(target.clone()), Tag::End(loc, key))); + .map(|loc| (Tag::Start(elem.clone()), Tag::End(loc, key))); // Ensure that this preparation only runs once by marking the element as // prepared. - target.mark_prepared(); + elem.mark_prepared(); Ok(tags) } diff --git a/crates/typst/src/lib.rs b/crates/typst/src/lib.rs index eee7966a7..0673c3259 100644 --- a/crates/typst/src/lib.rs +++ b/crates/typst/src/lib.rs @@ -39,6 +39,7 @@ pub use typst_syntax as syntax; pub use typst_utils as utils; use std::collections::HashSet; +use std::sync::LazyLock; use comemo::{Track, Tracked, Validate}; use ecow::{eco_format, eco_vec, EcoString, EcoVec}; @@ -46,7 +47,7 @@ use typst_library::diag::{ bail, warning, FileError, SourceDiagnostic, SourceResult, Warned, }; use typst_library::engine::{Engine, Route, Sink, Traced}; -use typst_library::foundations::{StyleChain, Styles, Value}; +use typst_library::foundations::{NativeRuleMap, StyleChain, Styles, Value}; use typst_library::html::HtmlDocument; use typst_library::introspection::Introspector; use typst_library::layout::PagedDocument; @@ -326,33 +327,15 @@ mod sealed { /// function pointers. /// /// This is essentially dynamic linking and done to allow for crate splitting. -pub static ROUTINES: Routines = Routines { +pub static ROUTINES: LazyLock = LazyLock::new(|| Routines { + rules: { + let mut rules = NativeRuleMap::new(); + typst_layout::register(&mut rules); + typst_html::register(&mut rules); + rules + }, eval_string: typst_eval::eval_string, eval_closure: typst_eval::eval_closure, realize: typst_realize::realize, - layout_fragment: typst_layout::layout_fragment, layout_frame: typst_layout::layout_frame, - layout_list: typst_layout::layout_list, - layout_enum: typst_layout::layout_enum, - layout_grid: typst_layout::layout_grid, - layout_table: typst_layout::layout_table, - layout_stack: typst_layout::layout_stack, - layout_columns: typst_layout::layout_columns, - layout_move: typst_layout::layout_move, - layout_rotate: typst_layout::layout_rotate, - layout_scale: typst_layout::layout_scale, - layout_skew: typst_layout::layout_skew, - layout_repeat: typst_layout::layout_repeat, - layout_pad: typst_layout::layout_pad, - layout_line: typst_layout::layout_line, - layout_curve: typst_layout::layout_curve, - layout_path: typst_layout::layout_path, - layout_polygon: typst_layout::layout_polygon, - layout_rect: typst_layout::layout_rect, - layout_square: typst_layout::layout_square, - layout_ellipse: typst_layout::layout_ellipse, - layout_circle: typst_layout::layout_circle, - layout_image: typst_layout::layout_image, - layout_equation_block: typst_layout::layout_equation_block, - layout_equation_inline: typst_layout::layout_equation_inline, -}; +}); From e71674f6b3db0768c3e9d6e0271628377f8c82d8 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 9 Jul 2025 11:28:26 +0200 Subject: [PATCH 02/18] Construct library via extension trait instead of default & inherent impl (#6576) --- crates/typst-cli/src/world.rs | 2 +- crates/typst-ide/src/tests.rs | 2 +- crates/typst-library/src/lib.rs | 36 +++++++++++++++------------- crates/typst-library/src/routines.rs | 7 ++++++ crates/typst/src/lib.rs | 19 +++++++++++++++ docs/src/lib.rs | 4 ++-- tests/fuzz/src/compile.rs | 2 +- tests/src/world.rs | 2 +- 8 files changed, 52 insertions(+), 22 deletions(-) diff --git a/crates/typst-cli/src/world.rs b/crates/typst-cli/src/world.rs index 95bee235c..8ad766b14 100644 --- a/crates/typst-cli/src/world.rs +++ b/crates/typst-cli/src/world.rs @@ -12,7 +12,7 @@ use typst::foundations::{Bytes, Datetime, Dict, IntoValue}; use typst::syntax::{FileId, Lines, Source, VirtualPath}; use typst::text::{Font, FontBook}; use typst::utils::LazyHash; -use typst::{Library, World}; +use typst::{Library, LibraryExt, World}; use typst_kit::fonts::{FontSlot, Fonts}; use typst_kit::package::PackageStorage; use typst_timing::timed; diff --git a/crates/typst-ide/src/tests.rs b/crates/typst-ide/src/tests.rs index b3f368f2e..168dfc9f2 100644 --- a/crates/typst-ide/src/tests.rs +++ b/crates/typst-ide/src/tests.rs @@ -10,7 +10,7 @@ use typst::syntax::package::{PackageSpec, PackageVersion}; use typst::syntax::{FileId, Source, VirtualPath}; use typst::text::{Font, FontBook, TextElem, TextSize}; use typst::utils::{singleton, LazyHash}; -use typst::{Feature, Library, World}; +use typst::{Feature, Library, LibraryExt, World}; use crate::IdeWorld; diff --git a/crates/typst-library/src/lib.rs b/crates/typst-library/src/lib.rs index fa7977888..3e2ce99ec 100644 --- a/crates/typst-library/src/lib.rs +++ b/crates/typst-library/src/lib.rs @@ -36,6 +36,7 @@ use typst_utils::{LazyHash, SmallBitSet}; use crate::diag::FileResult; use crate::foundations::{Array, Binding, Bytes, Datetime, Dict, Module, Scope, Styles}; use crate::layout::{Alignment, Dir}; +use crate::routines::Routines; use crate::text::{Font, FontBook}; use crate::visualize::Color; @@ -139,6 +140,11 @@ impl WorldExt for T { } /// Definition of Typst's standard library. +/// +/// To create and configure the standard library, use the `LibraryExt` trait +/// and call +/// - `Library::default()` for a standard configuration +/// - `Library::builder().build()` if you want to customize the library #[derive(Debug, Clone, Hash)] pub struct Library { /// The module that contains the definitions that are available everywhere. @@ -154,30 +160,28 @@ pub struct Library { pub features: Features, } -impl Library { - /// Create a new builder for a library. - pub fn builder() -> LibraryBuilder { - LibraryBuilder::default() - } -} - -impl Default for Library { - /// Constructs the standard library with the default configuration. - fn default() -> Self { - Self::builder().build() - } -} - /// Configurable builder for the standard library. /// -/// This struct is created by [`Library::builder`]. -#[derive(Debug, Clone, Default)] +/// Constructed via the `LibraryExt` trait. +#[derive(Debug, Clone)] pub struct LibraryBuilder { + #[expect(unused, reason = "will be used in the future")] + routines: &'static Routines, inputs: Option, features: Features, } impl LibraryBuilder { + /// Creates a new builder. + #[doc(hidden)] + pub fn from_routines(routines: &'static Routines) -> Self { + Self { + routines, + inputs: None, + features: Features::default(), + } + } + /// Configure the inputs visible through `sys.inputs`. pub fn with_inputs(mut self, inputs: Dict) -> Self { self.inputs = Some(inputs); diff --git a/crates/typst-library/src/routines.rs b/crates/typst-library/src/routines.rs index 4bf8d60c8..6db99ba51 100644 --- a/crates/typst-library/src/routines.rs +++ b/crates/typst-library/src/routines.rs @@ -1,3 +1,4 @@ +use std::fmt::{self, Debug, Formatter}; use std::hash::{Hash, Hasher}; use comemo::{Tracked, TrackedMut}; @@ -38,6 +39,12 @@ macro_rules! routines { impl Hash for Routines { fn hash(&self, _: &mut H) {} } + + impl Debug for Routines { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad("Routines(..)") + } + } }; } diff --git a/crates/typst/src/lib.rs b/crates/typst/src/lib.rs index 0673c3259..11d5c9e05 100644 --- a/crates/typst/src/lib.rs +++ b/crates/typst/src/lib.rs @@ -323,6 +323,25 @@ mod sealed { } } +/// Provides ways to construct a [`Library`]. +pub trait LibraryExt { + /// Creates the default library. + fn default() -> Library; + + /// Creates a builder for configuring a library. + fn builder() -> LibraryBuilder; +} + +impl LibraryExt for Library { + fn default() -> Library { + Self::builder().build() + } + + fn builder() -> LibraryBuilder { + LibraryBuilder::from_routines(&ROUTINES) + } +} + /// Defines implementation of various Typst compiler routines as a table of /// function pointers. /// diff --git a/docs/src/lib.rs b/docs/src/lib.rs index ddc956e60..e3eb21f98 100644 --- a/docs/src/lib.rs +++ b/docs/src/lib.rs @@ -24,7 +24,7 @@ use typst::foundations::{ use typst::layout::{Abs, Margin, PageElem, PagedDocument}; use typst::text::{Font, FontBook}; use typst::utils::LazyHash; -use typst::{Category, Feature, Library, LibraryBuilder}; +use typst::{Category, Feature, Library, LibraryExt}; use unicode_math_class::MathClass; macro_rules! load { @@ -51,7 +51,7 @@ static GROUPS: LazyLock> = LazyLock::new(|| { }); static LIBRARY: LazyLock> = LazyLock::new(|| { - let mut lib = LibraryBuilder::default() + let mut lib = Library::builder() .with_features([Feature::Html].into_iter().collect()) .build(); let scope = lib.global.scope_mut(); diff --git a/tests/fuzz/src/compile.rs b/tests/fuzz/src/compile.rs index 3dedfb737..945e9fce8 100644 --- a/tests/fuzz/src/compile.rs +++ b/tests/fuzz/src/compile.rs @@ -7,7 +7,7 @@ use typst::layout::PagedDocument; use typst::syntax::{FileId, Source}; use typst::text::{Font, FontBook}; use typst::utils::LazyHash; -use typst::{Library, World}; +use typst::{Library, LibraryExt, World}; struct FuzzWorld { library: LazyHash, diff --git a/tests/src/world.rs b/tests/src/world.rs index 9b16d6126..4b6cf5a34 100644 --- a/tests/src/world.rs +++ b/tests/src/world.rs @@ -19,7 +19,7 @@ use typst::syntax::{FileId, Source, Span}; use typst::text::{Font, FontBook, TextElem, TextSize}; use typst::utils::{singleton, LazyHash}; use typst::visualize::Color; -use typst::{Feature, Library, World}; +use typst::{Feature, Library, LibraryExt, World}; use typst_syntax::Lines; /// A world that provides access to the tests environment. From 52a708b988cf7d13898194e886790acb7edd510f Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 9 Jul 2025 11:46:40 +0200 Subject: [PATCH 03/18] Move `html` module to `typst-html` crate (#6577) --- Cargo.lock | 4 + crates/typst-html/Cargo.toml | 4 + crates/typst-html/src/css.rs | 135 ++++++++++++++ crates/typst-html/src/lib.rs | 18 +- .../src/html => typst-html/src}/typed.rs | 166 ++---------------- crates/typst-library/src/html/mod.rs | 13 +- crates/typst-library/src/lib.rs | 12 +- crates/typst-library/src/routines.rs | 7 +- crates/typst/src/lib.rs | 1 + 9 files changed, 184 insertions(+), 176 deletions(-) create mode 100644 crates/typst-html/src/css.rs rename crates/{typst-library/src/html => typst-html/src}/typed.rs (79%) diff --git a/Cargo.lock b/Cargo.lock index 550c4141a..5526da48c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2971,8 +2971,12 @@ dependencies = [ name = "typst-html" version = "0.13.1" dependencies = [ + "bumpalo", "comemo", "ecow", + "palette", + "time", + "typst-assets", "typst-library", "typst-macros", "typst-svg", diff --git a/crates/typst-html/Cargo.toml b/crates/typst-html/Cargo.toml index 534848f96..54cad0124 100644 --- a/crates/typst-html/Cargo.toml +++ b/crates/typst-html/Cargo.toml @@ -13,14 +13,18 @@ keywords = { workspace = true } readme = { workspace = true } [dependencies] +typst-assets = { workspace = true } typst-library = { workspace = true } typst-macros = { workspace = true } typst-syntax = { workspace = true } typst-timing = { workspace = true } typst-utils = { workspace = true } typst-svg = { workspace = true } +bumpalo = { workspace = true } comemo = { workspace = true } ecow = { workspace = true } +palette = { workspace = true } +time = { workspace = true } [lints] workspace = true diff --git a/crates/typst-html/src/css.rs b/crates/typst-html/src/css.rs new file mode 100644 index 000000000..2b659188a --- /dev/null +++ b/crates/typst-html/src/css.rs @@ -0,0 +1,135 @@ +//! Conversion from Typst data types into CSS data types. + +use std::fmt::{self, Display}; + +use typst_library::layout::Length; +use typst_library::visualize::{Color, Hsl, LinearRgb, Oklab, Oklch, Rgb}; +use typst_utils::Numeric; + +pub fn length(length: Length) -> impl Display { + typst_utils::display(move |f| match (length.abs.is_zero(), length.em.is_zero()) { + (false, false) => { + write!(f, "calc({}pt + {}em)", length.abs.to_pt(), length.em.get()) + } + (true, false) => write!(f, "{}em", length.em.get()), + (_, true) => write!(f, "{}pt", length.abs.to_pt()), + }) +} + +pub fn color(color: Color) -> impl Display { + typst_utils::display(move |f| match color { + Color::Rgb(_) | Color::Cmyk(_) | Color::Luma(_) => rgb(f, color.to_rgb()), + Color::Oklab(v) => oklab(f, v), + Color::Oklch(v) => oklch(f, v), + Color::LinearRgb(v) => linear_rgb(f, v), + Color::Hsl(_) | Color::Hsv(_) => hsl(f, color.to_hsl()), + }) +} + +fn oklab(f: &mut fmt::Formatter<'_>, v: Oklab) -> fmt::Result { + write!(f, "oklab({} {} {}{})", percent(v.l), number(v.a), number(v.b), alpha(v.alpha)) +} + +fn oklch(f: &mut fmt::Formatter<'_>, v: Oklch) -> fmt::Result { + write!( + f, + "oklch({} {} {}deg{})", + percent(v.l), + number(v.chroma), + number(v.hue.into_degrees()), + alpha(v.alpha) + ) +} + +fn rgb(f: &mut fmt::Formatter<'_>, v: Rgb) -> fmt::Result { + if let Some(v) = rgb_to_8_bit_lossless(v) { + let (r, g, b, a) = v.into_components(); + write!(f, "#{r:02x}{g:02x}{b:02x}")?; + if a != u8::MAX { + write!(f, "{a:02x}")?; + } + Ok(()) + } else { + write!( + f, + "rgb({} {} {}{})", + percent(v.red), + percent(v.green), + percent(v.blue), + alpha(v.alpha) + ) + } +} + +/// Converts an f32 RGBA color to its 8-bit representation if the result is +/// [very close](is_very_close) to the original. +fn rgb_to_8_bit_lossless( + v: Rgb, +) -> Option> { + let l = v.into_format::(); + let h = l.into_format::(); + (is_very_close(v.red, h.red) + && is_very_close(v.blue, h.blue) + && is_very_close(v.green, h.green) + && is_very_close(v.alpha, h.alpha)) + .then_some(l) +} + +fn linear_rgb(f: &mut fmt::Formatter<'_>, v: LinearRgb) -> fmt::Result { + write!( + f, + "color(srgb-linear {} {} {}{})", + percent(v.red), + percent(v.green), + percent(v.blue), + alpha(v.alpha), + ) +} + +fn hsl(f: &mut fmt::Formatter<'_>, v: Hsl) -> fmt::Result { + write!( + f, + "hsl({}deg {} {}{})", + number(v.hue.into_degrees()), + percent(v.saturation), + percent(v.lightness), + alpha(v.alpha), + ) +} + +/// Displays an alpha component if it not 1. +fn alpha(value: f32) -> impl Display { + typst_utils::display(move |f| { + if !is_very_close(value, 1.0) { + write!(f, " / {}", percent(value))?; + } + Ok(()) + }) +} + +/// Displays a rounded percentage. +/// +/// For a percentage, two significant digits after the comma gives us a +/// precision of 1/10_000, which is more than 12 bits (see `is_very_close`). +fn percent(ratio: f32) -> impl Display { + typst_utils::display(move |f| { + write!(f, "{}%", typst_utils::round_with_precision(ratio as f64 * 100.0, 2)) + }) +} + +/// Rounds a number for display. +/// +/// For a number between 0 and 1, four significant digits give us a +/// precision of 1/10_000, which is more than 12 bits (see `is_very_close`). +fn number(value: f32) -> impl Display { + typst_utils::round_with_precision(value as f64, 4) +} + +/// Whether two component values are close enough that there is no +/// difference when encoding them with 12-bit. 12 bit is the highest +/// reasonable color bit depth found in the industry. +fn is_very_close(a: f32, b: f32) -> bool { + const MAX_BIT_DEPTH: u32 = 12; + const EPS: f32 = 0.5 / 2_i32.pow(MAX_BIT_DEPTH) as f32; + (a - b).abs() < EPS +} diff --git a/crates/typst-html/src/lib.rs b/crates/typst-html/src/lib.rs index 19eb14464..7063931b7 100644 --- a/crates/typst-html/src/lib.rs +++ b/crates/typst-html/src/lib.rs @@ -1,7 +1,9 @@ //! Typst's HTML exporter. +mod css; mod encode; mod rules; +mod typed; pub use self::encode::html; pub use self::rules::register; @@ -9,7 +11,9 @@ pub use self::rules::register; use comemo::{Track, Tracked, TrackedMut}; use typst_library::diag::{bail, warning, At, SourceResult}; use typst_library::engine::{Engine, Route, Sink, Traced}; -use typst_library::foundations::{Content, StyleChain, Target, TargetElem}; +use typst_library::foundations::{ + Content, Module, Scope, StyleChain, Target, TargetElem, +}; use typst_library::html::{ attr, tag, FrameElem, HtmlDocument, HtmlElem, HtmlElement, HtmlFrame, HtmlNode, }; @@ -20,9 +24,19 @@ use typst_library::layout::{Abs, Axes, BlockBody, BlockElem, BoxElem, Region, Si use typst_library::model::{DocumentInfo, ParElem}; use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind, Routines}; use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem}; -use typst_library::World; +use typst_library::{Category, World}; use typst_syntax::Span; +/// Create a module with all HTML definitions. +pub fn module() -> Module { + let mut html = Scope::deduplicating(); + html.start_category(Category::Html); + html.define_elem::(); + html.define_elem::(); + crate::typed::define(&mut html); + Module::new("html", html) +} + /// Produce an HTML document from content. /// /// This first performs root-level realization and then turns the resulting diff --git a/crates/typst-library/src/html/typed.rs b/crates/typst-html/src/typed.rs similarity index 79% rename from crates/typst-library/src/html/typed.rs rename to crates/typst-html/src/typed.rs index 8240b2963..4b794bbba 100644 --- a/crates/typst-library/src/html/typed.rs +++ b/crates/typst-html/src/typed.rs @@ -11,19 +11,20 @@ use bumpalo::Bump; use comemo::Tracked; use ecow::{eco_format, eco_vec, EcoString}; use typst_assets::html as data; -use typst_macros::cast; - -use crate::diag::{bail, At, Hint, HintedStrResult, SourceResult}; -use crate::engine::Engine; -use crate::foundations::{ +use typst_library::diag::{bail, At, Hint, HintedStrResult, SourceResult}; +use typst_library::engine::Engine; +use typst_library::foundations::{ Args, Array, AutoValue, CastInfo, Content, Context, Datetime, Dict, Duration, FromValue, IntoValue, NativeFuncData, NativeFuncPtr, NoneValue, ParamInfo, PositiveF64, Reflect, Scope, Str, Type, Value, }; -use crate::html::tag; -use crate::html::{HtmlAttr, HtmlAttrs, HtmlElem, HtmlTag}; -use crate::layout::{Axes, Axis, Dir, Length}; -use crate::visualize::Color; +use typst_library::html::tag; +use typst_library::html::{HtmlAttr, HtmlAttrs, HtmlElem, HtmlTag}; +use typst_library::layout::{Axes, Axis, Dir, Length}; +use typst_library::visualize::Color; +use typst_macros::cast; + +use crate::css; /// Hook up all typed HTML definitions. pub(super) fn define(html: &mut Scope) { @@ -705,153 +706,6 @@ impl IntoAttr for SourceSize { } } -/// Conversion from Typst data types into CSS data types. -/// -/// This can be moved elsewhere once we start supporting more CSS stuff. -mod css { - use std::fmt::{self, Display}; - - use typst_utils::Numeric; - - use crate::layout::Length; - use crate::visualize::{Color, Hsl, LinearRgb, Oklab, Oklch, Rgb}; - - pub fn length(length: Length) -> impl Display { - typst_utils::display(move |f| match (length.abs.is_zero(), length.em.is_zero()) { - (false, false) => { - write!(f, "calc({}pt + {}em)", length.abs.to_pt(), length.em.get()) - } - (true, false) => write!(f, "{}em", length.em.get()), - (_, true) => write!(f, "{}pt", length.abs.to_pt()), - }) - } - - pub fn color(color: Color) -> impl Display { - typst_utils::display(move |f| match color { - Color::Rgb(_) | Color::Cmyk(_) | Color::Luma(_) => rgb(f, color.to_rgb()), - Color::Oklab(v) => oklab(f, v), - Color::Oklch(v) => oklch(f, v), - Color::LinearRgb(v) => linear_rgb(f, v), - Color::Hsl(_) | Color::Hsv(_) => hsl(f, color.to_hsl()), - }) - } - - fn oklab(f: &mut fmt::Formatter<'_>, v: Oklab) -> fmt::Result { - write!( - f, - "oklab({} {} {}{})", - percent(v.l), - number(v.a), - number(v.b), - alpha(v.alpha) - ) - } - - fn oklch(f: &mut fmt::Formatter<'_>, v: Oklch) -> fmt::Result { - write!( - f, - "oklch({} {} {}deg{})", - percent(v.l), - number(v.chroma), - number(v.hue.into_degrees()), - alpha(v.alpha) - ) - } - - fn rgb(f: &mut fmt::Formatter<'_>, v: Rgb) -> fmt::Result { - if let Some(v) = rgb_to_8_bit_lossless(v) { - let (r, g, b, a) = v.into_components(); - write!(f, "#{r:02x}{g:02x}{b:02x}")?; - if a != u8::MAX { - write!(f, "{a:02x}")?; - } - Ok(()) - } else { - write!( - f, - "rgb({} {} {}{})", - percent(v.red), - percent(v.green), - percent(v.blue), - alpha(v.alpha) - ) - } - } - - /// Converts an f32 RGBA color to its 8-bit representation if the result is - /// [very close](is_very_close) to the original. - fn rgb_to_8_bit_lossless( - v: Rgb, - ) -> Option> { - let l = v.into_format::(); - let h = l.into_format::(); - (is_very_close(v.red, h.red) - && is_very_close(v.blue, h.blue) - && is_very_close(v.green, h.green) - && is_very_close(v.alpha, h.alpha)) - .then_some(l) - } - - fn linear_rgb(f: &mut fmt::Formatter<'_>, v: LinearRgb) -> fmt::Result { - write!( - f, - "color(srgb-linear {} {} {}{})", - percent(v.red), - percent(v.green), - percent(v.blue), - alpha(v.alpha), - ) - } - - fn hsl(f: &mut fmt::Formatter<'_>, v: Hsl) -> fmt::Result { - write!( - f, - "hsl({}deg {} {}{})", - number(v.hue.into_degrees()), - percent(v.saturation), - percent(v.lightness), - alpha(v.alpha), - ) - } - - /// Displays an alpha component if it not 1. - fn alpha(value: f32) -> impl Display { - typst_utils::display(move |f| { - if !is_very_close(value, 1.0) { - write!(f, " / {}", percent(value))?; - } - Ok(()) - }) - } - - /// Displays a rounded percentage. - /// - /// For a percentage, two significant digits after the comma gives us a - /// precision of 1/10_000, which is more than 12 bits (see `is_very_close`). - fn percent(ratio: f32) -> impl Display { - typst_utils::display(move |f| { - write!(f, "{}%", typst_utils::round_with_precision(ratio as f64 * 100.0, 2)) - }) - } - - /// Rounds a number for display. - /// - /// For a number between 0 and 1, four significant digits give us a - /// precision of 1/10_000, which is more than 12 bits (see `is_very_close`). - fn number(value: f32) -> impl Display { - typst_utils::round_with_precision(value as f64, 4) - } - - /// Whether two component values are close enough that there is no - /// difference when encoding them with 12-bit. 12 bit is the highest - /// reasonable color bit depth found in the industry. - fn is_very_close(a: f32, b: f32) -> bool { - const MAX_BIT_DEPTH: u32 = 12; - const EPS: f32 = 0.5 / 2_i32.pow(MAX_BIT_DEPTH) as f32; - (a - b).abs() < EPS - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/typst-library/src/html/mod.rs b/crates/typst-library/src/html/mod.rs index f98352061..ca2cc0311 100644 --- a/crates/typst-library/src/html/mod.rs +++ b/crates/typst-library/src/html/mod.rs @@ -1,23 +1,12 @@ //! HTML output. mod dom; -mod typed; pub use self::dom::*; use ecow::EcoString; -use crate::foundations::{elem, Content, Module, Scope}; - -/// Create a module with all HTML definitions. -pub fn module() -> Module { - let mut html = Scope::deduplicating(); - html.start_category(crate::Category::Html); - html.define_elem::(); - html.define_elem::(); - self::typed::define(&mut html); - Module::new("html", html) -} +use crate::foundations::{elem, Content}; /// An HTML element that can contain Typst content. /// diff --git a/crates/typst-library/src/lib.rs b/crates/typst-library/src/lib.rs index 3e2ce99ec..5d047570b 100644 --- a/crates/typst-library/src/lib.rs +++ b/crates/typst-library/src/lib.rs @@ -165,7 +165,6 @@ pub struct Library { /// Constructed via the `LibraryExt` trait. #[derive(Debug, Clone)] pub struct LibraryBuilder { - #[expect(unused, reason = "will be used in the future")] routines: &'static Routines, inputs: Option, features: Features, @@ -200,7 +199,7 @@ impl LibraryBuilder { pub fn build(self) -> Library { let math = math::module(); let inputs = self.inputs.unwrap_or_default(); - let global = global(math.clone(), inputs, &self.features); + let global = global(self.routines, math.clone(), inputs, &self.features); Library { global: global.clone(), math, @@ -282,7 +281,12 @@ impl Category { } /// Construct the module with global definitions. -fn global(math: Module, inputs: Dict, features: &Features) -> Module { +fn global( + routines: &Routines, + math: Module, + inputs: Dict, + features: &Features, +) -> Module { let mut global = Scope::deduplicating(); self::foundations::define(&mut global, inputs, features); @@ -297,7 +301,7 @@ fn global(math: Module, inputs: Dict, features: &Features) -> Module { global.define("math", math); global.define("pdf", self::pdf::module()); if features.is_enabled(Feature::Html) { - global.define("html", self::html::module()); + global.define("html", (routines.html_module)()); } prelude(&mut global); diff --git a/crates/typst-library/src/routines.rs b/crates/typst-library/src/routines.rs index 6db99ba51..a81806fd5 100644 --- a/crates/typst-library/src/routines.rs +++ b/crates/typst-library/src/routines.rs @@ -8,8 +8,8 @@ use typst_utils::LazyHash; use crate::diag::SourceResult; use crate::engine::{Engine, Route, Sink, Traced}; use crate::foundations::{ - Args, Closure, Content, Context, Func, NativeRuleMap, Scope, StyleChain, Styles, - Value, + Args, Closure, Content, Context, Func, Module, NativeRuleMap, Scope, StyleChain, + Styles, Value, }; use crate::introspection::{Introspector, Locator, SplitLocator}; use crate::layout::{Frame, Region}; @@ -92,6 +92,9 @@ routines! { styles: StyleChain, region: Region, ) -> SourceResult + + /// Constructs the `html` module. + fn html_module() -> Module } /// Defines what kind of realization we are performing. diff --git a/crates/typst/src/lib.rs b/crates/typst/src/lib.rs index 11d5c9e05..591e5a9b9 100644 --- a/crates/typst/src/lib.rs +++ b/crates/typst/src/lib.rs @@ -357,4 +357,5 @@ pub static ROUTINES: LazyLock = LazyLock::new(|| Routines { eval_closure: typst_eval::eval_closure, realize: typst_realize::realize, layout_frame: typst_layout::layout_frame, + html_module: typst_html::module, }); From e5e813219edd3553535c4c6cf885138dbc9f3f9a Mon Sep 17 00:00:00 2001 From: Robin Date: Wed, 9 Jul 2025 14:01:57 +0200 Subject: [PATCH 04/18] Fix typo of Typst domain in quote docs (#6573) --- crates/typst-library/src/model/quote.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/typst-library/src/model/quote.rs b/crates/typst-library/src/model/quote.rs index 9960b7587..5ce4a92f5 100644 --- a/crates/typst-library/src/model/quote.rs +++ b/crates/typst-library/src/model/quote.rs @@ -102,7 +102,7 @@ pub struct QuoteElem { /// } /// /// #quote( - /// attribution: link("https://typst.app/home")[typst.com] + /// attribution: link("https://typst.app/home")[typst.app] /// )[ /// Compose papers faster /// ] From 9ad1879e9d0978d828790fdf7978ba4aff240a71 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 9 Jul 2025 14:02:13 +0200 Subject: [PATCH 05/18] Anti-alias clip paths (#6570) --- crates/typst-render/src/lib.rs | 4 ++-- tests/ref/block-clip-svg-glyphs.png | Bin 1862 -> 1938 bytes tests/ref/block-clip-text.png | Bin 908 -> 965 bytes tests/ref/block-clipping-multiple-pages.png | Bin 2029 -> 2085 bytes tests/ref/box-clip-radius-without-stroke.png | Bin 1255 -> 1256 bytes tests/ref/box-clip-radius.png | Bin 1250 -> 1255 bytes tests/ref/box-clip-rect.png | Bin 1680 -> 1691 bytes .../closure-path-resolve-in-layout-phase.png | Bin 2256 -> 2263 bytes tests/ref/hide-image.png | Bin 8877 -> 9123 bytes tests/ref/image-baseline-with-box.png | Bin 6375 -> 6613 bytes .../issue-5499-text-fill-in-clip-block.png | Bin 1502 -> 1518 bytes tests/ref/issue-6267-clip-anti-alias.png | Bin 0 -> 251 bytes .../ref/transform-rotate-relative-sizing.png | Bin 2414 -> 2491 bytes tests/ref/transform-scale-relative-sizing.png | Bin 2023 -> 2034 bytes tests/ref/transform-skew-relative-sizing.png | Bin 828 -> 834 bytes tests/suite/layout/container.typ | 7 +++++++ 16 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 tests/ref/issue-6267-clip-anti-alias.png diff --git a/crates/typst-render/src/lib.rs b/crates/typst-render/src/lib.rs index f43cd019b..2c57fe2db 100644 --- a/crates/typst-render/src/lib.rs +++ b/crates/typst-render/src/lib.rs @@ -202,7 +202,7 @@ fn render_group(canvas: &mut sk::Pixmap, state: State, pos: Point, group: &Group mask.intersect_path( &path, sk::FillRule::default(), - false, + true, sk::Transform::default(), ); storage = mask; @@ -218,7 +218,7 @@ fn render_group(canvas: &mut sk::Pixmap, state: State, pos: Point, group: &Group mask.fill_path( &path, sk::FillRule::default(), - false, + true, sk::Transform::default(), ); storage = mask; diff --git a/tests/ref/block-clip-svg-glyphs.png b/tests/ref/block-clip-svg-glyphs.png index 0fc2c962fc63fb26f6828d5c67a72bc22da6c4cc..bf13b7ea6f19a6fc430029ae27cc8ee4bbf8d26b 100644 GIT binary patch delta 1923 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 diff --git a/tests/ref/block-clip-text.png b/tests/ref/block-clip-text.png index 8c82bc30934ebef06296654c08655e8a7d4df64d..2b099b48a2b6071e1bf84cf639b4618a5cc3c6bd 100644 GIT binary patch delta 943 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 ZzXRF7RM$iAlvDr!002ovPDHLkV1jmfYYqSa delta 2244 zcmV;#2s`)J5zrBkB!8DlL_t(|+U%LjPuf`&$NNv1_y@Q!(YR1o##yO}nM5;*b!sLq zbfG3wqp73CnkXuakC@iPM;&z#9Ry2J9{%KE`N>NyFsO(?K_~(W%EQ-;4-ld=C-Y0l zL`=GAUBEqyoAW#Oo14q$o_lh0c=8XjN3kae0RoLcBSWAOXnzD683LVLpl@$)y`OX* z^PdRTHv}CHhgT-LB(GnH-n_uZx+0Otx7dV6quJixPHNCbAzkveTgzjva~OO|onFd? z;Pv%&G#ZT;@9*yy78c6o@>ry*s%mFFVp$w>{`yJr=SQatO@E$lsefb{p- z@y@D5I7CwXPJg{R^~LL(nwrYX%L(7@c4ua0dcEE+ZbB>;cXf3oHR#LBOT9{>;m~FD zq0#3hS|RHrHtRHRu5x}`nS1~Zoleio%k%ks;IrB6=H_OT$prrT`ugkt%;|Lc{r-xI z3V_?t&;Yp{1ATOK1c!vH6#V74}?f`UYX=zSQ>nX1ncYT;TSv~4^egu1_NX|m&-jnJ4;y5ZY#y4{Yj0B zm@l6)TFPYIKdR+S6Y2Xb(1Y%Cg$W@Kc*EZhTv zcx7cJR0D9bv$LX0svC57_ReN-O2>sBwcv zwkl*AR>hkuT%&wmBE;gw&=N{XNr7gCNF*Y98aR%VNTmO378Vww(I^6a-xmKE7Jtu8 z;Cz;RiM_haQ-9#fF&6%QY*Qt4I6lYg)6>&o42433qf{zeTU(*IL9~z{&9z!BbWj6< zfW=}727_v~nkYdqYBU;9CMjr)F`~klYAMg~QEJxW8!E}rzwMgKPZLoX#aH|Z?hGLz z1)qecNScDRQc?>deL)LU$0?OQz<)lZXv-j$wu+)aNeL1aMOy*ikt(QYG=du2qN0f# zmu?J+fi(+Hya_P{g9}~F{T7pNn%ime>&-Va=bV81`rz5G^KYI;@2p;*o}Lc(4|)(( ze*#)6l|re|Xf)82A$tl00;rwPP9chhLLrOA0<8`!(ag@y_V)Jr{eC`zHh& zjw2m*!(bQZb(y<5T;ZYfgXcOqyJ1bx7#C3Yp@$IAFj>%yjEoEn44?t`#l^*ig$0>R zhEu5f5J+Kq$>nl#h1AqkK7yWj@ZkR4+q!bGvaHysk>zd`nAo~=URUIDXzJGR>h+nK znK(uw5i**wi(M`k8f@EOFn{*Yd(TbB;xb)A9v<#c|SMz^2Kwf z#RwK%og8N~4D=)`@o-gAQc|#siL?(Z%j5CD7stqk-G}LcM~<|#wDIwA{1ccV(5Ag! zuUITzT3UiajYkv=6L>Uy1U+^mf+5hTSAG2O9;d$JHXN}Z9VX^Ve}D2Yg@J&(6lIM3Yr!LO+i!8Yt`-7vZH5(T&t|}Si!}h+H-o};efE#y~`Nbt!vp%@%=N< zgO4l1QB9M&q%2Qx(SN4A5oV4Im9<`|aP&y5{*t7{RaI5c-n3dR#Av)*LpEAoUPhXW zQeGdlb2!W6XVs-5W?!Dq&OV8Tyyy4&`YZd#)T7b5q{Y+I(>NmM*w)sD2GM_F#8ArX zgI0FrnB0v0pz>sW;kD42`16S&L-Dcd(nFO(j@`fJSKwo<#DDjr(I^64B>fKokK;IK zc(GUvng&!;gnE&4jK|}8y&jr1V(R<_UF$12U=leQmA}2^-i6L@4^IV|ER(9VvP^2O zuUi*1WTT>@A~a^R8BAfX5P8~QFpQ3lLaS3K6gY*Fhqx(3KK_Chvgzi=T{iZB-OS!R zss7R+i`E4VO%+jdG_X#P#Kjh#oSY<%7yNZZ&56`kSXjvS(XkELFq zM9niYGVqu18uV7lW@Dqi^^hdwsR^{~sbTWV4JBe#-hVdvrff~|>h%EE-rf#-g#>Lu zv$?rBkw}EYVGL`AVOW+0Zm6$Lr?aoG5A=~Xw_2?T{qq`hQ+JW#XujN9pyl$UO<7V$ zez|M2kl7?^$Q2oL{{ClRuaE||%goFSedY{FdMs%jIZiA& zL7c2Ni)4R@H$h-vTj1E(BuKKGjW2K!E>z%z_lDD^W z?{f18cz)dg{1POo9~>TD5d7j7zxeg9e_c_O?s)G$x>pan2i>a&lj#CMlg$Dz ze}734;1~8kUU*PiDq~nRy_Oe%MY@R>$h-vF2Y2;G-uV35TX?}EKZjPkVQ6Z|E9oGjq5D3E3 z+iD6w8>j*WxoUm;gSRiee({~N=Z_Y4e`@*i_Md$53zb53>7xt%X&&O_>b7p0`k|>b z&yc2Nq|<$gME}Od+WQOnXFl`!em1!H){7A#taRWLpZL_Vho1fOzx=ZofBe#=%kR!F zY<}}^zVatu{tqV~|Fmx-fH|TR`QV3Nzwo1PWuj74WLcJRZ1m>*&UiNZne+RWe|NUO z_WEK?gU_7b=Xz#UGe(D#J4)xqZUtvZ7d5guDPZUThC{=NslFM{v{y@82&;26oz0vq zmI}dY4gb=YzVr*A*UKL{me0`9jrE(Q=DkE}&sKTcz)FVWtnXffOxSKyI80O;8x#cv zfeT5!qu27aD}%Y2Oj;c5+e=V^e@pm-xh%_vEZ~EEX)U z?E#`(t>olmp$;{@Nsxr9v{pA(AVK`*=brnwpZ{EOx4{e2#rLn^B#DQFp|@f(krZVd zVmOIMl!q|IIV! zI_=KYt6QHvHanM%6;~Rr!gN|N#nKU0EEn31hC*^Mzyc5gT?eRYsw-A)z6QZBtf8&h zp1PHvmr@~4v4!2zTK>Ief2*{)oG<39O4Wn_u5a93Tf5`fs%felyUXi^MV=mK*vxuy z>!bM>SJv;=s)g0f>x#a!Rs3*yrEcXN}48Q1Yp z=rRn$(!$K_)RRv<419n2-ZDc-6XO#rD@&h#{CFx7({(o6% zzyGy23B>;KZ$JFme+Ne=IO_VFb+I6x0$KB_vHo5K=uwu8l9qQmwTHHFnnw58(O><} zzpGSgZCf#&Kr>w&Hz*P%qyx|;$0u@_V}bx|mRBiCW*DMYD=CU$d4Z*(d~v;4tpl4j zO)V)$IGEw6SgpCzYT%fUgcuW$dbNI!fCNeP)s%d>yfEH3e>#xZXBxi5%BF6IT|Syj zCX*t|ZEkK^juDN@lM~Zgr(N1!%*F^Bhb+xeG(%C8BuOZQ03!p>VKCdrpuoz{Ja)E! z|1nw~f8?RZ_Z~fQ_x2r4(O!P(M==TizZ@{d;dNkENn#4$huE zJRFbWuY7p7FPYdg7_r)3{$52{v(AnmiQ??f-+BA&zU+OVzkT7GVl?G?*2;ns24NcJ__ULCn!Ta(S&VF>%n;h@IW_O2Le0qNc7(fAl2BF|r)&AAN|$lG)g3LKxJo zrr}o-(GiIFXqr^5rsZ{1O{v#fhKb^dtRQAJ$8wyILA=svmdbfS5EzDuMG}Ql`NsVG z(W57R`s#&F@y=L^z)1?=gzNc|Adn=*vRn{g1WvOg4ly?-(c8tkMGwfaiCW9TP2XmSM0QZ~ItX zZI&wbzkd4g#oKp&{PxOcA3JyA;PB5sx_fABxTN=@_x{-U;RJjMxFp{e_Vo8nqa%jJ6lB(kO_cJG_9;jEs>-TPnLUv-7-l={6jrY#ZO`ke*VqpLNjbspo{JH?ymlapMBzk z#ntD3eC4qd2cJB5xKgi<4WqX{g<>TD6Yi$_a7UQjJ!FYSe7tN0l~PZ(6NRZDYA!e=C+{ zCLWDspoetb)OC+y=m7X6NllDRXK~ekB@;t?&b;!{YcId{^597S!2<^*hRO`&q(l;ez_Cq6kV1fItJiK_ z|3Ed(2ltO$Td!j{9LdI4cUwGzf6oqO?rc^z3avkQ_Tims^~?YGmg|CJhYtpU!}GDr z3)>N1NTrf5ymf7Pt#o+LWI9fDT4jz3Lf6b?QXWo!ywv^s~ z_}K`F<3N@KmZu4lK`00T!T{tsUN;&NLvpOxuplq@v60y5?D6Nn_N|NWy?*5Q+yiGH zkRvh;K(a5*Mx=J79*Mz4huKYnjJ-y9#xBqcr(qu#r-YMaivqq7@3+nT8X<&Bxww%ESoF08lgw5u2jnn#q(*?GRY7Me<8tz>{>zJRw};Z z<3zI3#H$@tthZ{dma2C;I&JA#tz_zMOjA5dGZ8?pqqa5N3s@3TI8FmPqI#I;AUwcl zhNgLv4UykzE4FDO-|fb5d?X?b^vP>0cgClNO)R-n)-?x8GT|cV5R52B`~c@^oM)-s zqC#;Tpc7yD)30uBf8F^{U;M(s!zT!WFmzoKn85QOq+_WxMGGMSW_h=;d>uCnHKi$W zA_>Ww8Z2&9PVOIi?b?zghMeW=T9HI5A+L{e_y6p(Z?7Jd8V$Ju0zsp zrKN0_syOf&npVA_uBvW;YAq*US|=dhHte>on_g%mhLt6|J5jR?3y6XoAhPY^DM9p1 zRZ|pzQ4o*mmaE!YT~jsP2oSLSfFQ`h)G!$U2;4482m;@CJF4P`L8DfW3JgZT?v%mt zx$17sK_Q}}e;6HiTxO-vPNXu>(TbJ&%z>jbM<0Fjy&D+rOpWLI`}*UlG(pjlB-cw7 z57_{DG{ei$6!PKj)i+Cv7Xz~z0Ib?}2eZkx?lyG0WjHJ^d~j=dxIcGfvM(Boe&eUt z5F)!@ed##;{@t|?=QkLF9qdodPUi?Z#)*+qX=At2e<(DpbYk$iU;dTHAAOJ)Io}KK z1O4}}{2hit0>`^Mi{&JNW6~mn1J%Kh5Ka2F&Cr-ck(yB$8tdbkB#EgOf|k|xT?c0v zjs`|s2>}}d6ZnRuJ64E7%x!8K&Iv5V*?}KIXxg?H__!aE0Cim5@nAnI=a<$g%G)!& zhsKGHf366Uh+~-VI|NCQ7$z|CfzywKoK(B>v4Bjiq2%l8@kc*7J(gWtT^*R*J9Xr2 zy`&J5GVdL(>QlOK<-$9u_sdEeLF>UdIRu zl5|`WOds$hDGKt^PIWMqn8*#(ZSURNYZN3;e;gVg>r2)e%{Q*y-7U8`lH;W`B*M*| zwQ9>3gvj*h07n5a7UTHH)wQkrK>y|UzVgq9ri%g>!|_a7CNT?khZr=FNs%}yr>L39 z;r$bX)8pA{O^avfWNc7v`wSiOEQUL3Um`+Nl%+R07`9qUG$~c{g?L03BT__ygiq;) ze-3d6$2m8!I;O^x3=HrXZh1AeSgno?4XBM;L=w`O9L;cH5R|KpsoA{&K`HId_|(*H zv0m7?W036JCx7GU@e`eD&2jARWlE`7s5T3gItT*CFjx{c%H>9JGa}Lh0|VpJ`}#-r z2x0^o3d0MlmtPdz0!a%5<~KB_)HDT7f7DDbD)3F!!XQ8-ov$eqqx}?5FKt!V3sn@5 zdxz3{#s)c#x_WQ9Tr*15f@>paC9ig#rs}-``Qm)e&;^WfBoIR3PYD> zXa=$Z!x>f`hju(BQXD-p9A_vQGWN)5>{lNCt;Lmjhpfr5fyuGqM!u0ri=2c=f3iJ0 zHOg8JMT4=?QOmGmS=@BI;hdCa{6viD%P_84jfet82(nDrCJ~bdLKNgUd~e^-&#x?! zxEE(!hGIS6U0PW-ETf}pckgbHG#QV}hT4er_Z19y==9laHeK0X!+c+gr-yRGxDZvW zK;*fxk->JS&C(wZuCet%R97TsD znQV7%3XvJthpk2xCP^{w`39L{gnivlW;}XtZFf)qtZ7zoDh51HH7ww}jb>wU*-fNn zhGjN)Dy3Ro*xvF(FD}QLe~k_XNK@mQt#$zX07HhZX9u#$M0|U7apmp}0yz&n`nW)a z+UAEejsp;Q)pfVFD9AjC(GChkfzLz&!|_EPuI6hLPVx-ZU5(+$wR}yC#*ghAzj1H7 zFC8fqi*H=My;ExLo1T6A)Hz6pZ@l%^Mxk=_z`j~XU%Pl|yHpxXe~?*db?UOnfY6S~3Cpno^qchtbZrthI>v4y7HzaP2-=TE zJcNvFU%wO)JDruN6jjvqM2bqu5)2K%TK%!{tSre0o`~kUhU}bUSBye)qiYyf3T=@t}m7M95|OuL>Zd6 zx4L9mAs^}M9~?cje{O7Q^wPDDUwHMxeW0Izm_Z*DL ziA*vvl*>$xPo6pR;DNcL`;VVYq_bW0(7g5P zUnf%Wz_9}^!DMFb{Egj>Tgg}w1(<3&j^*+!-7;;Kf1+aX#O<~Gp~;E8Q>mM)`Q`2A z@qLpKfsRP>wWZ~|8|4H~MI+SO_AbdvoWKb@8MG^LK6+H`me}+&3J* z5A@f5@^#Cw8qJ2~=p^Q}+6~3_04ZXat~NE(;X?%5O~VDI>%)9mX_}qB=-|$FnPi{~ zT(cEef8_>61_OO#J`6lniExrYiyopa9}+AUActabzxzrvxq%_IU7~rZzHHGZM4mG= zm=xt$wXT$!itZ6S$5RBBh)E=lAru&z4IvT)fQ3dpCJv5_Ces7;T4QVZ28S83C`+-# zy_KS^xA%^XKJn1WnH1$4=FNA%b>Q&Pj^@-Ff7PM6rvi*}>pPXr8xtdAO~bNX*AGIP zCfkNf5M(S8`CuWhYu@4g(~fQ3S}TD7PEL*zz>P$tkM3@46dSFMI@&+9lV52m_Soo% z6#%8)TwlFYtCR@}%2CO7?D^Hr`<&73rj3vRQ0N0Fh7rx~022Tnlr5aVa4htF2%2p> ze~HmrQ%6vyZQ|_@ZiUb+Yt@PARFov?aNyQTITCRdM^{ZX&}zUUAO(fUsBP#t(b9CK zKQpAYSKEbhM4HiC28}yy!^_5`VnrPs96U5Nys%j|3^!kDj}D~hkl^Kbi2VG<4bumi zzJ7uLwrMx&3d3B%Y-e-vYAnt-+VI=du)OxuFjrO@gkesym+RuIEdYx-vU9v|4DFTkV_6`Psq9 z%wYC+KJ~~;Z(dp{DbrJjGP%qP7hV}3A97H*ys>y}?toz!0>{Q!GVuIt>V9YRe}Ddm zzqHM!K=Q6j`T)5m?t2uCYz(6n%>ft!K&WVzWrc=q`ays}+R$y56)1`!2!*5)J2fvv zoYqupb<2h^L=q-)TWuv8kC__5@^K&VE4wQg$-9Bx>gbB$=7{L}?dsHcuG%z763g`^ zJEj{DWU*SIIFVvx8zF|GA#|}Ye^8rx_wcoBy`$8$PE%>sOXbSFm90vxarVTq@u5tL zhpt{*URk6ubnelouHD=I`Gr@0`}4ogB!^r}#e4@iHV?ee(aZH3%S+|9ML>$ganz->91bv?!jWmTNytblZIB6OoC&?rkDy9c4fOo+y)YMyoBw7x_><+|X zNRSpzVm_6kNTUX-TX``e2MB93f$2it_9L+23$K7uT} z*=p{U62qDFhYR^9&K=Z_znnYwakbz)>9pI0=G=ugOjAN0isMbW}>)^U*= zc$%tds;L{+t)<Ji^N%n7th&AOJHPXLBqLkZHBwBY z`Z8%~EDaRR#qp3Ja0plkNEf=M%VzqiGoJvQdrB=-}fApGSrxN{bO&{wY z-nW0>*4l;``_;aIkz#c_-5T88Ud95GB1z5EaR^;I;sxM7JNkcq>&w80o`X>=>o|_x zu|4hZ8Fce9Iw8XQzr67b81g6<`xz=!O%hYY# zByAmas=6Dv1X3N1f8E|jrh{>$NC7+uoK(P1M*3g-;07ttQ@OEHy*@cUVcLGnFfyss z^2#m2{OdM+S+QdRKB;iv$D3;pH2Pxv!97a5EmR1T)e}W=(4|H@V7LVTBEc<|P zJa>8|qq)IWp-Pi@NA*vhK0{Ef7vfeBUcYhoF>`|N0v6)6^H2wv}c(v0tTjeLWT@t$uEjSK`G#i}YwT@iVf6y=ttaU5^j zh8T|sbi{B6Dwbi}t`_*3#9I=_yPC%_IoEY4*mgU5ofJe0lOqDO zDT;D`^ONVk{gaQ}6a1=f=H!t$OF~4UVSM>D1|?bB~-q&q7C0mHX`IKY#vDEjKhQjlhC*Dg}Z- zR}gaDe|Ed2d+x+Y+CWaH)e$Mu^ZgJ&3a1zv4?_ndFyHrCiUA<>F%%^ep6-ym)Kt_i zeIhDaejEk~&(;JMkHxZWwQc!<>H;=EQ&iG3?C#efOA_CD<*j&MZttGi5D=M6pQ5zu zm1;B|wM<)4Emn-1rX7tXJEli6!m-1L$PnQKf6VpwF$7dBaA{%fyFYk&GMo6qum5sV zK$@~;tGg(0WjS?gWp{16{Hdp&4m|^yN>R~qg7OenX&G`f?I2I>sL4cnx7J!KbyyM~ z&1JjYSlU89>PttiEo|0W8pVo(LzCC%uWzhvPVPAz;P~doqN>z=&&?O|*KXdrw^p!nakDz}C0cLx?Y~th&CfY7 zFR$EOTV7^kse%6SnVJ1Z4<9{q?AYPiJ)Uj6^7;qg{_zL*IXVcAi2{-&g59mW9Rv}9 zX9&7~VqddTLzv?u9=A~F>#E*>bkuFzBg0WEv})x%?F9~?o4&*ILNcAHb~M8QjBh0q zk%}G3m#a|@;sPaS23%m;o+U;4e+a5O$D#nvaTG<9wy8-{^qEhb_W|(_-~9G(Jol?p zgE>whvi)g6~6>P&_V?9RKYf{cP#>^Q}`xqXK(ynlOsc{!ELV4+y8G<--if&>7lR`V%d7#_%Ff41J{A@120hEtANg-jSqBbJXt%!WQ@*$qUHzU4sxYgMOA zyRk$J00;t0j>ymb>ZhOo=C@yX;l-z)`laqqVmLwI5I=U}GzzfyKfYYr*%morD3?2Y z@Gus5JS$$jc$M+(-+k`MfkadkNr?&^!d3P5VxhIPS>CFu$g@6qe<-teB&{22UsCSg zB$(~yzUf2PHp*m3nGOOJr8X1~k+x+eV-gN4s%Zu|jk63R#KtD3FTDOUi1E?EY-xAf zMYtHr000p1un+)($PNq*CTC9W9e?-o^_O11dgjQ;(+?iH4|FCQ(>1SSbvT+98Bq^m ztJP3ib)FNFG&2;7f9&Y>DhdKfI=WR%e`mysVixi>2CwyDa==pX&TAN=rze|q8j z-(5X4_vEAJIGWLP{rK?{O|`kax(t(jCr_R-)J`nMeRy?&e`Tn><9je$vGfLs(JiyB zx0UtnV!mESwmB#Z$M%hm#|c%}agx4r=l0h2#^Wc>U^q!ojOEdukCCj1!8*^egzAKV zbiBZY4CIp_2nm*-*>m`(7v8i1Mvz3U(KcL2FcJWOA$Wwr8@E4RUAS_5&(y)`!B0PZ zYJ6DC>S!3u#tsJ zg5zMGWsnpG0-wNfF95EmgU~Z|han<91!&RpLMQBK$b}NiLj(aHV8G!>Ho*x`&W^P+ z{M)xz-}!ju(NhOmuZq0xLekTZotqsWe)Zy|p9b*hf5*?ezJ}S>=;%bLQe!x=T&gEz zzSY(x0_>U2i8Pjt4;rlPXhfIMeY4FV4U3}P@0y5rrtR{;#-LFhpodY%tJ2qA_;qC4n$4vvHF2nHa8c=u|oI}reg zK_4NBp^^-PeAh?Fas%5#Jj-wljk=2hoFGU9f3Rk=#R`0P+;Lop0pIg!nr0b>CPDW} zKrHl3hy`JYnTBS&b{GN*hd7RfVQ9O)hl1|AE_3%mK)^8!4g>uEMYSzocY$rYvMBhz z*PYW+O@NcOZ3BSia=GqBVSr3G*xlU`Igy|l%QiCuBbCao*=Z*;InQ%p;QF3pTTTeN zIYdBUDV||yn&|zTwD-5&9(1oBbPu`*-6QBh_n>?Ap#Q(^{{lU?S>*vYS4IE;002ov JPDHLkV1i7=kkkMG delta 8280 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 CF4mL) delta 1484 zcmV;-1vC2Y3*HNmB!8tzL_t(|+U?p|Pa9Vl2H^aP-d62R`wuGhruV(5)T-)5rS=!( zB2g2iiyEa(ONz2Ikdm!gAY_3O!eSr>vp5j2F-ri0jR)J<*v9)Zp6$%cnREJysa%KU zCMB&@eXmBN_soDTJ&X=#q;rI0{*@d-0)$3r3_>F`LSqmbp?@*|1@vrOdyyg%VM9T= zqFMY^qvGsnfWraEaH&PeWIgKLGlcG~y$X z{S7&9izmMh`on4!hCVrcEa7~vZ1TF8oL*O+tkQ6iRZc4xUhfnARztMbKWRaJ za7a1rEI|3F_2b&41KQt8FUA4^F>wT4rUtkC{CS z^f`YyB9pEuT~M6b!W@b60}@lz#&D=MJf0ld=evkIK*LZjXf3Z7`klt8tTX%gxHaWZ z>u_;YP&!^zo2JSZ+XuR3y8OGh(!6uG7Jot6k$=)=PT_TNs6U7-YjC1-;xfCJs0S`)SbCOv$4$4&wY|_jb<0(a4wRybW!64t zOVt3HSQcRvjuYF1_H{22e0s=htU`C_69Kxx0h%Na|v#`rGi9F?vYJunQY z#VbjFJ@l!d|5-URDW;~Rtz+ALaB;)T+?Gey&G>gxi^8=5eKbe~xPFOThI9LqUrE3^ZB5zc4ZN~r>c^4O~k zE@_7bO)dV$nQ(3R;jN@6rd5dr^NFZg#AJDy_O41N9XYuBXmtkgOOcmG~dOayW+f7V`g z%!Z3YOKDhjn3Ta|J{2mYN@_vUohuobSXL9_)8%wlB)Vx$3k8^7w#!=ms$+(3_>F` mLSqmbp)m-J&${JM8c` z;gUswD6@8jRq!m_y1`ZOQq(^19`WAV1KtBZTljE|IBTQBg404J!wREBh6PrI3Ky7V zDx6_9QDK4If5NyNJYH_cGt5RRyumC{;TB+}TzG+zu)oF8fRzoawP7~gR%6ga*j}^L z9&p=&F=sa1N&=2VJDxBlPmlL1sb7l@JM6H-M|b}(n^{TyPj~VD5d1St!AW&c(+zzwbWODLg&TSm922J=L!3}1nOiKb zTfMDmw)EdU-k;>2tXzASoUK0M_eGxPSB{ns_oUD7$pw)`&41D320s+TF62w(ia2`l zI4L3MpM=oF+=%NI2}uMHn-Ch_i<~|uLd-)FJ_W%#BoT0`23JrZUS=UePT`Iwl`_gE zLkhJQ$x7#B)#Y(wZ0Lo7pR#16J0G|cu|Vz&n&p0hefv(5*@*%SDr4G-H`s&_ zmnSdbITSO4Jc;o~mX_Ak8i0d(^#efFs8MGJp#7;DyxA@!j+}u=qAwES5ONbUNIk|8 zOAyiwK!4xAlQ+Pf#2}yNA$9g4B!=>FCru$u7{6-`@;tzKS~=js1&0vct1=v6k(ffZ zV0_pzBtXueW%qWS zD85CB5E?-=gYfufVhb|fR(>FTx&t0b8A7BT(0>&F<-J;HzfyFILTEVN4DuUZXC2b^ zte%QIh=KIJ5v|5LF*Xt?EL4+kMuPI|E z!1o4_bIZyvsP)Gf?`(%KBWGFL_~{JtCDhyi39Zpv*uH{qq1Z9tr+ zkbk<@>_DUypyx9KNIlNq@D>1}NQf(}#f`$eUaLwc$np0f}R!>XYiu+VGS7+6&N6 zlq|>U9h z(8|4XF8=*3Ck(^z#{8+I^U7x`S|_UhD>M2vuY*%`sE32*<>@5l7BRr$x3PY>gj7~q9wx*NhP%LXk+)_tVHgg6GBdp z{#!pXMFy!ifav)jvd|DeKRZl5)uk;8>tEprG7K>ZyAUA~@J0HPj6>)%#DK^&ghzTj zBFhjCA%Bb)2_Nr39)=-2a`5`x5n~q;MKM~0{r{DRp&Yvq(Gf3Hu?rzlAAg;Pz1q0_ zulDXewy7$P19-o;we4iJ8%Be&*f|%)I4Q(v=8SAcbTcdKP>=S`G2vVPZy(&-z&_1#BH$WUYdn^gwz^*`s{vp z{n&OOTVVh79aF&3F#| zJ|#$x>uN%FFLIi(gR#J}yqhs^l8{&OlAs;vsAi@ZV?=Fyd|& zCygVJS(rMc8OTrp@)B@7R@srGI^s0VL3&))de-r(lyfXDBv16~UyEjP;j$9;zntpFP0KuYl#;|bmZbGOo0tZJq+t$!e)6pTb_j04HS5*E!8;2)*O zK@E3l2Kf_c3Z5Sx9O&=sV`Cv9E1qM~tOj-}fmB8y`*SpdYz00WXC3JRo@CK%0veQ@ zG%k!lx+*n;>;T>z2f}Q~WzlQ|d=rE0lT#JlSZM}n1#SzC1Gx{jUdHnbY>M_H>OqzQ z-dJUZQ-2>=`XOft&c`J4s5~Cr`X{ z z)9kjS>(Y3YZ?Y!aldYyza`8zP0Lub-kyvg@y1|xYdh4*!l5C%5HZwWf-FEZLv}LE+ z#1gJ(=Besr{b^E#rl%+;|2i08<0SBRC!2WM_w#t=e>_6p`9KhY5QHEEAqYVTLJ)!w vgeo8eAqYVTLJ)!wgdhYV2tf!!5D{_#d#rD1uy(vO00000NkvXXu0mjfiot-D delta 2403 zcmaKrYdjMQ1IEYPbD5cGscbXm5+cg$ZkycMP`QLySnikHDu#x2#O9K7V!hMo$imzs zEF_lX7E$i^q;eYNTf&FWA0kMK0mnuE zTK(D57!ziDb_E}~LLwMELG% zbuy~&#Bn3BoHXLXD3L1Hx3mJyTE7?(QlzyXPuoVfpf$&59tCC5KJOw2H#MzaXWkrH z-Cj!Lu~x_v;`vH;1R04kz7;sk+;T;>;UnlRs*So{C}x`uDh3^!p-l;#!}Mca1mru7 zQ1a0sBF^sc#CX!$o^vkmYYpuAHVD3e0^H81+2W7TAn`P0T0>=QD} zQERfmKSriS*W8*7$WhN9A1Cfnu+$V3Q9H^~llSz(c2KCr0QV5m)69SKVu!Qt0|dx? zF}Dg-l|EgP13NeK5ri?}rcqY$T@rlB9_ECnXMTu)G;GWna8Eh#!HJZEa`rc9IUN<9 z>0dG9PQmi*E8($6dqXX49j0Bc9M zaU?N$u|-O!bpf!@L>VP>=Y^0QA!Xk`EIvifXPg6ysbEY9raq8mVSAf>0W?TpuKu}> z%%MSn8M$-G6gevqh8xsQImm+}O=&1^aewh^MhilGgvH?TUEb*wJyN3R+-Bn8I2aTm zo@gH_xCZ>l0Ae%{7P|}eDwb6IRP29@5uR8lpxdBuB47$eW<8S9SQ>%rBQe!v8UCpb zcvW9#xpe**AXD5?3BZh4WM0gCs+;^fsYUd;$szP*R+p1ARbL7KJg+NR<5_%55+7?f z>9I~G_}m2SP>bZ&bd&jV0E~s)NvYk z2K-$ zO3mlWGQ0ICShYXqRJzcUximj9+T}Lgc$JCtZbBM#c4MP47(R~ymxQ|rCJfufNKWkyJplHe5 zssSnC&SwgGpx(5B+KNXr8ge?*m0HtRoH~s%&((xebmK{vuB|oARE9mheDFFE18oZQ zb}nWZ$Q*7IXPP5)%;-yr(PHSqo@y3ZpBk&CDL;yolf;>d4%I{ zg+=AF?X%Z?hpHF8svDFGmXW;{118Nv1-ltXyFeMIDF{hFG3+nA1J74xeRIpC%zQxJ zh%tPL=ydddH9d?5;+_ytDWn;sRjvSwx|Gt)%oRX}=gbuXkcChWcsD46j1sML0`urd zQJiL;04vGug>}JCk*L;yl!8yz1;;(il8Y0tSAT`%xqxUw^vfL8-dHUf3v$mkYGE>{?+Q=*ssW&n0bpb;%_qG*h`SQi1f*fY23jWIbfW)7K#YC z0Ta|KjI^1ovu3k@zf{P9yv+!Zzc+SoS%zz^@FO0pT0^6@$pGbJ4hW7V6Ln{X>Hl5n z6}TE6q2LEVzQ)R$6xn;sk-H!KJrwb1KKi;18vh5Ls+NHMOW0~%kWs4N2rbFq z3MVKPZx-*n!cD({JO%d1R>OPHDW|C5EDV(Rl`aj}5vQ-F%GJI~&nv?o2-)QU*Rma9 z%{pszSBo!uX?F*tG4~Uu9eAzF<1_BNQ6F9CKYmN+k!y)(={JW$PhNf8H;if5d92dA zh}nz%>+|9ErZGNd#IOvSnFp-!Of>U?@F8dA2yi8t?({aw>J~Ym1dt^nh5a;4Jhr4@ z|L#7o)DR-dr%fl@_1{cs%s>>6o%yr9!Q}CV@xv3Qbx=|^u;K+4B{2n4mjn|vUBbZx!DY*maC7v?l@UWzpe78WgI}s zsh%bYpt+P)S&*L)cpV|XOd!Vrh(Oz^k3cPoVW{XV{}V_((GlS(2ty=Vf$;)$13Zw( zS3X1od_(Ixt8`LdvOnNzxguP2doe*JE@>_dyzF;7iigvK+o$eBs+5{@wzzI%4N7W^ z2wMf&B)~1J8`|G}lEkN_dgCEqy_T{ELD3eF46*~%_lu+cQs{=U20kGH?{Ij6BUDp; z6UpyYerLc;T)%95=U{u9&9={Rr0krnU_VMsq3*E#d}p!Two0a?65&HC1BX4P^+Q-Y z=);Bg{u)n>M!eUg*KZ^xJO`vCwQy}KYyb3Kbg1Xv81;9K*Q$AM6Y+a*fLB5Lj{cj< zq02qe!;1U*MOESY-!=MP%}VcHE82U${kDJoKWh9nXOo`oTUY+yuK)QURq_GQR3N6+ T7#wK{_$6B_CtNk=O4|Pbcz0U| diff --git a/tests/ref/transform-scale-relative-sizing.png b/tests/ref/transform-scale-relative-sizing.png index c53243c4b17180620f74b10ac540cd97cc5e6737..01f0878b308d10c5d505efa9aa0e635849e3891c 100644 GIT binary patch delta 1885 zcmV-j2cr1r5AqL?B!9U{L_t(|+U?qVa8%_P$MOBWNw_3wE|iOdB9~fVEK0aoq}nDH zl@=;WiM1ovO$i7HLP!Jxg=Aom0bwSk)3H>eu`<$9kOd?o_g&=b1ZOGNObl$NB`hH( zO9&>!kori_v+$|2#8$nVrwP=gfJ}nYHjcl7B@Y0uhKn1oEwcoU37D z*NVTzkYj1T>ZP`lb<18F7XEmLgg8=OnK|p#8L4xrTb(j>#;ob7)(_dX<5MDm-6S)H zD>arOD7pJbfkDo#P6E(%Q7(1cmiGeiqm_qvd#jal=;?`36BzsofRTYg$eMw`JM?xG zh`ho(+p5$N>VF2PNeq4)fapLWoCrAStyWcHM_cIIY~CsWlgZBCAWplfh!o0;iG*;G(*lt#NW`< z3U$&1;(z{LcnNhIfh=9tb+gg#MB!yN zl?LR~9O8+!XhELE%EpmLdiu>ZU6CV?h6wUDkmu(*a<%0Gwid9NK=wI}{=CMidvR~$ z_{Y>`?=5`Kcy-Q;bC&HdXX{F)g$S|*$npbeh{O|YY8_C-JFB_ozw0z91P~`M$Pa`= z@qhhlHuif&)_06M#Fv0C=&K8c>rvYW*uj=;fW3Je|BN2Xfw$S%gGgn^fQ$h?rMGh6 z9_n&|__H)B9e&F4nD-LeS>YYvU#v{PbZ<2ghsm=#!RDbvgVsonKPKesUz}v0VfWROhghQu(K-~ld zU05c`x~!hcXxI*tuPGR*tl~~e7fqvEYlnx$KiXsL1qPwAXk`ry@>NM`d8Mb!?WV5G zUFNB*xO({-b+u00?n~5p+-29UmANZ#m6TRgmY0@ze|~#G4TDp;qd^|f2}B?Q5tE|_ zAb+{*kP^q5@t={radPAIvL$l1UHo$1R{7Jwev&nt!!Vw~uQUpi1A~zENTr^sV{_?-BbRsf zPv8)BeK3*1?<1gRpb$O<9HzGlpf^qW7=JiI-9SuY@Y@V@4;bSIA)WH1dN1IdR!nj2E&e>Y-}#aHp_$O(D*nF?&faS5a><2&JksL4Sa> zsTorHfci|&G3f3wgN(!u>SjS5WZ*4UUj_rR^^mXVX2=>OQI`$uF@bEv{SDN8p)jn0 z0k^I*Lv)S|!`Mah=gppxpww0q$Qb;ZP5lJ8$mXgqwe1Sq+u!dE5u_qaO`J7z`n0K2 zQc{XcAou$AMyY`B*gBY*GBdZRD1UcmO2+o0qM}vvOWDr$C4>lK1Gf0Nj$BvD3&~b$ z6Pr6xl^EwC>9km>OLnuJoqQrhkX+!8ejqoL8po#AD|Ms=Vz*?|=vj;QqxEoyqjM=++JEWYA`fah z%O^mc@NS+TWGjeW6P~kM4NrF7`MU~vR;kbF?WR%>c=yB;8mK!^Msg$Y33a{kq#ma` z;g@wZ`Z7xGX${d!Vw+2sCEJt8_Ct=X3-~&6y}wfH=xv`;U0pQ#cNFkK1zEXp@D-Ao zz$zM*bVk1#8oi+~v8wTn-+!?+#2};%^^n%M=j`o=TnJNR1A{mhj8IBNOkYdghO{`P zRR4_KB#-t0qJKO<{p=F?@v9L)mpS_?G9LzzltrVxeG#9z<+X^6>b4MCEq0Qw*hID; za!9EmfkBSVo4;u39~Ne0QTLbh1&fz1UXX61(N}++{MeIedug;OV=>{9B@5GcT%9+6 z@zO=Fr&CwEKlAYyRyx_L(|a8b$IiDLy4;-($F3^2vlZhH%X2N0aR*+Lt_Kwu@!$Ud XQ#OI1#c6W300000NkvXXu0mjf$IPlR delta 1907 zcmV-(2aNdg59be%B!8|+L_t(|+U?qFaFo>@$MOB0BwUg-7s|y!kxMO5ixMstDYjV) zN(%)|iM3O#O9><>AR*i-WJd-a5XO|gXszJH%0P#LETWl~n;Yalp|g~0CI+_C5|$8? zB_t-qko@{QyV*?wPj@o2`)G9j-#2IW^5!$=oH=KoSqr}*S$_l~5P=9pAYU8E#d`X> zS^71GoXGxpAGMooeD#G9(NABkL03} zN=>8*rT2V4JjliMRsh{sY@yzsHGKel|MerhyY)&HQp=m5rZVys0i(l%koALs%?xxL zh`r8x+pW|w>VFMV(-`@-0`cKOm!>0#=#_ z*^@DB%#x4)FXZg(N0Y4U&eIrD=(40xYk|cWl%z^M9@AGU{yya*ZLhTim48nP??#4l+N{OS&v}(rHQc?rslxP1T#Uw0?ygva!Bq z74VS(WFAuK>vj*s-ZF;hQ<7(AzQyTIB{?9KTB#Nt;<$ThZBC*|kY4B#qx|hLfTZA; z^fwLomobEPi!+sEf2++!GSR>JeGuo}kUifu34d}Hc+Y*Mw50gr`3p_=LYm|7BK>6p z1vG|u6D_(R-(sV0$S1u678uURvByn&;)ksl7X8)Re44m26wtY?C0nfZPy}=2$#Qf17|3-dp|6&>8G2Qy?y2NC*grWwCnO_PB5 zwTE}*_O+9&eGK@?ZIbUO)%7s7iXTH_UVjZeDRFxle*2Uf6dvTgXy~c$P;aV&Uet1u zjk&#*)`-0%=PPKb+|n*eFP>U=Hja!+e!SPj%Zxx3@yZq+c+L3)N61#_Fbi(*HdxxW`(EvPFZUh*2=RprybA(HhwqcE9~uRIRZ!-J5{Sf!>kvZWl&v4!{c zH{b~M`XP;x?|qt{q}fql9j7s* z^rvcAdY|r-iW!NXMnNPXr>_M|2tfA7JyJ&=?Gp8lA;i@?;Q+}sD(;VrKpEe+$)29WJ|u$g+FDvW4m z*lp@cllI66OkA;K;k-F1O6@j)jK?qN?}xw@wp4eueb?F1{(8Sjkg6z^Hut5h*|TQO zm{DQ?xj%R}$^e3E>-h9B6V{cKtbdy@CT(j;Ny+?ipnI}zxYyo~B0#d8gB>HX>Q)GSQ{wHfd%VL>d1y+;kXgWg7%V}JrIsfeUCY_pM}L7U@(}+n zp9*z4aCv^1Z6Hpa@T}9OdF=U@zEsFFO8t|8ZY%Xr;7&ZXnR*{rklYUZoqBySfm-Lf z;Af50`YcZEZwt|<#Ib;Gi`|>bjzdmt3i}+nHBhNd40KSb?rv)RI}UihimYBX^tx{W zVI8%~x?(^*wO&(5tMPsC8-KQiXoT$HUecEIth3{g%Ta1#co5gpQA(+ptc}#$nw_M* z8n|d5$>Y6%_#X^XKfX$S_;L);?UjR7%O3?`&860X{zzWFYkth4y7my-EKZW{#8h@1 zazv?N;XzI;T(V;I@0KmfrQV-%mabgAa%ql(TA%-U`nU<%2dK4U(JHc4tCr>Ly|Hk~ z%GE0t=TNWV(DEmqf8E74ojXw2u7A|9gRPx>RGw=IlfefWlg|eWlg|eWlg|eWlg|eW tlg|eWlg|eWlg|eWlg|eW84*b6{s)Vfd})l49_s)A002ovPDHLkV1jHjtrh?P diff --git a/tests/ref/transform-skew-relative-sizing.png b/tests/ref/transform-skew-relative-sizing.png index af44fee98ebe4ec0ab502ad9b08c5c293bcf19bd..4453a4811cdbbadf2778ec26d94a057f2769dd15 100644 GIT binary patch delta 811 zcmV+`1JwMy2EqoAB!3A>L_t(|+U?eFNE2Zk2XNoi3t!c{gh0z63hH`Q3fgHZA(g)H z4~2@uvZi6POfwW$iJGWrC`&NC$ZWOJk;+7ZP7yW7+>??m!*(L6%gSqvl_K-ov0eJc zp=S|6_l4)UA3wOy{dw;0@xfn+kBrEOjBFm6dN}>{*@##SbbqZKUNX{5#ZRC!INIc{ zJ_Nj)3vCoz2}*^bMRB^-a8FTe_?4C`8A=z}Q<4ybJOz+cAy$z_sP2pVea#dBVi`cs zVl|Xrht|W=kmCjb=QV)i#U~)jh-?AjI_v;S$7urIpMX;Xq=yqWgm7$F>8vSHB67B( zq9RBGTY@#FOMgV3WW9@N1K!sXL7)Xm1&-#`*ra_V7PTVyS z*);>xZUBw1iEwP;S_C8jU=&cR0FG)jS8nchMLY)JL0nrFVfO{_tq_D(5kjkZsH)x1 zhvETse!++|B(xPkN)Wqt$5b)8NZsli>9F-%>a(7#M2+iyD6uGQ{mlQ}?t(>eJ?4R@?u79mdnDh0tRW3s78DW%od-m-cK%zPU zoU21zApk@S;ZhU)EtuTrY4_=%Kcohs^KNc_3Y&JM&%<&cW_r+1(>OT3v*=@RvZFnG zFs~Dyl=hAmhmVfx!fU*oa`e!;h{ZfS5370DhA*rQlV;P5dDwyPy*7whsA~|@zl^mq pwj?NjAZ0{GWJE?}L`MD*fGUSq5jnV*jB z%qI>#iwL?m{9s>xu-CTR9X~((h5TehMr35?$ZX5p*B7JWFn`dsdAwvqN5xLT7!qyq zOdkS=7Q>pvtvIE^#3(}ardmaoX+0)hndSz2MG}J0=K$i%#Ytg?`Z2uqF%$um3}9ff z5vA9o?Tj?!f(gJ)E#Q3h35YTyQ$V;*2Y`|Znt%@=;4}c~;Q&Gi$A*_ISmGoi*UQVx zgSD_?}-dp%C0ul%?$CKmSQ~)EN~FgF4xH;ldOdNRa&Tbg?uzw%{EO?3qM}tX%@UlfG3DF%> zPQgf>x7ed%i*4P2by3xN-;XY9+_(SG@*lk8 zodcUUCO^AmL`G!fe?#~ma5*+6PHf@#5M6fO6pVLrEuQK1-u`z%4OmZ8TD_IQo?BkYxs3q!74myhiC#`kl1#gNI2OB zQg5@_;Ur6H8HjsOIyr4615!q0 jL>@+DL`G!fA3=TsZv0-svFpJf00000NkvXXu0mjfcnNnS diff --git a/tests/suite/layout/container.typ b/tests/suite/layout/container.typ index f15ddfe4a..489c88925 100644 --- a/tests/suite/layout/container.typ +++ b/tests/suite/layout/container.typ @@ -325,3 +325,10 @@ b a #block(height: -25pt)[b] c + +--- issue-6267-clip-anti-alias --- +#block( + clip: true, + radius: 100%, + rect(fill: gray, height: 1cm, width: 1cm), +) From 4534167656f34ea2a459fcc526ba41c6a89a5314 Mon Sep 17 00:00:00 2001 From: Robin Date: Wed, 9 Jul 2025 14:02:50 +0200 Subject: [PATCH 06/18] Use "displayed" instead of "repeated" to avoid ambiguity in numbering docs (#6565) --- crates/typst-library/src/model/numbering.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/typst-library/src/model/numbering.rs b/crates/typst-library/src/model/numbering.rs index 236ced361..449dfdb33 100644 --- a/crates/typst-library/src/model/numbering.rs +++ b/crates/typst-library/src/model/numbering.rs @@ -18,7 +18,7 @@ use crate::foundations::{cast, func, Context, Func, Str, Value}; /// /// A numbering pattern consists of counting symbols, for which the actual /// number is substituted, their prefixes, and one suffix. The prefixes and the -/// suffix are repeated as-is. +/// suffix are displayed as-is. /// /// # Example /// ```example @@ -66,10 +66,10 @@ pub fn numbering( /// items, the number is represented using repeated symbols. /// /// **Suffixes** are all characters after the last counting symbol. They are - /// repeated as-is at the end of any rendered number. + /// displayed as-is at the end of any rendered number. /// /// **Prefixes** are all characters that are neither counting symbols nor - /// suffixes. They are repeated as-is at in front of their rendered + /// suffixes. They are displayed as-is at in front of their rendered /// equivalent of their counting symbol. /// /// This parameter can also be an arbitrary function that gets each number From 9e6adb6f4577a7bfdd119163168e8c6902bd1b21 Mon Sep 17 00:00:00 2001 From: frozolotl <44589151+frozolotl@users.noreply.github.com> Date: Wed, 9 Jul 2025 14:04:22 +0200 Subject: [PATCH 07/18] Ignore spans when checking for RawElem equality (#6560) --- crates/typst-library/src/text/raw.rs | 22 +++++++++++++++++++++- tests/suite/text/raw.typ | 5 +++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/crates/typst-library/src/text/raw.rs b/crates/typst-library/src/text/raw.rs index 8cddfbfb5..0e61a8ef1 100644 --- a/crates/typst-library/src/text/raw.rs +++ b/crates/typst-library/src/text/raw.rs @@ -456,7 +456,11 @@ impl PlainText for Packed { } /// The content of the raw text. -#[derive(Debug, Clone, Hash, PartialEq)] +#[derive(Debug, Clone, Hash)] +#[allow( + clippy::derived_hash_with_manual_eq, + reason = "https://github.com/typst/typst/pull/6560#issuecomment-3045393640" +)] pub enum RawContent { /// From a string. Text(EcoString), @@ -481,6 +485,22 @@ impl RawContent { } } +impl PartialEq for RawContent { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (RawContent::Text(a), RawContent::Text(b)) => a == b, + (lines @ RawContent::Lines(_), RawContent::Text(text)) + | (RawContent::Text(text), lines @ RawContent::Lines(_)) => { + *text == lines.get() + } + (RawContent::Lines(a), RawContent::Lines(b)) => Iterator::eq( + a.iter().map(|(line, _)| line), + b.iter().map(|(line, _)| line), + ), + } + } +} + cast! { RawContent, self => self.get().into_value(), diff --git a/tests/suite/text/raw.typ b/tests/suite/text/raw.typ index a7f58a8d0..827edaf8c 100644 --- a/tests/suite/text/raw.typ +++ b/tests/suite/text/raw.typ @@ -687,6 +687,11 @@ a b c -------------------- #let hi = "你好world" ``` +--- issue-6559-equality-between-raws --- + +#test(`foo`, `foo`) +#assert.ne(`foo`, `bar`) + --- raw-theme-set-to-auto --- ```typ #let hi = "Hello World" From 1dc4c248d1022dc9f3b6e3e899857404f6c680a1 Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Wed, 9 Jul 2025 13:10:24 +0100 Subject: [PATCH 08/18] Add `default` argument for `str.first` and `str.last` (#6554) Co-authored-by: Laurenz --- crates/typst-library/src/foundations/str.rs | 24 +++++++++++++++++---- tests/suite/foundations/str.typ | 4 ++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/crates/typst-library/src/foundations/str.rs b/crates/typst-library/src/foundations/str.rs index 23a1bd4cf..e500b1a4d 100644 --- a/crates/typst-library/src/foundations/str.rs +++ b/crates/typst-library/src/foundations/str.rs @@ -179,24 +179,40 @@ impl Str { } /// Extracts the first grapheme cluster of the string. - /// Fails with an error if the string is empty. + /// + /// Returns the provided default value if the string is empty or fails with + /// an error if no default value was specified. #[func] - pub fn first(&self) -> StrResult { + pub fn first( + &self, + /// A default value to return if the string is empty. + #[named] + default: Option, + ) -> StrResult { self.0 .graphemes(true) .next() .map(Into::into) + .or(default) .ok_or_else(string_is_empty) } /// Extracts the last grapheme cluster of the string. - /// Fails with an error if the string is empty. + /// + /// Returns the provided default value if the string is empty or fails with + /// an error if no default value was specified. #[func] - pub fn last(&self) -> StrResult { + pub fn last( + &self, + /// A default value to return if the string is empty. + #[named] + default: Option, + ) -> StrResult { self.0 .graphemes(true) .next_back() .map(Into::into) + .or(default) .ok_or_else(string_is_empty) } diff --git a/tests/suite/foundations/str.typ b/tests/suite/foundations/str.typ index 66fb912c0..aeaa0a0af 100644 --- a/tests/suite/foundations/str.typ +++ b/tests/suite/foundations/str.typ @@ -103,6 +103,10 @@ #test("Hello".last(), "o") #test("🏳️‍🌈A🏳️‍⚧️".first(), "🏳️‍🌈") #test("🏳️‍🌈A🏳️‍⚧️".last(), "🏳️‍⚧️") +#test("hey".first(default: "d"), "h") +#test("".first(default: "d"), "d") +#test("hey".last(default: "d"), "y") +#test("".last(default: "d"), "d") --- string-first-empty --- // Error: 2-12 string is empty From 1bbb58c43f6f5d94be4b2609728039a9922c75f8 Mon Sep 17 00:00:00 2001 From: Jassiel Ovando Date: Wed, 9 Jul 2025 08:41:40 -0400 Subject: [PATCH 09/18] Add completions subcommand (#6568) --- crates/typst-cli/Cargo.toml | 1 + crates/typst-cli/src/args.rs | 12 ++++++++++++ crates/typst-cli/src/completions.rs | 13 +++++++++++++ crates/typst-cli/src/main.rs | 2 ++ 4 files changed, 28 insertions(+) create mode 100644 crates/typst-cli/src/completions.rs diff --git a/crates/typst-cli/Cargo.toml b/crates/typst-cli/Cargo.toml index 7e9b93f93..792cabae1 100644 --- a/crates/typst-cli/Cargo.toml +++ b/crates/typst-cli/Cargo.toml @@ -29,6 +29,7 @@ typst-svg = { workspace = true } typst-timing = { workspace = true } chrono = { workspace = true } clap = { workspace = true } +clap_complete = { workspace = true } codespan-reporting = { workspace = true } color-print = { workspace = true } comemo = { workspace = true } diff --git a/crates/typst-cli/src/args.rs b/crates/typst-cli/src/args.rs index c3fd541ad..7459be0f2 100644 --- a/crates/typst-cli/src/args.rs +++ b/crates/typst-cli/src/args.rs @@ -7,6 +7,7 @@ use std::str::FromStr; use chrono::{DateTime, Utc}; use clap::builder::{TypedValueParser, ValueParser}; use clap::{ArgAction, Args, ColorChoice, Parser, Subcommand, ValueEnum, ValueHint}; +use clap_complete::Shell; use semver::Version; /// The character typically used to separate path components @@ -81,6 +82,9 @@ pub enum Command { /// Self update the Typst CLI. #[cfg_attr(not(feature = "self-update"), clap(hide = true))] Update(UpdateCommand), + + /// Generates shell completion scripts. + Completions(CompletionsCommand), } /// Compiles an input file into a supported output format. @@ -198,6 +202,14 @@ pub struct UpdateCommand { pub backup_path: Option, } +/// Generates shell completion scripts. +#[derive(Debug, Clone, Parser)] +pub struct CompletionsCommand { + /// The shell to generate completions for. + #[arg(value_enum)] + pub shell: Shell, +} + /// Arguments for compilation and watching. #[derive(Debug, Clone, Args)] pub struct CompileArgs { diff --git a/crates/typst-cli/src/completions.rs b/crates/typst-cli/src/completions.rs new file mode 100644 index 000000000..51e7db103 --- /dev/null +++ b/crates/typst-cli/src/completions.rs @@ -0,0 +1,13 @@ +use std::io::stdout; + +use clap::CommandFactory; +use clap_complete::generate; + +use crate::args::{CliArguments, CompletionsCommand}; + +/// Execute the completions command. +pub fn completions(command: &CompletionsCommand) { + let mut cmd = CliArguments::command(); + let bin_name = cmd.get_name().to_string(); + generate(command.shell, &mut cmd, bin_name, &mut stdout()); +} diff --git a/crates/typst-cli/src/main.rs b/crates/typst-cli/src/main.rs index 14f8a665d..6a3b337d8 100644 --- a/crates/typst-cli/src/main.rs +++ b/crates/typst-cli/src/main.rs @@ -1,5 +1,6 @@ mod args; mod compile; +mod completions; mod download; mod fonts; mod greet; @@ -71,6 +72,7 @@ fn dispatch() -> HintedStrResult<()> { Command::Query(command) => crate::query::query(command)?, Command::Fonts(command) => crate::fonts::fonts(command), Command::Update(command) => crate::update::update(command)?, + Command::Completions(command) => crate::completions::completions(command), } Ok(()) From eed34070513e6060f86f86b56ba7071507fe811b Mon Sep 17 00:00:00 2001 From: Robin Date: Wed, 9 Jul 2025 14:44:42 +0200 Subject: [PATCH 10/18] Update Swedish translations based on defaults used for LaTeX and cleveref (#6519) --- crates/typst-library/translations/sv.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/typst-library/translations/sv.txt b/crates/typst-library/translations/sv.txt index 20cea6f96..538f466b0 100644 --- a/crates/typst-library/translations/sv.txt +++ b/crates/typst-library/translations/sv.txt @@ -1,8 +1,8 @@ figure = Figur table = Tabell equation = Ekvation -bibliography = Bibliografi -heading = Kapitel +bibliography = Referenser +heading = Avsnitt outline = Innehåll -raw = Listing +raw = Kodlistning page = sida From f9b01f595dba96044c725a97ecb4972bec7d57ed Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 9 Jul 2025 13:08:49 +0000 Subject: [PATCH 11/18] Move math styling to codex and add `math.scr` (#6309) --- Cargo.lock | 5 +- Cargo.toml | 4 +- crates/typst-layout/Cargo.toml | 1 + crates/typst-layout/src/math/text.rs | 254 +++------------------ crates/typst-library/src/math/equation.rs | 7 +- crates/typst-library/src/math/mod.rs | 1 + crates/typst-library/src/math/style.rs | 68 +++--- docs/reference/groups.yml | 2 +- tests/ref/math-style-fallback.png | Bin 0 -> 935 bytes tests/ref/math-style-hebrew-exceptions.png | Bin 296 -> 489 bytes tests/ref/math-style-script.png | Bin 0 -> 585 bytes tests/suite/math/style.typ | 21 +- 12 files changed, 94 insertions(+), 269 deletions(-) create mode 100644 tests/ref/math-style-fallback.png create mode 100644 tests/ref/math-style-script.png diff --git a/Cargo.lock b/Cargo.lock index 5526da48c..1893f89fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -413,7 +413,7 @@ dependencies = [ [[package]] name = "codex" version = "0.1.1" -source = "git+https://github.com/typst/codex?rev=a5428cb#a5428cb9c81a41354d44b44dbd5a16a710bbd928" +source = "git+https://github.com/typst/codex?rev=9ac86f9#9ac86f96af5b89fce555e6bba8b6d1ac7b44ef00" [[package]] name = "color-print" @@ -2861,7 +2861,7 @@ dependencies = [ [[package]] name = "typst-assets" version = "0.13.1" -source = "git+https://github.com/typst/typst-assets?rev=c1089b4#c1089b46c461bdde579c55caa941a3cc7dec3e8a" +source = "git+https://github.com/typst/typst-assets?rev=edf0d64#edf0d648376e29738a05a933af9ea99bb81557b1" [[package]] name = "typst-cli" @@ -3032,6 +3032,7 @@ version = "0.13.1" dependencies = [ "az", "bumpalo", + "codex", "comemo", "ecow", "hypher", diff --git a/Cargo.toml b/Cargo.toml index 6cc59ee89..9657f207f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ typst-svg = { path = "crates/typst-svg", version = "0.13.1" } typst-syntax = { path = "crates/typst-syntax", version = "0.13.1" } typst-timing = { path = "crates/typst-timing", version = "0.13.1" } typst-utils = { path = "crates/typst-utils", version = "0.13.1" } -typst-assets = { git = "https://github.com/typst/typst-assets", rev = "c1089b4" } +typst-assets = { git = "https://github.com/typst/typst-assets", rev = "edf0d64" } typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "bfa947f" } arrayvec = "0.7.4" az = "1.2" @@ -47,7 +47,7 @@ clap = { version = "4.4", features = ["derive", "env", "wrap_help"] } clap_complete = "4.2.1" clap_mangen = "0.2.10" codespan-reporting = "0.11" -codex = { git = "https://github.com/typst/codex", rev = "a5428cb" } +codex = { git = "https://github.com/typst/codex", rev = "9ac86f9" } color-print = "0.3.6" comemo = "0.4" csv = "1" diff --git a/crates/typst-layout/Cargo.toml b/crates/typst-layout/Cargo.toml index cc355a3db..2c314e5c5 100644 --- a/crates/typst-layout/Cargo.toml +++ b/crates/typst-layout/Cargo.toml @@ -21,6 +21,7 @@ typst-timing = { workspace = true } typst-utils = { workspace = true } az = { workspace = true } bumpalo = { workspace = true } +codex = { workspace = true } comemo = { workspace = true } ecow = { workspace = true } hypher = { workspace = true } diff --git a/crates/typst-layout/src/math/text.rs b/crates/typst-layout/src/math/text.rs index 53f88f2b6..634969cd4 100644 --- a/crates/typst-layout/src/math/text.rs +++ b/crates/typst-layout/src/math/text.rs @@ -1,10 +1,11 @@ use std::f64::consts::SQRT_2; +use codex::styling::{to_style, MathStyle}; use ecow::EcoString; use typst_library::diag::SourceResult; use typst_library::foundations::{Packed, StyleChain, SymbolElem}; use typst_library::layout::{Abs, Size}; -use typst_library::math::{EquationElem, MathSize, MathVariant}; +use typst_library::math::{EquationElem, MathSize}; use typst_library::text::{ BottomEdge, BottomEdgeMetric, TextElem, TopEdge, TopEdgeMetric, }; @@ -64,12 +65,21 @@ fn layout_inline_text( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult { + let variant = styles.get(EquationElem::variant); + let bold = styles.get(EquationElem::bold); + // Disable auto-italic. + let italic = styles.get(EquationElem::italic).or(Some(false)); + if text.chars().all(|c| c.is_ascii_digit() || c == '.') { // Small optimization for numbers. Note that this lays out slightly // differently to normal text and is worth re-evaluating in the future. let mut fragments = vec![]; for unstyled_c in text.chars() { - let c = styled_char(styles, unstyled_c, false); + // This is fine as ascii digits and '.' can never end up as more + // than a single char after styling. + let style = MathStyle::select(unstyled_c, variant, bold, italic); + let c = to_style(unstyled_c, style).next().unwrap(); + let glyph = GlyphFragment::new_char(ctx.font, styles, c, span)?; fragments.push(glyph.into()); } @@ -83,8 +93,10 @@ fn layout_inline_text( .map(|p| p.wrap()); let styles = styles.chain(&local); - let styled_text: EcoString = - text.chars().map(|c| styled_char(styles, c, false)).collect(); + let styled_text: EcoString = text + .chars() + .flat_map(|c| to_style(c, MathStyle::select(c, variant, bold, italic))) + .collect(); let spaced = styled_text.graphemes(true).nth(1).is_some(); let elem = TextElem::packed(styled_text).spanned(span); @@ -124,9 +136,16 @@ pub fn layout_symbol( Some(c) if has_dtls_feat(ctx.font) => (c, styles.chain(&dtls)), _ => (elem.text, styles), }; - let c = styled_char(styles, unstyled_c, true); + + let variant = styles.get(EquationElem::variant); + let bold = styles.get(EquationElem::bold); + let italic = styles.get(EquationElem::italic); + + let style = MathStyle::select(unstyled_c, variant, bold, italic); + let text: EcoString = to_style(unstyled_c, style).collect(); + let fragment: MathFragment = - match GlyphFragment::new_char(ctx.font, symbol_styles, c, elem.span()) { + match GlyphFragment::new(ctx.font, symbol_styles, &text, elem.span()) { Ok(mut glyph) => { adjust_glyph_layout(&mut glyph, ctx, styles); glyph.into() @@ -134,8 +153,7 @@ pub fn layout_symbol( Err(_) => { // Not in the math font, fallback to normal inline text layout. // TODO: Should replace this with proper fallback in [`GlyphFragment::new`]. - layout_inline_text(c.encode_utf8(&mut [0; 4]), elem.span(), ctx, styles)? - .into() + layout_inline_text(&text, elem.span(), ctx, styles)?.into() } }; ctx.push(fragment); @@ -161,226 +179,6 @@ fn adjust_glyph_layout( } } -/// Style the character by selecting the unicode codepoint for italic, bold, -/// caligraphic, etc. -/// -/// -/// -fn styled_char(styles: StyleChain, c: char, auto_italic: bool) -> char { - use MathVariant::*; - - let variant = styles.get(EquationElem::variant); - let bold = styles.get(EquationElem::bold); - let italic = styles.get(EquationElem::italic).unwrap_or( - auto_italic - && matches!( - c, - 'a'..='z' | 'ħ' | 'ı' | 'ȷ' | 'A'..='Z' | - 'α'..='ω' | '∂' | 'ϵ' | 'ϑ' | 'ϰ' | 'ϕ' | 'ϱ' | 'ϖ' - ) - && matches!(variant, Sans | Serif), - ); - - if let Some(c) = basic_exception(c) { - return c; - } - - if let Some(c) = latin_exception(c, variant, bold, italic) { - return c; - } - - if let Some(c) = greek_exception(c, variant, bold, italic) { - return c; - } - - let base = match c { - 'A'..='Z' => 'A', - 'a'..='z' => 'a', - 'Α'..='Ω' => 'Α', - 'α'..='ω' => 'α', - '0'..='9' => '0', - // Hebrew Alef -> Dalet. - '\u{05D0}'..='\u{05D3}' => '\u{05D0}', - _ => return c, - }; - - let tuple = (variant, bold, italic); - let start = match c { - // Latin upper. - 'A'..='Z' => match tuple { - (Serif, false, false) => 0x0041, - (Serif, true, false) => 0x1D400, - (Serif, false, true) => 0x1D434, - (Serif, true, true) => 0x1D468, - (Sans, false, false) => 0x1D5A0, - (Sans, true, false) => 0x1D5D4, - (Sans, false, true) => 0x1D608, - (Sans, true, true) => 0x1D63C, - (Cal, false, _) => 0x1D49C, - (Cal, true, _) => 0x1D4D0, - (Frak, false, _) => 0x1D504, - (Frak, true, _) => 0x1D56C, - (Mono, _, _) => 0x1D670, - (Bb, _, _) => 0x1D538, - }, - - // Latin lower. - 'a'..='z' => match tuple { - (Serif, false, false) => 0x0061, - (Serif, true, false) => 0x1D41A, - (Serif, false, true) => 0x1D44E, - (Serif, true, true) => 0x1D482, - (Sans, false, false) => 0x1D5BA, - (Sans, true, false) => 0x1D5EE, - (Sans, false, true) => 0x1D622, - (Sans, true, true) => 0x1D656, - (Cal, false, _) => 0x1D4B6, - (Cal, true, _) => 0x1D4EA, - (Frak, false, _) => 0x1D51E, - (Frak, true, _) => 0x1D586, - (Mono, _, _) => 0x1D68A, - (Bb, _, _) => 0x1D552, - }, - - // Greek upper. - 'Α'..='Ω' => match tuple { - (Serif, false, false) => 0x0391, - (Serif, true, false) => 0x1D6A8, - (Serif, false, true) => 0x1D6E2, - (Serif, true, true) => 0x1D71C, - (Sans, _, false) => 0x1D756, - (Sans, _, true) => 0x1D790, - (Cal | Frak | Mono | Bb, _, _) => return c, - }, - - // Greek lower. - 'α'..='ω' => match tuple { - (Serif, false, false) => 0x03B1, - (Serif, true, false) => 0x1D6C2, - (Serif, false, true) => 0x1D6FC, - (Serif, true, true) => 0x1D736, - (Sans, _, false) => 0x1D770, - (Sans, _, true) => 0x1D7AA, - (Cal | Frak | Mono | Bb, _, _) => return c, - }, - - // Hebrew Alef -> Dalet. - '\u{05D0}'..='\u{05D3}' => 0x2135, - - // Numbers. - '0'..='9' => match tuple { - (Serif, false, _) => 0x0030, - (Serif, true, _) => 0x1D7CE, - (Bb, _, _) => 0x1D7D8, - (Sans, false, _) => 0x1D7E2, - (Sans, true, _) => 0x1D7EC, - (Mono, _, _) => 0x1D7F6, - (Cal | Frak, _, _) => return c, - }, - - _ => unreachable!(), - }; - - std::char::from_u32(start + (c as u32 - base as u32)).unwrap() -} - -fn basic_exception(c: char) -> Option { - Some(match c { - '〈' => '⟨', - '〉' => '⟩', - '《' => '⟪', - '》' => '⟫', - _ => return None, - }) -} - -fn latin_exception( - c: char, - variant: MathVariant, - bold: bool, - italic: bool, -) -> Option { - use MathVariant::*; - Some(match (c, variant, bold, italic) { - ('B', Cal, false, _) => 'ℬ', - ('E', Cal, false, _) => 'ℰ', - ('F', Cal, false, _) => 'ℱ', - ('H', Cal, false, _) => 'ℋ', - ('I', Cal, false, _) => 'ℐ', - ('L', Cal, false, _) => 'ℒ', - ('M', Cal, false, _) => 'ℳ', - ('R', Cal, false, _) => 'ℛ', - ('C', Frak, false, _) => 'ℭ', - ('H', Frak, false, _) => 'ℌ', - ('I', Frak, false, _) => 'ℑ', - ('R', Frak, false, _) => 'ℜ', - ('Z', Frak, false, _) => 'ℨ', - ('C', Bb, ..) => 'ℂ', - ('H', Bb, ..) => 'ℍ', - ('N', Bb, ..) => 'ℕ', - ('P', Bb, ..) => 'ℙ', - ('Q', Bb, ..) => 'ℚ', - ('R', Bb, ..) => 'ℝ', - ('Z', Bb, ..) => 'ℤ', - ('D', Bb, _, true) => 'ⅅ', - ('d', Bb, _, true) => 'ⅆ', - ('e', Bb, _, true) => 'ⅇ', - ('i', Bb, _, true) => 'ⅈ', - ('j', Bb, _, true) => 'ⅉ', - ('h', Serif, false, true) => 'ℎ', - ('e', Cal, false, _) => 'ℯ', - ('g', Cal, false, _) => 'ℊ', - ('o', Cal, false, _) => 'ℴ', - ('ħ', Serif, .., true) => 'ℏ', - ('ı', Serif, .., true) => '𝚤', - ('ȷ', Serif, .., true) => '𝚥', - _ => return None, - }) -} - -fn greek_exception( - c: char, - variant: MathVariant, - bold: bool, - italic: bool, -) -> Option { - use MathVariant::*; - if c == 'Ϝ' && variant == Serif && bold { - return Some('𝟊'); - } - if c == 'ϝ' && variant == Serif && bold { - return Some('𝟋'); - } - - let list = match c { - 'ϴ' => ['𝚹', '𝛳', '𝜭', '𝝧', '𝞡', 'ϴ'], - '∇' => ['𝛁', '𝛻', '𝜵', '𝝯', '𝞩', '∇'], - '∂' => ['𝛛', '𝜕', '𝝏', '𝞉', '𝟃', '∂'], - 'ϵ' => ['𝛜', '𝜖', '𝝐', '𝞊', '𝟄', 'ϵ'], - 'ϑ' => ['𝛝', '𝜗', '𝝑', '𝞋', '𝟅', 'ϑ'], - 'ϰ' => ['𝛞', '𝜘', '𝝒', '𝞌', '𝟆', 'ϰ'], - 'ϕ' => ['𝛟', '𝜙', '𝝓', '𝞍', '𝟇', 'ϕ'], - 'ϱ' => ['𝛠', '𝜚', '𝝔', '𝞎', '𝟈', 'ϱ'], - 'ϖ' => ['𝛡', '𝜛', '𝝕', '𝞏', '𝟉', 'ϖ'], - 'Γ' => ['𝚪', '𝛤', '𝜞', '𝝘', '𝞒', 'ℾ'], - 'γ' => ['𝛄', '𝛾', '𝜸', '𝝲', '𝞬', 'ℽ'], - 'Π' => ['𝚷', '𝛱', '𝜫', '𝝥', '𝞟', 'ℿ'], - 'π' => ['𝛑', '𝜋', '𝝅', '𝝿', '𝞹', 'ℼ'], - '∑' => ['∑', '∑', '∑', '∑', '∑', '⅀'], - _ => return None, - }; - - Some(match (variant, bold, italic) { - (Serif, true, false) => list[0], - (Serif, false, true) => list[1], - (Serif, true, true) => list[2], - (Sans, _, false) => list[3], - (Sans, _, true) => list[4], - (Bb, ..) => list[5], - _ => return None, - }) -} - /// The non-dotless version of a dotless character that can be used with the /// `dtls` OpenType feature. pub fn try_dotless(c: char) -> Option { diff --git a/crates/typst-library/src/math/equation.rs b/crates/typst-library/src/math/equation.rs index 0c9ba11df..a2ae54471 100644 --- a/crates/typst-library/src/math/equation.rs +++ b/crates/typst-library/src/math/equation.rs @@ -1,5 +1,6 @@ use std::num::NonZeroUsize; +use codex::styling::MathVariant; use typst_utils::NonZeroExt; use unicode_math_class::MathClass; @@ -12,7 +13,7 @@ use crate::introspection::{Count, Counter, CounterUpdate, Locatable}; use crate::layout::{ AlignElem, Alignment, BlockElem, OuterHAlignment, SpecificAlignment, VAlignment, }; -use crate::math::{MathSize, MathVariant}; +use crate::math::MathSize; use crate::model::{Numbering, Outlinable, ParLine, Refable, Supplement}; use crate::text::{FontFamily, FontList, FontWeight, LocalName, TextElem}; @@ -111,7 +112,7 @@ pub struct EquationElem { /// The style variant to select. #[internal] #[ghost] - pub variant: MathVariant, + pub variant: Option, /// Affects the height of exponents. #[internal] @@ -128,7 +129,7 @@ pub struct EquationElem { /// Whether to use italic glyphs. #[internal] #[ghost] - pub italic: Smart, + pub italic: Option, /// A forced class to use for all fragment. #[internal] diff --git a/crates/typst-library/src/math/mod.rs b/crates/typst-library/src/math/mod.rs index 2e6d42b13..3d39e2fd2 100644 --- a/crates/typst-library/src/math/mod.rs +++ b/crates/typst-library/src/math/mod.rs @@ -80,6 +80,7 @@ pub fn module() -> Module { math.define_func::(); math.define_func::(); math.define_func::(); + math.define_func::(); math.define_func::(); math.define_func::(); math.define_func::(); diff --git a/crates/typst-library/src/math/style.rs b/crates/typst-library/src/math/style.rs index 53242e6e0..6a85fd123 100644 --- a/crates/typst-library/src/math/style.rs +++ b/crates/typst-library/src/math/style.rs @@ -1,4 +1,6 @@ -use crate::foundations::{func, Cast, Content, Smart}; +use codex::styling::MathVariant; + +use crate::foundations::{func, Cast, Content}; use crate::math::EquationElem; /// Bold font style in math. @@ -24,7 +26,7 @@ pub fn upright( /// The content to style. body: Content, ) -> Content { - body.set(EquationElem::italic, Smart::Custom(false)) + body.set(EquationElem::italic, Some(false)) } /// Italic font style in math. @@ -35,7 +37,7 @@ pub fn italic( /// The content to style. body: Content, ) -> Content { - body.set(EquationElem::italic, Smart::Custom(true)) + body.set(EquationElem::italic, Some(true)) } /// Serif (roman) font style in math. @@ -46,7 +48,7 @@ pub fn serif( /// The content to style. body: Content, ) -> Content { - body.set(EquationElem::variant, MathVariant::Serif) + body.set(EquationElem::variant, Some(MathVariant::Plain)) } /// Sans-serif font style in math. @@ -59,23 +61,39 @@ pub fn sans( /// The content to style. body: Content, ) -> Content { - body.set(EquationElem::variant, MathVariant::Sans) + body.set(EquationElem::variant, Some(MathVariant::SansSerif)) } -/// Calligraphic font style in math. +/// Calligraphic (chancery) font style in math. /// /// ```example /// Let $cal(P)$ be the set of ... /// ``` /// -/// This corresponds both to LaTeX's `\mathcal` and `\mathscr` as both of these -/// styles share the same Unicode codepoints. Switching between the styles is -/// thus only possible if supported by the font via -/// [font features]($text.features). +/// This is the default calligraphic/script style for most math fonts. See +/// [`scr`]($math.scr) for more on how to get the other style (roundhand). +#[func(title = "Calligraphic", keywords = ["mathcal", "chancery"])] +pub fn cal( + /// The content to style. + body: Content, +) -> Content { + body.set(EquationElem::variant, Some(MathVariant::Chancery)) +} + +/// Script (roundhand) font style in math. /// -/// For the default math font, the roundhand style is available through the -/// `ss01` feature. Therefore, you could define your own version of `\mathscr` -/// like this: +/// ```example +/// $ scr(S) $ +/// ``` +/// +/// There are two ways that fonts can support differentiating `cal` and `scr`. +/// The first is using Unicode variation sequences. This works out of the box +/// in Typst, however only a few math fonts currently support this. +/// +/// The other way is using [font features]($text.features). For example, the +/// roundhand style might be available in a font through the `ss01` feature. +/// To use it in Typst, you could then define your own version of `scr` like +/// this: /// /// ```example /// #let scr(it) = text( @@ -88,12 +106,12 @@ pub fn sans( /// /// (The box is not conceptually necessary, but unfortunately currently needed /// due to limitations in Typst's text style handling in math.) -#[func(title = "Calligraphic", keywords = ["mathcal", "mathscr"])] -pub fn cal( +#[func(title = "Script Style", keywords = ["mathscr", "roundhand"])] +pub fn scr( /// The content to style. body: Content, ) -> Content { - body.set(EquationElem::variant, MathVariant::Cal) + body.set(EquationElem::variant, Some(MathVariant::Roundhand)) } /// Fraktur font style in math. @@ -106,7 +124,7 @@ pub fn frak( /// The content to style. body: Content, ) -> Content { - body.set(EquationElem::variant, MathVariant::Frak) + body.set(EquationElem::variant, Some(MathVariant::Fraktur)) } /// Monospace font style in math. @@ -119,7 +137,7 @@ pub fn mono( /// The content to style. body: Content, ) -> Content { - body.set(EquationElem::variant, MathVariant::Mono) + body.set(EquationElem::variant, Some(MathVariant::Monospace)) } /// Blackboard bold (double-struck) font style in math. @@ -137,7 +155,7 @@ pub fn bb( /// The content to style. body: Content, ) -> Content { - body.set(EquationElem::variant, MathVariant::Bb) + body.set(EquationElem::variant, Some(MathVariant::DoubleStruck)) } /// Forced display style in math. @@ -240,15 +258,3 @@ pub enum MathSize { /// Math on its own line. Display, } - -/// A mathematical style variant, as defined by Unicode. -#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Cast, Hash)] -pub enum MathVariant { - #[default] - Serif, - Sans, - Cal, - Frak, - Mono, - Bb, -} diff --git a/docs/reference/groups.yml b/docs/reference/groups.yml index 1aaa8f229..b187443e4 100644 --- a/docs/reference/groups.yml +++ b/docs/reference/groups.yml @@ -5,7 +5,7 @@ title: Variants category: math path: ["math"] - filter: ["serif", "sans", "frak", "mono", "bb", "cal"] + filter: ["serif", "sans", "frak", "mono", "bb", "cal", "scr"] details: | Alternate typefaces within formulas. diff --git a/tests/ref/math-style-fallback.png b/tests/ref/math-style-fallback.png new file mode 100644 index 0000000000000000000000000000000000000000..de0283762d8208eb46d0d34b6d1727c5692ef64f GIT binary patch literal 935 zcmV;Y16cftP)Nmq2JB?V>G;e89BdSM5) zTJ8q_!hk6J!-Ei;d)EHN(R(1CcTyC_UDxa>Ti1)N-CnI~%S)Z2Fr*;6l=_bqd;`Og z_67rc=Z`cuq@-c*hjrV#0n>A-^8l~-YqtF_4Hungv#@8ak9-Hx@H$WN5cV&&p4_%o z9$rsYRtd<%E6Hj^9){%oWYzRG@^IWuR_#aBaw%BBR|2Q@y}Z110PIbD^up85F;RH= zihr%DJ36p+Vae;0@pta*7lgmP#Q-)IVC&Oc6Tr&Vf^cKrmk!4hHS>TOEtt*#To3ph zj@6})3BvVxeJ-><*Raf?~Ags^r0-i4bW@UzE4QQ<4Y#>qUrRJa9W8(`q~p@RcA~Z?mF1r zde8)za5LLak}7 zVgCw35n$AKuiG&G2CBm(X=Q4F~7Ok6oxmy;dI{hAJ7**{8l(T0)Q_RT6#T05xoO@L6}K_GzgZIkclJ_g%~Cwrua%t)9Y2g;XU_W z|AJ!=?pc1H-}ek3&TzgZ_@PrGi6yZl{%_(O86GiU{Q`xkiGS5r1SSX`);F|UIQo<+ z;v}1?tE=v$A`bRM(QgvAmmxAEs$B@@QAb6L**eS><3x=*50@3?vTz=fRsE!~%1X=4^u+F3qxOj-mN95Jh%3ldnwihi6YtoX zHUy5G@fmot0`$akqp8uuv%|rSSoa&*z2g+c>%BNxx>X+q)UB;8)80Ygrtm^4?qh<-C)x?0ANzI|8eqKAUzooOR)O!7k?WF00000l5Z6H0002*Nkl&=Ye?{nuE>&Jq%{NyHAIb>ja#2_YGWZw5%Sr gc+}!ii!lKJM-vcqv*9Pp#{d8T07*qoM6N<$g4#KY5&!@I diff --git a/tests/ref/math-style-script.png b/tests/ref/math-style-script.png new file mode 100644 index 0000000000000000000000000000000000000000..379d270e70b7f94412429b785f9285a7de95939a GIT binary patch literal 585 zcmV-P0=E5$P)L5)nX8u2gyWP4vBPv1TNb`$uLK; z&0uC_gEEz^xmeR$#x}P$iqegACS!Z{tapc<`U8F>@q90z@8|Toee(E6m#4rAtib;v zEX#k@@P}L`Qexr6P-#wJ{3MLc-F;Hh4p=JGohFg6eD!?;08F;)7;EYtq;D6r?tFPs z6A=k_d5l)@>J57P{$H7*dFwcxhjDIkHz$jI0^va8?{hJ@g^7{vhx(e~~>JKF9YW3U&pJZwc1KW%a@Zi@Ewc69p1n>W9)klC@r8 z@Icob*vn_-+Iv&4j}J;@ubk>_eFnTK*BbrGaw)I^E3g7@40zoUhm7C1@CCpV1d>3= z4UiHCe=2oPtj^o-oSkmb6EJ^0KgHqziBXT?CP`gd9NeaV8g-1&-48@&M}0d+ejJn1 zwq?r+Rkr#g!r*k9&Kq8W8Yl27rminpJ92}`jJhX3*i?Zqc=^D)_&PxGutmaeN^Ezk zHb;q>Lshr$Z!(F4H7zR-rh&vE00Jvk+rBeEJmh**S15^to2_z#-OmATr zC{y9uW Date: Wed, 9 Jul 2025 15:40:22 +0200 Subject: [PATCH 12/18] More consistent `Packed` to `Content` conversion methods (#6579) --- .../src/foundations/content/packed.rs | 14 ++++++++++---- .../typst-library/src/foundations/content/raw.rs | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/crates/typst-library/src/foundations/content/packed.rs b/crates/typst-library/src/foundations/content/packed.rs index 71bb66a94..e99162629 100644 --- a/crates/typst-library/src/foundations/content/packed.rs +++ b/crates/typst-library/src/foundations/content/packed.rs @@ -64,6 +64,16 @@ impl Packed { self.0 } + /// Pack back into a reference to content. + pub fn pack_ref(&self) -> &Content { + &self.0 + } + + /// Pack back into a mutable reference to content. + pub fn pack_mut(&mut self) -> &mut Content { + &mut self.0 + } + /// Extract the raw underlying element. pub fn unpack(self) -> T { // This function doesn't yet need owned self, but might in the future. @@ -94,10 +104,6 @@ impl Packed { pub fn set_location(&mut self, location: Location) { self.0.set_location(location); } - - pub fn as_content(&self) -> &Content { - &self.0 - } } impl AsRef for Packed { diff --git a/crates/typst-library/src/foundations/content/raw.rs b/crates/typst-library/src/foundations/content/raw.rs index f5dfffd73..dde26bd79 100644 --- a/crates/typst-library/src/foundations/content/raw.rs +++ b/crates/typst-library/src/foundations/content/raw.rs @@ -141,7 +141,7 @@ impl RawContent { /// Clones a packed element into new raw content. pub(super) fn clone_impl(elem: &Packed) -> Self { - let raw = &elem.as_content().0; + let raw = &elem.pack_ref().0; let header = raw.header(); RawContent::create( elem.as_ref().clone(), From 3aa7e861e7ffe03193d94c2cfd249739ef746f09 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 9 Jul 2025 15:48:43 +0200 Subject: [PATCH 13/18] Support images in HTML export (#6578) --- crates/typst-html/src/css.rs | 65 +++++++++++++- crates/typst-html/src/rules.rs | 45 +++++++++- crates/typst-layout/src/image.rs | 85 ++----------------- crates/typst-library/src/html/dom.rs | 5 ++ .../typst-library/src/visualize/image/mod.rs | 82 +++++++++++++++++- crates/typst-svg/src/image.rs | 26 +++--- crates/typst-svg/src/lib.rs | 2 + tests/ref/html/image-jpg-html-base64.html | 8 ++ tests/ref/html/image-scaling-methods.html | 10 +++ tests/suite/visualize/image.typ | 23 ++++- 10 files changed, 250 insertions(+), 101 deletions(-) create mode 100644 tests/ref/html/image-jpg-html-base64.html create mode 100644 tests/ref/html/image-scaling-methods.html diff --git a/crates/typst-html/src/css.rs b/crates/typst-html/src/css.rs index 2b659188a..6c84cba0f 100644 --- a/crates/typst-html/src/css.rs +++ b/crates/typst-html/src/css.rs @@ -1,11 +1,72 @@ //! Conversion from Typst data types into CSS data types. -use std::fmt::{self, Display}; +use std::fmt::{self, Display, Write}; -use typst_library::layout::Length; +use ecow::EcoString; +use typst_library::html::{attr, HtmlElem}; +use typst_library::layout::{Length, Rel}; use typst_library::visualize::{Color, Hsl, LinearRgb, Oklab, Oklch, Rgb}; use typst_utils::Numeric; +/// Additional methods for [`HtmlElem`]. +pub trait HtmlElemExt { + /// Adds the styles to an element if the property list is non-empty. + fn with_styles(self, properties: Properties) -> Self; +} + +impl HtmlElemExt for HtmlElem { + /// Adds CSS styles to an element. + fn with_styles(self, properties: Properties) -> Self { + if let Some(value) = properties.into_inline_styles() { + self.with_attr(attr::style, value) + } else { + self + } + } +} + +/// A list of CSS properties with values. +#[derive(Debug, Default)] +pub struct Properties(EcoString); + +impl Properties { + /// Creates an empty list. + pub fn new() -> Self { + Self::default() + } + + /// Adds a new property to the list. + pub fn push(&mut self, property: &str, value: impl Display) { + if !self.0.is_empty() { + self.0.push_str("; "); + } + write!(&mut self.0, "{property}: {value}").unwrap(); + } + + /// Adds a new property in builder-style. + #[expect(unused)] + pub fn with(mut self, property: &str, value: impl Display) -> Self { + self.push(property, value); + self + } + + /// Turns this into a string suitable for use as an inline `style` + /// attribute. + pub fn into_inline_styles(self) -> Option { + (!self.0.is_empty()).then_some(self.0) + } +} + +pub fn rel(rel: Rel) -> impl Display { + typst_utils::display(move |f| match (rel.abs.is_zero(), rel.rel.is_zero()) { + (false, false) => { + write!(f, "calc({}% + {})", rel.rel.get(), length(rel.abs)) + } + (true, false) => write!(f, "{}%", rel.rel.get()), + (_, true) => write!(f, "{}", length(rel.abs)), + }) +} + pub fn length(length: Length) -> impl Display { typst_utils::display(move |f| match (length.abs.is_zero(), length.em.is_zero()) { (false, false) => { diff --git a/crates/typst-html/src/rules.rs b/crates/typst-html/src/rules.rs index f361bfbb3..5bf25e79b 100644 --- a/crates/typst-html/src/rules.rs +++ b/crates/typst-html/src/rules.rs @@ -3,12 +3,12 @@ use std::num::NonZeroUsize; use ecow::{eco_format, EcoVec}; use typst_library::diag::warning; use typst_library::foundations::{ - Content, NativeElement, NativeRuleMap, ShowFn, StyleChain, Target, + Content, NativeElement, NativeRuleMap, ShowFn, Smart, StyleChain, Target, }; use typst_library::html::{attr, tag, HtmlAttrs, HtmlElem, HtmlTag}; use typst_library::introspection::{Counter, Locator}; use typst_library::layout::resolve::{table_to_cellgrid, Cell, CellGrid, Entry}; -use typst_library::layout::OuterVAlignment; +use typst_library::layout::{OuterVAlignment, Sizing}; use typst_library::model::{ Attribution, CiteElem, CiteGroup, Destination, EmphElem, EnumElem, FigureCaption, FigureElem, HeadingElem, LinkElem, LinkTarget, ListElem, ParbreakElem, QuoteElem, @@ -18,6 +18,9 @@ use typst_library::text::{ HighlightElem, LinebreakElem, OverlineElem, RawElem, RawLine, SpaceElem, StrikeElem, SubElem, SuperElem, UnderlineElem, }; +use typst_library::visualize::ImageElem; + +use crate::css::{self, HtmlElemExt}; /// Register show rules for the [HTML target](Target::Html). pub fn register(rules: &mut NativeRuleMap) { @@ -47,6 +50,9 @@ pub fn register(rules: &mut NativeRuleMap) { rules.register(Html, HIGHLIGHT_RULE); rules.register(Html, RAW_RULE); rules.register(Html, RAW_LINE_RULE); + + // Visualize. + rules.register(Html, IMAGE_RULE); } const STRONG_RULE: ShowFn = |elem, _, _| { @@ -338,7 +344,7 @@ fn show_cellgrid(grid: CellGrid, styles: StyleChain) -> Content { fn show_cell(tag: HtmlTag, cell: &Cell, styles: StyleChain) -> Content { let cell = cell.body.clone(); let Some(cell) = cell.to_packed::() else { return cell }; - let mut attrs = HtmlAttrs::default(); + let mut attrs = HtmlAttrs::new(); let span = |n: NonZeroUsize| (n != NonZeroUsize::MIN).then(|| n.to_string()); if let Some(colspan) = span(cell.colspan.get(styles)) { attrs.push(attr::colspan, colspan); @@ -409,3 +415,36 @@ const RAW_RULE: ShowFn = |elem, _, styles| { }; const RAW_LINE_RULE: ShowFn = |elem, _, _| Ok(elem.body.clone()); + +const IMAGE_RULE: ShowFn = |elem, engine, styles| { + let image = elem.decode(engine, styles)?; + + let mut attrs = HtmlAttrs::new(); + attrs.push(attr::src, typst_svg::convert_image_to_base64_url(&image)); + + if let Some(alt) = elem.alt.get_cloned(styles) { + attrs.push(attr::alt, alt); + } + + let mut inline = css::Properties::new(); + + // TODO: Exclude in semantic profile. + if let Some(value) = typst_svg::convert_image_scaling(image.scaling()) { + inline.push("image-rendering", value); + } + + // TODO: Exclude in semantic profile? + match elem.width.get(styles) { + Smart::Auto => {} + Smart::Custom(rel) => inline.push("width", css::rel(rel)), + } + + // TODO: Exclude in semantic profile? + match elem.height.get(styles) { + Sizing::Auto => {} + Sizing::Rel(rel) => inline.push("height", css::rel(rel)), + Sizing::Fr(_) => {} + } + + Ok(HtmlElem::new(tag::img).with_attrs(attrs).with_styles(inline).pack()) +}; diff --git a/crates/typst-layout/src/image.rs b/crates/typst-layout/src/image.rs index 261a58fa3..d4fd121ec 100644 --- a/crates/typst-layout/src/image.rs +++ b/crates/typst-layout/src/image.rs @@ -1,18 +1,11 @@ -use std::ffi::OsStr; - -use typst_library::diag::{warning, At, LoadedWithin, SourceResult, StrResult}; +use typst_library::diag::SourceResult; use typst_library::engine::Engine; -use typst_library::foundations::{Bytes, Derived, Packed, Smart, StyleChain}; +use typst_library::foundations::{Packed, StyleChain}; use typst_library::introspection::Locator; use typst_library::layout::{ Abs, Axes, FixedAlignment, Frame, FrameItem, Point, Region, Size, }; -use typst_library::loading::DataSource; -use typst_library::text::families; -use typst_library::visualize::{ - Curve, ExchangeFormat, Image, ImageElem, ImageFit, ImageFormat, ImageKind, - RasterImage, SvgImage, VectorFormat, -}; +use typst_library::visualize::{Curve, Image, ImageElem, ImageFit}; /// Layout the image. #[typst_macros::time(span = elem.span())] @@ -23,53 +16,7 @@ pub fn layout_image( styles: StyleChain, region: Region, ) -> SourceResult { - let span = elem.span(); - - // Take the format that was explicitly defined, or parse the extension, - // or try to detect the format. - let Derived { source, derived: loaded } = &elem.source; - let format = match elem.format.get(styles) { - Smart::Custom(v) => v, - Smart::Auto => determine_format(source, &loaded.data).at(span)?, - }; - - // Warn the user if the image contains a foreign object. Not perfect - // because the svg could also be encoded, but that's an edge case. - if format == ImageFormat::Vector(VectorFormat::Svg) { - let has_foreign_object = - memchr::memmem::find(&loaded.data, b" ImageKind::Raster( - RasterImage::new( - loaded.data.clone(), - format, - elem.icc.get_ref(styles).as_ref().map(|icc| icc.derived.clone()), - ) - .at(span)?, - ), - ImageFormat::Vector(VectorFormat::Svg) => ImageKind::Svg( - SvgImage::with_fonts( - loaded.data.clone(), - engine.world, - &families(styles).map(|f| f.as_str()).collect::>(), - ) - .within(loaded)?, - ), - }; - - let image = Image::new(kind, elem.alt.get_cloned(styles), elem.scaling.get(styles)); + let image = elem.decode(engine, styles)?; // Determine the image's pixel aspect ratio. let pxw = image.width(); @@ -122,7 +69,7 @@ pub fn layout_image( // the frame to the target size, center aligning the image in the // process. let mut frame = Frame::soft(fitted); - frame.push(Point::zero(), FrameItem::Image(image, fitted, span)); + frame.push(Point::zero(), FrameItem::Image(image, fitted, elem.span())); frame.resize(target, Axes::splat(FixedAlignment::Center)); // Create a clipping group if only part of the image should be visible. @@ -132,25 +79,3 @@ pub fn layout_image( Ok(frame) } - -/// Try to determine the image format based on the data. -fn determine_format(source: &DataSource, data: &Bytes) -> StrResult { - if let DataSource::Path(path) = source { - let ext = std::path::Path::new(path.as_str()) - .extension() - .and_then(OsStr::to_str) - .unwrap_or_default() - .to_lowercase(); - - match ext.as_str() { - "png" => return Ok(ExchangeFormat::Png.into()), - "jpg" | "jpeg" => return Ok(ExchangeFormat::Jpg.into()), - "gif" => return Ok(ExchangeFormat::Gif.into()), - "svg" | "svgz" => return Ok(VectorFormat::Svg.into()), - "webp" => return Ok(ExchangeFormat::Webp.into()), - _ => {} - } - } - - Ok(ImageFormat::detect(data).ok_or("unknown image format")?) -} diff --git a/crates/typst-library/src/html/dom.rs b/crates/typst-library/src/html/dom.rs index 47bcf9954..49ff37c45 100644 --- a/crates/typst-library/src/html/dom.rs +++ b/crates/typst-library/src/html/dom.rs @@ -165,6 +165,11 @@ cast! { pub struct HtmlAttrs(pub EcoVec<(HtmlAttr, EcoString)>); impl HtmlAttrs { + /// Creates an empty attribute list. + pub fn new() -> Self { + Self::default() + } + /// Add an attribute. pub fn push(&mut self, attr: HtmlAttr, value: impl Into) { self.0.push((attr, value.into())); diff --git a/crates/typst-library/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs index 95021b818..f1fa6381b 100644 --- a/crates/typst-library/src/visualize/image/mod.rs +++ b/crates/typst-library/src/visualize/image/mod.rs @@ -8,6 +8,7 @@ pub use self::raster::{ }; pub use self::svg::SvgImage; +use std::ffi::OsStr; use std::fmt::{self, Debug, Formatter}; use std::sync::Arc; @@ -15,14 +16,16 @@ use ecow::EcoString; use typst_syntax::{Span, Spanned}; use typst_utils::LazyHash; -use crate::diag::StrResult; +use crate::diag::{warning, At, LoadedWithin, SourceResult, StrResult}; +use crate::engine::Engine; use crate::foundations::{ cast, elem, func, scope, Bytes, Cast, Content, Derived, NativeElement, Packed, Smart, + StyleChain, }; use crate::layout::{Length, Rel, Sizing}; use crate::loading::{DataSource, Load, LoadSource, Loaded, Readable}; use crate::model::Figurable; -use crate::text::LocalName; +use crate::text::{families, LocalName}; /// A raster or vector graphic. /// @@ -217,6 +220,81 @@ impl ImageElem { } } +impl Packed { + /// Decodes the image. + pub fn decode(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { + let span = self.span(); + let loaded = &self.source.derived; + let format = self.determine_format(styles).at(span)?; + + // Warn the user if the image contains a foreign object. Not perfect + // because the svg could also be encoded, but that's an edge case. + if format == ImageFormat::Vector(VectorFormat::Svg) { + let has_foreign_object = + memchr::memmem::find(&loaded.data, b" ImageKind::Raster( + RasterImage::new( + loaded.data.clone(), + format, + self.icc.get_ref(styles).as_ref().map(|icc| icc.derived.clone()), + ) + .at(span)?, + ), + ImageFormat::Vector(VectorFormat::Svg) => ImageKind::Svg( + SvgImage::with_fonts( + loaded.data.clone(), + engine.world, + &families(styles).map(|f| f.as_str()).collect::>(), + ) + .within(loaded)?, + ), + }; + + Ok(Image::new(kind, self.alt.get_cloned(styles), self.scaling.get(styles))) + } + + /// Tries to determine the image format based on the format that was + /// explicitly defined, or else the extension, or else the data. + fn determine_format(&self, styles: StyleChain) -> StrResult { + if let Smart::Custom(v) = self.format.get(styles) { + return Ok(v); + }; + + let Derived { source, derived: loaded } = &self.source; + if let DataSource::Path(path) = source { + let ext = std::path::Path::new(path.as_str()) + .extension() + .and_then(OsStr::to_str) + .unwrap_or_default() + .to_lowercase(); + + match ext.as_str() { + "png" => return Ok(ExchangeFormat::Png.into()), + "jpg" | "jpeg" => return Ok(ExchangeFormat::Jpg.into()), + "gif" => return Ok(ExchangeFormat::Gif.into()), + "svg" | "svgz" => return Ok(VectorFormat::Svg.into()), + "webp" => return Ok(ExchangeFormat::Webp.into()), + _ => {} + } + } + + Ok(ImageFormat::detect(&loaded.data).ok_or("unknown image format")?) + } +} + impl LocalName for Packed { const KEY: &'static str = "figure"; } diff --git a/crates/typst-svg/src/image.rs b/crates/typst-svg/src/image.rs index 1868ca39b..e6dd579f3 100644 --- a/crates/typst-svg/src/image.rs +++ b/crates/typst-svg/src/image.rs @@ -18,21 +18,27 @@ impl SVGRenderer { self.xml.write_attribute("width", &size.x.to_pt()); self.xml.write_attribute("height", &size.y.to_pt()); self.xml.write_attribute("preserveAspectRatio", "none"); - match image.scaling() { - Smart::Auto => {} - Smart::Custom(ImageScaling::Smooth) => { - // This is still experimental and not implemented in all major browsers. - // https://developer.mozilla.org/en-US/docs/Web/CSS/image-rendering#browser_compatibility - self.xml.write_attribute("style", "image-rendering: smooth") - } - Smart::Custom(ImageScaling::Pixelated) => { - self.xml.write_attribute("style", "image-rendering: pixelated") - } + if let Some(value) = convert_image_scaling(image.scaling()) { + self.xml + .write_attribute("style", &format_args!("image-rendering: {value}")) } self.xml.end_element(); } } +/// Converts an image scaling to a CSS `image-rendering` propery value. +pub fn convert_image_scaling(scaling: Smart) -> Option<&'static str> { + match scaling { + Smart::Auto => None, + Smart::Custom(ImageScaling::Smooth) => { + // This is still experimental and not implemented in all major browsers. + // https://developer.mozilla.org/en-US/docs/Web/CSS/image-rendering#browser_compatibility + Some("smooth") + } + Smart::Custom(ImageScaling::Pixelated) => Some("pixelated"), + } +} + /// Encode an image into a data URL. The format of the URL is /// `data:image/{format};base64,`. #[comemo::memoize] diff --git a/crates/typst-svg/src/lib.rs b/crates/typst-svg/src/lib.rs index f4e81250f..3931b67f7 100644 --- a/crates/typst-svg/src/lib.rs +++ b/crates/typst-svg/src/lib.rs @@ -5,6 +5,8 @@ mod paint; mod shape; mod text; +pub use image::{convert_image_scaling, convert_image_to_base64_url}; + use std::collections::HashMap; use std::fmt::{self, Display, Formatter, Write}; diff --git a/tests/ref/html/image-jpg-html-base64.html b/tests/ref/html/image-jpg-html-base64.html new file mode 100644 index 000000000..89075323c --- /dev/null +++ b/tests/ref/html/image-jpg-html-base64.html @@ -0,0 +1,8 @@ + + + + + + + The letter F + diff --git a/tests/ref/html/image-scaling-methods.html b/tests/ref/html/image-scaling-methods.html new file mode 100644 index 000000000..a15664d51 --- /dev/null +++ b/tests/ref/html/image-scaling-methods.html @@ -0,0 +1,10 @@ + + + + + + + +

+ + diff --git a/tests/suite/visualize/image.typ b/tests/suite/visualize/image.typ index 45c70c4b8..36ec06cb1 100644 --- a/tests/suite/visualize/image.typ +++ b/tests/suite/visualize/image.typ @@ -9,6 +9,9 @@ #set page(height: 60pt) #image("/assets/images/tiger.jpg") +--- image-jpg-html-base64 html --- +#image("/assets/images/f2t.jpg", alt: "The letter F") + --- image-sizing --- // Test configuring the size and fitting behaviour of images. @@ -128,7 +131,7 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B width: 1cm, ) ---- image-scaling-methods --- +--- image-scaling-methods render html --- #let img(scaling) = image( bytes(( 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, @@ -144,14 +147,26 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B scaling: scaling, ) -#stack( - dir: ltr, - spacing: 4pt, +#let images = ( img(auto), img("smooth"), img("pixelated"), ) +#context if target() == "html" { + // TODO: Remove this once `stack` is supported in HTML export. + html.div( + style: "display: flex; flex-direction: row; gap: 4pt", + images.join(), + ) +} else { + stack( + dir: ltr, + spacing: 4pt, + ..images, + ) +} + --- image-natural-dpi-sizing --- // Test that images aren't upscaled. // Image is just 48x80 at 220dpi. It should not be scaled to fit the page From ac77fdbb6ee9c4a33813a75e056cb5953d14b1db Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 9 Jul 2025 15:50:54 +0200 Subject: [PATCH 14/18] Fix tooltip for figure reference (#6580) --- crates/typst-ide/src/analyze.rs | 11 ++++++++--- crates/typst-ide/src/tooltip.rs | 5 +++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/crates/typst-ide/src/analyze.rs b/crates/typst-ide/src/analyze.rs index c493da81a..76739fec0 100644 --- a/crates/typst-ide/src/analyze.rs +++ b/crates/typst-ide/src/analyze.rs @@ -2,7 +2,7 @@ use comemo::Track; use ecow::{eco_vec, EcoString, EcoVec}; use typst::foundations::{Label, Styles, Value}; use typst::layout::PagedDocument; -use typst::model::BibliographyElem; +use typst::model::{BibliographyElem, FigureElem}; use typst::syntax::{ast, LinkedNode, SyntaxKind}; use crate::IdeWorld; @@ -75,8 +75,13 @@ pub fn analyze_labels( for elem in document.introspector.all() { let Some(label) = elem.label() else { continue }; let details = elem - .get_by_name("caption") - .or_else(|_| elem.get_by_name("body")) + .to_packed::() + .and_then(|figure| match figure.caption.as_option() { + Some(Some(caption)) => Some(caption.pack_ref()), + _ => None, + }) + .unwrap_or(elem) + .get_by_name("body") .ok() .and_then(|field| match field { Value::Content(content) => Some(content), diff --git a/crates/typst-ide/src/tooltip.rs b/crates/typst-ide/src/tooltip.rs index 528f679cf..e0d66a89b 100644 --- a/crates/typst-ide/src/tooltip.rs +++ b/crates/typst-ide/src/tooltip.rs @@ -378,4 +378,9 @@ mod tests { .with_source("other.typ", "#let f = (x) => 1"); test(&world, -4, Side::After).must_be_code("(..) => .."); } + + #[test] + fn test_tooltip_reference() { + test("#figure(caption: [Hi])[] @f", -1, Side::Before).must_be_text("Hi"); + } } From 98802dde7e3eab456bf4892b586076431e3bb386 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 10 Jul 2025 12:42:34 +0200 Subject: [PATCH 15/18] Complete movement of HTML export code to `typst-html` (#6584) --- crates/typst-cli/src/compile.rs | 2 +- crates/typst-html/src/attr.rs | 195 +++++ crates/typst-html/src/charsets.rs | 81 ++ crates/typst-html/src/convert.rs | 125 +++ crates/typst-html/src/css.rs | 18 - crates/typst-html/src/document.rs | 219 +++++ crates/typst-html/src/dom.rs | 281 ++++++ crates/typst-html/src/encode.rs | 5 +- crates/typst-html/src/fragment.rs | 76 ++ crates/typst-html/src/lib.rs | 421 ++------- crates/typst-html/src/rules.rs | 12 +- crates/typst-html/src/tag.rs | 271 ++++++ crates/typst-html/src/typed.rs | 4 +- crates/typst-layout/src/flow/mod.rs | 2 +- crates/typst-layout/src/pages/mod.rs | 34 +- crates/typst-library/src/html/dom.rs | 828 ------------------ crates/typst-library/src/html/mod.rs | 75 -- .../src/introspection/introspector.rs | 86 +- crates/typst-library/src/lib.rs | 1 - crates/typst-library/src/routines.rs | 29 +- crates/typst-realize/src/lib.rs | 39 +- crates/typst/src/lib.rs | 2 +- tests/src/run.rs | 2 +- 23 files changed, 1421 insertions(+), 1387 deletions(-) create mode 100644 crates/typst-html/src/attr.rs create mode 100644 crates/typst-html/src/charsets.rs create mode 100644 crates/typst-html/src/convert.rs create mode 100644 crates/typst-html/src/document.rs create mode 100644 crates/typst-html/src/dom.rs create mode 100644 crates/typst-html/src/fragment.rs create mode 100644 crates/typst-html/src/tag.rs delete mode 100644 crates/typst-library/src/html/dom.rs delete mode 100644 crates/typst-library/src/html/mod.rs diff --git a/crates/typst-cli/src/compile.rs b/crates/typst-cli/src/compile.rs index 207bb7d09..0db67b454 100644 --- a/crates/typst-cli/src/compile.rs +++ b/crates/typst-cli/src/compile.rs @@ -14,10 +14,10 @@ use typst::diag::{ bail, At, Severity, SourceDiagnostic, SourceResult, StrResult, Warned, }; use typst::foundations::{Datetime, Smart}; -use typst::html::HtmlDocument; use typst::layout::{Frame, Page, PageRanges, PagedDocument}; use typst::syntax::{FileId, Lines, Span}; use typst::WorldExt; +use typst_html::HtmlDocument; use typst_pdf::{PdfOptions, PdfStandards, Timestamp}; use crate::args::{ diff --git a/crates/typst-html/src/attr.rs b/crates/typst-html/src/attr.rs new file mode 100644 index 000000000..0fec3955d --- /dev/null +++ b/crates/typst-html/src/attr.rs @@ -0,0 +1,195 @@ + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(non_upper_case_globals)] +#![allow(dead_code)] + +use crate::HtmlAttr; + +pub const abbr: HtmlAttr = HtmlAttr::constant("abbr"); +pub const accept: HtmlAttr = HtmlAttr::constant("accept"); +pub const accept_charset: HtmlAttr = HtmlAttr::constant("accept-charset"); +pub const accesskey: HtmlAttr = HtmlAttr::constant("accesskey"); +pub const action: HtmlAttr = HtmlAttr::constant("action"); +pub const allow: HtmlAttr = HtmlAttr::constant("allow"); +pub const allowfullscreen: HtmlAttr = HtmlAttr::constant("allowfullscreen"); +pub const alpha: HtmlAttr = HtmlAttr::constant("alpha"); +pub const alt: HtmlAttr = HtmlAttr::constant("alt"); +pub const aria_activedescendant: HtmlAttr = HtmlAttr::constant("aria-activedescendant"); +pub const aria_atomic: HtmlAttr = HtmlAttr::constant("aria-atomic"); +pub const aria_autocomplete: HtmlAttr = HtmlAttr::constant("aria-autocomplete"); +pub const aria_busy: HtmlAttr = HtmlAttr::constant("aria-busy"); +pub const aria_checked: HtmlAttr = HtmlAttr::constant("aria-checked"); +pub const aria_colcount: HtmlAttr = HtmlAttr::constant("aria-colcount"); +pub const aria_colindex: HtmlAttr = HtmlAttr::constant("aria-colindex"); +pub const aria_colspan: HtmlAttr = HtmlAttr::constant("aria-colspan"); +pub const aria_controls: HtmlAttr = HtmlAttr::constant("aria-controls"); +pub const aria_current: HtmlAttr = HtmlAttr::constant("aria-current"); +pub const aria_describedby: HtmlAttr = HtmlAttr::constant("aria-describedby"); +pub const aria_details: HtmlAttr = HtmlAttr::constant("aria-details"); +pub const aria_disabled: HtmlAttr = HtmlAttr::constant("aria-disabled"); +pub const aria_errormessage: HtmlAttr = HtmlAttr::constant("aria-errormessage"); +pub const aria_expanded: HtmlAttr = HtmlAttr::constant("aria-expanded"); +pub const aria_flowto: HtmlAttr = HtmlAttr::constant("aria-flowto"); +pub const aria_haspopup: HtmlAttr = HtmlAttr::constant("aria-haspopup"); +pub const aria_hidden: HtmlAttr = HtmlAttr::constant("aria-hidden"); +pub const aria_invalid: HtmlAttr = HtmlAttr::constant("aria-invalid"); +pub const aria_keyshortcuts: HtmlAttr = HtmlAttr::constant("aria-keyshortcuts"); +pub const aria_label: HtmlAttr = HtmlAttr::constant("aria-label"); +pub const aria_labelledby: HtmlAttr = HtmlAttr::constant("aria-labelledby"); +pub const aria_level: HtmlAttr = HtmlAttr::constant("aria-level"); +pub const aria_live: HtmlAttr = HtmlAttr::constant("aria-live"); +pub const aria_modal: HtmlAttr = HtmlAttr::constant("aria-modal"); +pub const aria_multiline: HtmlAttr = HtmlAttr::constant("aria-multiline"); +pub const aria_multiselectable: HtmlAttr = HtmlAttr::constant("aria-multiselectable"); +pub const aria_orientation: HtmlAttr = HtmlAttr::constant("aria-orientation"); +pub const aria_owns: HtmlAttr = HtmlAttr::constant("aria-owns"); +pub const aria_placeholder: HtmlAttr = HtmlAttr::constant("aria-placeholder"); +pub const aria_posinset: HtmlAttr = HtmlAttr::constant("aria-posinset"); +pub const aria_pressed: HtmlAttr = HtmlAttr::constant("aria-pressed"); +pub const aria_readonly: HtmlAttr = HtmlAttr::constant("aria-readonly"); +pub const aria_relevant: HtmlAttr = HtmlAttr::constant("aria-relevant"); +pub const aria_required: HtmlAttr = HtmlAttr::constant("aria-required"); +pub const aria_roledescription: HtmlAttr = HtmlAttr::constant("aria-roledescription"); +pub const aria_rowcount: HtmlAttr = HtmlAttr::constant("aria-rowcount"); +pub const aria_rowindex: HtmlAttr = HtmlAttr::constant("aria-rowindex"); +pub const aria_rowspan: HtmlAttr = HtmlAttr::constant("aria-rowspan"); +pub const aria_selected: HtmlAttr = HtmlAttr::constant("aria-selected"); +pub const aria_setsize: HtmlAttr = HtmlAttr::constant("aria-setsize"); +pub const aria_sort: HtmlAttr = HtmlAttr::constant("aria-sort"); +pub const aria_valuemax: HtmlAttr = HtmlAttr::constant("aria-valuemax"); +pub const aria_valuemin: HtmlAttr = HtmlAttr::constant("aria-valuemin"); +pub const aria_valuenow: HtmlAttr = HtmlAttr::constant("aria-valuenow"); +pub const aria_valuetext: HtmlAttr = HtmlAttr::constant("aria-valuetext"); +pub const r#as: HtmlAttr = HtmlAttr::constant("as"); +pub const r#async: HtmlAttr = HtmlAttr::constant("async"); +pub const autocapitalize: HtmlAttr = HtmlAttr::constant("autocapitalize"); +pub const autocomplete: HtmlAttr = HtmlAttr::constant("autocomplete"); +pub const autocorrect: HtmlAttr = HtmlAttr::constant("autocorrect"); +pub const autofocus: HtmlAttr = HtmlAttr::constant("autofocus"); +pub const autoplay: HtmlAttr = HtmlAttr::constant("autoplay"); +pub const blocking: HtmlAttr = HtmlAttr::constant("blocking"); +pub const charset: HtmlAttr = HtmlAttr::constant("charset"); +pub const checked: HtmlAttr = HtmlAttr::constant("checked"); +pub const cite: HtmlAttr = HtmlAttr::constant("cite"); +pub const class: HtmlAttr = HtmlAttr::constant("class"); +pub const closedby: HtmlAttr = HtmlAttr::constant("closedby"); +pub const color: HtmlAttr = HtmlAttr::constant("color"); +pub const colorspace: HtmlAttr = HtmlAttr::constant("colorspace"); +pub const cols: HtmlAttr = HtmlAttr::constant("cols"); +pub const colspan: HtmlAttr = HtmlAttr::constant("colspan"); +pub const command: HtmlAttr = HtmlAttr::constant("command"); +pub const commandfor: HtmlAttr = HtmlAttr::constant("commandfor"); +pub const content: HtmlAttr = HtmlAttr::constant("content"); +pub const contenteditable: HtmlAttr = HtmlAttr::constant("contenteditable"); +pub const controls: HtmlAttr = HtmlAttr::constant("controls"); +pub const coords: HtmlAttr = HtmlAttr::constant("coords"); +pub const crossorigin: HtmlAttr = HtmlAttr::constant("crossorigin"); +pub const data: HtmlAttr = HtmlAttr::constant("data"); +pub const datetime: HtmlAttr = HtmlAttr::constant("datetime"); +pub const decoding: HtmlAttr = HtmlAttr::constant("decoding"); +pub const default: HtmlAttr = HtmlAttr::constant("default"); +pub const defer: HtmlAttr = HtmlAttr::constant("defer"); +pub const dir: HtmlAttr = HtmlAttr::constant("dir"); +pub const dirname: HtmlAttr = HtmlAttr::constant("dirname"); +pub const disabled: HtmlAttr = HtmlAttr::constant("disabled"); +pub const download: HtmlAttr = HtmlAttr::constant("download"); +pub const draggable: HtmlAttr = HtmlAttr::constant("draggable"); +pub const enctype: HtmlAttr = HtmlAttr::constant("enctype"); +pub const enterkeyhint: HtmlAttr = HtmlAttr::constant("enterkeyhint"); +pub const fetchpriority: HtmlAttr = HtmlAttr::constant("fetchpriority"); +pub const r#for: HtmlAttr = HtmlAttr::constant("for"); +pub const form: HtmlAttr = HtmlAttr::constant("form"); +pub const formaction: HtmlAttr = HtmlAttr::constant("formaction"); +pub const formenctype: HtmlAttr = HtmlAttr::constant("formenctype"); +pub const formmethod: HtmlAttr = HtmlAttr::constant("formmethod"); +pub const formnovalidate: HtmlAttr = HtmlAttr::constant("formnovalidate"); +pub const formtarget: HtmlAttr = HtmlAttr::constant("formtarget"); +pub const headers: HtmlAttr = HtmlAttr::constant("headers"); +pub const height: HtmlAttr = HtmlAttr::constant("height"); +pub const hidden: HtmlAttr = HtmlAttr::constant("hidden"); +pub const high: HtmlAttr = HtmlAttr::constant("high"); +pub const href: HtmlAttr = HtmlAttr::constant("href"); +pub const hreflang: HtmlAttr = HtmlAttr::constant("hreflang"); +pub const http_equiv: HtmlAttr = HtmlAttr::constant("http-equiv"); +pub const id: HtmlAttr = HtmlAttr::constant("id"); +pub const imagesizes: HtmlAttr = HtmlAttr::constant("imagesizes"); +pub const imagesrcset: HtmlAttr = HtmlAttr::constant("imagesrcset"); +pub const inert: HtmlAttr = HtmlAttr::constant("inert"); +pub const inputmode: HtmlAttr = HtmlAttr::constant("inputmode"); +pub const integrity: HtmlAttr = HtmlAttr::constant("integrity"); +pub const is: HtmlAttr = HtmlAttr::constant("is"); +pub const ismap: HtmlAttr = HtmlAttr::constant("ismap"); +pub const itemid: HtmlAttr = HtmlAttr::constant("itemid"); +pub const itemprop: HtmlAttr = HtmlAttr::constant("itemprop"); +pub const itemref: HtmlAttr = HtmlAttr::constant("itemref"); +pub const itemscope: HtmlAttr = HtmlAttr::constant("itemscope"); +pub const itemtype: HtmlAttr = HtmlAttr::constant("itemtype"); +pub const kind: HtmlAttr = HtmlAttr::constant("kind"); +pub const label: HtmlAttr = HtmlAttr::constant("label"); +pub const lang: HtmlAttr = HtmlAttr::constant("lang"); +pub const list: HtmlAttr = HtmlAttr::constant("list"); +pub const loading: HtmlAttr = HtmlAttr::constant("loading"); +pub const r#loop: HtmlAttr = HtmlAttr::constant("loop"); +pub const low: HtmlAttr = HtmlAttr::constant("low"); +pub const max: HtmlAttr = HtmlAttr::constant("max"); +pub const maxlength: HtmlAttr = HtmlAttr::constant("maxlength"); +pub const media: HtmlAttr = HtmlAttr::constant("media"); +pub const method: HtmlAttr = HtmlAttr::constant("method"); +pub const min: HtmlAttr = HtmlAttr::constant("min"); +pub const minlength: HtmlAttr = HtmlAttr::constant("minlength"); +pub const multiple: HtmlAttr = HtmlAttr::constant("multiple"); +pub const muted: HtmlAttr = HtmlAttr::constant("muted"); +pub const name: HtmlAttr = HtmlAttr::constant("name"); +pub const nomodule: HtmlAttr = HtmlAttr::constant("nomodule"); +pub const nonce: HtmlAttr = HtmlAttr::constant("nonce"); +pub const novalidate: HtmlAttr = HtmlAttr::constant("novalidate"); +pub const open: HtmlAttr = HtmlAttr::constant("open"); +pub const optimum: HtmlAttr = HtmlAttr::constant("optimum"); +pub const pattern: HtmlAttr = HtmlAttr::constant("pattern"); +pub const ping: HtmlAttr = HtmlAttr::constant("ping"); +pub const placeholder: HtmlAttr = HtmlAttr::constant("placeholder"); +pub const playsinline: HtmlAttr = HtmlAttr::constant("playsinline"); +pub const popover: HtmlAttr = HtmlAttr::constant("popover"); +pub const popovertarget: HtmlAttr = HtmlAttr::constant("popovertarget"); +pub const popovertargetaction: HtmlAttr = HtmlAttr::constant("popovertargetaction"); +pub const poster: HtmlAttr = HtmlAttr::constant("poster"); +pub const preload: HtmlAttr = HtmlAttr::constant("preload"); +pub const readonly: HtmlAttr = HtmlAttr::constant("readonly"); +pub const referrerpolicy: HtmlAttr = HtmlAttr::constant("referrerpolicy"); +pub const rel: HtmlAttr = HtmlAttr::constant("rel"); +pub const required: HtmlAttr = HtmlAttr::constant("required"); +pub const reversed: HtmlAttr = HtmlAttr::constant("reversed"); +pub const role: HtmlAttr = HtmlAttr::constant("role"); +pub const rows: HtmlAttr = HtmlAttr::constant("rows"); +pub const rowspan: HtmlAttr = HtmlAttr::constant("rowspan"); +pub const sandbox: HtmlAttr = HtmlAttr::constant("sandbox"); +pub const scope: HtmlAttr = HtmlAttr::constant("scope"); +pub const selected: HtmlAttr = HtmlAttr::constant("selected"); +pub const shadowrootclonable: HtmlAttr = HtmlAttr::constant("shadowrootclonable"); +pub const shadowrootcustomelementregistry: HtmlAttr = HtmlAttr::constant("shadowrootcustomelementregistry"); +pub const shadowrootdelegatesfocus: HtmlAttr = HtmlAttr::constant("shadowrootdelegatesfocus"); +pub const shadowrootmode: HtmlAttr = HtmlAttr::constant("shadowrootmode"); +pub const shadowrootserializable: HtmlAttr = HtmlAttr::constant("shadowrootserializable"); +pub const shape: HtmlAttr = HtmlAttr::constant("shape"); +pub const size: HtmlAttr = HtmlAttr::constant("size"); +pub const sizes: HtmlAttr = HtmlAttr::constant("sizes"); +pub const slot: HtmlAttr = HtmlAttr::constant("slot"); +pub const span: HtmlAttr = HtmlAttr::constant("span"); +pub const spellcheck: HtmlAttr = HtmlAttr::constant("spellcheck"); +pub const src: HtmlAttr = HtmlAttr::constant("src"); +pub const srcdoc: HtmlAttr = HtmlAttr::constant("srcdoc"); +pub const srclang: HtmlAttr = HtmlAttr::constant("srclang"); +pub const srcset: HtmlAttr = HtmlAttr::constant("srcset"); +pub const start: HtmlAttr = HtmlAttr::constant("start"); +pub const step: HtmlAttr = HtmlAttr::constant("step"); +pub const style: HtmlAttr = HtmlAttr::constant("style"); +pub const tabindex: HtmlAttr = HtmlAttr::constant("tabindex"); +pub const target: HtmlAttr = HtmlAttr::constant("target"); +pub const title: HtmlAttr = HtmlAttr::constant("title"); +pub const translate: HtmlAttr = HtmlAttr::constant("translate"); +pub const r#type: HtmlAttr = HtmlAttr::constant("type"); +pub const usemap: HtmlAttr = HtmlAttr::constant("usemap"); +pub const value: HtmlAttr = HtmlAttr::constant("value"); +pub const width: HtmlAttr = HtmlAttr::constant("width"); +pub const wrap: HtmlAttr = HtmlAttr::constant("wrap"); +pub const writingsuggestions: HtmlAttr = HtmlAttr::constant("writingsuggestions"); diff --git a/crates/typst-html/src/charsets.rs b/crates/typst-html/src/charsets.rs new file mode 100644 index 000000000..251ff15c9 --- /dev/null +++ b/crates/typst-html/src/charsets.rs @@ -0,0 +1,81 @@ +//! Defines syntactical properties of HTML tags, attributes, and text. + +/// Check whether a character is in a tag name. +pub const fn is_valid_in_tag_name(c: char) -> bool { + c.is_ascii_alphanumeric() +} + +/// Check whether a character is valid in an attribute name. +pub const fn is_valid_in_attribute_name(c: char) -> bool { + match c { + // These are forbidden. + '\0' | ' ' | '"' | '\'' | '>' | '/' | '=' => false, + c if is_whatwg_control_char(c) => false, + c if is_whatwg_non_char(c) => false, + // _Everything_ else is allowed, including U+2029 paragraph + // separator. Go wild. + _ => true, + } +} + +/// Check whether a character can be an used in an attribute value without +/// escaping. +/// +/// See +pub const fn is_valid_in_attribute_value(c: char) -> bool { + match c { + // Ampersands are sometimes legal (i.e. when they are not _ambiguous + // ampersands_) but it is not worth the trouble to check for that. + '&' => false, + // Quotation marks are not allowed in double-quote-delimited attribute + // values. + '"' => false, + // All other text characters are allowed. + c => is_w3c_text_char(c), + } +} + +/// Check whether a character can be an used in normal text without +/// escaping. +pub const fn is_valid_in_normal_element_text(c: char) -> bool { + match c { + // Ampersands are sometimes legal (i.e. when they are not _ambiguous + // ampersands_) but it is not worth the trouble to check for that. + '&' => false, + // Less-than signs are not allowed in text. + '<' => false, + // All other text characters are allowed. + c => is_w3c_text_char(c), + } +} + +/// Check if something is valid text in HTML. +pub const fn is_w3c_text_char(c: char) -> bool { + match c { + // Non-characters are obviously not text characters. + c if is_whatwg_non_char(c) => false, + // Control characters are disallowed, except for whitespace. + c if is_whatwg_control_char(c) => c.is_ascii_whitespace(), + // Everything else is allowed. + _ => true, + } +} + +const fn is_whatwg_non_char(c: char) -> bool { + match c { + '\u{fdd0}'..='\u{fdef}' => true, + // Non-characters matching xxFFFE or xxFFFF up to x10FFFF (inclusive). + c if c as u32 & 0xfffe == 0xfffe && c as u32 <= 0x10ffff => true, + _ => false, + } +} + +const fn is_whatwg_control_char(c: char) -> bool { + match c { + // C0 control characters. + '\u{00}'..='\u{1f}' => true, + // Other control characters. + '\u{7f}'..='\u{9f}' => true, + _ => false, + } +} diff --git a/crates/typst-html/src/convert.rs b/crates/typst-html/src/convert.rs new file mode 100644 index 000000000..49995e0aa --- /dev/null +++ b/crates/typst-html/src/convert.rs @@ -0,0 +1,125 @@ +use typst_library::diag::{warning, SourceResult}; +use typst_library::engine::Engine; +use typst_library::foundations::{Content, StyleChain, Target, TargetElem}; +use typst_library::introspection::{SplitLocator, TagElem}; +use typst_library::layout::{Abs, Axes, BlockBody, BlockElem, BoxElem, Region, Size}; +use typst_library::model::ParElem; +use typst_library::routines::Pair; +use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem}; + +use crate::fragment::html_fragment; +use crate::{attr, tag, FrameElem, HtmlElem, HtmlElement, HtmlFrame, HtmlNode}; + +/// Converts realized content into HTML nodes. +pub fn convert_to_nodes<'a>( + engine: &mut Engine, + locator: &mut SplitLocator, + children: impl IntoIterator>, +) -> SourceResult> { + let mut output = Vec::new(); + for (child, styles) in children { + handle(engine, child, locator, styles, &mut output)?; + } + Ok(output) +} + +/// Convert one element into HTML node(s). +fn handle( + engine: &mut Engine, + child: &Content, + locator: &mut SplitLocator, + styles: StyleChain, + output: &mut Vec, +) -> SourceResult<()> { + if let Some(elem) = child.to_packed::() { + output.push(HtmlNode::Tag(elem.tag.clone())); + } else if let Some(elem) = child.to_packed::() { + let mut children = vec![]; + if let Some(body) = elem.body.get_ref(styles) { + children = html_fragment(engine, body, locator.next(&elem.span()), styles)?; + } + let element = HtmlElement { + tag: elem.tag, + attrs: elem.attrs.get_cloned(styles), + children, + span: elem.span(), + }; + output.push(element.into()); + } else if let Some(elem) = child.to_packed::() { + let children = + html_fragment(engine, &elem.body, locator.next(&elem.span()), styles)?; + output.push( + HtmlElement::new(tag::p) + .with_children(children) + .spanned(elem.span()) + .into(), + ); + } else if let Some(elem) = child.to_packed::() { + // TODO: This is rather incomplete. + if let Some(body) = elem.body.get_ref(styles) { + let children = + html_fragment(engine, body, locator.next(&elem.span()), styles)?; + output.push( + HtmlElement::new(tag::span) + .with_attr(attr::style, "display: inline-block;") + .with_children(children) + .spanned(elem.span()) + .into(), + ) + } + } else if let Some((elem, body)) = + child + .to_packed::() + .and_then(|elem| match elem.body.get_ref(styles) { + Some(BlockBody::Content(body)) => Some((elem, body)), + _ => None, + }) + { + // TODO: This is rather incomplete. + let children = html_fragment(engine, body, locator.next(&elem.span()), styles)?; + output.push( + HtmlElement::new(tag::div) + .with_children(children) + .spanned(elem.span()) + .into(), + ); + } else if child.is::() { + output.push(HtmlNode::text(' ', child.span())); + } else if let Some(elem) = child.to_packed::() { + output.push(HtmlNode::text(elem.text.clone(), elem.span())); + } else if let Some(elem) = child.to_packed::() { + output.push(HtmlElement::new(tag::br).spanned(elem.span()).into()); + } else if let Some(elem) = child.to_packed::() { + output.push(HtmlNode::text( + if elem.double.get(styles) { '"' } else { '\'' }, + child.span(), + )); + } else if let Some(elem) = child.to_packed::() { + let locator = locator.next(&elem.span()); + let style = TargetElem::target.set(Target::Paged).wrap(); + let frame = (engine.routines.layout_frame)( + engine, + &elem.body, + locator, + styles.chain(&style), + Region::new(Size::splat(Abs::inf()), Axes::splat(false)), + )?; + output.push(HtmlNode::Frame(HtmlFrame { + inner: frame, + text_size: styles.resolve(TextElem::size), + })); + } else { + engine.sink.warn(warning!( + child.span(), + "{} was ignored during HTML export", + child.elem().name() + )); + } + Ok(()) +} + +/// Checks whether the given element is an inline-level HTML element. +pub fn is_inline(elem: &Content) -> bool { + elem.to_packed::() + .is_some_and(|elem| tag::is_inline_by_default(elem.tag)) +} diff --git a/crates/typst-html/src/css.rs b/crates/typst-html/src/css.rs index 6c84cba0f..5916d3147 100644 --- a/crates/typst-html/src/css.rs +++ b/crates/typst-html/src/css.rs @@ -3,28 +3,10 @@ use std::fmt::{self, Display, Write}; use ecow::EcoString; -use typst_library::html::{attr, HtmlElem}; use typst_library::layout::{Length, Rel}; use typst_library::visualize::{Color, Hsl, LinearRgb, Oklab, Oklch, Rgb}; use typst_utils::Numeric; -/// Additional methods for [`HtmlElem`]. -pub trait HtmlElemExt { - /// Adds the styles to an element if the property list is non-empty. - fn with_styles(self, properties: Properties) -> Self; -} - -impl HtmlElemExt for HtmlElem { - /// Adds CSS styles to an element. - fn with_styles(self, properties: Properties) -> Self { - if let Some(value) = properties.into_inline_styles() { - self.with_attr(attr::style, value) - } else { - self - } - } -} - /// A list of CSS properties with values. #[derive(Debug, Default)] pub struct Properties(EcoString); diff --git a/crates/typst-html/src/document.rs b/crates/typst-html/src/document.rs new file mode 100644 index 000000000..9f0124e57 --- /dev/null +++ b/crates/typst-html/src/document.rs @@ -0,0 +1,219 @@ +use std::num::NonZeroUsize; + +use comemo::{Tracked, TrackedMut}; +use typst_library::diag::{bail, SourceResult}; +use typst_library::engine::{Engine, Route, Sink, Traced}; +use typst_library::foundations::{Content, StyleChain}; +use typst_library::introspection::{Introspector, IntrospectorBuilder, Locator}; +use typst_library::layout::{Point, Position, Transform}; +use typst_library::model::DocumentInfo; +use typst_library::routines::{Arenas, RealizationKind, Routines}; +use typst_library::World; +use typst_syntax::Span; +use typst_utils::NonZeroExt; + +use crate::{attr, tag, HtmlDocument, HtmlElement, HtmlNode}; + +/// Produce an HTML document from content. +/// +/// This first performs root-level realization and then turns the resulting +/// elements into HTML. +#[typst_macros::time(name = "html document")] +pub fn html_document( + engine: &mut Engine, + content: &Content, + styles: StyleChain, +) -> SourceResult { + html_document_impl( + engine.routines, + engine.world, + engine.introspector, + engine.traced, + TrackedMut::reborrow_mut(&mut engine.sink), + engine.route.track(), + content, + styles, + ) +} + +/// The internal implementation of `html_document`. +#[comemo::memoize] +#[allow(clippy::too_many_arguments)] +fn html_document_impl( + routines: &Routines, + world: Tracked, + introspector: Tracked, + traced: Tracked, + sink: TrackedMut, + route: Tracked, + content: &Content, + styles: StyleChain, +) -> SourceResult { + let mut locator = Locator::root().split(); + let mut engine = Engine { + routines, + world, + introspector, + traced, + sink, + route: Route::extend(route).unnested(), + }; + + // Mark the external styles as "outside" so that they are valid at the page + // level. + let styles = styles.to_map().outside(); + let styles = StyleChain::new(&styles); + + let arenas = Arenas::default(); + let mut info = DocumentInfo::default(); + let children = (engine.routines.realize)( + RealizationKind::HtmlDocument { + info: &mut info, + is_inline: crate::convert::is_inline, + }, + &mut engine, + &mut locator, + &arenas, + content, + styles, + )?; + + let output = crate::convert::convert_to_nodes( + &mut engine, + &mut locator, + children.iter().copied(), + )?; + let introspector = introspect_html(&output); + let root = root_element(output, &info)?; + + Ok(HtmlDocument { info, root, introspector }) +} + +/// Introspects HTML nodes. +#[typst_macros::time(name = "introspect html")] +fn introspect_html(output: &[HtmlNode]) -> Introspector { + fn discover( + builder: &mut IntrospectorBuilder, + sink: &mut Vec<(Content, Position)>, + nodes: &[HtmlNode], + ) { + for node in nodes { + match node { + HtmlNode::Tag(tag) => builder.discover_in_tag( + sink, + tag, + Position { page: NonZeroUsize::ONE, point: Point::zero() }, + ), + HtmlNode::Text(_, _) => {} + HtmlNode::Element(elem) => discover(builder, sink, &elem.children), + HtmlNode::Frame(frame) => builder.discover_in_frame( + sink, + &frame.inner, + NonZeroUsize::ONE, + Transform::identity(), + ), + } + } + } + + let mut elems = Vec::new(); + let mut builder = IntrospectorBuilder::new(); + discover(&mut builder, &mut elems, output); + builder.finalize(elems) +} + +/// Wrap the nodes in `` and `` if they are not yet rooted, +/// supplying a suitable ``. +fn root_element(output: Vec, info: &DocumentInfo) -> SourceResult { + let head = head_element(info); + let body = match classify_output(output)? { + OutputKind::Html(element) => return Ok(element), + OutputKind::Body(body) => body, + OutputKind::Leafs(leafs) => HtmlElement::new(tag::body).with_children(leafs), + }; + Ok(HtmlElement::new(tag::html).with_children(vec![head.into(), body.into()])) +} + +/// Generate a `` element. +fn head_element(info: &DocumentInfo) -> HtmlElement { + let mut children = vec![]; + + children.push(HtmlElement::new(tag::meta).with_attr(attr::charset, "utf-8").into()); + + children.push( + HtmlElement::new(tag::meta) + .with_attr(attr::name, "viewport") + .with_attr(attr::content, "width=device-width, initial-scale=1") + .into(), + ); + + if let Some(title) = &info.title { + children.push( + HtmlElement::new(tag::title) + .with_children(vec![HtmlNode::Text(title.clone(), Span::detached())]) + .into(), + ); + } + + if let Some(description) = &info.description { + children.push( + HtmlElement::new(tag::meta) + .with_attr(attr::name, "description") + .with_attr(attr::content, description.clone()) + .into(), + ); + } + + if !info.author.is_empty() { + children.push( + HtmlElement::new(tag::meta) + .with_attr(attr::name, "authors") + .with_attr(attr::content, info.author.join(", ")) + .into(), + ) + } + + if !info.keywords.is_empty() { + children.push( + HtmlElement::new(tag::meta) + .with_attr(attr::name, "keywords") + .with_attr(attr::content, info.keywords.join(", ")) + .into(), + ) + } + + HtmlElement::new(tag::head).with_children(children) +} + +/// Determine which kind of output the user generated. +fn classify_output(mut output: Vec) -> SourceResult { + let count = output.iter().filter(|node| !matches!(node, HtmlNode::Tag(_))).count(); + for node in &mut output { + let HtmlNode::Element(elem) = node else { continue }; + let tag = elem.tag; + let mut take = || std::mem::replace(elem, HtmlElement::new(tag::html)); + match (tag, count) { + (tag::html, 1) => return Ok(OutputKind::Html(take())), + (tag::body, 1) => return Ok(OutputKind::Body(take())), + (tag::html | tag::body, _) => bail!( + elem.span, + "`{}` element must be the only element in the document", + elem.tag, + ), + _ => {} + } + } + Ok(OutputKind::Leafs(output)) +} + +/// What kinds of output the user generated. +enum OutputKind { + /// The user generated their own `` element. We do not need to supply + /// one. + Html(HtmlElement), + /// The user generate their own `` element. We do not need to supply + /// one, but need supply the `` element. + Body(HtmlElement), + /// The user generated leafs which we wrap in a `` and ``. + Leafs(Vec), +} diff --git a/crates/typst-html/src/dom.rs b/crates/typst-html/src/dom.rs new file mode 100644 index 000000000..cf74e1bfc --- /dev/null +++ b/crates/typst-html/src/dom.rs @@ -0,0 +1,281 @@ +use std::fmt::{self, Debug, Display, Formatter}; + +use ecow::{EcoString, EcoVec}; +use typst_library::diag::{bail, HintedStrResult, StrResult}; +use typst_library::foundations::{cast, Dict, Repr, Str}; +use typst_library::introspection::{Introspector, Tag}; +use typst_library::layout::{Abs, Frame}; +use typst_library::model::DocumentInfo; +use typst_syntax::Span; +use typst_utils::{PicoStr, ResolvedPicoStr}; + +use crate::charsets; + +/// An HTML document. +#[derive(Debug, Clone)] +pub struct HtmlDocument { + /// The document's root HTML element. + pub root: HtmlElement, + /// Details about the document. + pub info: DocumentInfo, + /// Provides the ability to execute queries on the document. + pub introspector: Introspector, +} + +/// A child of an HTML element. +#[derive(Debug, Clone, Hash)] +pub enum HtmlNode { + /// An introspectable element that produced something within this node. + Tag(Tag), + /// Plain text. + Text(EcoString, Span), + /// Another element. + Element(HtmlElement), + /// Layouted content that will be embedded into HTML as an SVG. + Frame(HtmlFrame), +} + +impl HtmlNode { + /// Create a plain text node. + pub fn text(text: impl Into, span: Span) -> Self { + Self::Text(text.into(), span) + } +} + +impl From for HtmlNode { + fn from(element: HtmlElement) -> Self { + Self::Element(element) + } +} + +/// An HTML element. +#[derive(Debug, Clone, Hash)] +pub struct HtmlElement { + /// The HTML tag. + pub tag: HtmlTag, + /// The element's attributes. + pub attrs: HtmlAttrs, + /// The element's children. + pub children: Vec, + /// The span from which the element originated, if any. + pub span: Span, +} + +impl HtmlElement { + /// Create a new, blank element without attributes or children. + pub fn new(tag: HtmlTag) -> Self { + Self { + tag, + attrs: HtmlAttrs::default(), + children: vec![], + span: Span::detached(), + } + } + + /// Attach children to the element. + /// + /// Note: This overwrites potential previous children. + pub fn with_children(mut self, children: Vec) -> Self { + self.children = children; + self + } + + /// Add an atribute to the element. + pub fn with_attr(mut self, key: HtmlAttr, value: impl Into) -> Self { + self.attrs.push(key, value); + self + } + + /// Attach a span to the element. + pub fn spanned(mut self, span: Span) -> Self { + self.span = span; + self + } +} + +/// The tag of an HTML element. +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct HtmlTag(PicoStr); + +impl HtmlTag { + /// Intern an HTML tag string at runtime. + pub fn intern(string: &str) -> StrResult { + if string.is_empty() { + bail!("tag name must not be empty"); + } + + if let Some(c) = string.chars().find(|&c| !charsets::is_valid_in_tag_name(c)) { + bail!("the character {} is not valid in a tag name", c.repr()); + } + + Ok(Self(PicoStr::intern(string))) + } + + /// Creates a compile-time constant `HtmlTag`. + /// + /// Should only be used in const contexts because it can panic. + #[track_caller] + pub const fn constant(string: &'static str) -> Self { + if string.is_empty() { + panic!("tag name must not be empty"); + } + + let bytes = string.as_bytes(); + let mut i = 0; + while i < bytes.len() { + if !bytes[i].is_ascii() || !charsets::is_valid_in_tag_name(bytes[i] as char) { + panic!("not all characters are valid in a tag name"); + } + i += 1; + } + + Self(PicoStr::constant(string)) + } + + /// Resolves the tag to a string. + pub fn resolve(self) -> ResolvedPicoStr { + self.0.resolve() + } + + /// Turns the tag into its inner interned string. + pub const fn into_inner(self) -> PicoStr { + self.0 + } +} + +impl Debug for HtmlTag { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + Display::fmt(self, f) + } +} + +impl Display for HtmlTag { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "<{}>", self.resolve()) + } +} + +cast! { + HtmlTag, + self => self.0.resolve().as_str().into_value(), + v: Str => Self::intern(&v)?, +} + +/// Attributes of an HTML element. +#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] +pub struct HtmlAttrs(pub EcoVec<(HtmlAttr, EcoString)>); + +impl HtmlAttrs { + /// Creates an empty attribute list. + pub fn new() -> Self { + Self::default() + } + + /// Add an attribute. + pub fn push(&mut self, attr: HtmlAttr, value: impl Into) { + self.0.push((attr, value.into())); + } +} + +cast! { + HtmlAttrs, + self => self.0 + .into_iter() + .map(|(key, value)| (key.resolve().as_str().into(), value.into_value())) + .collect::() + .into_value(), + values: Dict => Self(values + .into_iter() + .map(|(k, v)| { + let attr = HtmlAttr::intern(&k)?; + let value = v.cast::()?; + Ok((attr, value)) + }) + .collect::>()?), +} + +/// An attribute of an HTML element. +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct HtmlAttr(PicoStr); + +impl HtmlAttr { + /// Intern an HTML attribute string at runtime. + pub fn intern(string: &str) -> StrResult { + if string.is_empty() { + bail!("attribute name must not be empty"); + } + + if let Some(c) = + string.chars().find(|&c| !charsets::is_valid_in_attribute_name(c)) + { + bail!("the character {} is not valid in an attribute name", c.repr()); + } + + Ok(Self(PicoStr::intern(string))) + } + + /// Creates a compile-time constant `HtmlAttr`. + /// + /// Must only be used in const contexts (in a constant definition or + /// explicit `const { .. }` block) because otherwise a panic for a malformed + /// attribute or not auto-internible constant will only be caught at + /// runtime. + #[track_caller] + pub const fn constant(string: &'static str) -> Self { + if string.is_empty() { + panic!("attribute name must not be empty"); + } + + let bytes = string.as_bytes(); + let mut i = 0; + while i < bytes.len() { + if !bytes[i].is_ascii() + || !charsets::is_valid_in_attribute_name(bytes[i] as char) + { + panic!("not all characters are valid in an attribute name"); + } + i += 1; + } + + Self(PicoStr::constant(string)) + } + + /// Resolves the attribute to a string. + pub fn resolve(self) -> ResolvedPicoStr { + self.0.resolve() + } + + /// Turns the attribute into its inner interned string. + pub const fn into_inner(self) -> PicoStr { + self.0 + } +} + +impl Debug for HtmlAttr { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + Display::fmt(self, f) + } +} + +impl Display for HtmlAttr { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.resolve()) + } +} + +cast! { + HtmlAttr, + self => self.0.resolve().as_str().into_value(), + v: Str => Self::intern(&v)?, +} + +/// Layouted content that will be embedded into HTML as an SVG. +#[derive(Debug, Clone, Hash)] +pub struct HtmlFrame { + /// The frame that will be displayed as an SVG. + pub inner: Frame, + /// The text size where the frame was defined. This is used to size the + /// frame with em units to make text in and outside of the frame sized + /// consistently. + pub text_size: Abs, +} diff --git a/crates/typst-html/src/encode.rs b/crates/typst-html/src/encode.rs index 84860dbe9..be8137399 100644 --- a/crates/typst-html/src/encode.rs +++ b/crates/typst-html/src/encode.rs @@ -2,10 +2,11 @@ use std::fmt::Write; use typst_library::diag::{bail, At, SourceResult, StrResult}; use typst_library::foundations::Repr; -use typst_library::html::{ +use typst_syntax::Span; + +use crate::{ attr, charsets, tag, HtmlDocument, HtmlElement, HtmlFrame, HtmlNode, HtmlTag, }; -use typst_syntax::Span; /// Encodes an HTML document into a string. pub fn html(document: &HtmlDocument) -> SourceResult { diff --git a/crates/typst-html/src/fragment.rs b/crates/typst-html/src/fragment.rs new file mode 100644 index 000000000..78ae7dee0 --- /dev/null +++ b/crates/typst-html/src/fragment.rs @@ -0,0 +1,76 @@ +use comemo::{Track, Tracked, TrackedMut}; +use typst_library::diag::{At, SourceResult}; +use typst_library::engine::{Engine, Route, Sink, Traced}; +use typst_library::foundations::{Content, StyleChain}; +use typst_library::introspection::{Introspector, Locator, LocatorLink}; + +use typst_library::routines::{Arenas, FragmentKind, RealizationKind, Routines}; +use typst_library::World; + +use crate::HtmlNode; + +/// Produce HTML nodes from content. +#[typst_macros::time(name = "html fragment")] +pub fn html_fragment( + engine: &mut Engine, + content: &Content, + locator: Locator, + styles: StyleChain, +) -> SourceResult> { + html_fragment_impl( + engine.routines, + engine.world, + engine.introspector, + engine.traced, + TrackedMut::reborrow_mut(&mut engine.sink), + engine.route.track(), + content, + locator.track(), + styles, + ) +} + +/// The cached, internal implementation of [`html_fragment`]. +#[comemo::memoize] +#[allow(clippy::too_many_arguments)] +fn html_fragment_impl( + routines: &Routines, + world: Tracked, + introspector: Tracked, + traced: Tracked, + sink: TrackedMut, + route: Tracked, + content: &Content, + locator: Tracked, + styles: StyleChain, +) -> SourceResult> { + let link = LocatorLink::new(locator); + let mut locator = Locator::link(&link).split(); + let mut engine = Engine { + routines, + world, + introspector, + traced, + sink, + route: Route::extend(route), + }; + + engine.route.check_html_depth().at(content.span())?; + + let arenas = Arenas::default(); + let children = (engine.routines.realize)( + // No need to know about the `FragmentKind` because we handle both + // uniformly. + RealizationKind::HtmlFragment { + kind: &mut FragmentKind::Block, + is_inline: crate::convert::is_inline, + }, + &mut engine, + &mut locator, + &arenas, + content, + styles, + )?; + + crate::convert::convert_to_nodes(&mut engine, &mut locator, children.iter().copied()) +} diff --git a/crates/typst-html/src/lib.rs b/crates/typst-html/src/lib.rs index 7063931b7..d7b29dbbc 100644 --- a/crates/typst-html/src/lib.rs +++ b/crates/typst-html/src/lib.rs @@ -1,33 +1,28 @@ //! Typst's HTML exporter. +mod attr; +mod charsets; +mod convert; mod css; +mod document; +mod dom; mod encode; +mod fragment; mod rules; +mod tag; mod typed; +pub use self::document::html_document; +pub use self::dom::*; pub use self::encode::html; pub use self::rules::register; -use comemo::{Track, Tracked, TrackedMut}; -use typst_library::diag::{bail, warning, At, SourceResult}; -use typst_library::engine::{Engine, Route, Sink, Traced}; -use typst_library::foundations::{ - Content, Module, Scope, StyleChain, Target, TargetElem, -}; -use typst_library::html::{ - attr, tag, FrameElem, HtmlDocument, HtmlElem, HtmlElement, HtmlFrame, HtmlNode, -}; -use typst_library::introspection::{ - Introspector, Locator, LocatorLink, SplitLocator, TagElem, -}; -use typst_library::layout::{Abs, Axes, BlockBody, BlockElem, BoxElem, Region, Size}; -use typst_library::model::{DocumentInfo, ParElem}; -use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind, Routines}; -use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem}; -use typst_library::{Category, World}; -use typst_syntax::Span; +use ecow::EcoString; +use typst_library::foundations::{Content, Module, Scope}; +use typst_library::Category; +use typst_macros::elem; -/// Create a module with all HTML definitions. +/// Creates the module with all HTML definitions. pub fn module() -> Module { let mut html = Scope::deduplicating(); html.start_category(Category::Html); @@ -37,337 +32,77 @@ pub fn module() -> Module { Module::new("html", html) } -/// Produce an HTML document from content. +/// An HTML element that can contain Typst content. /// -/// This first performs root-level realization and then turns the resulting -/// elements into HTML. -#[typst_macros::time(name = "html document")] -pub fn html_document( - engine: &mut Engine, - content: &Content, - styles: StyleChain, -) -> SourceResult { - html_document_impl( - engine.routines, - engine.world, - engine.introspector, - engine.traced, - TrackedMut::reborrow_mut(&mut engine.sink), - engine.route.track(), - content, - styles, - ) +/// Typst's HTML export automatically generates the appropriate tags for most +/// elements. However, sometimes, it is desirable to retain more control. For +/// example, when using Typst to generate your blog, you could use this function +/// to wrap each article in an `
` tag. +/// +/// Typst is aware of what is valid HTML. A tag and its attributes must form +/// syntactically valid HTML. Some tags, like `meta` do not accept content. +/// Hence, you must not provide a body for them. We may add more checks in the +/// future, so be sure that you are generating valid HTML when using this +/// function. +/// +/// Normally, Typst will generate `html`, `head`, and `body` tags for you. If +/// you instead create them with this function, Typst will omit its own tags. +/// +/// ```typ +/// #html.elem("div", attrs: (style: "background: aqua"))[ +/// A div with _Typst content_ inside! +/// ] +/// ``` +#[elem(name = "elem")] +pub struct HtmlElem { + /// The element's tag. + #[required] + pub tag: HtmlTag, + + /// The element's HTML attributes. + pub attrs: HtmlAttrs, + + /// The contents of the HTML element. + /// + /// The body can be arbitrary Typst content. + #[positional] + pub body: Option, } -/// The internal implementation of `html_document`. -#[comemo::memoize] -#[allow(clippy::too_many_arguments)] -fn html_document_impl( - routines: &Routines, - world: Tracked, - introspector: Tracked, - traced: Tracked, - sink: TrackedMut, - route: Tracked, - content: &Content, - styles: StyleChain, -) -> SourceResult { - let mut locator = Locator::root().split(); - let mut engine = Engine { - routines, - world, - introspector, - traced, - sink, - route: Route::extend(route).unnested(), - }; - - // Mark the external styles as "outside" so that they are valid at the page - // level. - let styles = styles.to_map().outside(); - let styles = StyleChain::new(&styles); - - let arenas = Arenas::default(); - let mut info = DocumentInfo::default(); - let children = (engine.routines.realize)( - RealizationKind::HtmlDocument(&mut info), - &mut engine, - &mut locator, - &arenas, - content, - styles, - )?; - - let output = handle_list(&mut engine, &mut locator, children.iter().copied())?; - let introspector = Introspector::html(&output); - let root = root_element(output, &info)?; - - Ok(HtmlDocument { info, root, introspector }) -} - -/// Produce HTML nodes from content. -#[typst_macros::time(name = "html fragment")] -pub fn html_fragment( - engine: &mut Engine, - content: &Content, - locator: Locator, - styles: StyleChain, -) -> SourceResult> { - html_fragment_impl( - engine.routines, - engine.world, - engine.introspector, - engine.traced, - TrackedMut::reborrow_mut(&mut engine.sink), - engine.route.track(), - content, - locator.track(), - styles, - ) -} - -/// The cached, internal implementation of [`html_fragment`]. -#[comemo::memoize] -#[allow(clippy::too_many_arguments)] -fn html_fragment_impl( - routines: &Routines, - world: Tracked, - introspector: Tracked, - traced: Tracked, - sink: TrackedMut, - route: Tracked, - content: &Content, - locator: Tracked, - styles: StyleChain, -) -> SourceResult> { - let link = LocatorLink::new(locator); - let mut locator = Locator::link(&link).split(); - let mut engine = Engine { - routines, - world, - introspector, - traced, - sink, - route: Route::extend(route), - }; - - engine.route.check_html_depth().at(content.span())?; - - let arenas = Arenas::default(); - let children = (engine.routines.realize)( - // No need to know about the `FragmentKind` because we handle both - // uniformly. - RealizationKind::HtmlFragment(&mut FragmentKind::Block), - &mut engine, - &mut locator, - &arenas, - content, - styles, - )?; - - handle_list(&mut engine, &mut locator, children.iter().copied()) -} - -/// Convert children into HTML nodes. -fn handle_list<'a>( - engine: &mut Engine, - locator: &mut SplitLocator, - children: impl IntoIterator>, -) -> SourceResult> { - let mut output = Vec::new(); - for (child, styles) in children { - handle(engine, child, locator, styles, &mut output)?; - } - Ok(output) -} - -/// Convert a child into HTML node(s). -fn handle( - engine: &mut Engine, - child: &Content, - locator: &mut SplitLocator, - styles: StyleChain, - output: &mut Vec, -) -> SourceResult<()> { - if let Some(elem) = child.to_packed::() { - output.push(HtmlNode::Tag(elem.tag.clone())); - } else if let Some(elem) = child.to_packed::() { - let mut children = vec![]; - if let Some(body) = elem.body.get_ref(styles) { - children = html_fragment(engine, body, locator.next(&elem.span()), styles)?; - } - let element = HtmlElement { - tag: elem.tag, - attrs: elem.attrs.get_cloned(styles), - children, - span: elem.span(), - }; - output.push(element.into()); - } else if let Some(elem) = child.to_packed::() { - let children = - html_fragment(engine, &elem.body, locator.next(&elem.span()), styles)?; - output.push( - HtmlElement::new(tag::p) - .with_children(children) - .spanned(elem.span()) - .into(), - ); - } else if let Some(elem) = child.to_packed::() { - // TODO: This is rather incomplete. - if let Some(body) = elem.body.get_ref(styles) { - let children = - html_fragment(engine, body, locator.next(&elem.span()), styles)?; - output.push( - HtmlElement::new(tag::span) - .with_attr(attr::style, "display: inline-block;") - .with_children(children) - .spanned(elem.span()) - .into(), - ) - } - } else if let Some((elem, body)) = - child - .to_packed::() - .and_then(|elem| match elem.body.get_ref(styles) { - Some(BlockBody::Content(body)) => Some((elem, body)), - _ => None, - }) - { - // TODO: This is rather incomplete. - let children = html_fragment(engine, body, locator.next(&elem.span()), styles)?; - output.push( - HtmlElement::new(tag::div) - .with_children(children) - .spanned(elem.span()) - .into(), - ); - } else if child.is::() { - output.push(HtmlNode::text(' ', child.span())); - } else if let Some(elem) = child.to_packed::() { - output.push(HtmlNode::text(elem.text.clone(), elem.span())); - } else if let Some(elem) = child.to_packed::() { - output.push(HtmlElement::new(tag::br).spanned(elem.span()).into()); - } else if let Some(elem) = child.to_packed::() { - output.push(HtmlNode::text( - if elem.double.get(styles) { '"' } else { '\'' }, - child.span(), - )); - } else if let Some(elem) = child.to_packed::() { - let locator = locator.next(&elem.span()); - let style = TargetElem::target.set(Target::Paged).wrap(); - let frame = (engine.routines.layout_frame)( - engine, - &elem.body, - locator, - styles.chain(&style), - Region::new(Size::splat(Abs::inf()), Axes::splat(false)), - )?; - output.push(HtmlNode::Frame(HtmlFrame { - inner: frame, - text_size: styles.resolve(TextElem::size), - })); - } else { - engine.sink.warn(warning!( - child.span(), - "{} was ignored during HTML export", - child.elem().name() - )); - } - Ok(()) -} - -/// Wrap the nodes in `` and `` if they are not yet rooted, -/// supplying a suitable ``. -fn root_element(output: Vec, info: &DocumentInfo) -> SourceResult { - let head = head_element(info); - let body = match classify_output(output)? { - OutputKind::Html(element) => return Ok(element), - OutputKind::Body(body) => body, - OutputKind::Leafs(leafs) => HtmlElement::new(tag::body).with_children(leafs), - }; - Ok(HtmlElement::new(tag::html).with_children(vec![head.into(), body.into()])) -} - -/// Generate a `` element. -fn head_element(info: &DocumentInfo) -> HtmlElement { - let mut children = vec![]; - - children.push(HtmlElement::new(tag::meta).with_attr(attr::charset, "utf-8").into()); - - children.push( - HtmlElement::new(tag::meta) - .with_attr(attr::name, "viewport") - .with_attr(attr::content, "width=device-width, initial-scale=1") - .into(), - ); - - if let Some(title) = &info.title { - children.push( - HtmlElement::new(tag::title) - .with_children(vec![HtmlNode::Text(title.clone(), Span::detached())]) - .into(), - ); +impl HtmlElem { + /// Add an attribute to the element. + pub fn with_attr(mut self, attr: HtmlAttr, value: impl Into) -> Self { + self.attrs + .as_option_mut() + .get_or_insert_with(Default::default) + .push(attr, value); + self } - if let Some(description) = &info.description { - children.push( - HtmlElement::new(tag::meta) - .with_attr(attr::name, "description") - .with_attr(attr::content, description.clone()) - .into(), - ); - } - - if !info.author.is_empty() { - children.push( - HtmlElement::new(tag::meta) - .with_attr(attr::name, "authors") - .with_attr(attr::content, info.author.join(", ")) - .into(), - ) - } - - if !info.keywords.is_empty() { - children.push( - HtmlElement::new(tag::meta) - .with_attr(attr::name, "keywords") - .with_attr(attr::content, info.keywords.join(", ")) - .into(), - ) - } - - HtmlElement::new(tag::head).with_children(children) -} - -/// Determine which kind of output the user generated. -fn classify_output(mut output: Vec) -> SourceResult { - let count = output.iter().filter(|node| !matches!(node, HtmlNode::Tag(_))).count(); - for node in &mut output { - let HtmlNode::Element(elem) = node else { continue }; - let tag = elem.tag; - let mut take = || std::mem::replace(elem, HtmlElement::new(tag::html)); - match (tag, count) { - (tag::html, 1) => return Ok(OutputKind::Html(take())), - (tag::body, 1) => return Ok(OutputKind::Body(take())), - (tag::html | tag::body, _) => bail!( - elem.span, - "`{}` element must be the only element in the document", - elem.tag, - ), - _ => {} + /// Adds CSS styles to an element. + fn with_styles(self, properties: css::Properties) -> Self { + if let Some(value) = properties.into_inline_styles() { + self.with_attr(attr::style, value) + } else { + self } } - Ok(OutputKind::Leafs(output)) } -/// What kinds of output the user generated. -enum OutputKind { - /// The user generated their own `` element. We do not need to supply - /// one. - Html(HtmlElement), - /// The user generate their own `` element. We do not need to supply - /// one, but need supply the `` element. - Body(HtmlElement), - /// The user generated leafs which we wrap in a `` and ``. - Leafs(Vec), +/// An element that lays out its content as an inline SVG. +/// +/// Sometimes, converting Typst content to HTML is not desirable. This can be +/// the case for plots and other content that relies on positioning and styling +/// to convey its message. +/// +/// This function allows you to use the Typst layout engine that would also be +/// used for PDF, SVG, and PNG export to render a part of your document exactly +/// how it would appear when exported in one of these formats. It embeds the +/// content as an inline SVG. +#[elem] +pub struct FrameElem { + /// The content that shall be laid out. + #[positional] + #[required] + pub body: Content, } diff --git a/crates/typst-html/src/rules.rs b/crates/typst-html/src/rules.rs index 5bf25e79b..04a58ca47 100644 --- a/crates/typst-html/src/rules.rs +++ b/crates/typst-html/src/rules.rs @@ -5,7 +5,6 @@ use typst_library::diag::warning; use typst_library::foundations::{ Content, NativeElement, NativeRuleMap, ShowFn, Smart, StyleChain, Target, }; -use typst_library::html::{attr, tag, HtmlAttrs, HtmlElem, HtmlTag}; use typst_library::introspection::{Counter, Locator}; use typst_library::layout::resolve::{table_to_cellgrid, Cell, CellGrid, Entry}; use typst_library::layout::{OuterVAlignment, Sizing}; @@ -20,11 +19,11 @@ use typst_library::text::{ }; use typst_library::visualize::ImageElem; -use crate::css::{self, HtmlElemExt}; +use crate::{attr, css, tag, FrameElem, HtmlAttrs, HtmlElem, HtmlTag}; -/// Register show rules for the [HTML target](Target::Html). +/// Registers show rules for the [HTML target](Target::Html). pub fn register(rules: &mut NativeRuleMap) { - use Target::Html; + use Target::{Html, Paged}; // Model. rules.register(Html, STRONG_RULE); @@ -53,6 +52,11 @@ pub fn register(rules: &mut NativeRuleMap) { // Visualize. rules.register(Html, IMAGE_RULE); + + // For the HTML target, `html.frame` is a primitive. In the laid-out target, + // it should be a no-op so that nested frames don't break (things like `show + // math.equation: html.frame` can result in nested ones). + rules.register::(Paged, |elem, _, _| Ok(elem.body.clone())); } const STRONG_RULE: ShowFn = |elem, _, _| { diff --git a/crates/typst-html/src/tag.rs b/crates/typst-html/src/tag.rs new file mode 100644 index 000000000..89c50e1a8 --- /dev/null +++ b/crates/typst-html/src/tag.rs @@ -0,0 +1,271 @@ +//! Predefined constants for HTML tags. + +#![allow(non_upper_case_globals)] +#![allow(dead_code)] + +use crate::HtmlTag; + +pub const a: HtmlTag = HtmlTag::constant("a"); +pub const abbr: HtmlTag = HtmlTag::constant("abbr"); +pub const address: HtmlTag = HtmlTag::constant("address"); +pub const area: HtmlTag = HtmlTag::constant("area"); +pub const article: HtmlTag = HtmlTag::constant("article"); +pub const aside: HtmlTag = HtmlTag::constant("aside"); +pub const audio: HtmlTag = HtmlTag::constant("audio"); +pub const b: HtmlTag = HtmlTag::constant("b"); +pub const base: HtmlTag = HtmlTag::constant("base"); +pub const bdi: HtmlTag = HtmlTag::constant("bdi"); +pub const bdo: HtmlTag = HtmlTag::constant("bdo"); +pub const blockquote: HtmlTag = HtmlTag::constant("blockquote"); +pub const body: HtmlTag = HtmlTag::constant("body"); +pub const br: HtmlTag = HtmlTag::constant("br"); +pub const button: HtmlTag = HtmlTag::constant("button"); +pub const canvas: HtmlTag = HtmlTag::constant("canvas"); +pub const caption: HtmlTag = HtmlTag::constant("caption"); +pub const cite: HtmlTag = HtmlTag::constant("cite"); +pub const code: HtmlTag = HtmlTag::constant("code"); +pub const col: HtmlTag = HtmlTag::constant("col"); +pub const colgroup: HtmlTag = HtmlTag::constant("colgroup"); +pub const data: HtmlTag = HtmlTag::constant("data"); +pub const datalist: HtmlTag = HtmlTag::constant("datalist"); +pub const dd: HtmlTag = HtmlTag::constant("dd"); +pub const del: HtmlTag = HtmlTag::constant("del"); +pub const details: HtmlTag = HtmlTag::constant("details"); +pub const dfn: HtmlTag = HtmlTag::constant("dfn"); +pub const dialog: HtmlTag = HtmlTag::constant("dialog"); +pub const div: HtmlTag = HtmlTag::constant("div"); +pub const dl: HtmlTag = HtmlTag::constant("dl"); +pub const dt: HtmlTag = HtmlTag::constant("dt"); +pub const em: HtmlTag = HtmlTag::constant("em"); +pub const embed: HtmlTag = HtmlTag::constant("embed"); +pub const fieldset: HtmlTag = HtmlTag::constant("fieldset"); +pub const figcaption: HtmlTag = HtmlTag::constant("figcaption"); +pub const figure: HtmlTag = HtmlTag::constant("figure"); +pub const footer: HtmlTag = HtmlTag::constant("footer"); +pub const form: HtmlTag = HtmlTag::constant("form"); +pub const h1: HtmlTag = HtmlTag::constant("h1"); +pub const h2: HtmlTag = HtmlTag::constant("h2"); +pub const h3: HtmlTag = HtmlTag::constant("h3"); +pub const h4: HtmlTag = HtmlTag::constant("h4"); +pub const h5: HtmlTag = HtmlTag::constant("h5"); +pub const h6: HtmlTag = HtmlTag::constant("h6"); +pub const head: HtmlTag = HtmlTag::constant("head"); +pub const header: HtmlTag = HtmlTag::constant("header"); +pub const hgroup: HtmlTag = HtmlTag::constant("hgroup"); +pub const hr: HtmlTag = HtmlTag::constant("hr"); +pub const html: HtmlTag = HtmlTag::constant("html"); +pub const i: HtmlTag = HtmlTag::constant("i"); +pub const iframe: HtmlTag = HtmlTag::constant("iframe"); +pub const img: HtmlTag = HtmlTag::constant("img"); +pub const input: HtmlTag = HtmlTag::constant("input"); +pub const ins: HtmlTag = HtmlTag::constant("ins"); +pub const kbd: HtmlTag = HtmlTag::constant("kbd"); +pub const label: HtmlTag = HtmlTag::constant("label"); +pub const legend: HtmlTag = HtmlTag::constant("legend"); +pub const li: HtmlTag = HtmlTag::constant("li"); +pub const link: HtmlTag = HtmlTag::constant("link"); +pub const main: HtmlTag = HtmlTag::constant("main"); +pub const map: HtmlTag = HtmlTag::constant("map"); +pub const mark: HtmlTag = HtmlTag::constant("mark"); +pub const menu: HtmlTag = HtmlTag::constant("menu"); +pub const meta: HtmlTag = HtmlTag::constant("meta"); +pub const meter: HtmlTag = HtmlTag::constant("meter"); +pub const nav: HtmlTag = HtmlTag::constant("nav"); +pub const noscript: HtmlTag = HtmlTag::constant("noscript"); +pub const object: HtmlTag = HtmlTag::constant("object"); +pub const ol: HtmlTag = HtmlTag::constant("ol"); +pub const optgroup: HtmlTag = HtmlTag::constant("optgroup"); +pub const option: HtmlTag = HtmlTag::constant("option"); +pub const output: HtmlTag = HtmlTag::constant("output"); +pub const p: HtmlTag = HtmlTag::constant("p"); +pub const picture: HtmlTag = HtmlTag::constant("picture"); +pub const pre: HtmlTag = HtmlTag::constant("pre"); +pub const progress: HtmlTag = HtmlTag::constant("progress"); +pub const q: HtmlTag = HtmlTag::constant("q"); +pub const rp: HtmlTag = HtmlTag::constant("rp"); +pub const rt: HtmlTag = HtmlTag::constant("rt"); +pub const ruby: HtmlTag = HtmlTag::constant("ruby"); +pub const s: HtmlTag = HtmlTag::constant("s"); +pub const samp: HtmlTag = HtmlTag::constant("samp"); +pub const script: HtmlTag = HtmlTag::constant("script"); +pub const search: HtmlTag = HtmlTag::constant("search"); +pub const section: HtmlTag = HtmlTag::constant("section"); +pub const select: HtmlTag = HtmlTag::constant("select"); +pub const slot: HtmlTag = HtmlTag::constant("slot"); +pub const small: HtmlTag = HtmlTag::constant("small"); +pub const source: HtmlTag = HtmlTag::constant("source"); +pub const span: HtmlTag = HtmlTag::constant("span"); +pub const strong: HtmlTag = HtmlTag::constant("strong"); +pub const style: HtmlTag = HtmlTag::constant("style"); +pub const sub: HtmlTag = HtmlTag::constant("sub"); +pub const summary: HtmlTag = HtmlTag::constant("summary"); +pub const sup: HtmlTag = HtmlTag::constant("sup"); +pub const table: HtmlTag = HtmlTag::constant("table"); +pub const tbody: HtmlTag = HtmlTag::constant("tbody"); +pub const td: HtmlTag = HtmlTag::constant("td"); +pub const template: HtmlTag = HtmlTag::constant("template"); +pub const textarea: HtmlTag = HtmlTag::constant("textarea"); +pub const tfoot: HtmlTag = HtmlTag::constant("tfoot"); +pub const th: HtmlTag = HtmlTag::constant("th"); +pub const thead: HtmlTag = HtmlTag::constant("thead"); +pub const time: HtmlTag = HtmlTag::constant("time"); +pub const title: HtmlTag = HtmlTag::constant("title"); +pub const tr: HtmlTag = HtmlTag::constant("tr"); +pub const track: HtmlTag = HtmlTag::constant("track"); +pub const u: HtmlTag = HtmlTag::constant("u"); +pub const ul: HtmlTag = HtmlTag::constant("ul"); +pub const var: HtmlTag = HtmlTag::constant("var"); +pub const video: HtmlTag = HtmlTag::constant("video"); +pub const wbr: HtmlTag = HtmlTag::constant("wbr"); + +/// Whether this is a void tag whose associated element may not have +/// children. +pub fn is_void(tag: HtmlTag) -> bool { + matches!( + tag, + self::area + | self::base + | self::br + | self::col + | self::embed + | self::hr + | self::img + | self::input + | self::link + | self::meta + | self::source + | self::track + | self::wbr + ) +} + +/// Whether this is a tag containing raw text. +pub fn is_raw(tag: HtmlTag) -> bool { + matches!(tag, self::script | self::style) +} + +/// Whether this is a tag containing escapable raw text. +pub fn is_escapable_raw(tag: HtmlTag) -> bool { + matches!(tag, self::textarea | self::title) +} + +/// Whether an element is considered metadata. +pub fn is_metadata(tag: HtmlTag) -> bool { + matches!( + tag, + self::base + | self::link + | self::meta + | self::noscript + | self::script + | self::style + | self::template + | self::title + ) +} + +/// Whether nodes with the tag have the CSS property `display: block` by +/// default. +pub fn is_block_by_default(tag: HtmlTag) -> bool { + matches!( + tag, + self::html + | self::head + | self::body + | self::article + | self::aside + | self::h1 + | self::h2 + | self::h3 + | self::h4 + | self::h5 + | self::h6 + | self::hgroup + | self::nav + | self::section + | self::dd + | self::dl + | self::dt + | self::menu + | self::ol + | self::ul + | self::address + | self::blockquote + | self::dialog + | self::div + | self::fieldset + | self::figure + | self::figcaption + | self::footer + | self::form + | self::header + | self::hr + | self::legend + | self::main + | self::p + | self::pre + | self::search + ) +} + +/// Whether the element is inline-level as opposed to being block-level. +/// +/// Not sure whether this distinction really makes sense. But we somehow +/// need to decide what to put into automatic paragraphs. A `` +/// should merged into a paragraph created by realization, but a `
` +/// shouldn't. +/// +/// +/// +/// +pub fn is_inline_by_default(tag: HtmlTag) -> bool { + matches!( + tag, + self::abbr + | self::a + | self::bdi + | self::b + | self::br + | self::bdo + | self::code + | self::cite + | self::dfn + | self::data + | self::i + | self::em + | self::mark + | self::kbd + | self::rp + | self::q + | self::ruby + | self::rt + | self::samp + | self::s + | self::span + | self::small + | self::sub + | self::strong + | self::time + | self::sup + | self::var + | self::u + ) +} + +/// Whether nodes with the tag have the CSS property `display: table(-.*)?` +/// by default. +pub fn is_tabular_by_default(tag: HtmlTag) -> bool { + matches!( + tag, + self::table + | self::thead + | self::tbody + | self::tfoot + | self::tr + | self::th + | self::td + | self::caption + | self::col + | self::colgroup + ) +} diff --git a/crates/typst-html/src/typed.rs b/crates/typst-html/src/typed.rs index 4b794bbba..190ff4f16 100644 --- a/crates/typst-html/src/typed.rs +++ b/crates/typst-html/src/typed.rs @@ -18,13 +18,11 @@ use typst_library::foundations::{ FromValue, IntoValue, NativeFuncData, NativeFuncPtr, NoneValue, ParamInfo, PositiveF64, Reflect, Scope, Str, Type, Value, }; -use typst_library::html::tag; -use typst_library::html::{HtmlAttr, HtmlAttrs, HtmlElem, HtmlTag}; use typst_library::layout::{Axes, Axis, Dir, Length}; use typst_library::visualize::Color; use typst_macros::cast; -use crate::css; +use crate::{css, tag, HtmlAttr, HtmlAttrs, HtmlElem, HtmlTag}; /// Hook up all typed HTML definitions. pub(super) fn define(html: &mut Scope) { diff --git a/crates/typst-layout/src/flow/mod.rs b/crates/typst-layout/src/flow/mod.rs index f4f1c0915..cb029dce8 100644 --- a/crates/typst-layout/src/flow/mod.rs +++ b/crates/typst-layout/src/flow/mod.rs @@ -143,7 +143,7 @@ fn layout_fragment_impl( let mut kind = FragmentKind::Block; let arenas = Arenas::default(); let children = (engine.routines.realize)( - RealizationKind::LayoutFragment(&mut kind), + RealizationKind::LayoutFragment { kind: &mut kind }, &mut engine, &mut locator, &arenas, diff --git a/crates/typst-layout/src/pages/mod.rs b/crates/typst-layout/src/pages/mod.rs index 14dc0f3fb..ec0dc2c05 100644 --- a/crates/typst-layout/src/pages/mod.rs +++ b/crates/typst-layout/src/pages/mod.rs @@ -4,14 +4,16 @@ mod collect; mod finalize; mod run; +use std::num::NonZeroUsize; + use comemo::{Tracked, TrackedMut}; use typst_library::diag::SourceResult; use typst_library::engine::{Engine, Route, Sink, Traced}; use typst_library::foundations::{Content, StyleChain}; use typst_library::introspection::{ - Introspector, Locator, ManualPageCounter, SplitLocator, TagElem, + Introspector, IntrospectorBuilder, Locator, ManualPageCounter, SplitLocator, TagElem, }; -use typst_library::layout::{FrameItem, Page, PagedDocument, Point}; +use typst_library::layout::{FrameItem, Page, PagedDocument, Point, Transform}; use typst_library::model::DocumentInfo; use typst_library::routines::{Arenas, Pair, RealizationKind, Routines}; use typst_library::World; @@ -75,7 +77,7 @@ fn layout_document_impl( let arenas = Arenas::default(); let mut info = DocumentInfo::default(); let mut children = (engine.routines.realize)( - RealizationKind::LayoutDocument(&mut info), + RealizationKind::LayoutDocument { info: &mut info }, &mut engine, &mut locator, &arenas, @@ -84,7 +86,7 @@ fn layout_document_impl( )?; let pages = layout_pages(&mut engine, &mut children, &mut locator, styles)?; - let introspector = Introspector::paged(&pages); + let introspector = introspect_pages(&pages); Ok(PagedDocument { pages, info, introspector }) } @@ -157,3 +159,27 @@ fn layout_pages<'a>( Ok(pages) } + +/// Introspects pages. +#[typst_macros::time(name = "introspect pages")] +fn introspect_pages(pages: &[Page]) -> Introspector { + let mut builder = IntrospectorBuilder::new(); + builder.pages = pages.len(); + builder.page_numberings.reserve(pages.len()); + builder.page_supplements.reserve(pages.len()); + + // Discover all elements. + let mut elems = Vec::new(); + for (i, page) in pages.iter().enumerate() { + builder.page_numberings.push(page.numbering.clone()); + builder.page_supplements.push(page.supplement.clone()); + builder.discover_in_frame( + &mut elems, + &page.frame, + NonZeroUsize::new(1 + i).unwrap(), + Transform::identity(), + ); + } + + builder.finalize(elems) +} diff --git a/crates/typst-library/src/html/dom.rs b/crates/typst-library/src/html/dom.rs deleted file mode 100644 index 49ff37c45..000000000 --- a/crates/typst-library/src/html/dom.rs +++ /dev/null @@ -1,828 +0,0 @@ -use std::fmt::{self, Debug, Display, Formatter}; - -use ecow::{EcoString, EcoVec}; -use typst_syntax::Span; -use typst_utils::{PicoStr, ResolvedPicoStr}; - -use crate::diag::{bail, HintedStrResult, StrResult}; -use crate::foundations::{cast, Dict, Repr, Str}; -use crate::introspection::{Introspector, Tag}; -use crate::layout::{Abs, Frame}; -use crate::model::DocumentInfo; - -/// An HTML document. -#[derive(Debug, Clone)] -pub struct HtmlDocument { - /// The document's root HTML element. - pub root: HtmlElement, - /// Details about the document. - pub info: DocumentInfo, - /// Provides the ability to execute queries on the document. - pub introspector: Introspector, -} - -/// A child of an HTML element. -#[derive(Debug, Clone, Hash)] -pub enum HtmlNode { - /// An introspectable element that produced something within this node. - Tag(Tag), - /// Plain text. - Text(EcoString, Span), - /// Another element. - Element(HtmlElement), - /// Layouted content that will be embedded into HTML as an SVG. - Frame(HtmlFrame), -} - -impl HtmlNode { - /// Create a plain text node. - pub fn text(text: impl Into, span: Span) -> Self { - Self::Text(text.into(), span) - } -} - -impl From for HtmlNode { - fn from(element: HtmlElement) -> Self { - Self::Element(element) - } -} - -/// An HTML element. -#[derive(Debug, Clone, Hash)] -pub struct HtmlElement { - /// The HTML tag. - pub tag: HtmlTag, - /// The element's attributes. - pub attrs: HtmlAttrs, - /// The element's children. - pub children: Vec, - /// The span from which the element originated, if any. - pub span: Span, -} - -impl HtmlElement { - /// Create a new, blank element without attributes or children. - pub fn new(tag: HtmlTag) -> Self { - Self { - tag, - attrs: HtmlAttrs::default(), - children: vec![], - span: Span::detached(), - } - } - - /// Attach children to the element. - /// - /// Note: This overwrites potential previous children. - pub fn with_children(mut self, children: Vec) -> Self { - self.children = children; - self - } - - /// Add an atribute to the element. - pub fn with_attr(mut self, key: HtmlAttr, value: impl Into) -> Self { - self.attrs.push(key, value); - self - } - - /// Attach a span to the element. - pub fn spanned(mut self, span: Span) -> Self { - self.span = span; - self - } -} - -/// The tag of an HTML element. -#[derive(Copy, Clone, Eq, PartialEq, Hash)] -pub struct HtmlTag(PicoStr); - -impl HtmlTag { - /// Intern an HTML tag string at runtime. - pub fn intern(string: &str) -> StrResult { - if string.is_empty() { - bail!("tag name must not be empty"); - } - - if let Some(c) = string.chars().find(|&c| !charsets::is_valid_in_tag_name(c)) { - bail!("the character {} is not valid in a tag name", c.repr()); - } - - Ok(Self(PicoStr::intern(string))) - } - - /// Creates a compile-time constant `HtmlTag`. - /// - /// Should only be used in const contexts because it can panic. - #[track_caller] - pub const fn constant(string: &'static str) -> Self { - if string.is_empty() { - panic!("tag name must not be empty"); - } - - let bytes = string.as_bytes(); - let mut i = 0; - while i < bytes.len() { - if !bytes[i].is_ascii() || !charsets::is_valid_in_tag_name(bytes[i] as char) { - panic!("not all characters are valid in a tag name"); - } - i += 1; - } - - Self(PicoStr::constant(string)) - } - - /// Resolves the tag to a string. - pub fn resolve(self) -> ResolvedPicoStr { - self.0.resolve() - } - - /// Turns the tag into its inner interned string. - pub const fn into_inner(self) -> PicoStr { - self.0 - } -} - -impl Debug for HtmlTag { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - Display::fmt(self, f) - } -} - -impl Display for HtmlTag { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "<{}>", self.resolve()) - } -} - -cast! { - HtmlTag, - self => self.0.resolve().as_str().into_value(), - v: Str => Self::intern(&v)?, -} - -/// Attributes of an HTML element. -#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] -pub struct HtmlAttrs(pub EcoVec<(HtmlAttr, EcoString)>); - -impl HtmlAttrs { - /// Creates an empty attribute list. - pub fn new() -> Self { - Self::default() - } - - /// Add an attribute. - pub fn push(&mut self, attr: HtmlAttr, value: impl Into) { - self.0.push((attr, value.into())); - } -} - -cast! { - HtmlAttrs, - self => self.0 - .into_iter() - .map(|(key, value)| (key.resolve().as_str().into(), value.into_value())) - .collect::() - .into_value(), - values: Dict => Self(values - .into_iter() - .map(|(k, v)| { - let attr = HtmlAttr::intern(&k)?; - let value = v.cast::()?; - Ok((attr, value)) - }) - .collect::>()?), -} - -/// An attribute of an HTML element. -#[derive(Copy, Clone, Eq, PartialEq, Hash)] -pub struct HtmlAttr(PicoStr); - -impl HtmlAttr { - /// Intern an HTML attribute string at runtime. - pub fn intern(string: &str) -> StrResult { - if string.is_empty() { - bail!("attribute name must not be empty"); - } - - if let Some(c) = - string.chars().find(|&c| !charsets::is_valid_in_attribute_name(c)) - { - bail!("the character {} is not valid in an attribute name", c.repr()); - } - - Ok(Self(PicoStr::intern(string))) - } - - /// Creates a compile-time constant `HtmlAttr`. - /// - /// Must only be used in const contexts (in a constant definition or - /// explicit `const { .. }` block) because otherwise a panic for a malformed - /// attribute or not auto-internible constant will only be caught at - /// runtime. - #[track_caller] - pub const fn constant(string: &'static str) -> Self { - if string.is_empty() { - panic!("attribute name must not be empty"); - } - - let bytes = string.as_bytes(); - let mut i = 0; - while i < bytes.len() { - if !bytes[i].is_ascii() - || !charsets::is_valid_in_attribute_name(bytes[i] as char) - { - panic!("not all characters are valid in an attribute name"); - } - i += 1; - } - - Self(PicoStr::constant(string)) - } - - /// Resolves the attribute to a string. - pub fn resolve(self) -> ResolvedPicoStr { - self.0.resolve() - } - - /// Turns the attribute into its inner interned string. - pub const fn into_inner(self) -> PicoStr { - self.0 - } -} - -impl Debug for HtmlAttr { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - Display::fmt(self, f) - } -} - -impl Display for HtmlAttr { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.resolve()) - } -} - -cast! { - HtmlAttr, - self => self.0.resolve().as_str().into_value(), - v: Str => Self::intern(&v)?, -} - -/// Layouted content that will be embedded into HTML as an SVG. -#[derive(Debug, Clone, Hash)] -pub struct HtmlFrame { - /// The frame that will be displayed as an SVG. - pub inner: Frame, - /// The text size where the frame was defined. This is used to size the - /// frame with em units to make text in and outside of the frame sized - /// consistently. - pub text_size: Abs, -} - -/// Defines syntactical properties of HTML tags, attributes, and text. -pub mod charsets { - /// Check whether a character is in a tag name. - pub const fn is_valid_in_tag_name(c: char) -> bool { - c.is_ascii_alphanumeric() - } - - /// Check whether a character is valid in an attribute name. - pub const fn is_valid_in_attribute_name(c: char) -> bool { - match c { - // These are forbidden. - '\0' | ' ' | '"' | '\'' | '>' | '/' | '=' => false, - c if is_whatwg_control_char(c) => false, - c if is_whatwg_non_char(c) => false, - // _Everything_ else is allowed, including U+2029 paragraph - // separator. Go wild. - _ => true, - } - } - - /// Check whether a character can be an used in an attribute value without - /// escaping. - /// - /// See - pub const fn is_valid_in_attribute_value(c: char) -> bool { - match c { - // Ampersands are sometimes legal (i.e. when they are not _ambiguous - // ampersands_) but it is not worth the trouble to check for that. - '&' => false, - // Quotation marks are not allowed in double-quote-delimited attribute - // values. - '"' => false, - // All other text characters are allowed. - c => is_w3c_text_char(c), - } - } - - /// Check whether a character can be an used in normal text without - /// escaping. - pub const fn is_valid_in_normal_element_text(c: char) -> bool { - match c { - // Ampersands are sometimes legal (i.e. when they are not _ambiguous - // ampersands_) but it is not worth the trouble to check for that. - '&' => false, - // Less-than signs are not allowed in text. - '<' => false, - // All other text characters are allowed. - c => is_w3c_text_char(c), - } - } - - /// Check if something is valid text in HTML. - pub const fn is_w3c_text_char(c: char) -> bool { - match c { - // Non-characters are obviously not text characters. - c if is_whatwg_non_char(c) => false, - // Control characters are disallowed, except for whitespace. - c if is_whatwg_control_char(c) => c.is_ascii_whitespace(), - // Everything else is allowed. - _ => true, - } - } - - const fn is_whatwg_non_char(c: char) -> bool { - match c { - '\u{fdd0}'..='\u{fdef}' => true, - // Non-characters matching xxFFFE or xxFFFF up to x10FFFF (inclusive). - c if c as u32 & 0xfffe == 0xfffe && c as u32 <= 0x10ffff => true, - _ => false, - } - } - - const fn is_whatwg_control_char(c: char) -> bool { - match c { - // C0 control characters. - '\u{00}'..='\u{1f}' => true, - // Other control characters. - '\u{7f}'..='\u{9f}' => true, - _ => false, - } - } -} - -/// Predefined constants for HTML tags. -#[allow(non_upper_case_globals)] -pub mod tag { - use super::HtmlTag; - - pub const a: HtmlTag = HtmlTag::constant("a"); - pub const abbr: HtmlTag = HtmlTag::constant("abbr"); - pub const address: HtmlTag = HtmlTag::constant("address"); - pub const area: HtmlTag = HtmlTag::constant("area"); - pub const article: HtmlTag = HtmlTag::constant("article"); - pub const aside: HtmlTag = HtmlTag::constant("aside"); - pub const audio: HtmlTag = HtmlTag::constant("audio"); - pub const b: HtmlTag = HtmlTag::constant("b"); - pub const base: HtmlTag = HtmlTag::constant("base"); - pub const bdi: HtmlTag = HtmlTag::constant("bdi"); - pub const bdo: HtmlTag = HtmlTag::constant("bdo"); - pub const blockquote: HtmlTag = HtmlTag::constant("blockquote"); - pub const body: HtmlTag = HtmlTag::constant("body"); - pub const br: HtmlTag = HtmlTag::constant("br"); - pub const button: HtmlTag = HtmlTag::constant("button"); - pub const canvas: HtmlTag = HtmlTag::constant("canvas"); - pub const caption: HtmlTag = HtmlTag::constant("caption"); - pub const cite: HtmlTag = HtmlTag::constant("cite"); - pub const code: HtmlTag = HtmlTag::constant("code"); - pub const col: HtmlTag = HtmlTag::constant("col"); - pub const colgroup: HtmlTag = HtmlTag::constant("colgroup"); - pub const data: HtmlTag = HtmlTag::constant("data"); - pub const datalist: HtmlTag = HtmlTag::constant("datalist"); - pub const dd: HtmlTag = HtmlTag::constant("dd"); - pub const del: HtmlTag = HtmlTag::constant("del"); - pub const details: HtmlTag = HtmlTag::constant("details"); - pub const dfn: HtmlTag = HtmlTag::constant("dfn"); - pub const dialog: HtmlTag = HtmlTag::constant("dialog"); - pub const div: HtmlTag = HtmlTag::constant("div"); - pub const dl: HtmlTag = HtmlTag::constant("dl"); - pub const dt: HtmlTag = HtmlTag::constant("dt"); - pub const em: HtmlTag = HtmlTag::constant("em"); - pub const embed: HtmlTag = HtmlTag::constant("embed"); - pub const fieldset: HtmlTag = HtmlTag::constant("fieldset"); - pub const figcaption: HtmlTag = HtmlTag::constant("figcaption"); - pub const figure: HtmlTag = HtmlTag::constant("figure"); - pub const footer: HtmlTag = HtmlTag::constant("footer"); - pub const form: HtmlTag = HtmlTag::constant("form"); - pub const h1: HtmlTag = HtmlTag::constant("h1"); - pub const h2: HtmlTag = HtmlTag::constant("h2"); - pub const h3: HtmlTag = HtmlTag::constant("h3"); - pub const h4: HtmlTag = HtmlTag::constant("h4"); - pub const h5: HtmlTag = HtmlTag::constant("h5"); - pub const h6: HtmlTag = HtmlTag::constant("h6"); - pub const head: HtmlTag = HtmlTag::constant("head"); - pub const header: HtmlTag = HtmlTag::constant("header"); - pub const hgroup: HtmlTag = HtmlTag::constant("hgroup"); - pub const hr: HtmlTag = HtmlTag::constant("hr"); - pub const html: HtmlTag = HtmlTag::constant("html"); - pub const i: HtmlTag = HtmlTag::constant("i"); - pub const iframe: HtmlTag = HtmlTag::constant("iframe"); - pub const img: HtmlTag = HtmlTag::constant("img"); - pub const input: HtmlTag = HtmlTag::constant("input"); - pub const ins: HtmlTag = HtmlTag::constant("ins"); - pub const kbd: HtmlTag = HtmlTag::constant("kbd"); - pub const label: HtmlTag = HtmlTag::constant("label"); - pub const legend: HtmlTag = HtmlTag::constant("legend"); - pub const li: HtmlTag = HtmlTag::constant("li"); - pub const link: HtmlTag = HtmlTag::constant("link"); - pub const main: HtmlTag = HtmlTag::constant("main"); - pub const map: HtmlTag = HtmlTag::constant("map"); - pub const mark: HtmlTag = HtmlTag::constant("mark"); - pub const menu: HtmlTag = HtmlTag::constant("menu"); - pub const meta: HtmlTag = HtmlTag::constant("meta"); - pub const meter: HtmlTag = HtmlTag::constant("meter"); - pub const nav: HtmlTag = HtmlTag::constant("nav"); - pub const noscript: HtmlTag = HtmlTag::constant("noscript"); - pub const object: HtmlTag = HtmlTag::constant("object"); - pub const ol: HtmlTag = HtmlTag::constant("ol"); - pub const optgroup: HtmlTag = HtmlTag::constant("optgroup"); - pub const option: HtmlTag = HtmlTag::constant("option"); - pub const output: HtmlTag = HtmlTag::constant("output"); - pub const p: HtmlTag = HtmlTag::constant("p"); - pub const picture: HtmlTag = HtmlTag::constant("picture"); - pub const pre: HtmlTag = HtmlTag::constant("pre"); - pub const progress: HtmlTag = HtmlTag::constant("progress"); - pub const q: HtmlTag = HtmlTag::constant("q"); - pub const rp: HtmlTag = HtmlTag::constant("rp"); - pub const rt: HtmlTag = HtmlTag::constant("rt"); - pub const ruby: HtmlTag = HtmlTag::constant("ruby"); - pub const s: HtmlTag = HtmlTag::constant("s"); - pub const samp: HtmlTag = HtmlTag::constant("samp"); - pub const script: HtmlTag = HtmlTag::constant("script"); - pub const search: HtmlTag = HtmlTag::constant("search"); - pub const section: HtmlTag = HtmlTag::constant("section"); - pub const select: HtmlTag = HtmlTag::constant("select"); - pub const slot: HtmlTag = HtmlTag::constant("slot"); - pub const small: HtmlTag = HtmlTag::constant("small"); - pub const source: HtmlTag = HtmlTag::constant("source"); - pub const span: HtmlTag = HtmlTag::constant("span"); - pub const strong: HtmlTag = HtmlTag::constant("strong"); - pub const style: HtmlTag = HtmlTag::constant("style"); - pub const sub: HtmlTag = HtmlTag::constant("sub"); - pub const summary: HtmlTag = HtmlTag::constant("summary"); - pub const sup: HtmlTag = HtmlTag::constant("sup"); - pub const table: HtmlTag = HtmlTag::constant("table"); - pub const tbody: HtmlTag = HtmlTag::constant("tbody"); - pub const td: HtmlTag = HtmlTag::constant("td"); - pub const template: HtmlTag = HtmlTag::constant("template"); - pub const textarea: HtmlTag = HtmlTag::constant("textarea"); - pub const tfoot: HtmlTag = HtmlTag::constant("tfoot"); - pub const th: HtmlTag = HtmlTag::constant("th"); - pub const thead: HtmlTag = HtmlTag::constant("thead"); - pub const time: HtmlTag = HtmlTag::constant("time"); - pub const title: HtmlTag = HtmlTag::constant("title"); - pub const tr: HtmlTag = HtmlTag::constant("tr"); - pub const track: HtmlTag = HtmlTag::constant("track"); - pub const u: HtmlTag = HtmlTag::constant("u"); - pub const ul: HtmlTag = HtmlTag::constant("ul"); - pub const var: HtmlTag = HtmlTag::constant("var"); - pub const video: HtmlTag = HtmlTag::constant("video"); - pub const wbr: HtmlTag = HtmlTag::constant("wbr"); - - /// Whether this is a void tag whose associated element may not have - /// children. - pub fn is_void(tag: HtmlTag) -> bool { - matches!( - tag, - self::area - | self::base - | self::br - | self::col - | self::embed - | self::hr - | self::img - | self::input - | self::link - | self::meta - | self::source - | self::track - | self::wbr - ) - } - - /// Whether this is a tag containing raw text. - pub fn is_raw(tag: HtmlTag) -> bool { - matches!(tag, self::script | self::style) - } - - /// Whether this is a tag containing escapable raw text. - pub fn is_escapable_raw(tag: HtmlTag) -> bool { - matches!(tag, self::textarea | self::title) - } - - /// Whether an element is considered metadata. - pub fn is_metadata(tag: HtmlTag) -> bool { - matches!( - tag, - self::base - | self::link - | self::meta - | self::noscript - | self::script - | self::style - | self::template - | self::title - ) - } - - /// Whether nodes with the tag have the CSS property `display: block` by - /// default. - pub fn is_block_by_default(tag: HtmlTag) -> bool { - matches!( - tag, - self::html - | self::head - | self::body - | self::article - | self::aside - | self::h1 - | self::h2 - | self::h3 - | self::h4 - | self::h5 - | self::h6 - | self::hgroup - | self::nav - | self::section - | self::dd - | self::dl - | self::dt - | self::menu - | self::ol - | self::ul - | self::address - | self::blockquote - | self::dialog - | self::div - | self::fieldset - | self::figure - | self::figcaption - | self::footer - | self::form - | self::header - | self::hr - | self::legend - | self::main - | self::p - | self::pre - | self::search - ) - } - - /// Whether the element is inline-level as opposed to being block-level. - /// - /// Not sure whether this distinction really makes sense. But we somehow - /// need to decide what to put into automatic paragraphs. A `` - /// should merged into a paragraph created by realization, but a `
` - /// shouldn't. - /// - /// - /// - /// - pub fn is_inline_by_default(tag: HtmlTag) -> bool { - matches!( - tag, - self::abbr - | self::a - | self::bdi - | self::b - | self::br - | self::bdo - | self::code - | self::cite - | self::dfn - | self::data - | self::i - | self::em - | self::mark - | self::kbd - | self::rp - | self::q - | self::ruby - | self::rt - | self::samp - | self::s - | self::span - | self::small - | self::sub - | self::strong - | self::time - | self::sup - | self::var - | self::u - ) - } - - /// Whether nodes with the tag have the CSS property `display: table(-.*)?` - /// by default. - pub fn is_tabular_by_default(tag: HtmlTag) -> bool { - matches!( - tag, - self::table - | self::thead - | self::tbody - | self::tfoot - | self::tr - | self::th - | self::td - | self::caption - | self::col - | self::colgroup - ) - } -} - -#[allow(non_upper_case_globals)] -#[rustfmt::skip] -pub mod attr { - use crate::html::HtmlAttr; - pub const abbr: HtmlAttr = HtmlAttr::constant("abbr"); - pub const accept: HtmlAttr = HtmlAttr::constant("accept"); - pub const accept_charset: HtmlAttr = HtmlAttr::constant("accept-charset"); - pub const accesskey: HtmlAttr = HtmlAttr::constant("accesskey"); - pub const action: HtmlAttr = HtmlAttr::constant("action"); - pub const allow: HtmlAttr = HtmlAttr::constant("allow"); - pub const allowfullscreen: HtmlAttr = HtmlAttr::constant("allowfullscreen"); - pub const alpha: HtmlAttr = HtmlAttr::constant("alpha"); - pub const alt: HtmlAttr = HtmlAttr::constant("alt"); - pub const aria_activedescendant: HtmlAttr = HtmlAttr::constant("aria-activedescendant"); - pub const aria_atomic: HtmlAttr = HtmlAttr::constant("aria-atomic"); - pub const aria_autocomplete: HtmlAttr = HtmlAttr::constant("aria-autocomplete"); - pub const aria_busy: HtmlAttr = HtmlAttr::constant("aria-busy"); - pub const aria_checked: HtmlAttr = HtmlAttr::constant("aria-checked"); - pub const aria_colcount: HtmlAttr = HtmlAttr::constant("aria-colcount"); - pub const aria_colindex: HtmlAttr = HtmlAttr::constant("aria-colindex"); - pub const aria_colspan: HtmlAttr = HtmlAttr::constant("aria-colspan"); - pub const aria_controls: HtmlAttr = HtmlAttr::constant("aria-controls"); - pub const aria_current: HtmlAttr = HtmlAttr::constant("aria-current"); - pub const aria_describedby: HtmlAttr = HtmlAttr::constant("aria-describedby"); - pub const aria_details: HtmlAttr = HtmlAttr::constant("aria-details"); - pub const aria_disabled: HtmlAttr = HtmlAttr::constant("aria-disabled"); - pub const aria_errormessage: HtmlAttr = HtmlAttr::constant("aria-errormessage"); - pub const aria_expanded: HtmlAttr = HtmlAttr::constant("aria-expanded"); - pub const aria_flowto: HtmlAttr = HtmlAttr::constant("aria-flowto"); - pub const aria_haspopup: HtmlAttr = HtmlAttr::constant("aria-haspopup"); - pub const aria_hidden: HtmlAttr = HtmlAttr::constant("aria-hidden"); - pub const aria_invalid: HtmlAttr = HtmlAttr::constant("aria-invalid"); - pub const aria_keyshortcuts: HtmlAttr = HtmlAttr::constant("aria-keyshortcuts"); - pub const aria_label: HtmlAttr = HtmlAttr::constant("aria-label"); - pub const aria_labelledby: HtmlAttr = HtmlAttr::constant("aria-labelledby"); - pub const aria_level: HtmlAttr = HtmlAttr::constant("aria-level"); - pub const aria_live: HtmlAttr = HtmlAttr::constant("aria-live"); - pub const aria_modal: HtmlAttr = HtmlAttr::constant("aria-modal"); - pub const aria_multiline: HtmlAttr = HtmlAttr::constant("aria-multiline"); - pub const aria_multiselectable: HtmlAttr = HtmlAttr::constant("aria-multiselectable"); - pub const aria_orientation: HtmlAttr = HtmlAttr::constant("aria-orientation"); - pub const aria_owns: HtmlAttr = HtmlAttr::constant("aria-owns"); - pub const aria_placeholder: HtmlAttr = HtmlAttr::constant("aria-placeholder"); - pub const aria_posinset: HtmlAttr = HtmlAttr::constant("aria-posinset"); - pub const aria_pressed: HtmlAttr = HtmlAttr::constant("aria-pressed"); - pub const aria_readonly: HtmlAttr = HtmlAttr::constant("aria-readonly"); - pub const aria_relevant: HtmlAttr = HtmlAttr::constant("aria-relevant"); - pub const aria_required: HtmlAttr = HtmlAttr::constant("aria-required"); - pub const aria_roledescription: HtmlAttr = HtmlAttr::constant("aria-roledescription"); - pub const aria_rowcount: HtmlAttr = HtmlAttr::constant("aria-rowcount"); - pub const aria_rowindex: HtmlAttr = HtmlAttr::constant("aria-rowindex"); - pub const aria_rowspan: HtmlAttr = HtmlAttr::constant("aria-rowspan"); - pub const aria_selected: HtmlAttr = HtmlAttr::constant("aria-selected"); - pub const aria_setsize: HtmlAttr = HtmlAttr::constant("aria-setsize"); - pub const aria_sort: HtmlAttr = HtmlAttr::constant("aria-sort"); - pub const aria_valuemax: HtmlAttr = HtmlAttr::constant("aria-valuemax"); - pub const aria_valuemin: HtmlAttr = HtmlAttr::constant("aria-valuemin"); - pub const aria_valuenow: HtmlAttr = HtmlAttr::constant("aria-valuenow"); - pub const aria_valuetext: HtmlAttr = HtmlAttr::constant("aria-valuetext"); - pub const r#as: HtmlAttr = HtmlAttr::constant("as"); - pub const r#async: HtmlAttr = HtmlAttr::constant("async"); - pub const autocapitalize: HtmlAttr = HtmlAttr::constant("autocapitalize"); - pub const autocomplete: HtmlAttr = HtmlAttr::constant("autocomplete"); - pub const autocorrect: HtmlAttr = HtmlAttr::constant("autocorrect"); - pub const autofocus: HtmlAttr = HtmlAttr::constant("autofocus"); - pub const autoplay: HtmlAttr = HtmlAttr::constant("autoplay"); - pub const blocking: HtmlAttr = HtmlAttr::constant("blocking"); - pub const charset: HtmlAttr = HtmlAttr::constant("charset"); - pub const checked: HtmlAttr = HtmlAttr::constant("checked"); - pub const cite: HtmlAttr = HtmlAttr::constant("cite"); - pub const class: HtmlAttr = HtmlAttr::constant("class"); - pub const closedby: HtmlAttr = HtmlAttr::constant("closedby"); - pub const color: HtmlAttr = HtmlAttr::constant("color"); - pub const colorspace: HtmlAttr = HtmlAttr::constant("colorspace"); - pub const cols: HtmlAttr = HtmlAttr::constant("cols"); - pub const colspan: HtmlAttr = HtmlAttr::constant("colspan"); - pub const command: HtmlAttr = HtmlAttr::constant("command"); - pub const commandfor: HtmlAttr = HtmlAttr::constant("commandfor"); - pub const content: HtmlAttr = HtmlAttr::constant("content"); - pub const contenteditable: HtmlAttr = HtmlAttr::constant("contenteditable"); - pub const controls: HtmlAttr = HtmlAttr::constant("controls"); - pub const coords: HtmlAttr = HtmlAttr::constant("coords"); - pub const crossorigin: HtmlAttr = HtmlAttr::constant("crossorigin"); - pub const data: HtmlAttr = HtmlAttr::constant("data"); - pub const datetime: HtmlAttr = HtmlAttr::constant("datetime"); - pub const decoding: HtmlAttr = HtmlAttr::constant("decoding"); - pub const default: HtmlAttr = HtmlAttr::constant("default"); - pub const defer: HtmlAttr = HtmlAttr::constant("defer"); - pub const dir: HtmlAttr = HtmlAttr::constant("dir"); - pub const dirname: HtmlAttr = HtmlAttr::constant("dirname"); - pub const disabled: HtmlAttr = HtmlAttr::constant("disabled"); - pub const download: HtmlAttr = HtmlAttr::constant("download"); - pub const draggable: HtmlAttr = HtmlAttr::constant("draggable"); - pub const enctype: HtmlAttr = HtmlAttr::constant("enctype"); - pub const enterkeyhint: HtmlAttr = HtmlAttr::constant("enterkeyhint"); - pub const fetchpriority: HtmlAttr = HtmlAttr::constant("fetchpriority"); - pub const r#for: HtmlAttr = HtmlAttr::constant("for"); - pub const form: HtmlAttr = HtmlAttr::constant("form"); - pub const formaction: HtmlAttr = HtmlAttr::constant("formaction"); - pub const formenctype: HtmlAttr = HtmlAttr::constant("formenctype"); - pub const formmethod: HtmlAttr = HtmlAttr::constant("formmethod"); - pub const formnovalidate: HtmlAttr = HtmlAttr::constant("formnovalidate"); - pub const formtarget: HtmlAttr = HtmlAttr::constant("formtarget"); - pub const headers: HtmlAttr = HtmlAttr::constant("headers"); - pub const height: HtmlAttr = HtmlAttr::constant("height"); - pub const hidden: HtmlAttr = HtmlAttr::constant("hidden"); - pub const high: HtmlAttr = HtmlAttr::constant("high"); - pub const href: HtmlAttr = HtmlAttr::constant("href"); - pub const hreflang: HtmlAttr = HtmlAttr::constant("hreflang"); - pub const http_equiv: HtmlAttr = HtmlAttr::constant("http-equiv"); - pub const id: HtmlAttr = HtmlAttr::constant("id"); - pub const imagesizes: HtmlAttr = HtmlAttr::constant("imagesizes"); - pub const imagesrcset: HtmlAttr = HtmlAttr::constant("imagesrcset"); - pub const inert: HtmlAttr = HtmlAttr::constant("inert"); - pub const inputmode: HtmlAttr = HtmlAttr::constant("inputmode"); - pub const integrity: HtmlAttr = HtmlAttr::constant("integrity"); - pub const is: HtmlAttr = HtmlAttr::constant("is"); - pub const ismap: HtmlAttr = HtmlAttr::constant("ismap"); - pub const itemid: HtmlAttr = HtmlAttr::constant("itemid"); - pub const itemprop: HtmlAttr = HtmlAttr::constant("itemprop"); - pub const itemref: HtmlAttr = HtmlAttr::constant("itemref"); - pub const itemscope: HtmlAttr = HtmlAttr::constant("itemscope"); - pub const itemtype: HtmlAttr = HtmlAttr::constant("itemtype"); - pub const kind: HtmlAttr = HtmlAttr::constant("kind"); - pub const label: HtmlAttr = HtmlAttr::constant("label"); - pub const lang: HtmlAttr = HtmlAttr::constant("lang"); - pub const list: HtmlAttr = HtmlAttr::constant("list"); - pub const loading: HtmlAttr = HtmlAttr::constant("loading"); - pub const r#loop: HtmlAttr = HtmlAttr::constant("loop"); - pub const low: HtmlAttr = HtmlAttr::constant("low"); - pub const max: HtmlAttr = HtmlAttr::constant("max"); - pub const maxlength: HtmlAttr = HtmlAttr::constant("maxlength"); - pub const media: HtmlAttr = HtmlAttr::constant("media"); - pub const method: HtmlAttr = HtmlAttr::constant("method"); - pub const min: HtmlAttr = HtmlAttr::constant("min"); - pub const minlength: HtmlAttr = HtmlAttr::constant("minlength"); - pub const multiple: HtmlAttr = HtmlAttr::constant("multiple"); - pub const muted: HtmlAttr = HtmlAttr::constant("muted"); - pub const name: HtmlAttr = HtmlAttr::constant("name"); - pub const nomodule: HtmlAttr = HtmlAttr::constant("nomodule"); - pub const nonce: HtmlAttr = HtmlAttr::constant("nonce"); - pub const novalidate: HtmlAttr = HtmlAttr::constant("novalidate"); - pub const open: HtmlAttr = HtmlAttr::constant("open"); - pub const optimum: HtmlAttr = HtmlAttr::constant("optimum"); - pub const pattern: HtmlAttr = HtmlAttr::constant("pattern"); - pub const ping: HtmlAttr = HtmlAttr::constant("ping"); - pub const placeholder: HtmlAttr = HtmlAttr::constant("placeholder"); - pub const playsinline: HtmlAttr = HtmlAttr::constant("playsinline"); - pub const popover: HtmlAttr = HtmlAttr::constant("popover"); - pub const popovertarget: HtmlAttr = HtmlAttr::constant("popovertarget"); - pub const popovertargetaction: HtmlAttr = HtmlAttr::constant("popovertargetaction"); - pub const poster: HtmlAttr = HtmlAttr::constant("poster"); - pub const preload: HtmlAttr = HtmlAttr::constant("preload"); - pub const readonly: HtmlAttr = HtmlAttr::constant("readonly"); - pub const referrerpolicy: HtmlAttr = HtmlAttr::constant("referrerpolicy"); - pub const rel: HtmlAttr = HtmlAttr::constant("rel"); - pub const required: HtmlAttr = HtmlAttr::constant("required"); - pub const reversed: HtmlAttr = HtmlAttr::constant("reversed"); - pub const role: HtmlAttr = HtmlAttr::constant("role"); - pub const rows: HtmlAttr = HtmlAttr::constant("rows"); - pub const rowspan: HtmlAttr = HtmlAttr::constant("rowspan"); - pub const sandbox: HtmlAttr = HtmlAttr::constant("sandbox"); - pub const scope: HtmlAttr = HtmlAttr::constant("scope"); - pub const selected: HtmlAttr = HtmlAttr::constant("selected"); - pub const shadowrootclonable: HtmlAttr = HtmlAttr::constant("shadowrootclonable"); - pub const shadowrootcustomelementregistry: HtmlAttr = HtmlAttr::constant("shadowrootcustomelementregistry"); - pub const shadowrootdelegatesfocus: HtmlAttr = HtmlAttr::constant("shadowrootdelegatesfocus"); - pub const shadowrootmode: HtmlAttr = HtmlAttr::constant("shadowrootmode"); - pub const shadowrootserializable: HtmlAttr = HtmlAttr::constant("shadowrootserializable"); - pub const shape: HtmlAttr = HtmlAttr::constant("shape"); - pub const size: HtmlAttr = HtmlAttr::constant("size"); - pub const sizes: HtmlAttr = HtmlAttr::constant("sizes"); - pub const slot: HtmlAttr = HtmlAttr::constant("slot"); - pub const span: HtmlAttr = HtmlAttr::constant("span"); - pub const spellcheck: HtmlAttr = HtmlAttr::constant("spellcheck"); - pub const src: HtmlAttr = HtmlAttr::constant("src"); - pub const srcdoc: HtmlAttr = HtmlAttr::constant("srcdoc"); - pub const srclang: HtmlAttr = HtmlAttr::constant("srclang"); - pub const srcset: HtmlAttr = HtmlAttr::constant("srcset"); - pub const start: HtmlAttr = HtmlAttr::constant("start"); - pub const step: HtmlAttr = HtmlAttr::constant("step"); - pub const style: HtmlAttr = HtmlAttr::constant("style"); - pub const tabindex: HtmlAttr = HtmlAttr::constant("tabindex"); - pub const target: HtmlAttr = HtmlAttr::constant("target"); - pub const title: HtmlAttr = HtmlAttr::constant("title"); - pub const translate: HtmlAttr = HtmlAttr::constant("translate"); - pub const r#type: HtmlAttr = HtmlAttr::constant("type"); - pub const usemap: HtmlAttr = HtmlAttr::constant("usemap"); - pub const value: HtmlAttr = HtmlAttr::constant("value"); - pub const width: HtmlAttr = HtmlAttr::constant("width"); - pub const wrap: HtmlAttr = HtmlAttr::constant("wrap"); - pub const writingsuggestions: HtmlAttr = HtmlAttr::constant("writingsuggestions"); -} diff --git a/crates/typst-library/src/html/mod.rs b/crates/typst-library/src/html/mod.rs deleted file mode 100644 index ca2cc0311..000000000 --- a/crates/typst-library/src/html/mod.rs +++ /dev/null @@ -1,75 +0,0 @@ -//! HTML output. - -mod dom; - -pub use self::dom::*; - -use ecow::EcoString; - -use crate::foundations::{elem, Content}; - -/// An HTML element that can contain Typst content. -/// -/// Typst's HTML export automatically generates the appropriate tags for most -/// elements. However, sometimes, it is desirable to retain more control. For -/// example, when using Typst to generate your blog, you could use this function -/// to wrap each article in an `
` tag. -/// -/// Typst is aware of what is valid HTML. A tag and its attributes must form -/// syntactically valid HTML. Some tags, like `meta` do not accept content. -/// Hence, you must not provide a body for them. We may add more checks in the -/// future, so be sure that you are generating valid HTML when using this -/// function. -/// -/// Normally, Typst will generate `html`, `head`, and `body` tags for you. If -/// you instead create them with this function, Typst will omit its own tags. -/// -/// ```typ -/// #html.elem("div", attrs: (style: "background: aqua"))[ -/// A div with _Typst content_ inside! -/// ] -/// ``` -#[elem(name = "elem")] -pub struct HtmlElem { - /// The element's tag. - #[required] - pub tag: HtmlTag, - - /// The element's HTML attributes. - pub attrs: HtmlAttrs, - - /// The contents of the HTML element. - /// - /// The body can be arbitrary Typst content. - #[positional] - pub body: Option, -} - -impl HtmlElem { - /// Add an attribute to the element. - pub fn with_attr(mut self, attr: HtmlAttr, value: impl Into) -> Self { - self.attrs - .as_option_mut() - .get_or_insert_with(Default::default) - .push(attr, value); - self - } -} - -/// An element that lays out its content as an inline SVG. -/// -/// Sometimes, converting Typst content to HTML is not desirable. This can be -/// the case for plots and other content that relies on positioning and styling -/// to convey its message. -/// -/// This function allows you to use the Typst layout engine that would also be -/// used for PDF, SVG, and PNG export to render a part of your document exactly -/// how it would appear when exported in one of these formats. It embeds the -/// content as an inline SVG. -#[elem] -pub struct FrameElem { - /// The content that shall be laid out. - #[positional] - #[required] - pub body: Content, -} diff --git a/crates/typst-library/src/introspection/introspector.rs b/crates/typst-library/src/introspection/introspector.rs index d2ad0525b..de74c55f5 100644 --- a/crates/typst-library/src/introspection/introspector.rs +++ b/crates/typst-library/src/introspection/introspector.rs @@ -10,9 +10,8 @@ use typst_utils::NonZeroExt; use crate::diag::{bail, StrResult}; use crate::foundations::{Content, Label, Repr, Selector}; -use crate::html::HtmlNode; use crate::introspection::{Location, Tag}; -use crate::layout::{Frame, FrameItem, Page, Point, Position, Transform}; +use crate::layout::{Frame, FrameItem, Point, Position, Transform}; use crate::model::Numbering; /// Can be queried for elements and their positions. @@ -47,18 +46,6 @@ pub struct Introspector { type Pair = (Content, Position); impl Introspector { - /// Creates an introspector for a page list. - #[typst_macros::time(name = "introspect pages")] - pub fn paged(pages: &[Page]) -> Self { - IntrospectorBuilder::new().build_paged(pages) - } - - /// Creates an introspector for HTML. - #[typst_macros::time(name = "introspect html")] - pub fn html(output: &[HtmlNode]) -> Self { - IntrospectorBuilder::new().build_html(output) - } - /// Iterates over all locatable elements. pub fn all(&self) -> impl Iterator + '_ { self.elems.iter().map(|(c, _)| c) @@ -352,10 +339,10 @@ impl Clone for QueryCache { /// Builds the introspector. #[derive(Default)] -struct IntrospectorBuilder { - pages: usize, - page_numberings: Vec>, - page_supplements: Vec, +pub struct IntrospectorBuilder { + pub pages: usize, + pub page_numberings: Vec>, + pub page_supplements: Vec, seen: HashSet, insertions: MultiMap>, keys: MultiMap, @@ -365,41 +352,12 @@ struct IntrospectorBuilder { impl IntrospectorBuilder { /// Create an empty builder. - fn new() -> Self { + pub fn new() -> Self { Self::default() } - /// Build an introspector for a page list. - fn build_paged(mut self, pages: &[Page]) -> Introspector { - self.pages = pages.len(); - self.page_numberings.reserve(pages.len()); - self.page_supplements.reserve(pages.len()); - - // Discover all elements. - let mut elems = Vec::new(); - for (i, page) in pages.iter().enumerate() { - self.page_numberings.push(page.numbering.clone()); - self.page_supplements.push(page.supplement.clone()); - self.discover_in_frame( - &mut elems, - &page.frame, - NonZeroUsize::new(1 + i).unwrap(), - Transform::identity(), - ); - } - - self.finalize(elems) - } - - /// Build an introspector for an HTML document. - fn build_html(mut self, output: &[HtmlNode]) -> Introspector { - let mut elems = Vec::new(); - self.discover_in_html(&mut elems, output); - self.finalize(elems) - } - /// Processes the tags in the frame. - fn discover_in_frame( + pub fn discover_in_frame( &mut self, sink: &mut Vec, frame: &Frame, @@ -433,29 +391,13 @@ impl IntrospectorBuilder { } } - /// Processes the tags in the HTML element. - fn discover_in_html(&mut self, sink: &mut Vec, nodes: &[HtmlNode]) { - for node in nodes { - match node { - HtmlNode::Tag(tag) => self.discover_in_tag( - sink, - tag, - Position { page: NonZeroUsize::ONE, point: Point::zero() }, - ), - HtmlNode::Text(_, _) => {} - HtmlNode::Element(elem) => self.discover_in_html(sink, &elem.children), - HtmlNode::Frame(frame) => self.discover_in_frame( - sink, - &frame.inner, - NonZeroUsize::ONE, - Transform::identity(), - ), - } - } - } - /// Handle a tag. - fn discover_in_tag(&mut self, sink: &mut Vec, tag: &Tag, position: Position) { + pub fn discover_in_tag( + &mut self, + sink: &mut Vec, + tag: &Tag, + position: Position, + ) { match tag { Tag::Start(elem) => { let loc = elem.location().unwrap(); @@ -471,7 +413,7 @@ impl IntrospectorBuilder { /// Build a complete introspector with all acceleration structures from a /// list of top-level pairs. - fn finalize(mut self, root: Vec) -> Introspector { + pub fn finalize(mut self, root: Vec) -> Introspector { self.locations.reserve(self.seen.len()); // Save all pairs and their descendants in the correct order. diff --git a/crates/typst-library/src/lib.rs b/crates/typst-library/src/lib.rs index 5d047570b..025e997c6 100644 --- a/crates/typst-library/src/lib.rs +++ b/crates/typst-library/src/lib.rs @@ -15,7 +15,6 @@ extern crate self as typst_library; pub mod diag; pub mod engine; pub mod foundations; -pub mod html; pub mod introspection; pub mod layout; pub mod loading; diff --git a/crates/typst-library/src/routines.rs b/crates/typst-library/src/routines.rs index a81806fd5..01964800f 100644 --- a/crates/typst-library/src/routines.rs +++ b/crates/typst-library/src/routines.rs @@ -101,20 +101,25 @@ routines! { pub enum RealizationKind<'a> { /// This the root realization for layout. Requires a mutable reference /// to document metadata that will be filled from `set document` rules. - LayoutDocument(&'a mut DocumentInfo), + LayoutDocument { info: &'a mut DocumentInfo }, /// A nested realization in a container (e.g. a `block`). Requires a mutable /// reference to an enum that will be set to `FragmentKind::Inline` if the /// fragment's content was fully inline. - LayoutFragment(&'a mut FragmentKind), + LayoutFragment { kind: &'a mut FragmentKind }, /// A nested realization in a paragraph (i.e. a `par`) LayoutPar, - /// This the root realization for HTML. Requires a mutable reference - /// to document metadata that will be filled from `set document` rules. - HtmlDocument(&'a mut DocumentInfo), + /// This the root realization for HTML. Requires a mutable reference to + /// document metadata that will be filled from `set document` rules. + /// + /// The `is_inline` function checks whether content consists of an inline + /// HTML element. It's used by the `PAR` grouping rules. This is slightly + /// hacky and might be replaced by a mechanism to supply the grouping rules + /// as a realization user. + HtmlDocument { info: &'a mut DocumentInfo, is_inline: fn(&Content) -> bool }, /// A nested realization in a container (e.g. a `block`). Requires a mutable /// reference to an enum that will be set to `FragmentKind::Inline` if the /// fragment's content was fully inline. - HtmlFragment(&'a mut FragmentKind), + HtmlFragment { kind: &'a mut FragmentKind, is_inline: fn(&Content) -> bool }, /// A realization within math. Math, } @@ -122,18 +127,20 @@ pub enum RealizationKind<'a> { impl RealizationKind<'_> { /// It this a realization for HTML export? pub fn is_html(&self) -> bool { - matches!(self, Self::HtmlDocument(_) | Self::HtmlFragment(_)) + matches!(self, Self::HtmlDocument { .. } | Self::HtmlFragment { .. }) } /// It this a realization for a container? pub fn is_fragment(&self) -> bool { - matches!(self, Self::LayoutFragment(_) | Self::HtmlFragment(_)) + matches!(self, Self::LayoutFragment { .. } | Self::HtmlFragment { .. }) } /// If this is a document-level realization, accesses the document info. pub fn as_document_mut(&mut self) -> Option<&mut DocumentInfo> { match self { - Self::LayoutDocument(info) | Self::HtmlDocument(info) => Some(*info), + Self::LayoutDocument { info } | Self::HtmlDocument { info, .. } => { + Some(*info) + } _ => None, } } @@ -141,7 +148,9 @@ impl RealizationKind<'_> { /// If this is a container-level realization, accesses the fragment kind. pub fn as_fragment_mut(&mut self) -> Option<&mut FragmentKind> { match self { - Self::LayoutFragment(kind) | Self::HtmlFragment(kind) => Some(*kind), + Self::LayoutFragment { kind } | Self::HtmlFragment { kind, .. } => { + Some(*kind) + } _ => None, } } diff --git a/crates/typst-realize/src/lib.rs b/crates/typst-realize/src/lib.rs index 6af249cc3..5d9e0a23a 100644 --- a/crates/typst-realize/src/lib.rs +++ b/crates/typst-realize/src/lib.rs @@ -18,7 +18,6 @@ use typst_library::foundations::{ RecipeIndex, Selector, SequenceElem, ShowSet, Style, StyleChain, StyledElem, Styles, SymbolElem, Synthesize, TargetElem, Transformation, }; -use typst_library::html::{tag, FrameElem, HtmlElem}; use typst_library::introspection::{Locatable, SplitLocator, Tag, TagElem}; use typst_library::layout::{ AlignElem, BoxElem, HElem, InlineElem, PageElem, PagebreakElem, VElem, @@ -48,16 +47,16 @@ pub fn realize<'a>( locator, arenas, rules: match kind { - RealizationKind::LayoutDocument(_) => LAYOUT_RULES, - RealizationKind::LayoutFragment(_) => LAYOUT_RULES, + RealizationKind::LayoutDocument { .. } => LAYOUT_RULES, + RealizationKind::LayoutFragment { .. } => LAYOUT_RULES, RealizationKind::LayoutPar => LAYOUT_PAR_RULES, - RealizationKind::HtmlDocument(_) => HTML_DOCUMENT_RULES, - RealizationKind::HtmlFragment(_) => HTML_FRAGMENT_RULES, + RealizationKind::HtmlDocument { .. } => HTML_DOCUMENT_RULES, + RealizationKind::HtmlFragment { .. } => HTML_FRAGMENT_RULES, RealizationKind::Math => MATH_RULES, }, sink: vec![], groupings: ArrayVec::new(), - outside: matches!(kind, RealizationKind::LayoutDocument(_)), + outside: matches!(kind, RealizationKind::LayoutDocument { .. }), may_attach: false, saw_parbreak: false, kind, @@ -113,7 +112,7 @@ struct GroupingRule { /// be visible to `finish`. tags: bool, /// Defines which kinds of elements start and make up this kind of grouping. - trigger: fn(&Content, &RealizationKind) -> bool, + trigger: fn(&Content, &State) -> bool, /// Defines elements that may appear in the interior of the grouping, but /// not at the edges. inner: fn(&Content) -> bool, @@ -334,13 +333,6 @@ fn visit_kind_rules<'a>( } } - if !s.kind.is_html() { - if let Some(elem) = content.to_packed::() { - visit(s, &elem.body, styles)?; - return Ok(true); - } - } - Ok(false) } @@ -601,7 +593,7 @@ fn visit_styled<'a>( ); } } else if elem == PageElem::ELEM { - if !matches!(s.kind, RealizationKind::LayoutDocument(_)) { + if !matches!(s.kind, RealizationKind::LayoutDocument { .. }) { bail!( style.span(), "page configuration is not allowed inside of containers" @@ -659,7 +651,7 @@ fn visit_grouping_rules<'a>( content: &'a Content, styles: StyleChain<'a>, ) -> SourceResult { - let matching = s.rules.iter().find(|&rule| (rule.trigger)(content, &s.kind)); + let matching = s.rules.iter().find(|&rule| (rule.trigger)(content, s)); // Try to continue or finish an existing grouping. let mut i = 0; @@ -671,7 +663,7 @@ fn visit_grouping_rules<'a>( // If the element can be added to the active grouping, do it. if !active.interrupted - && ((active.rule.trigger)(content, &s.kind) || (active.rule.inner)(content)) + && ((active.rule.trigger)(content, s) || (active.rule.inner)(content)) { s.sink.push((content, styles)); return Ok(true); @@ -806,7 +798,7 @@ fn finish_innermost_grouping(s: &mut State) -> SourceResult<()> { let Grouping { start, rule, .. } = s.groupings.pop().unwrap(); // Trim trailing non-trigger elements. - let trimmed = s.sink[start..].trim_end_matches(|(c, _)| !(rule.trigger)(c, &s.kind)); + let trimmed = s.sink[start..].trim_end_matches(|(c, _)| !(rule.trigger)(c, s)); let end = start + trimmed.len(); let tail = s.store_slice(&s.sink[end..]); s.sink.truncate(end); @@ -885,7 +877,7 @@ static TEXTUAL: GroupingRule = GroupingRule { static PAR: GroupingRule = GroupingRule { priority: 1, tags: true, - trigger: |content, kind| { + trigger: |content, state| { let elem = content.elem(); elem == TextElem::ELEM || elem == HElem::ELEM @@ -893,10 +885,11 @@ static PAR: GroupingRule = GroupingRule { || elem == SmartQuoteElem::ELEM || elem == InlineElem::ELEM || elem == BoxElem::ELEM - || (kind.is_html() - && content - .to_packed::() - .is_some_and(|elem| tag::is_inline_by_default(elem.tag))) + || match state.kind { + RealizationKind::HtmlDocument { is_inline, .. } + | RealizationKind::HtmlFragment { is_inline, .. } => is_inline(content), + _ => false, + } }, inner: |content| content.elem() == SpaceElem::ELEM, interrupt: |elem| elem == ParElem::ELEM || elem == AlignElem::ELEM, diff --git a/crates/typst/src/lib.rs b/crates/typst/src/lib.rs index 591e5a9b9..8b4e60eee 100644 --- a/crates/typst/src/lib.rs +++ b/crates/typst/src/lib.rs @@ -43,12 +43,12 @@ use std::sync::LazyLock; use comemo::{Track, Tracked, Validate}; use ecow::{eco_format, eco_vec, EcoString, EcoVec}; +use typst_html::HtmlDocument; use typst_library::diag::{ bail, warning, FileError, SourceDiagnostic, SourceResult, Warned, }; use typst_library::engine::{Engine, Route, Sink, Traced}; use typst_library::foundations::{NativeRuleMap, StyleChain, Styles, Value}; -use typst_library::html::HtmlDocument; use typst_library::introspection::Introspector; use typst_library::layout::PagedDocument; use typst_library::routines::Routines; diff --git a/tests/src/run.rs b/tests/src/run.rs index 1d93ba392..9af5c7899 100644 --- a/tests/src/run.rs +++ b/tests/src/run.rs @@ -5,10 +5,10 @@ use std::path::PathBuf; use ecow::eco_vec; use tiny_skia as sk; use typst::diag::{SourceDiagnostic, SourceResult, Warned}; -use typst::html::HtmlDocument; use typst::layout::{Abs, Frame, FrameItem, PagedDocument, Transform}; use typst::visualize::Color; use typst::{Document, WorldExt}; +use typst_html::HtmlDocument; use typst_pdf::PdfOptions; use typst_syntax::{FileId, Lines}; From 275012d7c624be85173315286752888e20996072 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 10 Jul 2025 12:54:06 +0200 Subject: [PATCH 16/18] Handle `lower` and `upper` in HTML export (#6585) --- crates/typst-html/src/convert.rs | 7 ++++++- tests/ref/html/cases-content-html.html | 10 ++++++++++ tests/suite/text/case.typ | 4 ++++ 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 tests/ref/html/cases-content-html.html diff --git a/crates/typst-html/src/convert.rs b/crates/typst-html/src/convert.rs index 49995e0aa..171b4cb7e 100644 --- a/crates/typst-html/src/convert.rs +++ b/crates/typst-html/src/convert.rs @@ -86,7 +86,12 @@ fn handle( } else if child.is::() { output.push(HtmlNode::text(' ', child.span())); } else if let Some(elem) = child.to_packed::() { - output.push(HtmlNode::text(elem.text.clone(), elem.span())); + let text = if let Some(case) = styles.get(TextElem::case) { + case.apply(&elem.text).into() + } else { + elem.text.clone() + }; + output.push(HtmlNode::text(text, elem.span())); } else if let Some(elem) = child.to_packed::() { output.push(HtmlElement::new(tag::br).spanned(elem.span()).into()); } else if let Some(elem) = child.to_packed::() { diff --git a/tests/ref/html/cases-content-html.html b/tests/ref/html/cases-content-html.html new file mode 100644 index 000000000..0890f061a --- /dev/null +++ b/tests/ref/html/cases-content-html.html @@ -0,0 +1,10 @@ + + + + + + + +

my lower a
MY UPPER A

+ + diff --git a/tests/suite/text/case.typ b/tests/suite/text/case.typ index 964ff28b6..c045ce7a6 100644 --- a/tests/suite/text/case.typ +++ b/tests/suite/text/case.typ @@ -14,6 +14,10 @@ // Check that cases are applied to symbols nested in content #lower($H I !$.body) +--- cases-content-html html --- +#lower[MY #html.strong[Lower] #symbol("A")] \ +#upper[my #html.strong[Upper] #symbol("a")] \ + --- upper-bad-type --- // Error: 8-9 expected string or content, found integer #upper(1) From 70710deb2b813eacc79d57436f5bd4c15c215f2e Mon Sep 17 00:00:00 2001 From: "Said A." <47973576+Daaiid@users.noreply.github.com> Date: Thu, 10 Jul 2025 15:15:19 +0200 Subject: [PATCH 17/18] Deduplicate labels for code completion (#6516) --- crates/typst-ide/src/analyze.rs | 10 ++++++++++ crates/typst-ide/src/complete.rs | 19 +++++++++++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/crates/typst-ide/src/analyze.rs b/crates/typst-ide/src/analyze.rs index 76739fec0..e9fb8a7d7 100644 --- a/crates/typst-ide/src/analyze.rs +++ b/crates/typst-ide/src/analyze.rs @@ -1,3 +1,5 @@ +use std::collections::HashSet; + use comemo::Track; use ecow::{eco_vec, EcoString, EcoVec}; use typst::foundations::{Label, Styles, Value}; @@ -66,14 +68,22 @@ pub fn analyze_import(world: &dyn IdeWorld, source: &LinkedNode) -> Option (Vec<(Label, Option)>, usize) { let mut output = vec![]; + let mut seen_labels = HashSet::new(); // Labels in the document. for elem in document.introspector.all() { let Some(label) = elem.label() else { continue }; + if !seen_labels.insert(label) { + continue; + } + let details = elem .to_packed::() .and_then(|figure| match figure.caption.as_option() { diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index bc5b3e10e..0a560eb53 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -76,7 +76,7 @@ pub struct Completion { } /// A kind of item that can be completed. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum CompletionKind { /// A syntactical structure. @@ -1564,7 +1564,7 @@ mod tests { use typst::layout::PagedDocument; - use super::{autocomplete, Completion}; + use super::{autocomplete, Completion, CompletionKind}; use crate::tests::{FilePos, TestWorld, WorldLike}; /// Quote a string. @@ -1709,6 +1709,21 @@ mod tests { .must_exclude(["bib"]); } + #[test] + fn test_autocomplete_ref_identical_labels_returns_single_completion() { + let mut world = TestWorld::new("x y"); + let doc = typst::compile(&world).output.ok(); + + let end = world.main.text().len(); + world.main.edit(end..end, " @t"); + + let result = test_with_doc(&world, -1, doc.as_ref()); + let completions = result.completions(); + let label_count = + completions.iter().filter(|c| c.kind == CompletionKind::Label).count(); + assert_eq!(label_count, 1); + } + /// Test what kind of brackets we autocomplete for function calls depending /// on the function and existing parens. #[test] From 0264534928864c7aed0466d670824ac0ce5ca1a8 Mon Sep 17 00:00:00 2001 From: "Said A." <47973576+Daaiid@users.noreply.github.com> Date: Thu, 10 Jul 2025 17:02:23 +0200 Subject: [PATCH 18/18] Fix regression in reference autocomplete (#6586) --- crates/typst-ide/src/complete.rs | 45 ++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index 0a560eb53..5b6d6fd97 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -130,7 +130,14 @@ fn complete_markup(ctx: &mut CompletionContext) -> bool { return true; } - // Start of a reference: "@|" or "@he|". + // Start of a reference: "@|". + if ctx.leaf.kind() == SyntaxKind::Text && ctx.before.ends_with("@") { + ctx.from = ctx.cursor; + ctx.label_completions(); + return true; + } + + // An existing reference: "@he|". if ctx.leaf.kind() == SyntaxKind::RefMarker { ctx.from = ctx.leaf.offset() + 1; ctx.label_completions(); @@ -1644,6 +1651,19 @@ mod tests { test_with_doc(world, pos, doc.as_ref()) } + #[track_caller] + fn test_with_addition( + initial_text: &str, + addition: &str, + pos: impl FilePos, + ) -> Response { + let mut world = TestWorld::new(initial_text); + let doc = typst::compile(&world).output.ok(); + let end = world.main.text().len(); + world.main.edit(end..end, addition); + test_with_doc(&world, pos, doc.as_ref()) + } + #[track_caller] fn test_with_doc( world: impl WorldLike, @@ -1709,15 +1729,24 @@ mod tests { .must_exclude(["bib"]); } + #[test] + fn test_autocomplete_ref_function() { + test_with_addition("x", " #ref(<)", -2).must_include(["test"]); + } + + #[test] + fn test_autocomplete_ref_shorthand() { + test_with_addition("x", " @", -1).must_include(["test"]); + } + + #[test] + fn test_autocomplete_ref_shorthand_with_partial_identifier() { + test_with_addition("x", " @te", -1).must_include(["test"]); + } + #[test] fn test_autocomplete_ref_identical_labels_returns_single_completion() { - let mut world = TestWorld::new("x y"); - let doc = typst::compile(&world).output.ok(); - - let end = world.main.text().len(); - world.main.edit(end..end, " @t"); - - let result = test_with_doc(&world, -1, doc.as_ref()); + let result = test_with_addition("x y", " @t", -1); let completions = result.completions(); let label_count = completions.iter().filter(|c| c.kind == CompletionKind::Label).count();