Compare commits

...

23 Commits

Author SHA1 Message Date
Tobias Schmitz
d35640bf8e
refactor: revert some changes to FrameItem::Link 2025-06-25 15:10:54 +02:00
Tobias Schmitz
c20732bdbf
refactor: [no ci] derive(Cast) for ArtifactKind 2025-06-25 15:10:54 +02:00
Tobias Schmitz
4031811fc7
fix: [no ci] avoid empty marked-content sequences 2025-06-25 15:10:54 +02:00
Tobias Schmitz
03a73284da
feat: [no ci] generate tags for tables 2025-06-25 15:10:54 +02:00
Tobias Schmitz
8266f321f4
feat: [no ci] use local krilla version 2025-06-25 15:10:53 +02:00
Tobias Schmitz
a629f4cf94
feat: [no ci] pdf.tag function to manually create pdf tags 2025-06-25 15:09:56 +02:00
Tobias Schmitz
b2979fcb61
feat: [no ci] write tags for more elements 2025-06-25 15:09:56 +02:00
Tobias Schmitz
7d67db2346
feat: [no ci] write tags for links and use quadpoints in link annotations 2025-06-25 15:09:56 +02:00
Tobias Schmitz
ce997cf8b4
feat: pdf.artifact element 2025-06-25 15:09:56 +02:00
Tobias Schmitz
365dd69028
feat: [no ci] mark artifacts 2025-06-25 15:09:56 +02:00
Tobias Schmitz
cf15c62256
feat: [WIP] allow specifying alt text for links
skip-checks:true

