Repository: typst/typst
File: crates/typst-library/src/model/heading.rs (Rust)

Code before:
                .size();

                indent = size.x + SPACING_TO_NUMBERING.resolve(styles);
            }

            let spacing = if html {
                SpaceElem::shared().clone()
            } else {
                HElem::new(SPACING_TO_NUMBERING.into()).with_weak(true).pack()
            };

            realized = numbering + spacing + realized;
        }

        if indent != Abs::zero() && !html {
            realized = realized.styled(ParElem::set_hanging_indent(indent.into()));
        }

        if html {
            // HTML's h1 is closer to a title element. There should only be one.
            // 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).get();
            if level >= 6 {
                engine.sink.warn(warning!(span, "heading of level {} should be transformed to <h{}> in HTML, but only <h1> to <h6> are supported; instead transformed to <div role=\"heading\" aria-level=\"{}\">, but note that this is not supported by all screen readers", level, level + 1, level + 1;
                    hint: "you may want to restructure your document so that it doesn't contain deep headings"));
                Ok(HtmlElem::new(tag::div)
                    .with_body(Some(realized))
                    .with_attr(const { HtmlAttr::constant("role") }, "heading")
                    .with_attr(const { HtmlAttr::constant("aria-level") }, (level + 1).to_string())
                    .pack()
                    .spanned(span))
            } else {
                let t = [tag::h2, tag::h3, tag::h4, tag::h5, tag::h6][level - 1];
                Ok(HtmlElem::new(t).with_body(Some(realized)).pack().spanned(span))
            }
        } else {
            let realized = BlockBody::Content(realized);
            Ok(BlockElem::new().with_body(Some(realized)).pack().spanned(span))
        }
    }
}

impl ShowSet for Packed<HeadingElem> {
    fn show_set(&self, styles: StyleChain) -> Styles {
        let level = (**self).resolve_level(styles).get();
        let scale = match level {
            1 => 1.4,
            2 => 1.2,
            _ => 1.0,

Review comment (suggestion):
The error message should be line wrapped in source via (preventing extra whitespace via `\`, you can check other calls to `warning!` or `bail!` for that. It's also generally quite long. I wonder whether we could split it up more into a shorter main message with multiple hints.

Code after:
                )?
                .size();

                indent = size.x + SPACING_TO_NUMBERING.resolve(styles);
            }

            let spacing = if html {
                SpaceElem::shared().clone()
            } else {
                HElem::new(SPACING_TO_NUMBERING.into()).with_weak(true).pack()
            };

            realized = numbering + spacing + realized;
        }

        if indent != Abs::zero() && !html {
            realized = realized.styled(ParElem::set_hanging_indent(indent.into()));
        }

        Ok(if html {
            // HTML's h1 is closer to a title element. There should only be one.
            // 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).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)
        })
    }
}