Transform high level headings to HTML (#5525)

This commit is contained in:
Johann Birnick 2024-12-16 10:22:00 -08:00 committed by GitHub
parent 8b1e0d3a23
commit 75273937f7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 33 additions and 9 deletions

View File

@ -122,8 +122,8 @@ impl HtmlTag {
let bytes = string.as_bytes(); let bytes = string.as_bytes();
let mut i = 0; let mut i = 0;
while i < bytes.len() { while i < bytes.len() {
if !bytes[i].is_ascii_alphanumeric() { if !bytes[i].is_ascii() || !charsets::is_valid_in_tag_name(bytes[i] as char) {
panic!("constant tag name must be ASCII alphanumeric"); panic!("not all characters are valid in a tag name");
} }
i += 1; i += 1;
} }
@ -220,8 +220,10 @@ impl HtmlAttr {
let bytes = string.as_bytes(); let bytes = string.as_bytes();
let mut i = 0; let mut i = 0;
while i < bytes.len() { while i < bytes.len() {
if !bytes[i].is_ascii_alphanumeric() { if !bytes[i].is_ascii()
panic!("constant attribute name must be ASCII alphanumeric"); || !charsets::is_valid_in_attribute_name(bytes[i] as char)
{
panic!("not all characters are valid in an attribute name");
} }
i += 1; i += 1;
} }
@ -621,5 +623,9 @@ pub mod attr {
href href
name name
value value
role
} }
#[allow(non_upper_case_globals)]
pub const aria_level: HtmlAttr = HtmlAttr::constant("aria-level");
} }

View File

@ -1,14 +1,15 @@
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use ecow::eco_format;
use typst_utils::NonZeroExt; use typst_utils::NonZeroExt;
use crate::diag::SourceResult; use crate::diag::{warning, SourceResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
elem, Content, NativeElement, Packed, Resolve, Show, ShowSet, Smart, StyleChain, elem, Content, NativeElement, Packed, Resolve, Show, ShowSet, Smart, StyleChain,
Styles, Synthesize, TargetElem, Styles, Synthesize, TargetElem,
}; };
use crate::html::{tag, HtmlElem}; use crate::html::{attr, tag, HtmlElem};
use crate::introspection::{ use crate::introspection::{
Count, Counter, CounterUpdate, Locatable, Locator, LocatorLink, 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 // Meanwhile, a level 1 Typst heading is a section heading. For this
// reason, levels are offset by one: A Typst level 1 heading becomes // reason, levels are offset by one: A Typst level 1 heading becomes
// a `<h2>`. // a `<h2>`.
let level = self.resolve_level(styles); let level = self.resolve_level(styles).get();
let t = [tag::h2, tag::h3, tag::h4, tag::h5, tag::h6][level.get().min(5) - 1]; if level >= 6 {
HtmlElem::new(t).with_body(Some(realized)).pack().spanned(span) 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 { } else {
let realized = BlockBody::Content(realized); let realized = BlockBody::Content(realized);
BlockElem::new().with_body(Some(realized)).pack().spanned(span) BlockElem::new().with_body(Some(realized)).pack().spanned(span)