Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions crates/typst-eval/src/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ use typst_library::diag::{
};
use typst_library::engine::{Engine, Sink, Traced};
use typst_library::foundations::{
Arg, Args, Binding, Capturer, Closure, Content, Context, Func, NativeElement, Scope,
Scopes, SymbolElem, Value,
Arg, Args, Binding, Capturer, Closure, ClosureNode, Content, Context, Func,
NativeElement, Scope, Scopes, SymbolElem, Value,
};
use typst_library::introspection::Introspector;
use typst_library::math::LrElem;
Expand Down Expand Up @@ -154,7 +154,7 @@ impl Eval for ast::Closure<'_> {

// Define the closure.
let closure = Closure {
node: self.to_untyped().clone(),
node: ClosureNode::Closure(self.to_untyped().clone()),
defaults,
captured,
num_pos_params: self
Expand Down Expand Up @@ -183,9 +183,15 @@ pub fn eval_closure(
context: Tracked<Context>,
mut args: Args,
) -> SourceResult<Value> {
let (name, params, body) = match closure.node.cast::<ast::Closure>() {
Some(node) => (node.name(), node.params(), node.body()),
None => (None, ast::Params::default(), closure.node.cast().unwrap()),
let (name, params, body) = match closure.node {
ClosureNode::Closure(ref node) => {
let closure =
node.cast::<ast::Closure>().expect("node to be an `ast::Closure`");
(closure.name(), closure.params(), closure.body())
}
ClosureNode::Context(ref node) => {
(None, ast::Params::default(), node.cast().unwrap())
}
};

// Don't leak the scopes from the call site. Instead, we use the scope
Expand Down
6 changes: 3 additions & 3 deletions crates/typst-eval/src/code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ use ecow::{EcoVec, eco_vec};
use typst_library::diag::{At, SourceResult, bail, error, warning};
use typst_library::engine::Engine;
use typst_library::foundations::{
Array, Capturer, Closure, Content, ContextElem, Dict, Func, NativeElement, Selector,
Str, Value, ops,
Array, Capturer, Closure, ClosureNode, Content, ContextElem, Dict, Func,
NativeElement, Selector, Str, Value, ops,
};
use typst_library::introspection::{Counter, State};
use typst_syntax::ast::{self, AstNode};
Expand Down Expand Up @@ -356,7 +356,7 @@ impl Eval for ast::Contextual<'_> {

// Define the closure.
let closure = Closure {
node: self.body().to_untyped().clone(),
node: ClosureNode::Context(self.body().to_untyped().clone()),
defaults: vec![],
captured,
num_pos_params: 0,
Expand Down
4 changes: 3 additions & 1 deletion crates/typst-html/src/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,9 @@ fn handle_html_elem(
elem: &Packed<HtmlElem>,
styles: StyleChain,
) -> SourceResult<()> {
let role = styles.get_cloned(HtmlElem::role);
// See the docs of `HtmlElem::role` for why we filter out roles for `<p>`
// elements.
let role = styles.get_cloned(HtmlElem::role).filter(|_| elem.tag != tag::p);

let mut children = EcoVec::new();
if let Some(body) = elem.body.get_ref(styles) {
Expand Down
1 change: 1 addition & 0 deletions crates/typst-html/src/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ fn html_document_impl(
leaves.extend(notes);
leaves
} else {
FootnoteContainer::unsupported_with_custom_dom(&engine)?;
&nodes
};

Expand Down
6 changes: 6 additions & 0 deletions crates/typst-html/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ pub struct HtmlElem {
/// element instead of a subtree, they could supplant this. If we need the
/// same mechanism for things like `class`, this could potentially also be
/// extended to arbitrary attributes. It's minimal for now.
///
/// This is ignored for `<p>` elements as it otherwise tends to
/// unintentionally attach to paragraphs resulting from grouping of a single
/// element instead of attaching to that element. This is a bit of a hack,
/// but good enough for now as the `role` property is purely internal and
/// we control what it is used for.
#[internal]
#[ghost]
pub role: Option<EcoString>,
Expand Down
150 changes: 116 additions & 34 deletions crates/typst-html/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ use std::num::NonZeroUsize;

use comemo::Track;
use ecow::{EcoVec, eco_format};
use typst_library::diag::{At, bail, warning};
use typst_library::diag::{At, SourceResult, bail, error, warning};
use typst_library::engine::Engine;
use typst_library::foundations::{
Content, Context, NativeElement, NativeRuleMap, ShowFn, Smart, StyleChain, Target,
};
Expand All @@ -12,10 +13,11 @@ use typst_library::layout::{
BlockBody, BlockElem, BoxElem, HElem, OuterVAlignment, Sizing,
};
use typst_library::model::{
Attribution, CiteElem, CiteGroup, Destination, DirectLinkElem, EmphElem, EnumElem,
FigureCaption, FigureElem, FootnoteElem, FootnoteEntry, HeadingElem, LinkElem,
LinkTarget, ListElem, OutlineElem, OutlineEntry, OutlineNode, ParElem, ParbreakElem,
QuoteElem, RefElem, StrongElem, TableCell, TableElem, TermsElem, TitleElem,
Attribution, BibliographyElem, CiteElem, CiteGroup, CslIndentElem, CslLightElem,
Destination, DirectLinkElem, EmphElem, EnumElem, FigureCaption, FigureElem,
FootnoteElem, FootnoteEntry, HeadingElem, LinkElem, LinkTarget, ListElem,
OutlineElem, OutlineEntry, OutlineNode, ParElem, ParbreakElem, QuoteElem, RefElem,
StrongElem, TableCell, TableElem, TermsElem, TitleElem, Works,
};
use typst_library::text::{
HighlightElem, LinebreakElem, OverlineElem, RawElem, RawLine, SmallcapsElem,
Expand Down Expand Up @@ -52,6 +54,9 @@ pub fn register(rules: &mut NativeRuleMap) {
rules.register(Html, OUTLINE_ENTRY_RULE);
rules.register(Html, REF_RULE);
rules.register(Html, CITE_GROUP_RULE);
rules.register(Html, BIBLIOGRAPHY_RULE);
rules.register(Html, CSL_LIGHT_RULE);
rules.register(Html, CSL_INDENT_RULE);
rules.register(Html, TABLE_RULE);

// Text.
Expand Down Expand Up @@ -317,6 +322,25 @@ impl FootnoteContainer {
pub fn shared() -> &'static Content {
singleton!(Content, FootnoteContainer::new().pack())
}

/// Fails with an error if there are footnotes.
pub fn unsupported_with_custom_dom(engine: &Engine) -> SourceResult<()> {
let notes = engine.introspector.query(&FootnoteElem::ELEM.select());
if notes.is_empty() {
return Ok(());
}

Err(notes
.iter()
.map(|note| {
error!(
note.span(),
"footnotes are not currently supported in combination \
with a custom `<html>` or `<body>` element"
)
})
.collect())
}
}

const FOOTNOTE_CONTAINER_RULE: ShowFn<FootnoteContainer> = |_, engine, _| {
Expand All @@ -331,11 +355,10 @@ const FOOTNOTE_CONTAINER_RULE: ShowFn<FootnoteContainer> = |_, engine, _| {
let loc = note.location().unwrap();
let span = note.span();
HtmlElem::new(tag::li)
.with_body(Some(
FootnoteEntry::new(note).pack().spanned(span).located(loc.variant(1)),
))
.with_body(Some(FootnoteEntry::new(note).pack().spanned(span)))
.with_parent(loc)
.pack()
.located(loc.variant(1))
.spanned(span)
});

Expand All @@ -348,36 +371,27 @@ const FOOTNOTE_CONTAINER_RULE: ShowFn<FootnoteContainer> = |_, engine, _| {
.pack();

// The user may want to style the whole footnote element so we wrap it in an
// additional selectable container. `aside` has the right semantics as a
// container for auxiliary page content. There is no ARIA role for
// footnotes, so we use a class instead. (There is `doc-endnotes`, but has
// footnotes and endnotes have somewhat different semantics.)
Ok(HtmlElem::new(tag::aside)
.with_attr(attr::class, "footnotes")
// additional selectable container. This is also how it's done in the ARIA
// spec (although there, the section also contains an additional heading).
Ok(HtmlElem::new(tag::section)
.with_attr(attr::role, "doc-endnotes")
.with_body(Some(list))
.pack())
};

const FOOTNOTE_ENTRY_RULE: ShowFn<FootnoteEntry> = |elem, engine, styles| {
let span = elem.span();
let (dest, num, body) = elem.realize(engine, styles)?;
let sup = SuperElem::new(num).pack().spanned(span);

// We create a link back to the first footnote reference.
let link = LinkElem::new(dest.into(), sup)
.pack()
.spanned(span)
.styled(HtmlElem::role.set(Some("doc-backlink".into())));

// We want to use the Digital Publishing ARIA role `doc-footnote` and the
// fallback role `note` for each individual footnote. Because the enclosing
// `li`, as a child of an `ol`, must have the implicit `listitem` role, we
// need an additional container. We chose a `div` instead of a `span` to
// allow for block-level content in the footnote.
Ok(HtmlElem::new(tag::div)
.with_attr(attr::role, "doc-footnote note")
.with_body(Some(link + body))
.pack())
let (prefix, body) = elem.realize(engine, styles)?;

// The prefix is a link back to the first footnote reference, so
// `doc-backlink` is the appropriate ARIA role.
let backlink = prefix.styled(HtmlElem::role.set(Some("doc-backlink".into())));

// We do not use the ARIA role `doc-footnote` because it "is only for
// representing individual notes that occur within the body of a work" (see
// <https://www.w3.org/TR/dpub-aria-1.1/#doc-footnote>). Our footnotes more
// appropriately modelled as ARIA endnotes. This is also in line with how
// Pandoc handles footnotes.
Ok(backlink + body)
};

const OUTLINE_RULE: ShowFn<OutlineElem> = |elem, engine, styles| {
Expand Down Expand Up @@ -445,7 +459,75 @@ const OUTLINE_ENTRY_RULE: ShowFn<OutlineEntry> = |elem, engine, styles| {

const REF_RULE: ShowFn<RefElem> = |elem, engine, styles| elem.realize(engine, styles);

const CITE_GROUP_RULE: ShowFn<CiteGroup> = |elem, engine, _| elem.realize(engine);
const CITE_GROUP_RULE: ShowFn<CiteGroup> = |elem, engine, _| {
Ok(elem
.realize(engine)?
.styled(HtmlElem::role.set(Some("doc-biblioref".into()))))
};

// For the bibliography, we have a few elements that should be styled (e.g.
// indent), but inline styles are not apprioriate because they couldn't be
// properly overridden. For those, we currently emit classes so that a user can
// style them with CSS, but do not emit any styles ourselves.
const BIBLIOGRAPHY_RULE: ShowFn<BibliographyElem> = |elem, engine, styles| {
let span = elem.span();
let works = Works::generate(engine).at(span)?;
let references = works.references(elem, styles)?;

let items = references.iter().map(|(prefix, reference, loc)| {
let mut realized = reference.clone();

if let Some(mut prefix) = prefix.clone() {
// If we have a link back to the first citation referencing this
// entry, attach the appropriate role.
if prefix.is::<DirectLinkElem>() {
prefix = prefix.set(HtmlElem::role, Some("doc-backlink".into()));
}

let wrapped = HtmlElem::new(tag::span)
.with_attr(attr::class, "prefix")
.with_body(Some(prefix))
.pack()
.spanned(span);

let separator = SpaceElem::shared().clone();
realized = Content::sequence([wrapped, separator, realized]);
}

HtmlElem::new(tag::li)
.with_body(Some(realized))
.pack()
.located(*loc)
.spanned(span)
});

let title = elem.realize_title(styles);
let list = HtmlElem::new(tag::ul)
.with_styles(css::Properties::new().with("list-style-type", "none"))
.with_body(Some(Content::sequence(items)))
.pack()
.spanned(span);

Ok(HtmlElem::new(tag::section)
.with_attr(attr::role, "doc-bibliography")
.with_optional_attr(attr::class, works.hanging_indent.then_some("hanging-indent"))
.with_body(Some(title.unwrap_or_default() + list))
.pack())
};

const CSL_LIGHT_RULE: ShowFn<CslLightElem> = |elem, _, _| {
Ok(HtmlElem::new(tag::span)
.with_attr(attr::class, "light")
.with_body(Some(elem.body.clone()))
.pack())
};

const CSL_INDENT_RULE: ShowFn<CslIndentElem> = |elem, _, _| {
Ok(HtmlElem::new(tag::div)
.with_attr(attr::class, "indent")
.with_body(Some(elem.body.clone()))
.pack())
};

const TABLE_RULE: ShowFn<TableElem> = |elem, engine, styles| {
Ok(show_cellgrid(table_to_cellgrid(elem, engine, styles)?, styles))
Expand Down
Loading