# Please enter the commit message for your changes. Lines starting
# with '#' will be kept; you may remove them yourself if you want to.
# An empty message aborts the commit.
#
# Date:      Wed May 28 17:47:35 2025 +0200
#
# On branch pdf-accessibility
# Your branch and 'origin/pdf-accessibility' have diverged,
# and have 11 and 5 different commits each, respectively.
#
# Changes to be committed:
#	modified:   crates/typst-ide/src/jump.rs
#	modified:   crates/typst-layout/src/flow/distribute.rs
#	modified:   crates/typst-layout/src/modifiers.rs
#	modified:   crates/typst-library/src/foundations/content.rs
#	modified:   crates/typst-library/src/layout/frame.rs
#	modified:   crates/typst-library/src/model/bibliography.rs
#	modified:   crates/typst-library/src/model/footnote.rs
#	modified:   crates/typst-library/src/model/link.rs
#	modified:   crates/typst-library/src/model/outline.rs
#	modified:   crates/typst-library/src/model/reference.rs
#	modified:   crates/typst-pdf/src/convert.rs
#	modified:   crates/typst-pdf/src/link.rs
#	modified:   crates/typst-render/src/lib.rs
#	modified:   crates/typst-svg/src/lib.rs
#	modified:   tests/src/run.rs
#
2025-06-25 15:09:56 +02:00
Tobias Schmitz
af34645518
feat: [WIP] include links in tag tree
skip-checks:true
2025-06-25 15:09:56 +02:00
Tobias Schmitz
c17d24c335
feat: [WIP] write tags
skip-checks:true
2025-06-25 15:09:56 +02:00
Tobias Schmitz
9809112630
feat: [WIP] make more things locatable
skip-checks:true
2025-06-25 15:09:56 +02:00
Tobias Schmitz
0b3aa10bb8
feat: [draft] generate accessibility tag tree for headings
skip-checks:true
2025-06-25 15:09:56 +02:00
Laurenz
f2f527c451
Also fix encoding of <textarea> (#6497) 2025-06-24 15:52:15 +00:00
Laurenz
9e3c1199ed
Check that git tree is clean after build (#6495) 2025-06-24 15:05:02 +00:00
Tobias Schmitz
70399a94fd
Bump krilla to current Git version (#6488)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-06-24 13:23:37 +00:00
Andrew Voynov
d4be7c4ca5
Add page reference customization example (#6480)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-06-24 13:00:51 +00:00
Andrew Voynov
f162c37101
Improve equation reference example (#6481) 2025-06-24 12:49:28 +00:00
Andrew Voynov
87c5686560
Add docs for std module (#6407)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-06-24 10:22:55 +00:00
Max
899de6d5d5
Use ICU data to check if accent is bottom (#6393)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-06-24 10:03:10 +00:00
Andrew Voynov
24293a6c12
Rewrite outline.indent example (#6383)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-06-24 09:56:58 +00:00
49 changed files with 1067 additions and 206 deletions

View File

@ -81,6 +81,7 @@ jobs:
- run: cargo clippy --workspace --all-targets --no-default-features
- run: cargo fmt --check --all
- run: cargo doc --workspace --no-deps
- run: git diff --exit-code
min-version:
name: Check minimum Rust version

16
Cargo.lock generated
View File

@ -786,9 +786,9 @@ checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
[[package]]
name = "font-types"
version = "0.8.4"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa6a5e5a77b5f3f7f9e32879f484aa5b3632ddfbe568a16266c904a6f32cdaf"
checksum = "02a596f5713680923a2080d86de50fe472fb290693cf0f701187a1c8b36996b7"
dependencies = [
"bytemuck",
]
@ -1367,8 +1367,6 @@ dependencies = [
[[package]]
name = "krilla"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69ee6128ebf52d7ce684613b6431ead2959f2be9ff8cf776eeaaad0427c953e9"
dependencies = [
"base64",
"bumpalo",
@ -1396,8 +1394,6 @@ dependencies = [
[[package]]
name = "krilla-svg"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3462989578155cf620ef8035f8921533cc95c28e2a0c75de172f7219e6aba84e"
dependencies = [
"flate2",
"fontdb",
@ -2106,9 +2102,9 @@ dependencies = [
[[package]]
name = "read-fonts"
version = "0.28.0"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "600e807b48ac55bad68a8cb75cc3c7739f139b9248f7e003e01e080f589b5288"
checksum = "192735ef611aac958468e670cb98432c925426f3cb71521fda202130f7388d91"
dependencies = [
"bytemuck",
"font-types",
@ -2434,9 +2430,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]]
name = "skrifa"
version = "0.30.0"
version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fa1e5622e4f7b98877e8a19890efddcac1230cec6198bd9de91ec0e00010dc8"
checksum = "e6d632b5a73f566303dbeabd344dc3e716fd4ddc9a70d6fc8ea8e6f06617da97"
dependencies = [
"bytemuck",
"read-fonts",

View File

@ -73,8 +73,8 @@ image = { version = "0.25.5", default-features = false, features = ["png", "jpeg
indexmap = { version = "2", features = ["serde"] }
infer = { version = "0.19.0", default-features = false }
kamadak-exif = "0.6"
krilla = { version = "0.4.0", default-features = false, features = ["raster-images", "comemo", "rayon"] }
krilla-svg = "0.1.0"
krilla = { path = "../krilla/crates/krilla", default-features = false, features = ["raster-images", "comemo", "rayon"] }
krilla-svg = { path = "../krilla/crates/krilla-svg" }
kurbo = "0.11"
libfuzzer-sys = "0.4"
lipsum = "0.9"

View File

@ -113,7 +113,7 @@ fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
/// Encodes the children of an element.
fn write_children(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
// See HTML spec § 13.1.2.5.
if element.tag == tag::pre && starts_with_newline(element) {
if matches!(element.tag, tag::pre | tag::textarea) && starts_with_newline(element) {
w.buf.push('\n');
}

View File

@ -93,7 +93,7 @@ impl Item<'_, '_> {
Self::Frame(frame, _) => {
frame.size().is_zero()
&& frame.items().all(|(_, item)| {
matches!(item, FrameItem::Link(_, _) | FrameItem::Tag(_))
matches!(item, FrameItem::Link(..) | FrameItem::Tag(_))
})
}
Self::Placed(_, placed) => !placed.float,

View File

@ -13,6 +13,7 @@ use typst_library::layout::{
VAlignment,
};
use typst_library::model::Numbering;
use typst_library::pdf::ArtifactKind;
use typst_library::routines::{Pair, Routines};
use typst_library::text::{LocalName, TextElem};
use typst_library::visualize::Paint;
@ -200,6 +201,11 @@ fn layout_page_run_impl(
// Layout marginals.
let mut layouted = Vec::with_capacity(fragment.len());
let header = header.as_ref().map(|h| h.clone().artifact(ArtifactKind::Header));
let footer = footer.as_ref().map(|f| f.clone().artifact(ArtifactKind::Footer));
let background = background.as_ref().map(|b| b.clone().artifact(ArtifactKind::Page));
for inner in fragment {
let header_size = Size::new(inner.width(), margin.top - header_ascent);
let footer_size = Size::new(inner.width(), margin.bottom - footer_descent);
@ -210,9 +216,9 @@ fn layout_page_run_impl(
fill: fill.clone(),
numbering: numbering.clone(),
supplement: supplement.clone(),
header: layout_marginal(header, header_size, Alignment::BOTTOM)?,
footer: layout_marginal(footer, footer_size, Alignment::TOP)?,
background: layout_marginal(background, full_size, mid)?,
header: layout_marginal(&header, header_size, Alignment::BOTTOM)?,
footer: layout_marginal(&footer, footer_size, Alignment::TOP)?,
background: layout_marginal(&background, full_size, mid)?,
foreground: layout_marginal(foreground, full_size, mid)?,
margin,
binding,

View File

@ -16,12 +16,13 @@ use crate::diag::{SourceResult, StrResult};
use crate::engine::Engine;
use crate::foundations::{
elem, func, scope, ty, Context, Dict, Element, Fields, IntoValue, Label,
NativeElement, Recipe, RecipeIndex, Repr, Selector, Str, Style, StyleChain, Styles,
Value,
NativeElement, Recipe, RecipeIndex, Repr, Selector, Show, Str, Style, StyleChain,
Styles, Value,
};
use crate::introspection::Location;
use crate::introspection::{Locatable, Location};
use crate::layout::{AlignElem, Alignment, Axes, Length, MoveElem, PadElem, Rel, Sides};
use crate::model::{Destination, EmphElem, LinkElem, StrongElem};
use crate::pdf::{ArtifactElem, ArtifactKind};
use crate::text::UnderlineElem;
/// A piece of document content.
@ -503,8 +504,12 @@ impl Content {
}
/// Link the content somewhere.
pub fn linked(self, dest: Destination) -> Self {
self.styled(LinkElem::set_current(Some(dest)))
pub fn linked(self, dest: Destination, alt: Option<EcoString>) -> Self {
let span = self.span();
LinkMarker::new(self, dest.clone(), alt)
.pack()
.spanned(span)
.styled(LinkElem::set_current(Some(dest)))
}
/// Set alignments for this content.
@ -533,6 +538,12 @@ impl Content {
.pack()
.spanned(span)
}
/// Link the content somewhere.
pub fn artifact(self, kind: ArtifactKind) -> Self {
let span = self.span();
ArtifactElem::new(self).with_kind(kind).pack().spanned(span)
}
}
#[scope]
@ -980,6 +991,24 @@ pub trait PlainText {
fn plain_text(&self, text: &mut EcoString);
}
/// An element that associates the body of a link with the destination.
#[elem(Show, Locatable)]
pub struct LinkMarker {
/// The content.
#[required]
pub body: Content,
#[required]
pub dest: Destination,
#[required]
pub alt: Option<EcoString>,
}
impl Show for Packed<LinkMarker> {
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(self.body.clone())
}
}
/// An error arising when trying to access a field of content.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum FieldAccessError {

View File

@ -8,6 +8,7 @@ pub use self::dom::*;
use ecow::EcoString;
use crate::foundations::{elem, Content, Module, Scope};
use crate::introspection::Locatable;
/// Create a module with all HTML definitions.
pub fn module() -> Module {
@ -40,7 +41,7 @@ pub fn module() -> Module {
/// A div with _Typst content_ inside!
/// ]
/// ```
#[elem(name = "elem")]
#[elem(name = "elem", Locatable)]
pub struct HtmlElem {
/// The element's tag.
#[required]

View File

@ -13,6 +13,7 @@ use crate::foundations::{
cast, elem, scope, Array, CastInfo, Content, Context, Fold, FromValue, Func,
IntoValue, NativeElement, Packed, Reflect, Resolve, Show, Smart, StyleChain, Value,
};
use crate::introspection::Locatable;
use crate::layout::{
Alignment, BlockElem, Length, OuterHAlignment, OuterVAlignment, Rel, Sides, Sizing,
};
@ -136,7 +137,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, Locatable, Show)]
pub struct GridElem {
/// The column sizes.
///
@ -462,7 +463,7 @@ impl TryFrom<Content> for GridItem {
/// If `repeat` is set to `true`, the header will be repeated across pages. For
/// an example, refer to the [`table.header`]($table.header) element and the
/// [`grid.stroke`]($grid.stroke) parameter.
#[elem(name = "header", title = "Grid Header")]
#[elem(name = "header", title = "Grid Header", Locatable)]
pub struct GridHeader {
/// Whether this header should be repeated across pages.
#[default(true)]
@ -490,7 +491,7 @@ pub struct GridHeader {
/// itself on every page of the table.
///
/// No other grid cells may be placed after the footer.
#[elem(name = "footer", title = "Grid Footer")]
#[elem(name = "footer", title = "Grid Footer", Locatable)]
pub struct GridFooter {
/// Whether this footer should be repeated across pages.
#[default(true)]
@ -657,7 +658,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", Locatable, Show)]
pub struct GridCell {
/// The cell's body.
#[required]

View File

@ -1,6 +1,7 @@
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{elem, Content, Packed, Show, StyleChain};
use crate::introspection::Locatable;
/// Hides content without affecting layout.
///
@ -14,7 +15,7 @@ use crate::foundations::{elem, Content, Packed, Show, StyleChain};
/// Hello Jane \
/// #hide[Hello] Joe
/// ```
#[elem(Show)]
#[elem(Locatable, Show)]
pub struct HideElem {
/// The content to hide.
#[required]

View File

@ -1,6 +1,7 @@
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{elem, Content, NativeElement, Packed, Show, StyleChain};
use crate::introspection::Locatable;
use crate::layout::{BlockElem, Length};
/// Repeats content to the available space.
@ -24,7 +25,8 @@ use crate::layout::{BlockElem, Length};
/// Berlin, the 22nd of December, 2022
/// ]
/// ```
#[elem(Show)]
// TODO: should this be a PDF artifact by deafult?
#[elem(Locatable, Show)]
pub struct RepeatElem {
/// The content to repeat.
#[required]

View File

@ -148,7 +148,7 @@ pub struct Library {
/// The default style properties (for page size, font selection, and
/// everything else configurable via set and show rules).
pub styles: Styles,
/// The standard library as a value. Used to provide the `std` variable.
/// The standard library as a value. Used to provide the `std` module.
pub std: Binding,
/// In-development features that were enabled.
pub features: Features,

View File

@ -1,3 +1,10 @@
use std::sync::LazyLock;
use icu_properties::maps::CodePointMapData;
use icu_properties::CanonicalCombiningClass;
use icu_provider::AsDeserializingBufferProvider;
use icu_provider_blob::BlobDataProvider;
use crate::diag::bail;
use crate::foundations::{cast, elem, func, Content, NativeElement, SymbolElem};
use crate::layout::{Length, Rel};
@ -81,17 +88,22 @@ impl Accent {
Self(Self::combine(c).unwrap_or(c))
}
/// List of bottom accents. Currently just a list of ones included in the
/// Unicode math class document.
const BOTTOM: &[char] = &[
'\u{0323}', '\u{032C}', '\u{032D}', '\u{032E}', '\u{032F}', '\u{0330}',
'\u{0331}', '\u{0332}', '\u{0333}', '\u{033A}', '\u{20E8}', '\u{20EC}',
'\u{20ED}', '\u{20EE}', '\u{20EF}',
];
/// Whether this accent is a bottom accent or not.
pub fn is_bottom(&self) -> bool {
Self::BOTTOM.contains(&self.0)
static COMBINING_CLASS_DATA: LazyLock<CodePointMapData<CanonicalCombiningClass>> =
LazyLock::new(|| {
icu_properties::maps::load_canonical_combining_class(
&BlobDataProvider::try_new_from_static_blob(typst_assets::icu::ICU)
.unwrap()
.as_deserializing(),
)
.unwrap()
});
matches!(
COMBINING_CLASS_DATA.as_borrowed().get(self.0),
CanonicalCombiningClass::Below
)
}
}

View File

@ -28,6 +28,7 @@ use typst_utils::singleton;
use unicode_math_class::MathClass;
use crate::foundations::{elem, Content, Module, NativeElement, Scope};
use crate::introspection::Locatable;
use crate::layout::{Em, HElem};
use crate::text::TextElem;
@ -109,7 +110,7 @@ pub fn module() -> Module {
pub trait Mathy {}
/// A math alignment point: `&`, `&&`.
#[elem(title = "Alignment Point", Mathy)]
#[elem(title = "Alignment Point", Mathy, Locatable)]
pub struct AlignPointElem {}
impl AlignPointElem {
@ -136,7 +137,7 @@ impl AlignPointElem {
///
/// $x loves y and y loves 5$
/// ```
#[elem(Mathy)]
#[elem(Mathy, Locatable)]
pub struct ClassElem {
/// The class to apply to the content.
#[required]

View File

@ -1,6 +1,7 @@
use typst_syntax::Span;
use crate::foundations::{elem, func, Content, NativeElement};
use crate::introspection::Locatable;
use crate::math::Mathy;
/// A square root.
@ -22,7 +23,7 @@ pub fn sqrt(
/// ```example
/// $ root(3, x) $
/// ```
#[elem(Mathy)]
#[elem(Mathy, Locatable)]
pub struct RootElem {
/// Which root of the radicand to take.
#[positional]

View File

@ -873,7 +873,8 @@ impl<'a> Generator<'a> {
renderer.display_elem_child(elem, &mut None, false)?;
if let Some(location) = first_occurrences.get(item.key.as_str()) {
let dest = Destination::Location(*location);
content = content.linked(dest);
// TODO: accept user supplied alt text
content = content.linked(dest, None);
}
StrResult::Ok(content)
})
@ -1008,7 +1009,8 @@ impl ElemRenderer<'_> {
if let Some(hayagriva::ElemMeta::Entry(i)) = elem.meta {
if let Some(location) = (self.link)(i) {
let dest = Destination::Location(location);
content = content.linked(dest);
// TODO: accept user supplied alt text
content = content.linked(dest, None);
}
}

View File

@ -43,7 +43,7 @@ use crate::text::{Lang, Region, TextElem};
/// This function indirectly has dedicated syntax. [References]($ref) can be
/// used to cite works from the bibliography. The label then corresponds to the
/// citation key.
#[elem(Synthesize)]
#[elem(Locatable, Synthesize)]
pub struct CiteElem {
/// The citation key that identifies the entry in the bibliography that
/// shall be cited, as a label.

View File

@ -4,6 +4,7 @@ use crate::foundations::{
elem, Content, NativeElement, Packed, Show, StyleChain, TargetElem,
};
use crate::html::{tag, HtmlElem};
use crate::introspection::Locatable;
use crate::text::{ItalicToggle, TextElem};
/// Emphasizes content by toggling italics.
@ -29,7 +30,7 @@ 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"], Locatable, Show)]
pub struct EmphElem {
/// The content to emphasize.
#[required]

View File

@ -10,6 +10,7 @@ use crate::foundations::{
Styles, TargetElem,
};
use crate::html::{attr, tag, HtmlElem};
use crate::introspection::Locatable;
use crate::layout::{Alignment, BlockElem, Em, HAlignment, Length, VAlignment, VElem};
use crate::model::{
ListItemLike, ListLike, Numbering, NumberingPattern, ParElem, ParbreakElem,
@ -71,7 +72,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", Locatable, Show)]
pub struct EnumElem {
/// Defines the default [spacing]($enum.spacing) of the enumeration. If it
/// is `{false}`, the items are spaced apart with
@ -271,7 +272,7 @@ impl Show for Packed<EnumElem> {
}
/// An enumeration item.
#[elem(name = "item", title = "Numbered List Item")]
#[elem(name = "item", title = "Numbered List Item", Locatable)]
pub struct EnumItem {
/// The item's number.
#[positional]

View File

@ -473,7 +473,7 @@ impl Outlinable for Packed<FigureElem> {
/// caption: [A rectangle],
/// )
/// ```
#[elem(name = "caption", Synthesize, Show)]
#[elem(name = "caption", Locatable, Synthesize, Show)]
pub struct FigureCaption {
/// The caption's position in the figure. Either `{top}` or `{bottom}`.
///

View File

@ -147,7 +147,8 @@ impl Show for Packed<FootnoteElem> {
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)))
// TODO: accept user supplied alt text
Ok(HElem::hole().pack() + sup.linked(Destination::Location(loc), None))
}
}
@ -192,7 +193,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", Locatable, Show, ShowSet)]
pub struct FootnoteEntry {
/// The footnote for this entry. Its location can be used to determine
/// the footnote counter state.
@ -296,7 +297,8 @@ impl Show for Packed<FootnoteEntry> {
let sup = SuperElem::new(num)
.pack()
.spanned(span)
.linked(Destination::Location(loc))
// TODO: accept user supplied alt text
.linked(Destination::Location(loc), None)
.located(loc.variant(1));
Ok(Content::sequence([

View File

@ -9,7 +9,7 @@ use crate::foundations::{
StyleChain, Styles, TargetElem,
};
use crate::html::{attr, tag, HtmlElem};
use crate::introspection::Location;
use crate::introspection::{Locatable, Location};
use crate::layout::Position;
use crate::text::TextElem;
@ -38,8 +38,11 @@ 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(Locatable, Show)]
pub struct LinkElem {
/// A text describing the link.
pub alt: Option<EcoString>,
/// The destination the link points to.
///
/// - To link to web pages, `dest` should be a valid URL string. If the URL
@ -123,12 +126,13 @@ impl Show for Packed<LinkElem> {
body
}
} else {
let alt = self.alt(styles);
match &self.dest {
LinkTarget::Dest(dest) => body.linked(dest.clone()),
LinkTarget::Dest(dest) => body.linked(dest.clone(), alt),
LinkTarget::Label(label) => {
let elem = engine.introspector.query_label(*label).at(self.span())?;
let dest = Destination::Location(elem.location().unwrap());
body.clone().linked(dest)
body.linked(dest, alt)
}
}
})

View File

@ -7,6 +7,7 @@ use crate::foundations::{
Smart, StyleChain, Styles, TargetElem, Value,
};
use crate::html::{tag, HtmlElem};
use crate::introspection::Locatable;
use crate::layout::{BlockElem, Em, Length, VElem};
use crate::model::{ParElem, ParbreakElem};
use crate::text::TextElem;
@ -42,7 +43,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", Locatable, Show)]
pub struct ListElem {
/// Defines the default [spacing]($list.spacing) of the list. If it is
/// `{false}`, the items are spaced apart with
@ -178,7 +179,7 @@ impl Show for Packed<ListElem> {
}
/// A bullet list item.
#[elem(name = "item", title = "Bullet List Item")]
#[elem(name = "item", title = "Bullet List Item", Locatable)]
pub struct ListItem {
/// The item's body.
#[required]

View File

@ -2,6 +2,7 @@ use std::num::NonZeroUsize;
use std::str::FromStr;
use comemo::{Track, Tracked};
use ecow::eco_format;
use smallvec::SmallVec;
use typst_syntax::Span;
use typst_utils::{Get, NonZeroExt};
@ -17,8 +18,8 @@ use crate::introspection::{
Counter, CounterKey, Introspector, Locatable, Location, Locator, LocatorLink,
};
use crate::layout::{
Abs, Axes, BlockBody, BlockElem, BoxElem, Dir, Em, Fr, HElem, Length, Region, Rel,
RepeatElem, Sides,
Abs, Axes, BlockBody, BlockElem, BoxElem, Dir, Em, Fr, HElem, Length, PageElem,
Region, Rel, RepeatElem, Sides,
};
use crate::math::EquationElem;
use crate::model::{Destination, HeadingElem, NumberingPattern, ParElem, Refable};
@ -225,25 +226,21 @@ pub struct OutlineElem {
/// to just specifying `{2em}`.
///
/// ```example
/// #set heading(numbering: "1.a.")
/// >>> #show heading: none
/// #set heading(numbering: "I-I.")
/// #set outline(title: none)
///
/// #outline(
/// title: [Contents (Automatic)],
/// indent: auto,
/// )
/// #outline()
/// #line(length: 100%)
/// #outline(indent: 3em)
///
/// #outline(
/// title: [Contents (Length)],
/// indent: 2em,
/// )
///
/// = About ACME Corp.
/// == History
/// === Origins
/// #lorem(10)
///
/// == Products
/// #lorem(10)
/// = Software engineering technologies
/// == Requirements
/// == Tools and technologies
/// === Code editors
/// == Analyzing alternatives
/// = Designing software components
/// = Testing and integration
/// ```
pub indent: Smart<OutlineIndent>,
}
@ -368,7 +365,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", Locatable, Show)]
pub struct OutlineEntry {
/// The nesting level of this outline entry. Starts at `{1}` for top-level
/// entries.
@ -421,8 +418,19 @@ impl Show for Packed<OutlineEntry> {
let context = Context::new(None, Some(styles));
let context = context.track();
// TODO: prefix should be wrapped in a `Lbl` structure element
let prefix = self.prefix(engine, context, span)?;
let inner = self.inner(engine, context, span)?;
let body = self.body().at(span)?;
let page = self.page(engine, context, span)?;
let alt = {
// TODO: accept user supplied alt text
let prefix = prefix.as_ref().map(|p| p.plain_text()).unwrap_or_default();
let body = body.plain_text();
let page_str = PageElem::local_name_in(styles);
let page_nr = page.plain_text();
eco_format!("{prefix} \"{body}\", {page_str} {page_nr}")
};
let inner = self.inner(context, span, body, page)?;
let block = if self.element.is::<EquationElem>() {
let body = prefix.unwrap_or_default() + inner;
BlockElem::new()
@ -434,7 +442,7 @@ impl Show for Packed<OutlineEntry> {
};
let loc = self.element_location().at(span)?;
Ok(block.linked(Destination::Location(loc)))
Ok(block.linked(Destination::Location(loc), Some(alt)))
}
}
@ -450,8 +458,9 @@ impl OutlineEntry {
/// at the same level are aligned.
///
/// If the outline's indent is a fixed value or a function, the prefixes are
/// indented, but the inner contents are simply inset from the prefix by the
/// specified `gap`, rather than aligning outline-wide.
/// indented, but the inner contents are simply offset from the prefix by
/// the specified `gap`, rather than aligning outline-wide. For a visual
/// explanation, see [`outline.indent`]($outline.indent).
#[func(contextual)]
pub fn indented(
&self,
@ -568,9 +577,10 @@ impl OutlineEntry {
#[func(contextual)]
pub fn inner(
&self,
engine: &mut Engine,
context: Tracked<Context>,
span: Span,
body: Content,
page: Content,
) -> SourceResult<Content> {
let styles = context.styles().at(span)?;
@ -591,7 +601,7 @@ impl OutlineEntry {
seq.push(TextElem::packed("\u{202B}"));
}
seq.push(self.body().at(span)?);
seq.push(body);
if rtl {
// "Pop Directional Formatting"
@ -616,7 +626,7 @@ impl OutlineEntry {
// Add the page number. The word joiner in front ensures that the page
// number doesn't stand alone in its line.
seq.push(TextElem::packed("\u{2060}"));
seq.push(self.page(engine, context, span)?);
seq.push(page);
Ok(Content::sequence(seq))
}

View File

@ -93,7 +93,7 @@ use crate::model::Numbering;
/// let $a$ be the smallest of the
/// three integers. Then, we ...
/// ```
#[elem(scope, title = "Paragraph")]
#[elem(scope, title = "Paragraph", Locatable)]
pub struct ParElem {
/// The spacing between lines.
///

View File

@ -79,6 +79,36 @@ use crate::text::TextElem;
/// reference: `[@intro[Chapter]]`.
///
/// # Customization
/// When you only ever need to reference pages of a figure/table/heading/etc. in
/// a document, the default `form` field value can be changed to `{"page"}` with
/// a set rule. If you prefer a short "p." supplement over "page", the
/// [`page.supplement`]($page.supplement) field can be used for changing this:
///
/// ```example
/// #set page(
/// numbering: "1",
/// supplement: "p.",
/// >>> margin: (bottom: 3em),
/// >>> footer-descent: 1.25em,
/// )
/// #set ref(form: "page")
///
/// #figure(
/// stack(
/// dir: ltr,
/// spacing: 1em,
/// circle(),
/// square(),
/// ),
/// caption: [Shapes],
/// ) <shapes>
///
/// #pagebreak()
///
/// See @shapes for examples
/// of different shapes.
/// ```
///
/// If you write a show rule for references, you can access the referenced
/// element through the `element` field of the reference. The `element` may
/// be `{none}` even if it exists if Typst hasn't discovered it yet, so you
@ -91,16 +121,13 @@ use crate::text::TextElem;
/// #show ref: it => {
/// let eq = math.equation
/// let el = it.element
/// if el != none and el.func() == eq {
/// // Skip all other references.
/// if el == none or el.func() != eq { return it }
/// // Override equation references.
/// link(el.location(), numbering(
/// el.numbering,
/// ..counter(eq).at(el.location())
/// ))
/// } else {
/// // Other references as usual.
/// it
/// }
/// }
///
/// = Beginnings <beginning>
@ -316,7 +343,8 @@ fn show_reference(
content = supplement + TextElem::packed("\u{a0}") + content;
}
Ok(content.linked(Destination::Location(loc)))
// TODO: accept user supplied alt text
Ok(content.linked(Destination::Location(loc), None))
}
/// Turn a reference into a citation.

View File

@ -4,6 +4,7 @@ use crate::foundations::{
elem, Content, NativeElement, Packed, Show, StyleChain, TargetElem,
};
use crate::html::{tag, HtmlElem};
use crate::introspection::Locatable;
use crate::text::{TextElem, WeightDelta};
/// Strongly emphasizes content by increasing the font weight.
@ -24,7 +25,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"], Locatable, Show)]
pub struct StrongElem {
/// The delta to apply on the font weight.
///

View File

@ -1,6 +1,7 @@
use std::num::{NonZeroU32, NonZeroUsize};
use std::sync::Arc;
use ecow::EcoString;
use typst_utils::NonZeroExt;
use crate::diag::{bail, HintedStrResult, HintedString, SourceResult};
@ -10,7 +11,7 @@ use crate::foundations::{
TargetElem,
};
use crate::html::{attr, tag, HtmlAttrs, HtmlElem, HtmlTag};
use crate::introspection::Locator;
use crate::introspection::{Locatable, Locator};
use crate::layout::grid::resolve::{table_to_cellgrid, Cell, CellGrid, Entry};
use crate::layout::{
show_grid_cell, Abs, Alignment, BlockElem, Celled, GridCell, GridFooter, GridHLine,
@ -121,7 +122,7 @@ use crate::visualize::{Paint, Stroke};
/// [Robert], b, a, b,
/// )
/// ```
#[elem(scope, Show, LocalName, Figurable)]
#[elem(scope, Locatable, Show, LocalName, Figurable)]
pub struct TableElem {
/// The column sizes. See the [grid documentation]($grid) for more
/// information on track sizing.
@ -237,6 +238,9 @@ pub struct TableElem {
#[default(Celled::Value(Sides::splat(Some(Abs::pt(5.0).into()))))]
pub inset: Celled<Sides<Option<Rel<Length>>>>,
// TODO: docs
pub summary: Option<EcoString>,
/// The contents of the table cells, plus any extra table lines specified
/// with the [`table.hline`]($table.hline) and
/// [`table.vline`]($table.vline) elements.
@ -531,7 +535,7 @@ impl TryFrom<Content> for TableItem {
/// [7.34], [57], [2],
/// )
/// ```
#[elem(name = "header", title = "Table Header")]
#[elem(name = "header", title = "Table Header", Locatable)]
pub struct TableHeader {
/// Whether this header should be repeated across pages.
#[default(true)]
@ -561,7 +565,7 @@ pub struct TableHeader {
/// totals, or other information that should be visible on every page.
///
/// No other table cells may be placed after the footer.
#[elem(name = "footer", title = "Table Footer")]
#[elem(name = "footer", title = "Table Footer", Locatable)]
pub struct TableFooter {
/// Whether this footer should be repeated across pages.
#[default(true)]
@ -604,7 +608,7 @@ pub struct TableFooter {
/// [19:00], [Day 1 Attendee Mixer],
/// )
/// ```
#[elem(name = "hline", title = "Table Horizontal Line")]
#[elem(name = "hline", title = "Table Horizontal Line", Locatable)]
pub struct TableHLine {
/// The row above which the horizontal line is placed (zero-indexed).
/// Functions identically to the `y` field in [`grid.hline`]($grid.hline.y).
@ -649,7 +653,7 @@ pub struct TableHLine {
/// use the [table's `stroke`]($table.stroke) field or [`table.cell`'s
/// `stroke`]($table.cell.stroke) field instead if the line you want to place is
/// part of all your tables' designs.
#[elem(name = "vline", title = "Table Vertical Line")]
#[elem(name = "vline", title = "Table Vertical Line", Locatable)]
pub struct TableVLine {
/// The column before which the horizontal line is placed (zero-indexed).
/// Functions identically to the `x` field in [`grid.vline`]($grid.vline).
@ -770,7 +774,7 @@ pub struct TableVLine {
/// [Vikram], [49], [Perseverance],
/// )
/// ```
#[elem(name = "cell", title = "Table Cell", Show)]
#[elem(name = "cell", title = "Table Cell", Locatable, Show)]
pub struct TableCell {
/// The cell's body.
#[required]

View File

@ -7,6 +7,7 @@ use crate::foundations::{
Styles, TargetElem,
};
use crate::html::{tag, HtmlElem};
use crate::introspection::Locatable;
use crate::layout::{Em, HElem, Length, Sides, StackChild, StackElem, VElem};
use crate::model::{ListItemLike, ListLike, ParElem, ParbreakElem};
use crate::text::TextElem;
@ -27,7 +28,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", Locatable, Show)]
pub struct TermsElem {
/// Defines the default [spacing]($terms.spacing) of the term list. If it is
/// `{false}`, the items are spaced apart with
@ -205,7 +206,7 @@ impl Show for Packed<TermsElem> {
}
/// A term list item.
#[elem(name = "item", title = "Term List Item")]
#[elem(name = "item", title = "Term List Item", Locatable)]
pub struct TermItem {
/// The term described by the list item.
#[required]

View File

@ -0,0 +1,221 @@
use ecow::EcoString;
use typst_macros::{cast, elem, Cast};
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{Content, Packed, Show, StyleChain};
use crate::introspection::Locatable;
// TODO: docs
#[elem(Locatable, Show)]
pub struct PdfTagElem {
#[default(PdfTagKind::NonStruct)]
pub kind: PdfTagKind,
/// An alternate description.
pub alt: Option<EcoString>,
/// Exact replacement for this structure element and its children.
pub actual_text: Option<EcoString>,
/// The expanded form of an abbreviation/acronym.
pub expansion: Option<EcoString>,
/// The content to underline.
#[required]
pub body: Content,
}
impl Show for Packed<PdfTagElem> {
#[typst_macros::time(name = "pdf.tag", span = self.span())]
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(self.body.clone())
}
}
// TODO: docs
/// PDF structure elements
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum PdfTagKind {
// grouping elements
/// (Part)
Part,
/// (Article)
Art,
/// (Section)
Sect,
/// (Division)
Div,
/// (Block quotation)
BlockQuote,
/// (Caption)
Caption,
/// (Table of contents)
TOC,
/// (Table of contents item)
TOCI,
/// (Index)
Index,
/// (Nonstructural element)
NonStruct,
/// (Private element)
Private,
// paragraph like elements
/// (Heading)
H { title: Option<EcoString> },
/// (Heading level 1)
H1 { title: Option<EcoString> },
/// (Heading level 2)
H2 { title: Option<EcoString> },
/// (Heading level 3)
H4 { title: Option<EcoString> },
/// (Heading level 4)
H3 { title: Option<EcoString> },
/// (Heading level 5)
H5 { title: Option<EcoString> },
/// (Heading level 6)
H6 { title: Option<EcoString> },
/// (Paragraph)
P,
// list elements
/// (List)
L { numbering: ListNumbering },
/// (List item)
LI,
/// (Label)
Lbl,
/// (List body)
LBody,
// table elements
/// (Table)
Table,
/// (Table row)
TR,
/// (Table header)
TH { scope: TableHeaderScope },
/// (Table data cell)
TD,
/// (Table header row group)
THead,
/// (Table body row group)
TBody,
/// (Table footer row group)
TFoot,
// inline elements
/// (Span)
Span,
/// (Quotation)
Quote,
/// (Note)
Note,
/// (Reference)
Reference,
/// (Bibliography Entry)
BibEntry,
/// (Code)
Code,
/// (Link)
Link,
/// (Annotation)
Annot,
/// (Ruby)
Ruby,
/// (Ruby base text)
RB,
/// (Ruby annotation text)
RT,
/// (Ruby punctuation)
RP,
/// (Warichu)
Warichu,
/// (Warichu text)
WT,
/// (Warichu punctuation)
WP,
/// (Figure)
Figure,
/// (Formula)
Formula,
/// (Form)
Form,
}
cast! {
PdfTagKind,
self => match self {
PdfTagKind::Part => "part".into_value(),
_ => todo!(),
},
"part" => Self::Part,
// TODO
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ListNumbering {
/// No numbering.
None,
/// Solid circular bullets.
Disc,
/// Open circular bullets.
Circle,
/// Solid square bullets.
Square,
/// Decimal numbers.
Decimal,
/// Lowercase Roman numerals.
LowerRoman,
/// Uppercase Roman numerals.
UpperRoman,
/// Lowercase letters.
LowerAlpha,
/// Uppercase letters.
UpperAlpha,
}
/// The scope of a table header cell.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum TableHeaderScope {
/// The header cell refers to the row.
Row,
/// The header cell refers to the column.
Column,
/// The header cell refers to both the row and the column.
Both,
}
/// Mark content as a PDF artifact.
/// TODO: maybe generalize this and use it to mark html elements with `aria-hidden="true"`?
#[elem(Locatable, Show)]
pub struct ArtifactElem {
#[default(ArtifactKind::Other)]
pub kind: ArtifactKind,
/// The content to underline.
#[required]
pub body: Content,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Cast)]
pub enum ArtifactKind {
/// Page header artifacts.
Header,
/// Page footer artifacts.
Footer,
/// Other page artifacts.
Page,
/// Other artifacts.
#[default]
Other,
}
impl Show for Packed<ArtifactElem> {
#[typst_macros::time(name = "pdf.artifact", span = self.span())]
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(self.body.clone())
}
}

View File

@ -1,7 +1,9 @@
//! PDF-specific functionality.
mod accessibility;
mod embed;
pub use self::accessibility::*;
pub use self::embed::*;
use crate::foundations::{Module, Scope};
@ -11,5 +13,7 @@ pub fn module() -> Module {
let mut pdf = Scope::deduplicating();
pdf.start_category(crate::Category::Pdf);
pdf.define_elem::<EmbedElem>();
pdf.define_elem::<PdfTagElem>();
pdf.define_elem::<ArtifactElem>();
Module::new("pdf", pdf)
}

View File

@ -3,6 +3,7 @@ use smallvec::smallvec;
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{elem, Content, Packed, Show, Smart, StyleChain};
use crate::introspection::Locatable;
use crate::layout::{Abs, Corners, Length, Rel, Sides};
use crate::text::{BottomEdge, BottomEdgeMetric, TextElem, TopEdge, TopEdgeMetric};
use crate::visualize::{Color, FixedStroke, Paint, Stroke};
@ -13,7 +14,7 @@ use crate::visualize::{Color, FixedStroke, Paint, Stroke};
/// ```example
/// This is #underline[important].
/// ```
#[elem(Show)]
#[elem(Locatable, Show)]
pub struct UnderlineElem {
/// How to [stroke] the line.
///
@ -99,7 +100,7 @@ impl Show for Packed<UnderlineElem> {
/// ```example
/// #overline[A line over text.]
/// ```
#[elem(Show)]
#[elem(Locatable, Show)]
pub struct OverlineElem {
/// How to [stroke] the line.
///
@ -191,7 +192,7 @@ impl Show for Packed<OverlineElem> {
/// ```example
/// This is #strike[not] relevant.
/// ```
#[elem(title = "Strikethrough", Show)]
#[elem(title = "Strikethrough", Locatable, Show)]
pub struct StrikeElem {
/// How to [stroke] the line.
///
@ -268,7 +269,7 @@ impl Show for Packed<StrikeElem> {
/// ```example
/// This is #highlight[important].
/// ```
#[elem(Show)]
#[elem(Locatable, Show)]
pub struct HighlightElem {
/// The color to highlight the text with.
///

View File

@ -20,6 +20,7 @@ use crate::foundations::{
PlainText, Show, ShowSet, Smart, StyleChain, Styles, Synthesize, TargetElem,
};
use crate::html::{tag, HtmlElem};
use crate::introspection::Locatable;
use crate::layout::{BlockBody, BlockElem, Em, HAlignment};
use crate::loading::{DataSource, Load};
use crate::model::{Figurable, ParElem};
@ -78,6 +79,7 @@ use crate::World;
scope,
title = "Raw Text / Code",
Synthesize,
Locatable,
Show,
ShowSet,
LocalName,
@ -636,7 +638,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", Locatable, Show, PlainText)]
pub struct RawLine {
/// The line number of the raw line inside of the raw block, starts at 1.
#[required]

View File

@ -6,6 +6,7 @@ use crate::foundations::{
elem, Content, NativeElement, Packed, SequenceElem, Show, StyleChain, TargetElem,
};
use crate::html::{tag, HtmlElem};
use crate::introspection::Locatable;
use crate::layout::{Em, Length};
use crate::text::{variant, SpaceElem, TextElem, TextSize};
use crate::World;
@ -18,7 +19,7 @@ use crate::World;
/// ```example
/// Revenue#sub[yearly]
/// ```
#[elem(title = "Subscript", Show)]
#[elem(title = "Subscript", Locatable, Show)]
pub struct SubElem {
/// Whether to prefer the dedicated subscript characters of the font.
///
@ -84,7 +85,7 @@ impl Show for Packed<SubElem> {
/// ```example
/// 1#super[st] try!
/// ```
#[elem(title = "Superscript", Show)]
#[elem(title = "Superscript", Locatable, Show)]
pub struct SuperElem {
/// Whether to prefer the dedicated superscript characters of the font.
///

View File

@ -21,6 +21,7 @@ use crate::foundations::{
cast, elem, func, scope, Bytes, Cast, Content, Derived, NativeElement, Packed, Show,
Smart, StyleChain,
};
use crate::introspection::Locatable;
use crate::layout::{BlockElem, Length, Rel, Sizing};
use crate::loading::{DataSource, Load, LoadSource, Loaded, Readable};
use crate::model::Figurable;
@ -44,7 +45,7 @@ use crate::text::LocalName;
/// ],
/// )
/// ```
#[elem(scope, Show, LocalName, Figurable)]
#[elem(scope, Locatable, Show, 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).

View File

@ -2,7 +2,6 @@ use std::collections::{BTreeMap, HashMap, HashSet};
use std::num::NonZeroU64;
use ecow::{eco_format, EcoVec};
use krilla::annotation::Annotation;
use krilla::configure::{Configuration, ValidationError, Validator};
use krilla::destination::{NamedDestination, XyzDestination};
use krilla::embed::EmbedError;
@ -14,7 +13,7 @@ use krilla::{Document, SerializeSettings};
use krilla_svg::render_svg_glyph;
use typst_library::diag::{bail, error, SourceDiagnostic, SourceResult};
use typst_library::foundations::{NativeElement, Repr};
use typst_library::introspection::Location;
use typst_library::introspection::{Location, Tag};
use typst_library::layout::{
Abs, Frame, FrameItem, GroupItem, PagedDocument, Size, Transform,
};
@ -25,11 +24,12 @@ use typst_syntax::Span;
use crate::embed::embed_files;
use crate::image::handle_image;
use crate::link::handle_link;
use crate::link::{handle_link, LinkAnnotation};
use crate::metadata::build_metadata;
use crate::outline::build_outline;
use crate::page::PageLabelExt;
use crate::shape::handle_shape;
use crate::tags::{self, Tags};
use crate::text::handle_text;
use crate::util::{convert_path, display_font, AbsExt, TransformExt};
use crate::PdfOptions;
@ -39,14 +39,17 @@ pub fn convert(
typst_document: &PagedDocument,
options: &PdfOptions,
) -> SourceResult<Vec<u8>> {
// HACK
let config = Configuration::new_with_validator(Validator::UA1);
let settings = SerializeSettings {
compress_content_streams: true,
compress_content_streams: false, // true,
no_device_cs: true,
ascii_compatible: false,
ascii_compatible: true, // false,
xmp_metadata: true,
cmyk_profile: None,
configuration: options.standards.config,
enable_tagging: false,
configuration: config, // options.standards.config,
// TODO: allow opting out of tagging PDFs
enable_tagging: true,
render_svg_glyph_fn: render_svg_glyph,
};
@ -54,6 +57,7 @@ pub fn convert(
let page_index_converter = PageIndexConverter::new(typst_document, options);
let named_destinations =
collect_named_destinations(typst_document, &page_index_converter);
let mut gc = GlobalContext::new(
typst_document,
options,
@ -66,6 +70,7 @@ pub fn convert(
document.set_outline(build_outline(&gc));
document.set_metadata(build_metadata(&gc));
document.set_tag_tree(gc.tags.build_tree());
finish(document, gc, options.standards.config)
}
@ -115,9 +120,7 @@ fn convert_pages(gc: &mut GlobalContext, document: &mut Document) -> SourceResul
surface.finish();
for annotation in fc.annotations {
page.add_annotation(annotation);
}
tags::add_annotations(gc, &mut page, fc.link_annotations);
}
}
@ -171,14 +174,14 @@ impl State {
/// Context needed for converting a single frame.
pub(crate) struct FrameContext {
states: Vec<State>,
annotations: Vec<Annotation>,
pub(crate) link_annotations: HashMap<tags::LinkId, LinkAnnotation>,
}
impl FrameContext {
pub(crate) fn new(size: Size) -> Self {
Self {
states: vec![State::new(size)],
annotations: vec![],
link_annotations: HashMap::new(),
}
}
@ -197,10 +200,6 @@ impl FrameContext {
pub(crate) fn state_mut(&mut self) -> &mut State {
self.states.last_mut().unwrap()
}
pub(crate) fn push_annotation(&mut self, annotation: Annotation) {
self.annotations.push(annotation);
}
}
/// Globally needed context for converting a typst document.
@ -225,6 +224,8 @@ pub(crate) struct GlobalContext<'a> {
/// The languages used throughout the document.
pub(crate) languages: BTreeMap<Lang, usize>,
pub(crate) page_index_converter: PageIndexConverter,
/// Tagged PDF context.
pub(crate) tags: Tags,
}
impl<'a> GlobalContext<'a> {
@ -244,6 +245,8 @@ impl<'a> GlobalContext<'a> {
image_spans: HashSet::new(),
languages: BTreeMap::new(),
page_index_converter,
tags: Tags::new(),
}
}
}
@ -278,8 +281,9 @@ pub(crate) fn handle_frame(
FrameItem::Image(image, size, span) => {
handle_image(gc, fc, image, *size, surface, *span)?
}
FrameItem::Link(d, s) => handle_link(fc, gc, d, *s),
FrameItem::Tag(_) => {}
FrameItem::Link(dest, size) => handle_link(fc, gc, dest, *size),
FrameItem::Tag(Tag::Start(elem)) => tags::handle_start(gc, elem),
FrameItem::Tag(Tag::End(loc, _)) => tags::handle_end(gc, *loc),
}
fc.pop();
@ -294,7 +298,7 @@ pub(crate) fn handle_group(
fc: &mut FrameContext,
group: &GroupItem,
surface: &mut Surface,
context: &mut GlobalContext,
gc: &mut GlobalContext,
) -> SourceResult<()> {
fc.push();
fc.state_mut().pre_concat(group.transform);
@ -310,10 +314,12 @@ pub(crate) fn handle_group(
.and_then(|p| p.transform(fc.state().transform.to_krilla()));
if let Some(clip_path) = &clip_path {
let mut handle = tags::start_marked(gc, surface);
let surface = handle.surface();
surface.push_clip_path(clip_path, &krilla::paint::FillRule::NonZero);
}
handle_frame(fc, &group.frame, None, surface, context)?;
handle_frame(fc, &group.frame, None, surface, gc)?;
if clip_path.is_some() {
surface.pop();
@ -576,6 +582,16 @@ fn convert_error(
"{prefix} missing document date";
hint: "set the date of the document"
),
ValidationError::DuplicateTagId(_id, loc) => {
// TODO: display the id and better error message
let span = to_span(*loc);
error!(span, "{prefix} duplicate tag id")
}
ValidationError::UnknownHeaderTagId(_id, loc) => {
// TODO: display the id and better error message
let span = to_span(*loc);
error!(span, "{prefix} unknown header tag id")
}
}
}

View File

@ -34,8 +34,7 @@ pub(crate) fn embed_files(
},
};
let data: Arc<dyn AsRef<[u8]> + Send + Sync> = Arc::new(embed.data.clone());
// TODO: update when new krilla version lands (https://github.com/LaurenzV/krilla/pull/203)
let compress = should_compress(&embed.data).unwrap_or(true);
let compress = should_compress(&embed.data);
let file = EmbeddedFile {
path,

View File

@ -14,6 +14,7 @@ use typst_library::visualize::{
use typst_syntax::Span;
use crate::convert::{FrameContext, GlobalContext};
use crate::tags;
use crate::util::{SizeExt, TransformExt};
#[typst_macros::time(name = "handle image")]
@ -30,12 +31,10 @@ pub(crate) fn handle_image(
let interpolate = image.scaling() == Smart::Custom(ImageScaling::Smooth);
if let Some(alt) = image.alt() {
surface.start_alt_text(alt);
}
gc.image_spans.insert(span);
let mut handle = tags::start_marked(gc, surface);
let surface = handle.surface();
match image.kind() {
ImageKind::Raster(raster) => {
let (exif_transform, new_size) = exif_transform(raster, size);
@ -62,10 +61,6 @@ pub(crate) fn handle_image(
}
}
if image.alt().is_some() {
surface.end_alt_text();
}
surface.pop();
surface.reset_location();

View File

@ -9,6 +9,7 @@ mod outline;
mod page;
mod paint;
mod shape;
mod tags;
mod text;
mod util;

View File

@ -1,19 +1,87 @@
use std::collections::hash_map::Entry;
use ecow::EcoString;
use krilla::action::{Action, LinkAction};
use krilla::annotation::{LinkAnnotation, Target};
use krilla::annotation::Target;
use krilla::destination::XyzDestination;
use krilla::geom::Rect;
use typst_library::layout::{Abs, Point, Size};
use krilla::geom as kg;
use typst_library::layout::{Abs, Point, Position, Size};
use typst_library::model::Destination;
use crate::convert::{FrameContext, GlobalContext};
use crate::tags::{Placeholder, StackEntryKind, TagNode};
use crate::util::{AbsExt, PointExt};
pub(crate) struct LinkAnnotation {
pub(crate) placeholder: Placeholder,
pub(crate) alt: Option<String>,
pub(crate) rect: kg::Rect,
pub(crate) quad_points: Vec<kg::Point>,
pub(crate) target: Target,
}
pub(crate) fn handle_link(
fc: &mut FrameContext,
gc: &mut GlobalContext,
dest: &Destination,
size: Size,
) {
let target = match dest {
Destination::Url(u) => {
Target::Action(Action::Link(LinkAction::new(u.to_string())))
}
Destination::Position(p) => match pos_to_target(gc, *p) {
Some(target) => target,
None => return,
},
Destination::Location(loc) => {
if let Some(nd) = gc.loc_to_names.get(loc) {
// If a named destination has been registered, it's already guaranteed to
// not point to an excluded page.
Target::Destination(krilla::destination::Destination::Named(nd.clone()))
} else {
let pos = gc.document.introspector.position(*loc);
match pos_to_target(gc, pos) {
Some(target) => target,
None => return,
}
}
}
};
let entry = gc.tags.stack.last_mut().expect("a link parent");
let StackEntryKind::Link(link_id, link) = &entry.kind else {
unreachable!("expected a link parent")
};
let alt = link.alt.as_ref().map(EcoString::to_string);
let rect = to_rect(fc, size);
let quadpoints = quadpoints(rect);
match fc.link_annotations.entry(*link_id) {
Entry::Occupied(occupied) => {
// Update the bounding box and add the quadpoints of an existing link annotation.
let annotation = occupied.into_mut();
annotation.rect = bounding_rect(annotation.rect, rect);
annotation.quad_points.extend_from_slice(&quadpoints);
}
Entry::Vacant(vacant) => {
let placeholder = gc.tags.reserve_placeholder();
gc.tags.push(TagNode::Placeholder(placeholder));
vacant.insert(LinkAnnotation {
placeholder,
rect,
quad_points: quadpoints.to_vec(),
alt,
target,
});
}
}
}
// Compute the bounding box of the transformed link.
fn to_rect(fc: &FrameContext, size: Size) -> kg::Rect {
let mut min_x = Abs::inf();
let mut min_y = Abs::inf();
let mut max_x = -Abs::inf();
@ -21,7 +89,6 @@ pub(crate) fn handle_link(
let pos = Point::zero();
// Compute the bounding box of the transformed link.
for point in [
pos,
pos + Point::with_x(size.x),
@ -40,55 +107,32 @@ pub(crate) fn handle_link(
let y1 = min_y.to_f32();
let y2 = max_y.to_f32();
let rect = Rect::from_ltrb(x1, y1, x2, y2).unwrap();
kg::Rect::from_ltrb(x1, y1, x2, y2).unwrap()
}
// TODO: Support quad points.
let pos = match dest {
Destination::Url(u) => {
fc.push_annotation(
LinkAnnotation::new(
rect,
None,
Target::Action(Action::Link(LinkAction::new(u.to_string()))),
fn bounding_rect(a: kg::Rect, b: kg::Rect) -> kg::Rect {
kg::Rect::from_ltrb(
a.left().min(b.left()),
a.top().min(b.top()),
a.right().max(b.right()),
a.bottom().max(b.bottom()),
)
.into(),
);
return;
.unwrap()
}
Destination::Position(p) => *p,
Destination::Location(loc) => {
if let Some(nd) = gc.loc_to_names.get(loc) {
// If a named destination has been registered, it's already guaranteed to
// not point to an excluded page.
fc.push_annotation(
LinkAnnotation::new(
rect,
None,
Target::Destination(krilla::destination::Destination::Named(
nd.clone(),
)),
)
.into(),
);
return;
} else {
gc.document.introspector.position(*loc)
}
}
};
fn quadpoints(rect: kg::Rect) -> [kg::Point; 4] {
[
kg::Point::from_xy(rect.left(), rect.bottom()),
kg::Point::from_xy(rect.right(), rect.bottom()),
kg::Point::from_xy(rect.right(), rect.top()),
kg::Point::from_xy(rect.left(), rect.top()),
]
}
fn pos_to_target(gc: &mut GlobalContext, pos: Position) -> Option<Target> {
let page_index = pos.page.get() - 1;
if let Some(index) = gc.page_index_converter.pdf_page_index(page_index) {
fc.push_annotation(
LinkAnnotation::new(
rect,
None,
Target::Destination(krilla::destination::Destination::Xyz(
XyzDestination::new(index, pos.point.to_krilla()),
)),
)
.into(),
);
}
let index = gc.page_index_converter.pdf_page_index(page_index)?;
let dest = XyzDestination::new(index, pos.point.to_krilla());
Some(Target::Destination(krilla::destination::Destination::Xyz(dest)))
}

View File

@ -5,8 +5,8 @@ use typst_library::visualize::{Geometry, Shape};
use typst_syntax::Span;
use crate::convert::{FrameContext, GlobalContext};
use crate::paint;
use crate::util::{convert_path, AbsExt, TransformExt};
use crate::{paint, tags};
#[typst_macros::time(name = "handle shape")]
pub(crate) fn handle_shape(
@ -16,6 +16,9 @@ pub(crate) fn handle_shape(
gc: &mut GlobalContext,
span: Span,
) -> SourceResult<()> {
let mut handle = tags::start_marked(gc, surface);
let surface = handle.surface();
surface.set_location(span.into_raw().get());
surface.push_transform(&fc.state().transform().to_krilla());

View File

@ -0,0 +1,398 @@
use std::cell::OnceCell;
use std::collections::HashMap;
use ecow::EcoString;
use krilla::page::Page;
use krilla::surface::Surface;
use krilla::tagging::{
ArtifactType, ContentTag, Identifier, Node, TableCellSpan, TableDataCell,
TableHeaderCell, TableHeaderScope, Tag, TagBuilder, TagGroup, TagKind, TagTree,
};
use typst_library::foundations::{Content, LinkMarker, Packed, StyleChain};
use typst_library::introspection::Location;
use typst_library::model::{
Destination, FigureCaption, FigureElem, HeadingElem, Outlinable, OutlineElem,
OutlineEntry, TableCell, TableElem, TableHLine, TableVLine,
};
use typst_library::pdf::{ArtifactElem, ArtifactKind, PdfTagElem, PdfTagKind};
use typst_library::visualize::ImageElem;
use crate::convert::GlobalContext;
use crate::link::LinkAnnotation;
pub(crate) struct Tags {
/// The intermediary stack of nested tag groups.
pub(crate) stack: Vec<StackEntry>,
/// A list of placeholders corresponding to a [`TagNode::Placeholder`].
pub(crate) placeholders: Vec<OnceCell<Node>>,
pub(crate) in_artifact: Option<(Location, ArtifactKind)>,
pub(crate) link_id: LinkId,
/// The output.
pub(crate) tree: Vec<TagNode>,
}
pub(crate) struct StackEntry {
pub(crate) loc: Location,
pub(crate) kind: StackEntryKind,
pub(crate) nodes: Vec<TagNode>,
}
pub(crate) enum StackEntryKind {
Standard(Tag),
Link(LinkId, Packed<LinkMarker>),
Table(TableCtx),
TableCell(Packed<TableCell>),
}
impl StackEntryKind {
pub(crate) fn as_standard_mut(&mut self) -> Option<&mut Tag> {
if let Self::Standard(v) = self {
Some(v)
} else {
None
}
}
}
pub(crate) struct TableCtx {
table: Packed<TableElem>,
rows: Vec<Vec<Option<(Packed<TableCell>, Tag, Vec<TagNode>)>>>,
}
impl TableCtx {
fn insert(&mut self, cell: Packed<TableCell>, nodes: Vec<TagNode>) {
let x = cell.x(StyleChain::default()).unwrap_or_else(|| unreachable!());
let y = cell.y(StyleChain::default()).unwrap_or_else(|| unreachable!());
let rowspan = cell.rowspan(StyleChain::default()).get();
let colspan = cell.colspan(StyleChain::default()).get();
// TODO: possibly set internal field on TableCell when resolving
// the cell grid.
let is_header = false;
let span = TableCellSpan { rows: rowspan as i32, cols: colspan as i32 };
let tag = if is_header {
let scope = TableHeaderScope::Column; // TODO
TagKind::TH(TableHeaderCell::new(scope).with_span(span))
} else {
TagKind::TD(TableDataCell::new().with_span(span))
};
let required_height = y + rowspan;
if self.rows.len() < required_height {
self.rows.resize_with(required_height, Vec::new);
}
let required_width = x + colspan;
let row = &mut self.rows[y];
if row.len() < required_width {
row.resize_with(required_width, || None);
}
row[x] = Some((cell, tag.into(), nodes));
}
fn build_table(self, mut nodes: Vec<TagNode>) -> Vec<TagNode> {
// Table layouting ensures that there are no overlapping cells, and that
// any gaps left by the user are filled with empty cells.
for row in self.rows.into_iter() {
let mut row_nodes = Vec::new();
for (_, tag, nodes) in row.into_iter().flatten() {
row_nodes.push(TagNode::Group(tag, nodes));
}
// TODO: generate `THead`, `TBody`, and `TFoot`
nodes.push(TagNode::Group(TagKind::TR.into(), row_nodes));
}
nodes
}
}
pub(crate) enum TagNode {
Group(Tag, Vec<TagNode>),
Leaf(Identifier),
/// Allows inserting a placeholder into the tag tree.
/// Currently used for [`krilla::page::Page::add_tagged_annotation`].
Placeholder(Placeholder),
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct LinkId(u32);
#[derive(Clone, Copy)]
pub(crate) struct Placeholder(usize);
impl Tags {
pub(crate) fn new() -> Self {
Self {
stack: Vec::new(),
placeholders: Vec::new(),
in_artifact: None,
tree: Vec::new(),
link_id: LinkId(0),
}
}
pub(crate) fn reserve_placeholder(&mut self) -> Placeholder {
let idx = self.placeholders.len();
self.placeholders.push(OnceCell::new());
Placeholder(idx)
}
pub(crate) fn init_placeholder(&mut self, placeholder: Placeholder, node: Node) {
self.placeholders[placeholder.0]
.set(node)
.map_err(|_| ())
.expect("placeholder to be uninitialized");
}
pub(crate) fn take_placeholder(&mut self, placeholder: Placeholder) -> Node {
self.placeholders[placeholder.0]
.take()
.expect("initialized placeholder node")
}
/// Returns the current parent's list of children and the structure type ([Tag]).
/// In case of the document root the structure type will be `None`.
pub(crate) fn parent(&mut self) -> (Option<&mut StackEntryKind>, &mut Vec<TagNode>) {
if let Some(entry) = self.stack.last_mut() {
(Some(&mut entry.kind), &mut entry.nodes)
} else {
(None, &mut self.tree)
}
}
pub(crate) fn push(&mut self, node: TagNode) {
self.parent().1.push(node);
}
pub(crate) fn build_tree(&mut self) -> TagTree {
let children = std::mem::take(&mut self.tree)
.into_iter()
.map(|node| self.resolve_node(node))
.collect::<Vec<_>>();
TagTree::from(children)
}
/// Resolves [`Placeholder`] nodes.
fn resolve_node(&mut self, node: TagNode) -> Node {
match node {
TagNode::Group(tag, nodes) => {
let children = nodes
.into_iter()
.map(|node| self.resolve_node(node))
.collect::<Vec<_>>();
Node::Group(TagGroup::with_children(tag, children))
}
TagNode::Leaf(identifier) => Node::Leaf(identifier),
TagNode::Placeholder(placeholder) => self.take_placeholder(placeholder),
}
}
fn context_supports(&self, _tag: &StackEntryKind) -> bool {
// TODO: generate using: https://pdfa.org/resource/iso-ts-32005-hierarchical-inclusion-rules/
true
}
fn next_link_id(&mut self) -> LinkId {
self.link_id.0 += 1;
self.link_id
}
}
/// Automatically calls [`Surface::end_tagged`] when dropped.
pub(crate) struct TagHandle<'a, 'b> {
surface: &'b mut Surface<'a>,
}
impl Drop for TagHandle<'_, '_> {
fn drop(&mut self) {
self.surface.end_tagged();
}
}
impl<'a> TagHandle<'a, '_> {
pub(crate) fn surface<'c>(&'c mut self) -> &'c mut Surface<'a> {
&mut self.surface
}
}
/// Returns a [`TagHandle`] that automatically calls [`Surface::end_tagged`]
/// when dropped.
pub(crate) fn start_marked<'a, 'b>(
gc: &mut GlobalContext,
surface: &'b mut Surface<'a>,
) -> TagHandle<'a, 'b> {
let content = if let Some((_, kind)) = gc.tags.in_artifact {
let ty = artifact_type(kind);
ContentTag::Artifact(ty)
} else {
ContentTag::Other
};
let id = surface.start_tagged(content);
gc.tags.push(TagNode::Leaf(id));
TagHandle { surface }
}
/// Add all annotations that were found in the page frame.
pub(crate) fn add_annotations(
gc: &mut GlobalContext,
page: &mut Page,
annotations: HashMap<LinkId, LinkAnnotation>,
) {
for annotation in annotations.into_values() {
let LinkAnnotation { placeholder, alt, rect, quad_points, target } = annotation;
let annot = krilla::annotation::Annotation::new_link(
krilla::annotation::LinkAnnotation::new(rect, Some(quad_points), target),
alt,
);
let annot_id = page.add_tagged_annotation(annot);
gc.tags.init_placeholder(placeholder, Node::Leaf(annot_id));
}
}
pub(crate) fn handle_start(gc: &mut GlobalContext, elem: &Content) {
if gc.tags.in_artifact.is_some() {
// Don't nest artifacts
return;
}
let loc = elem.location().unwrap();
if let Some(artifact) = elem.to_packed::<ArtifactElem>() {
let kind = artifact.kind(StyleChain::default());
start_artifact(gc, loc, kind);
return;
}
let tag: Tag = if let Some(pdf_tag) = elem.to_packed::<PdfTagElem>() {
let kind = pdf_tag.kind(StyleChain::default());
match kind {
PdfTagKind::Part => TagKind::Part.into(),
_ => todo!(),
}
} else if let Some(heading) = elem.to_packed::<HeadingElem>() {
let level = heading.level();
let name = heading.body.plain_text().to_string();
match level.get() {
1 => TagKind::H1(Some(name)).into(),
2 => TagKind::H2(Some(name)).into(),
3 => TagKind::H3(Some(name)).into(),
4 => TagKind::H4(Some(name)).into(),
5 => TagKind::H5(Some(name)).into(),
// TODO: when targeting PDF 2.0 headings `> 6` are supported
_ => TagKind::H6(Some(name)).into(),
}
} else if let Some(_) = elem.to_packed::<OutlineElem>() {
TagKind::TOC.into()
} else if let Some(_) = elem.to_packed::<OutlineEntry>() {
TagKind::TOCI.into()
} else if let Some(_) = elem.to_packed::<FigureElem>() {
let alt = None; // TODO
TagKind::Figure.with_alt_text(alt)
} else if let Some(image) = elem.to_packed::<ImageElem>() {
let alt = image.alt(StyleChain::default()).map(|s| s.to_string());
let figure_tag = (gc.tags.parent().0)
.and_then(|parent| parent.as_standard_mut())
.filter(|tag| tag.kind == TagKind::Figure && tag.alt_text.is_none());
if let Some(figure_tag) = figure_tag {
// HACK: set alt text of outer figure tag, if the contained image
// has alt text specified
figure_tag.alt_text = alt;
return;
} else {
TagKind::Figure.with_alt_text(alt)
}
} else if let Some(_) = elem.to_packed::<FigureCaption>() {
TagKind::Caption.into()
} else if let Some(link) = elem.to_packed::<LinkMarker>() {
let link_id = gc.tags.next_link_id();
push_stack(gc, loc, StackEntryKind::Link(link_id, link.clone()));
return;
} else if let Some(table) = elem.to_packed::<TableElem>() {
let ctx = TableCtx { table: table.clone(), rows: Vec::new() };
push_stack(gc, loc, StackEntryKind::Table(ctx));
return;
} else if let Some(cell) = elem.to_packed::<TableCell>() {
push_stack(gc, loc, StackEntryKind::TableCell(cell.clone()));
return;
} else if let Some(_) = elem.to_packed::<TableHLine>() {
start_artifact(gc, loc, ArtifactKind::Other);
return;
} else if let Some(_) = elem.to_packed::<TableVLine>() {
start_artifact(gc, loc, ArtifactKind::Other);
return;
} else {
return;
};
push_stack(gc, loc, StackEntryKind::Standard(tag));
}
fn push_stack(gc: &mut GlobalContext, loc: Location, kind: StackEntryKind) {
if !gc.tags.context_supports(&kind) {
// TODO: error or warning?
}
gc.tags.stack.push(StackEntry { loc, kind, nodes: Vec::new() });
}
pub(crate) fn handle_end(gc: &mut GlobalContext, loc: Location) {
if let Some((l, _)) = gc.tags.in_artifact {
if l == loc {
gc.tags.in_artifact = None;
}
return;
}
let Some(entry) = gc.tags.stack.pop_if(|e| e.loc == loc) else {
return;
};
let node = match entry.kind {
StackEntryKind::Standard(tag) => TagNode::Group(tag, entry.nodes),
StackEntryKind::Link(_, link) => {
let alt = link.alt.as_ref().map(EcoString::to_string);
let tag = TagKind::Link.with_alt_text(alt);
let mut node = TagNode::Group(tag, entry.nodes);
// Wrap link in reference tag, if it's not a url.
if let Destination::Position(_) | Destination::Location(_) = link.dest {
node = TagNode::Group(TagKind::Reference.into(), vec![node]);
}
node
}
StackEntryKind::Table(ctx) => {
let summary = ctx.table.summary(StyleChain::default()).map(EcoString::into);
let nodes = ctx.build_table(entry.nodes);
TagNode::Group(TagKind::Table(summary).into(), nodes)
}
StackEntryKind::TableCell(cell) => {
let parent = gc.tags.stack.last_mut().expect("table");
let StackEntryKind::Table(table_ctx) = &mut parent.kind else {
unreachable!("expected table")
};
table_ctx.insert(cell, entry.nodes);
return;
}
};
gc.tags.push(node);
}
fn start_artifact(gc: &mut GlobalContext, loc: Location, kind: ArtifactKind) {
gc.tags.in_artifact = Some((loc, kind));
}
fn artifact_type(kind: ArtifactKind) -> ArtifactType {
match kind {
ArtifactKind::Header => ArtifactType::Header,
ArtifactKind::Footer => ArtifactType::Footer,
ArtifactKind::Page => ArtifactType::Page,
ArtifactKind::Other => ArtifactType::Other,
}
}

View File

@ -11,8 +11,8 @@ use typst_library::visualize::FillRule;
use typst_syntax::Span;
use crate::convert::{FrameContext, GlobalContext};
use crate::paint;
use crate::util::{display_font, AbsExt, TransformExt};
use crate::{paint, tags};
#[typst_macros::time(name = "handle text")]
pub(crate) fn handle_text(
@ -23,6 +23,9 @@ pub(crate) fn handle_text(
) -> SourceResult<()> {
*gc.languages.entry(t.lang).or_insert(0) += t.glyphs.len();
let mut handle = tags::start_marked(gc, surface);
let surface = handle.surface();
let font = convert_font(gc, t.font.clone())?;
let fill = paint::convert_fill(
gc,

View File

@ -167,7 +167,7 @@ fn render_frame(canvas: &mut sk::Pixmap, state: State, frame: &Frame) {
FrameItem::Image(image, size, _) => {
image::render_image(canvas, state.pre_translate(*pos), image, *size);
}
FrameItem::Link(_, _) => {}
FrameItem::Link(..) => {}
FrameItem::Tag(_) => {}
}
}

View File

@ -207,7 +207,7 @@ impl SVGRenderer {
for (pos, item) in frame.items() {
// File size optimization.
// TODO: SVGs could contain links, couldn't they?
if matches!(item, FrameItem::Link(_, _) | FrameItem::Tag(_)) {
if matches!(item, FrameItem::Link(..) | FrameItem::Tag(_)) {
continue;
}
@ -228,7 +228,7 @@ impl SVGRenderer {
self.render_shape(state.pre_translate(*pos), shape)
}
FrameItem::Image(image, size, _) => self.render_image(image, size),
FrameItem::Link(_, _) => unreachable!(),
FrameItem::Link(..) => unreachable!(),
FrameItem::Tag(_) => unreachable!(),
};

View File

@ -137,6 +137,59 @@
In addition to the functions listed below, the `calc` module also defines
the constants `pi`, `tau`, `e`, and `inf`.
- name: std
title: Standard library
category: foundations
path: ["std"]
details: |
A module that contains all globally accessible items.
# Using "shadowed" definitions
The `std` module is useful whenever you overrode a name from the global
scope (this is called _shadowing_). For instance, you might have used the
name `text` for a parameter. To still access the `text` element, write
`std.text`.
```example
>>> #set page(margin: (left: 3em))
#let par = [My special paragraph.]
#let special(text) = {
set std.text(style: "italic")
set std.par.line(numbering: "1")
text
}
#special(par)
#lorem(10)
```
# Conditional access
You can also use this in combination with the [dictionary
constructor]($dictionary) to conditionally access global definitions. This
can, for instance, be useful to use new or experimental functionality when
it is available, while falling back to an alternative implementation if
used on an older Typst version. In particular, this allows us to create
[polyfills](https://en.wikipedia.org/wiki/Polyfill_(programming)).
This can be as simple as creating an alias to prevent warning messages, for
example, conditionally using `pattern` in Typst version 0.12, but using
[`tiling`] in newer versions. Since the parameters accepted by the `tiling`
function match those of the older `pattern` function, using the `tiling`
function when available and falling back to `pattern` otherwise will unify
the usage across all versions. Note that, when creating a polyfill,
[`sys.version`]($category/foundations/sys) can also be very useful.
```typ
#let tiling = if "tiling" in dictionary(std) {
tiling
} else {
pattern
}
...
```
- name: sys
title: System
category: foundations

View File

@ -37,7 +37,7 @@ static GROUPS: LazyLock<Vec<GroupData>> = LazyLock::new(|| {
let mut groups: Vec<GroupData> =
yaml::from_str(load!("reference/groups.yml")).unwrap();
for group in &mut groups {
if group.filter.is_empty() {
if group.filter.is_empty() && group.name != "std" {
group.filter = group
.module()
.scope()

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body><textarea>
enter</textarea></body>
</html>

View File

@ -11,6 +11,9 @@
#html.pre("\nhello")
#html.pre("\n\nhello")
--- html-textarea-starting-with-newline html ---
#html.textarea("\nenter")
--- html-script html ---
// This should be pretty and indented.
#html.script(