summaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
authorJohann Birnick <6528009+jbirnick@users.noreply.github.com>2024-12-16 10:22:00 -0800
committerGitHub <noreply@github.com>2024-12-16 18:22:00 +0000
commit75273937f762ebeb05f71e91434b298b52b44670 (patch)
treeeee3f1c9520304d075ffae977c4f76f4536fbd8e /crates
parent8b1e0d3a233950bd8fd553e118ec6342efb42855 (diff)
Transform high level headings to HTML (#5525)
Diffstat (limited to 'crates')
-rw-r--r--crates/typst-library/src/html/dom.rs14
-rw-r--r--crates/typst-library/src/model/heading.rs28
2 files changed, 33 insertions, 9 deletions
diff --git a/crates/typst-library/src/html/dom.rs b/crates/typst-library/src/html/dom.rs
index 3d558fb0..5b6eab4d 100644
--- a/crates/typst-library/src/html/dom.rs
+++ b/crates/typst-library/src/html/dom.rs
@@ -122,8 +122,8 @@ impl HtmlTag {
let bytes = string.as_bytes();
let mut i = 0;
while i < bytes.len() {
- if !bytes[i].is_ascii_alphanumeric() {
- panic!("constant tag name must be ASCII alphanumeric");
+ 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;
}
@@ -220,8 +220,10 @@ impl HtmlAttr {
let bytes = string.as_bytes();
let mut i = 0;
while i < bytes.len() {
- if !bytes[i].is_ascii_alphanumeric() {
- panic!("constant attribute name must be ASCII alphanumeric");
+ 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;
}
@@ -621,5 +623,9 @@ pub mod attr {
href
name
value
+ role
}
+
+ #[allow(non_upper_case_globals)]
+ pub const aria_level: HtmlAttr = HtmlAttr::constant("aria-level");
}
diff --git a/crates/typst-library/src/model/heading.rs b/crates/typst-library/src/model/heading.rs
index fc0e4ad2..ec9cf4e9 100644
--- a/crates/typst-library/src/model/heading.rs
+++ b/crates/typst-library/src/model/heading.rs
@@ -1,14 +1,15 @@
use std::num::NonZeroUsize;
+use ecow::eco_format;
use typst_utils::NonZeroExt;
-use crate::diag::SourceResult;
+use crate::diag::{warning, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
elem, Content, NativeElement, Packed, Resolve, Show, ShowSet, Smart, StyleChain,
Styles, Synthesize, TargetElem,
};
-use crate::html::{tag, HtmlElem};
+use crate::html::{attr, tag, HtmlElem};
use crate::introspection::{
Count, Counter, CounterUpdate, Locatable, Locator, LocatorLink,
};
@@ -272,9 +273,26 @@ impl Show for Packed<HeadingElem> {
// 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 `<h2>`.
- let level = self.resolve_level(styles);
- let t = [tag::h2, tag::h3, tag::h4, tag::h5, tag::h6][level.get().min(5) - 1];
- HtmlElem::new(t).with_body(Some(realized)).pack().spanned(span)
+ let level = self.resolve_level(styles).get();
+ if level >= 6 {
+ engine.sink.warn(warning!(span,
+ "heading of level {} was transformed to \
+ <div role=\"heading\" aria-level=\"{}\">, which is not \
+ supported by all assistive technology",
+ level, level + 1;
+ hint: "HTML only supports <h1> to <h6>, not <h{}>", 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 realized = BlockBody::Content(realized);
BlockElem::new().with_body(Some(realized)).pack().spanned(span)