diff --git a/crates/typst-library/src/html/dom.rs b/crates/typst-library/src/html/dom.rs index 3d558fb0f..5b6eab4d6 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 fc0e4ad25..ec9cf4e99 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 { // 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); - 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 \ +
, 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 realized = BlockBody::Content(realized); BlockElem::new().with_body(Some(realized)).pack().spanned(span)