Compare commits

...

8 Commits

Author SHA1 Message Date
Andrew Voynov
268aa26734
Merge 6daae2e292ad148a458c04d9a244021373063649 into 70710deb2b813eacc79d57436f5bd4c15c215f2e 2025-07-10 15:28:31 +02:00
Said A.
70710deb2b
Deduplicate labels for code completion (#6516) 2025-07-10 13:15:19 +00:00
Laurenz
275012d7c6
Handle lower and upper in HTML export (#6585) 2025-07-10 10:54:06 +00:00
Laurenz
98802dde7e
Complete movement of HTML export code to typst-html (#6584) 2025-07-10 10:42:34 +00:00
Andrew Voynov
6daae2e292
Refactor "Making a Template" tutorial 2025-06-12 09:03:09 +03:00
Andrew Voynov
7f24cd9253
Refactor "Writing in Typst" tutorial 2025-06-12 09:01:57 +03:00
Andrew Voynov
fea153a6fc
Refactor "Formatting" tutorial 2025-06-12 09:01:57 +03:00
Andrew Voynov
cdb8a42c68
Refactor "Advanced Styling" tutorial 2025-06-12 09:01:50 +03:00
31 changed files with 1861 additions and 1737 deletions

View File

@ -14,10 +14,10 @@ use typst::diag::{
bail, At, Severity, SourceDiagnostic, SourceResult, StrResult, Warned,
};
use typst::foundations::{Datetime, Smart};
use typst::html::HtmlDocument;
use typst::layout::{Frame, Page, PageRanges, PagedDocument};
use typst::syntax::{FileId, Lines, Span};
use typst::WorldExt;
use typst_html::HtmlDocument;
use typst_pdf::{PdfOptions, PdfStandards, Timestamp};
use crate::args::{

View File

@ -0,0 +1,195 @@
#![cfg_attr(rustfmt, rustfmt_skip)]
#![allow(non_upper_case_globals)]
#![allow(dead_code)]
use crate::HtmlAttr;
pub const abbr: HtmlAttr = HtmlAttr::constant("abbr");
pub const accept: HtmlAttr = HtmlAttr::constant("accept");
pub const accept_charset: HtmlAttr = HtmlAttr::constant("accept-charset");
pub const accesskey: HtmlAttr = HtmlAttr::constant("accesskey");
pub const action: HtmlAttr = HtmlAttr::constant("action");
pub const allow: HtmlAttr = HtmlAttr::constant("allow");
pub const allowfullscreen: HtmlAttr = HtmlAttr::constant("allowfullscreen");
pub const alpha: HtmlAttr = HtmlAttr::constant("alpha");
pub const alt: HtmlAttr = HtmlAttr::constant("alt");
pub const aria_activedescendant: HtmlAttr = HtmlAttr::constant("aria-activedescendant");
pub const aria_atomic: HtmlAttr = HtmlAttr::constant("aria-atomic");
pub const aria_autocomplete: HtmlAttr = HtmlAttr::constant("aria-autocomplete");
pub const aria_busy: HtmlAttr = HtmlAttr::constant("aria-busy");
pub const aria_checked: HtmlAttr = HtmlAttr::constant("aria-checked");
pub const aria_colcount: HtmlAttr = HtmlAttr::constant("aria-colcount");
pub const aria_colindex: HtmlAttr = HtmlAttr::constant("aria-colindex");
pub const aria_colspan: HtmlAttr = HtmlAttr::constant("aria-colspan");
pub const aria_controls: HtmlAttr = HtmlAttr::constant("aria-controls");
pub const aria_current: HtmlAttr = HtmlAttr::constant("aria-current");
pub const aria_describedby: HtmlAttr = HtmlAttr::constant("aria-describedby");
pub const aria_details: HtmlAttr = HtmlAttr::constant("aria-details");
pub const aria_disabled: HtmlAttr = HtmlAttr::constant("aria-disabled");
pub const aria_errormessage: HtmlAttr = HtmlAttr::constant("aria-errormessage");
pub const aria_expanded: HtmlAttr = HtmlAttr::constant("aria-expanded");
pub const aria_flowto: HtmlAttr = HtmlAttr::constant("aria-flowto");
pub const aria_haspopup: HtmlAttr = HtmlAttr::constant("aria-haspopup");
pub const aria_hidden: HtmlAttr = HtmlAttr::constant("aria-hidden");
pub const aria_invalid: HtmlAttr = HtmlAttr::constant("aria-invalid");
pub const aria_keyshortcuts: HtmlAttr = HtmlAttr::constant("aria-keyshortcuts");
pub const aria_label: HtmlAttr = HtmlAttr::constant("aria-label");
pub const aria_labelledby: HtmlAttr = HtmlAttr::constant("aria-labelledby");
pub const aria_level: HtmlAttr = HtmlAttr::constant("aria-level");
pub const aria_live: HtmlAttr = HtmlAttr::constant("aria-live");
pub const aria_modal: HtmlAttr = HtmlAttr::constant("aria-modal");
pub const aria_multiline: HtmlAttr = HtmlAttr::constant("aria-multiline");
pub const aria_multiselectable: HtmlAttr = HtmlAttr::constant("aria-multiselectable");
pub const aria_orientation: HtmlAttr = HtmlAttr::constant("aria-orientation");
pub const aria_owns: HtmlAttr = HtmlAttr::constant("aria-owns");
pub const aria_placeholder: HtmlAttr = HtmlAttr::constant("aria-placeholder");
pub const aria_posinset: HtmlAttr = HtmlAttr::constant("aria-posinset");
pub const aria_pressed: HtmlAttr = HtmlAttr::constant("aria-pressed");
pub const aria_readonly: HtmlAttr = HtmlAttr::constant("aria-readonly");
pub const aria_relevant: HtmlAttr = HtmlAttr::constant("aria-relevant");
pub const aria_required: HtmlAttr = HtmlAttr::constant("aria-required");
pub const aria_roledescription: HtmlAttr = HtmlAttr::constant("aria-roledescription");
pub const aria_rowcount: HtmlAttr = HtmlAttr::constant("aria-rowcount");
pub const aria_rowindex: HtmlAttr = HtmlAttr::constant("aria-rowindex");
pub const aria_rowspan: HtmlAttr = HtmlAttr::constant("aria-rowspan");
pub const aria_selected: HtmlAttr = HtmlAttr::constant("aria-selected");
pub const aria_setsize: HtmlAttr = HtmlAttr::constant("aria-setsize");
pub const aria_sort: HtmlAttr = HtmlAttr::constant("aria-sort");
pub const aria_valuemax: HtmlAttr = HtmlAttr::constant("aria-valuemax");
pub const aria_valuemin: HtmlAttr = HtmlAttr::constant("aria-valuemin");
pub const aria_valuenow: HtmlAttr = HtmlAttr::constant("aria-valuenow");
pub const aria_valuetext: HtmlAttr = HtmlAttr::constant("aria-valuetext");
pub const r#as: HtmlAttr = HtmlAttr::constant("as");
pub const r#async: HtmlAttr = HtmlAttr::constant("async");
pub const autocapitalize: HtmlAttr = HtmlAttr::constant("autocapitalize");
pub const autocomplete: HtmlAttr = HtmlAttr::constant("autocomplete");
pub const autocorrect: HtmlAttr = HtmlAttr::constant("autocorrect");
pub const autofocus: HtmlAttr = HtmlAttr::constant("autofocus");
pub const autoplay: HtmlAttr = HtmlAttr::constant("autoplay");
pub const blocking: HtmlAttr = HtmlAttr::constant("blocking");
pub const charset: HtmlAttr = HtmlAttr::constant("charset");
pub const checked: HtmlAttr = HtmlAttr::constant("checked");
pub const cite: HtmlAttr = HtmlAttr::constant("cite");
pub const class: HtmlAttr = HtmlAttr::constant("class");
pub const closedby: HtmlAttr = HtmlAttr::constant("closedby");
pub const color: HtmlAttr = HtmlAttr::constant("color");
pub const colorspace: HtmlAttr = HtmlAttr::constant("colorspace");
pub const cols: HtmlAttr = HtmlAttr::constant("cols");
pub const colspan: HtmlAttr = HtmlAttr::constant("colspan");
pub const command: HtmlAttr = HtmlAttr::constant("command");
pub const commandfor: HtmlAttr = HtmlAttr::constant("commandfor");
pub const content: HtmlAttr = HtmlAttr::constant("content");
pub const contenteditable: HtmlAttr = HtmlAttr::constant("contenteditable");
pub const controls: HtmlAttr = HtmlAttr::constant("controls");
pub const coords: HtmlAttr = HtmlAttr::constant("coords");
pub const crossorigin: HtmlAttr = HtmlAttr::constant("crossorigin");
pub const data: HtmlAttr = HtmlAttr::constant("data");
pub const datetime: HtmlAttr = HtmlAttr::constant("datetime");
pub const decoding: HtmlAttr = HtmlAttr::constant("decoding");
pub const default: HtmlAttr = HtmlAttr::constant("default");
pub const defer: HtmlAttr = HtmlAttr::constant("defer");
pub const dir: HtmlAttr = HtmlAttr::constant("dir");
pub const dirname: HtmlAttr = HtmlAttr::constant("dirname");
pub const disabled: HtmlAttr = HtmlAttr::constant("disabled");
pub const download: HtmlAttr = HtmlAttr::constant("download");
pub const draggable: HtmlAttr = HtmlAttr::constant("draggable");
pub const enctype: HtmlAttr = HtmlAttr::constant("enctype");
pub const enterkeyhint: HtmlAttr = HtmlAttr::constant("enterkeyhint");
pub const fetchpriority: HtmlAttr = HtmlAttr::constant("fetchpriority");
pub const r#for: HtmlAttr = HtmlAttr::constant("for");
pub const form: HtmlAttr = HtmlAttr::constant("form");
pub const formaction: HtmlAttr = HtmlAttr::constant("formaction");
pub const formenctype: HtmlAttr = HtmlAttr::constant("formenctype");
pub const formmethod: HtmlAttr = HtmlAttr::constant("formmethod");
pub const formnovalidate: HtmlAttr = HtmlAttr::constant("formnovalidate");
pub const formtarget: HtmlAttr = HtmlAttr::constant("formtarget");
pub const headers: HtmlAttr = HtmlAttr::constant("headers");
pub const height: HtmlAttr = HtmlAttr::constant("height");
pub const hidden: HtmlAttr = HtmlAttr::constant("hidden");
pub const high: HtmlAttr = HtmlAttr::constant("high");
pub const href: HtmlAttr = HtmlAttr::constant("href");
pub const hreflang: HtmlAttr = HtmlAttr::constant("hreflang");
pub const http_equiv: HtmlAttr = HtmlAttr::constant("http-equiv");
pub const id: HtmlAttr = HtmlAttr::constant("id");
pub const imagesizes: HtmlAttr = HtmlAttr::constant("imagesizes");
pub const imagesrcset: HtmlAttr = HtmlAttr::constant("imagesrcset");
pub const inert: HtmlAttr = HtmlAttr::constant("inert");
pub const inputmode: HtmlAttr = HtmlAttr::constant("inputmode");
pub const integrity: HtmlAttr = HtmlAttr::constant("integrity");
pub const is: HtmlAttr = HtmlAttr::constant("is");
pub const ismap: HtmlAttr = HtmlAttr::constant("ismap");
pub const itemid: HtmlAttr = HtmlAttr::constant("itemid");
pub const itemprop: HtmlAttr = HtmlAttr::constant("itemprop");
pub const itemref: HtmlAttr = HtmlAttr::constant("itemref");
pub const itemscope: HtmlAttr = HtmlAttr::constant("itemscope");
pub const itemtype: HtmlAttr = HtmlAttr::constant("itemtype");
pub const kind: HtmlAttr = HtmlAttr::constant("kind");
pub const label: HtmlAttr = HtmlAttr::constant("label");
pub const lang: HtmlAttr = HtmlAttr::constant("lang");
pub const list: HtmlAttr = HtmlAttr::constant("list");
pub const loading: HtmlAttr = HtmlAttr::constant("loading");
pub const r#loop: HtmlAttr = HtmlAttr::constant("loop");
pub const low: HtmlAttr = HtmlAttr::constant("low");
pub const max: HtmlAttr = HtmlAttr::constant("max");
pub const maxlength: HtmlAttr = HtmlAttr::constant("maxlength");
pub const media: HtmlAttr = HtmlAttr::constant("media");
pub const method: HtmlAttr = HtmlAttr::constant("method");
pub const min: HtmlAttr = HtmlAttr::constant("min");
pub const minlength: HtmlAttr = HtmlAttr::constant("minlength");
pub const multiple: HtmlAttr = HtmlAttr::constant("multiple");
pub const muted: HtmlAttr = HtmlAttr::constant("muted");
pub const name: HtmlAttr = HtmlAttr::constant("name");
pub const nomodule: HtmlAttr = HtmlAttr::constant("nomodule");
pub const nonce: HtmlAttr = HtmlAttr::constant("nonce");
pub const novalidate: HtmlAttr = HtmlAttr::constant("novalidate");
pub const open: HtmlAttr = HtmlAttr::constant("open");
pub const optimum: HtmlAttr = HtmlAttr::constant("optimum");
pub const pattern: HtmlAttr = HtmlAttr::constant("pattern");
pub const ping: HtmlAttr = HtmlAttr::constant("ping");
pub const placeholder: HtmlAttr = HtmlAttr::constant("placeholder");
pub const playsinline: HtmlAttr = HtmlAttr::constant("playsinline");
pub const popover: HtmlAttr = HtmlAttr::constant("popover");
pub const popovertarget: HtmlAttr = HtmlAttr::constant("popovertarget");
pub const popovertargetaction: HtmlAttr = HtmlAttr::constant("popovertargetaction");
pub const poster: HtmlAttr = HtmlAttr::constant("poster");
pub const preload: HtmlAttr = HtmlAttr::constant("preload");
pub const readonly: HtmlAttr = HtmlAttr::constant("readonly");
pub const referrerpolicy: HtmlAttr = HtmlAttr::constant("referrerpolicy");
pub const rel: HtmlAttr = HtmlAttr::constant("rel");
pub const required: HtmlAttr = HtmlAttr::constant("required");
pub const reversed: HtmlAttr = HtmlAttr::constant("reversed");
pub const role: HtmlAttr = HtmlAttr::constant("role");
pub const rows: HtmlAttr = HtmlAttr::constant("rows");
pub const rowspan: HtmlAttr = HtmlAttr::constant("rowspan");
pub const sandbox: HtmlAttr = HtmlAttr::constant("sandbox");
pub const scope: HtmlAttr = HtmlAttr::constant("scope");
pub const selected: HtmlAttr = HtmlAttr::constant("selected");
pub const shadowrootclonable: HtmlAttr = HtmlAttr::constant("shadowrootclonable");
pub const shadowrootcustomelementregistry: HtmlAttr = HtmlAttr::constant("shadowrootcustomelementregistry");
pub const shadowrootdelegatesfocus: HtmlAttr = HtmlAttr::constant("shadowrootdelegatesfocus");
pub const shadowrootmode: HtmlAttr = HtmlAttr::constant("shadowrootmode");
pub const shadowrootserializable: HtmlAttr = HtmlAttr::constant("shadowrootserializable");
pub const shape: HtmlAttr = HtmlAttr::constant("shape");
pub const size: HtmlAttr = HtmlAttr::constant("size");
pub const sizes: HtmlAttr = HtmlAttr::constant("sizes");
pub const slot: HtmlAttr = HtmlAttr::constant("slot");
pub const span: HtmlAttr = HtmlAttr::constant("span");
pub const spellcheck: HtmlAttr = HtmlAttr::constant("spellcheck");
pub const src: HtmlAttr = HtmlAttr::constant("src");
pub const srcdoc: HtmlAttr = HtmlAttr::constant("srcdoc");
pub const srclang: HtmlAttr = HtmlAttr::constant("srclang");
pub const srcset: HtmlAttr = HtmlAttr::constant("srcset");
pub const start: HtmlAttr = HtmlAttr::constant("start");
pub const step: HtmlAttr = HtmlAttr::constant("step");
pub const style: HtmlAttr = HtmlAttr::constant("style");
pub const tabindex: HtmlAttr = HtmlAttr::constant("tabindex");
pub const target: HtmlAttr = HtmlAttr::constant("target");
pub const title: HtmlAttr = HtmlAttr::constant("title");
pub const translate: HtmlAttr = HtmlAttr::constant("translate");
pub const r#type: HtmlAttr = HtmlAttr::constant("type");
pub const usemap: HtmlAttr = HtmlAttr::constant("usemap");
pub const value: HtmlAttr = HtmlAttr::constant("value");
pub const width: HtmlAttr = HtmlAttr::constant("width");
pub const wrap: HtmlAttr = HtmlAttr::constant("wrap");
pub const writingsuggestions: HtmlAttr = HtmlAttr::constant("writingsuggestions");

View File

@ -0,0 +1,81 @@
//! Defines syntactical properties of HTML tags, attributes, and text.
/// Check whether a character is in a tag name.
pub const fn is_valid_in_tag_name(c: char) -> bool {
c.is_ascii_alphanumeric()
}
/// Check whether a character is valid in an attribute name.
pub const fn is_valid_in_attribute_name(c: char) -> bool {
match c {
// These are forbidden.
'\0' | ' ' | '"' | '\'' | '>' | '/' | '=' => false,
c if is_whatwg_control_char(c) => false,
c if is_whatwg_non_char(c) => false,
// _Everything_ else is allowed, including U+2029 paragraph
// separator. Go wild.
_ => true,
}
}
/// Check whether a character can be an used in an attribute value without
/// escaping.
///
/// See <https://html.spec.whatwg.org/multipage/syntax.html#attributes-2>
pub const fn is_valid_in_attribute_value(c: char) -> bool {
match c {
// Ampersands are sometimes legal (i.e. when they are not _ambiguous
// ampersands_) but it is not worth the trouble to check for that.
'&' => false,
// Quotation marks are not allowed in double-quote-delimited attribute
// values.
'"' => false,
// All other text characters are allowed.
c => is_w3c_text_char(c),
}
}
/// Check whether a character can be an used in normal text without
/// escaping.
pub const fn is_valid_in_normal_element_text(c: char) -> bool {
match c {
// Ampersands are sometimes legal (i.e. when they are not _ambiguous
// ampersands_) but it is not worth the trouble to check for that.
'&' => false,
// Less-than signs are not allowed in text.
'<' => false,
// All other text characters are allowed.
c => is_w3c_text_char(c),
}
}
/// Check if something is valid text in HTML.
pub const fn is_w3c_text_char(c: char) -> bool {
match c {
// Non-characters are obviously not text characters.
c if is_whatwg_non_char(c) => false,
// Control characters are disallowed, except for whitespace.
c if is_whatwg_control_char(c) => c.is_ascii_whitespace(),
// Everything else is allowed.
_ => true,
}
}
const fn is_whatwg_non_char(c: char) -> bool {
match c {
'\u{fdd0}'..='\u{fdef}' => true,
// Non-characters matching xxFFFE or xxFFFF up to x10FFFF (inclusive).
c if c as u32 & 0xfffe == 0xfffe && c as u32 <= 0x10ffff => true,
_ => false,
}
}
const fn is_whatwg_control_char(c: char) -> bool {
match c {
// C0 control characters.
'\u{00}'..='\u{1f}' => true,
// Other control characters.
'\u{7f}'..='\u{9f}' => true,
_ => false,
}
}

View File

@ -0,0 +1,130 @@
use typst_library::diag::{warning, SourceResult};
use typst_library::engine::Engine;
use typst_library::foundations::{Content, StyleChain, Target, TargetElem};
use typst_library::introspection::{SplitLocator, TagElem};
use typst_library::layout::{Abs, Axes, BlockBody, BlockElem, BoxElem, Region, Size};
use typst_library::model::ParElem;
use typst_library::routines::Pair;
use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem};
use crate::fragment::html_fragment;
use crate::{attr, tag, FrameElem, HtmlElem, HtmlElement, HtmlFrame, HtmlNode};
/// Converts realized content into HTML nodes.
pub fn convert_to_nodes<'a>(
engine: &mut Engine,
locator: &mut SplitLocator,
children: impl IntoIterator<Item = Pair<'a>>,
) -> SourceResult<Vec<HtmlNode>> {
let mut output = Vec::new();
for (child, styles) in children {
handle(engine, child, locator, styles, &mut output)?;
}
Ok(output)
}
/// Convert one element into HTML node(s).
fn handle(
engine: &mut Engine,
child: &Content,
locator: &mut SplitLocator,
styles: StyleChain,
output: &mut Vec<HtmlNode>,
) -> SourceResult<()> {
if let Some(elem) = child.to_packed::<TagElem>() {
output.push(HtmlNode::Tag(elem.tag.clone()));
} else if let Some(elem) = child.to_packed::<HtmlElem>() {
let mut children = vec![];
if let Some(body) = elem.body.get_ref(styles) {
children = html_fragment(engine, body, locator.next(&elem.span()), styles)?;
}
let element = HtmlElement {
tag: elem.tag,
attrs: elem.attrs.get_cloned(styles),
children,
span: elem.span(),
};
output.push(element.into());
} else if let Some(elem) = child.to_packed::<ParElem>() {
let children =
html_fragment(engine, &elem.body, locator.next(&elem.span()), styles)?;
output.push(
HtmlElement::new(tag::p)
.with_children(children)
.spanned(elem.span())
.into(),
);
} else if let Some(elem) = child.to_packed::<BoxElem>() {
// TODO: This is rather incomplete.
if let Some(body) = elem.body.get_ref(styles) {
let children =
html_fragment(engine, body, locator.next(&elem.span()), styles)?;
output.push(
HtmlElement::new(tag::span)
.with_attr(attr::style, "display: inline-block;")
.with_children(children)
.spanned(elem.span())
.into(),
)
}
} else if let Some((elem, body)) =
child
.to_packed::<BlockElem>()
.and_then(|elem| match elem.body.get_ref(styles) {
Some(BlockBody::Content(body)) => Some((elem, body)),
_ => None,
})
{
// TODO: This is rather incomplete.
let children = html_fragment(engine, body, locator.next(&elem.span()), styles)?;
output.push(
HtmlElement::new(tag::div)
.with_children(children)
.spanned(elem.span())
.into(),
);
} else if child.is::<SpaceElem>() {
output.push(HtmlNode::text(' ', child.span()));
} else if let Some(elem) = child.to_packed::<TextElem>() {
let text = if let Some(case) = styles.get(TextElem::case) {
case.apply(&elem.text).into()
} else {
elem.text.clone()
};
output.push(HtmlNode::text(text, elem.span()));
} else if let Some(elem) = child.to_packed::<LinebreakElem>() {
output.push(HtmlElement::new(tag::br).spanned(elem.span()).into());
} else if let Some(elem) = child.to_packed::<SmartQuoteElem>() {
output.push(HtmlNode::text(
if elem.double.get(styles) { '"' } else { '\'' },
child.span(),
));
} else if let Some(elem) = child.to_packed::<FrameElem>() {
let locator = locator.next(&elem.span());
let style = TargetElem::target.set(Target::Paged).wrap();
let frame = (engine.routines.layout_frame)(
engine,
&elem.body,
locator,
styles.chain(&style),
Region::new(Size::splat(Abs::inf()), Axes::splat(false)),
)?;
output.push(HtmlNode::Frame(HtmlFrame {
inner: frame,
text_size: styles.resolve(TextElem::size),
}));
} else {
engine.sink.warn(warning!(
child.span(),
"{} was ignored during HTML export",
child.elem().name()
));
}
Ok(())
}
/// Checks whether the given element is an inline-level HTML element.
pub fn is_inline(elem: &Content) -> bool {
elem.to_packed::<HtmlElem>()
.is_some_and(|elem| tag::is_inline_by_default(elem.tag))
}

View File

@ -3,28 +3,10 @@
use std::fmt::{self, Display, Write};
use ecow::EcoString;
use typst_library::html::{attr, HtmlElem};
use typst_library::layout::{Length, Rel};
use typst_library::visualize::{Color, Hsl, LinearRgb, Oklab, Oklch, Rgb};
use typst_utils::Numeric;
/// Additional methods for [`HtmlElem`].
pub trait HtmlElemExt {
/// Adds the styles to an element if the property list is non-empty.
fn with_styles(self, properties: Properties) -> Self;
}
impl HtmlElemExt for HtmlElem {
/// Adds CSS styles to an element.
fn with_styles(self, properties: Properties) -> Self {
if let Some(value) = properties.into_inline_styles() {
self.with_attr(attr::style, value)
} else {
self
}
}
}
/// A list of CSS properties with values.
#[derive(Debug, Default)]
pub struct Properties(EcoString);

View File

@ -0,0 +1,219 @@
use std::num::NonZeroUsize;
use comemo::{Tracked, TrackedMut};
use typst_library::diag::{bail, SourceResult};
use typst_library::engine::{Engine, Route, Sink, Traced};
use typst_library::foundations::{Content, StyleChain};
use typst_library::introspection::{Introspector, IntrospectorBuilder, Locator};
use typst_library::layout::{Point, Position, Transform};
use typst_library::model::DocumentInfo;
use typst_library::routines::{Arenas, RealizationKind, Routines};
use typst_library::World;
use typst_syntax::Span;
use typst_utils::NonZeroExt;
use crate::{attr, tag, HtmlDocument, HtmlElement, HtmlNode};
/// Produce an HTML document from content.
///
/// This first performs root-level realization and then turns the resulting
/// elements into HTML.
#[typst_macros::time(name = "html document")]
pub fn html_document(
engine: &mut Engine,
content: &Content,
styles: StyleChain,
) -> SourceResult<HtmlDocument> {
html_document_impl(
engine.routines,
engine.world,
engine.introspector,
engine.traced,
TrackedMut::reborrow_mut(&mut engine.sink),
engine.route.track(),
content,
styles,
)
}
/// The internal implementation of `html_document`.
#[comemo::memoize]
#[allow(clippy::too_many_arguments)]
fn html_document_impl(
routines: &Routines,
world: Tracked<dyn World + '_>,
introspector: Tracked<Introspector>,
traced: Tracked<Traced>,
sink: TrackedMut<Sink>,
route: Tracked<Route>,
content: &Content,
styles: StyleChain,
) -> SourceResult<HtmlDocument> {
let mut locator = Locator::root().split();
let mut engine = Engine {
routines,
world,
introspector,
traced,
sink,
route: Route::extend(route).unnested(),
};
// Mark the external styles as "outside" so that they are valid at the page
// level.
let styles = styles.to_map().outside();
let styles = StyleChain::new(&styles);
let arenas = Arenas::default();
let mut info = DocumentInfo::default();
let children = (engine.routines.realize)(
RealizationKind::HtmlDocument {
info: &mut info,
is_inline: crate::convert::is_inline,
},
&mut engine,
&mut locator,
&arenas,
content,
styles,
)?;
let output = crate::convert::convert_to_nodes(
&mut engine,
&mut locator,
children.iter().copied(),
)?;
let introspector = introspect_html(&output);
let root = root_element(output, &info)?;
Ok(HtmlDocument { info, root, introspector })
}
/// Introspects HTML nodes.
#[typst_macros::time(name = "introspect html")]
fn introspect_html(output: &[HtmlNode]) -> Introspector {
fn discover(
builder: &mut IntrospectorBuilder,
sink: &mut Vec<(Content, Position)>,
nodes: &[HtmlNode],
) {
for node in nodes {
match node {
HtmlNode::Tag(tag) => builder.discover_in_tag(
sink,
tag,
Position { page: NonZeroUsize::ONE, point: Point::zero() },
),
HtmlNode::Text(_, _) => {}
HtmlNode::Element(elem) => discover(builder, sink, &elem.children),
HtmlNode::Frame(frame) => builder.discover_in_frame(
sink,
&frame.inner,
NonZeroUsize::ONE,
Transform::identity(),
),
}
}
}
let mut elems = Vec::new();
let mut builder = IntrospectorBuilder::new();
discover(&mut builder, &mut elems, output);
builder.finalize(elems)
}
/// Wrap the nodes in `<html>` and `<body>` if they are not yet rooted,
/// supplying a suitable `<head>`.
fn root_element(output: Vec<HtmlNode>, info: &DocumentInfo) -> SourceResult<HtmlElement> {
let head = head_element(info);
let body = match classify_output(output)? {
OutputKind::Html(element) => return Ok(element),
OutputKind::Body(body) => body,
OutputKind::Leafs(leafs) => HtmlElement::new(tag::body).with_children(leafs),
};
Ok(HtmlElement::new(tag::html).with_children(vec![head.into(), body.into()]))
}
/// Generate a `<head>` element.
fn head_element(info: &DocumentInfo) -> HtmlElement {
let mut children = vec![];
children.push(HtmlElement::new(tag::meta).with_attr(attr::charset, "utf-8").into());
children.push(
HtmlElement::new(tag::meta)
.with_attr(attr::name, "viewport")
.with_attr(attr::content, "width=device-width, initial-scale=1")
.into(),
);
if let Some(title) = &info.title {
children.push(
HtmlElement::new(tag::title)
.with_children(vec![HtmlNode::Text(title.clone(), Span::detached())])
.into(),
);
}
if let Some(description) = &info.description {
children.push(
HtmlElement::new(tag::meta)
.with_attr(attr::name, "description")
.with_attr(attr::content, description.clone())
.into(),
);
}
if !info.author.is_empty() {
children.push(
HtmlElement::new(tag::meta)
.with_attr(attr::name, "authors")
.with_attr(attr::content, info.author.join(", "))
.into(),
)
}
if !info.keywords.is_empty() {
children.push(
HtmlElement::new(tag::meta)
.with_attr(attr::name, "keywords")
.with_attr(attr::content, info.keywords.join(", "))
.into(),
)
}
HtmlElement::new(tag::head).with_children(children)
}
/// Determine which kind of output the user generated.
fn classify_output(mut output: Vec<HtmlNode>) -> SourceResult<OutputKind> {
let count = output.iter().filter(|node| !matches!(node, HtmlNode::Tag(_))).count();
for node in &mut output {
let HtmlNode::Element(elem) = node else { continue };
let tag = elem.tag;
let mut take = || std::mem::replace(elem, HtmlElement::new(tag::html));
match (tag, count) {
(tag::html, 1) => return Ok(OutputKind::Html(take())),
(tag::body, 1) => return Ok(OutputKind::Body(take())),
(tag::html | tag::body, _) => bail!(
elem.span,
"`{}` element must be the only element in the document",
elem.tag,
),
_ => {}
}
}
Ok(OutputKind::Leafs(output))
}
/// What kinds of output the user generated.
enum OutputKind {
/// The user generated their own `<html>` element. We do not need to supply
/// one.
Html(HtmlElement),
/// The user generate their own `<body>` element. We do not need to supply
/// one, but need supply the `<html>` element.
Body(HtmlElement),
/// The user generated leafs which we wrap in a `<body>` and `<html>`.
Leafs(Vec<HtmlNode>),
}

View File

@ -0,0 +1,281 @@
use std::fmt::{self, Debug, Display, Formatter};
use ecow::{EcoString, EcoVec};
use typst_library::diag::{bail, HintedStrResult, StrResult};
use typst_library::foundations::{cast, Dict, Repr, Str};
use typst_library::introspection::{Introspector, Tag};
use typst_library::layout::{Abs, Frame};
use typst_library::model::DocumentInfo;
use typst_syntax::Span;
use typst_utils::{PicoStr, ResolvedPicoStr};
use crate::charsets;
/// An HTML document.
#[derive(Debug, Clone)]
pub struct HtmlDocument {
/// The document's root HTML element.
pub root: HtmlElement,
/// Details about the document.
pub info: DocumentInfo,
/// Provides the ability to execute queries on the document.
pub introspector: Introspector,
}
/// A child of an HTML element.
#[derive(Debug, Clone, Hash)]
pub enum HtmlNode {
/// An introspectable element that produced something within this node.
Tag(Tag),
/// Plain text.
Text(EcoString, Span),
/// Another element.
Element(HtmlElement),
/// Layouted content that will be embedded into HTML as an SVG.
Frame(HtmlFrame),
}
impl HtmlNode {
/// Create a plain text node.
pub fn text(text: impl Into<EcoString>, span: Span) -> Self {
Self::Text(text.into(), span)
}
}
impl From<HtmlElement> for HtmlNode {
fn from(element: HtmlElement) -> Self {
Self::Element(element)
}
}
/// An HTML element.
#[derive(Debug, Clone, Hash)]
pub struct HtmlElement {
/// The HTML tag.
pub tag: HtmlTag,
/// The element's attributes.
pub attrs: HtmlAttrs,
/// The element's children.
pub children: Vec<HtmlNode>,
/// The span from which the element originated, if any.
pub span: Span,
}
impl HtmlElement {
/// Create a new, blank element without attributes or children.
pub fn new(tag: HtmlTag) -> Self {
Self {
tag,
attrs: HtmlAttrs::default(),
children: vec![],
span: Span::detached(),
}
}
/// Attach children to the element.
///
/// Note: This overwrites potential previous children.
pub fn with_children(mut self, children: Vec<HtmlNode>) -> Self {
self.children = children;
self
}
/// Add an atribute to the element.
pub fn with_attr(mut self, key: HtmlAttr, value: impl Into<EcoString>) -> Self {
self.attrs.push(key, value);
self
}
/// Attach a span to the element.
pub fn spanned(mut self, span: Span) -> Self {
self.span = span;
self
}
}
/// The tag of an HTML element.
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub struct HtmlTag(PicoStr);
impl HtmlTag {
/// Intern an HTML tag string at runtime.
pub fn intern(string: &str) -> StrResult<Self> {
if string.is_empty() {
bail!("tag name must not be empty");
}
if let Some(c) = string.chars().find(|&c| !charsets::is_valid_in_tag_name(c)) {
bail!("the character {} is not valid in a tag name", c.repr());
}
Ok(Self(PicoStr::intern(string)))
}
/// Creates a compile-time constant `HtmlTag`.
///
/// Should only be used in const contexts because it can panic.
#[track_caller]
pub const fn constant(string: &'static str) -> Self {
if string.is_empty() {
panic!("tag name must not be empty");
}
let bytes = string.as_bytes();
let mut i = 0;
while i < bytes.len() {
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;
}
Self(PicoStr::constant(string))
}
/// Resolves the tag to a string.
pub fn resolve(self) -> ResolvedPicoStr {
self.0.resolve()
}
/// Turns the tag into its inner interned string.
pub const fn into_inner(self) -> PicoStr {
self.0
}
}
impl Debug for HtmlTag {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Display::fmt(self, f)
}
}
impl Display for HtmlTag {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "<{}>", self.resolve())
}
}
cast! {
HtmlTag,
self => self.0.resolve().as_str().into_value(),
v: Str => Self::intern(&v)?,
}
/// Attributes of an HTML element.
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct HtmlAttrs(pub EcoVec<(HtmlAttr, EcoString)>);
impl HtmlAttrs {
/// Creates an empty attribute list.
pub fn new() -> Self {
Self::default()
}
/// Add an attribute.
pub fn push(&mut self, attr: HtmlAttr, value: impl Into<EcoString>) {
self.0.push((attr, value.into()));
}
}
cast! {
HtmlAttrs,
self => self.0
.into_iter()
.map(|(key, value)| (key.resolve().as_str().into(), value.into_value()))
.collect::<Dict>()
.into_value(),
values: Dict => Self(values
.into_iter()
.map(|(k, v)| {
let attr = HtmlAttr::intern(&k)?;
let value = v.cast::<EcoString>()?;
Ok((attr, value))
})
.collect::<HintedStrResult<_>>()?),
}
/// An attribute of an HTML element.
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub struct HtmlAttr(PicoStr);
impl HtmlAttr {
/// Intern an HTML attribute string at runtime.
pub fn intern(string: &str) -> StrResult<Self> {
if string.is_empty() {
bail!("attribute name must not be empty");
}
if let Some(c) =
string.chars().find(|&c| !charsets::is_valid_in_attribute_name(c))
{
bail!("the character {} is not valid in an attribute name", c.repr());
}
Ok(Self(PicoStr::intern(string)))
}
/// Creates a compile-time constant `HtmlAttr`.
///
/// Must only be used in const contexts (in a constant definition or
/// explicit `const { .. }` block) because otherwise a panic for a malformed
/// attribute or not auto-internible constant will only be caught at
/// runtime.
#[track_caller]
pub const fn constant(string: &'static str) -> Self {
if string.is_empty() {
panic!("attribute name must not be empty");
}
let bytes = string.as_bytes();
let mut i = 0;
while i < bytes.len() {
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;
}
Self(PicoStr::constant(string))
}
/// Resolves the attribute to a string.
pub fn resolve(self) -> ResolvedPicoStr {
self.0.resolve()
}
/// Turns the attribute into its inner interned string.
pub const fn into_inner(self) -> PicoStr {
self.0
}
}
impl Debug for HtmlAttr {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Display::fmt(self, f)
}
}
impl Display for HtmlAttr {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.resolve())
}
}
cast! {
HtmlAttr,
self => self.0.resolve().as_str().into_value(),
v: Str => Self::intern(&v)?,
}
/// Layouted content that will be embedded into HTML as an SVG.
#[derive(Debug, Clone, Hash)]
pub struct HtmlFrame {
/// The frame that will be displayed as an SVG.
pub inner: Frame,
/// The text size where the frame was defined. This is used to size the
/// frame with em units to make text in and outside of the frame sized
/// consistently.
pub text_size: Abs,
}

View File

@ -2,10 +2,11 @@ use std::fmt::Write;
use typst_library::diag::{bail, At, SourceResult, StrResult};
use typst_library::foundations::Repr;
use typst_library::html::{
use typst_syntax::Span;
use crate::{
attr, charsets, tag, HtmlDocument, HtmlElement, HtmlFrame, HtmlNode, HtmlTag,
};
use typst_syntax::Span;
/// Encodes an HTML document into a string.
pub fn html(document: &HtmlDocument) -> SourceResult<String> {

View File

@ -0,0 +1,76 @@
use comemo::{Track, Tracked, TrackedMut};
use typst_library::diag::{At, SourceResult};
use typst_library::engine::{Engine, Route, Sink, Traced};
use typst_library::foundations::{Content, StyleChain};
use typst_library::introspection::{Introspector, Locator, LocatorLink};
use typst_library::routines::{Arenas, FragmentKind, RealizationKind, Routines};
use typst_library::World;
use crate::HtmlNode;
/// Produce HTML nodes from content.
#[typst_macros::time(name = "html fragment")]
pub fn html_fragment(
engine: &mut Engine,
content: &Content,
locator: Locator,
styles: StyleChain,
) -> SourceResult<Vec<HtmlNode>> {
html_fragment_impl(
engine.routines,
engine.world,
engine.introspector,
engine.traced,
TrackedMut::reborrow_mut(&mut engine.sink),
engine.route.track(),
content,
locator.track(),
styles,
)
}
/// The cached, internal implementation of [`html_fragment`].
#[comemo::memoize]
#[allow(clippy::too_many_arguments)]
fn html_fragment_impl(
routines: &Routines,
world: Tracked<dyn World + '_>,
introspector: Tracked<Introspector>,
traced: Tracked<Traced>,
sink: TrackedMut<Sink>,
route: Tracked<Route>,
content: &Content,
locator: Tracked<Locator>,
styles: StyleChain,
) -> SourceResult<Vec<HtmlNode>> {
let link = LocatorLink::new(locator);
let mut locator = Locator::link(&link).split();
let mut engine = Engine {
routines,
world,
introspector,
traced,
sink,
route: Route::extend(route),
};
engine.route.check_html_depth().at(content.span())?;
let arenas = Arenas::default();
let children = (engine.routines.realize)(
// No need to know about the `FragmentKind` because we handle both
// uniformly.
RealizationKind::HtmlFragment {
kind: &mut FragmentKind::Block,
is_inline: crate::convert::is_inline,
},
&mut engine,
&mut locator,
&arenas,
content,
styles,
)?;
crate::convert::convert_to_nodes(&mut engine, &mut locator, children.iter().copied())
}

View File

@ -1,33 +1,28 @@
//! Typst's HTML exporter.
mod attr;
mod charsets;
mod convert;
mod css;
mod document;
mod dom;
mod encode;
mod fragment;
mod rules;
mod tag;
mod typed;
pub use self::document::html_document;
pub use self::dom::*;
pub use self::encode::html;
pub use self::rules::register;
use comemo::{Track, Tracked, TrackedMut};
use typst_library::diag::{bail, warning, At, SourceResult};
use typst_library::engine::{Engine, Route, Sink, Traced};
use typst_library::foundations::{
Content, Module, Scope, StyleChain, Target, TargetElem,
};
use typst_library::html::{
attr, tag, FrameElem, HtmlDocument, HtmlElem, HtmlElement, HtmlFrame, HtmlNode,
};
use typst_library::introspection::{
Introspector, Locator, LocatorLink, SplitLocator, TagElem,
};
use typst_library::layout::{Abs, Axes, BlockBody, BlockElem, BoxElem, Region, Size};
use typst_library::model::{DocumentInfo, ParElem};
use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind, Routines};
use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem};
use typst_library::{Category, World};
use typst_syntax::Span;
use ecow::EcoString;
use typst_library::foundations::{Content, Module, Scope};
use typst_library::Category;
use typst_macros::elem;
/// Create a module with all HTML definitions.
/// Creates the module with all HTML definitions.
pub fn module() -> Module {
let mut html = Scope::deduplicating();
html.start_category(Category::Html);
@ -37,337 +32,77 @@ pub fn module() -> Module {
Module::new("html", html)
}
/// Produce an HTML document from content.
/// An HTML element that can contain Typst content.
///
/// This first performs root-level realization and then turns the resulting
/// elements into HTML.
#[typst_macros::time(name = "html document")]
pub fn html_document(
engine: &mut Engine,
content: &Content,
styles: StyleChain,
) -> SourceResult<HtmlDocument> {
html_document_impl(
engine.routines,
engine.world,
engine.introspector,
engine.traced,
TrackedMut::reborrow_mut(&mut engine.sink),
engine.route.track(),
content,
styles,
)
/// Typst's HTML export automatically generates the appropriate tags for most
/// elements. However, sometimes, it is desirable to retain more control. For
/// example, when using Typst to generate your blog, you could use this function
/// to wrap each article in an `<article>` tag.
///
/// Typst is aware of what is valid HTML. A tag and its attributes must form
/// syntactically valid HTML. Some tags, like `meta` do not accept content.
/// Hence, you must not provide a body for them. We may add more checks in the
/// future, so be sure that you are generating valid HTML when using this
/// function.
///
/// Normally, Typst will generate `html`, `head`, and `body` tags for you. If
/// you instead create them with this function, Typst will omit its own tags.
///
/// ```typ
/// #html.elem("div", attrs: (style: "background: aqua"))[
/// A div with _Typst content_ inside!
/// ]
/// ```
#[elem(name = "elem")]
pub struct HtmlElem {
/// The element's tag.
#[required]
pub tag: HtmlTag,
/// The element's HTML attributes.
pub attrs: HtmlAttrs,
/// The contents of the HTML element.
///
/// The body can be arbitrary Typst content.
#[positional]
pub body: Option<Content>,
}
/// The internal implementation of `html_document`.
#[comemo::memoize]
#[allow(clippy::too_many_arguments)]
fn html_document_impl(
routines: &Routines,
world: Tracked<dyn World + '_>,
introspector: Tracked<Introspector>,
traced: Tracked<Traced>,
sink: TrackedMut<Sink>,
route: Tracked<Route>,
content: &Content,
styles: StyleChain,
) -> SourceResult<HtmlDocument> {
let mut locator = Locator::root().split();
let mut engine = Engine {
routines,
world,
introspector,
traced,
sink,
route: Route::extend(route).unnested(),
};
// Mark the external styles as "outside" so that they are valid at the page
// level.
let styles = styles.to_map().outside();
let styles = StyleChain::new(&styles);
let arenas = Arenas::default();
let mut info = DocumentInfo::default();
let children = (engine.routines.realize)(
RealizationKind::HtmlDocument(&mut info),
&mut engine,
&mut locator,
&arenas,
content,
styles,
)?;
let output = handle_list(&mut engine, &mut locator, children.iter().copied())?;
let introspector = Introspector::html(&output);
let root = root_element(output, &info)?;
Ok(HtmlDocument { info, root, introspector })
}
/// Produce HTML nodes from content.
#[typst_macros::time(name = "html fragment")]
pub fn html_fragment(
engine: &mut Engine,
content: &Content,
locator: Locator,
styles: StyleChain,
) -> SourceResult<Vec<HtmlNode>> {
html_fragment_impl(
engine.routines,
engine.world,
engine.introspector,
engine.traced,
TrackedMut::reborrow_mut(&mut engine.sink),
engine.route.track(),
content,
locator.track(),
styles,
)
}
/// The cached, internal implementation of [`html_fragment`].
#[comemo::memoize]
#[allow(clippy::too_many_arguments)]
fn html_fragment_impl(
routines: &Routines,
world: Tracked<dyn World + '_>,
introspector: Tracked<Introspector>,
traced: Tracked<Traced>,
sink: TrackedMut<Sink>,
route: Tracked<Route>,
content: &Content,
locator: Tracked<Locator>,
styles: StyleChain,
) -> SourceResult<Vec<HtmlNode>> {
let link = LocatorLink::new(locator);
let mut locator = Locator::link(&link).split();
let mut engine = Engine {
routines,
world,
introspector,
traced,
sink,
route: Route::extend(route),
};
engine.route.check_html_depth().at(content.span())?;
let arenas = Arenas::default();
let children = (engine.routines.realize)(
// No need to know about the `FragmentKind` because we handle both
// uniformly.
RealizationKind::HtmlFragment(&mut FragmentKind::Block),
&mut engine,
&mut locator,
&arenas,
content,
styles,
)?;
handle_list(&mut engine, &mut locator, children.iter().copied())
}
/// Convert children into HTML nodes.
fn handle_list<'a>(
engine: &mut Engine,
locator: &mut SplitLocator,
children: impl IntoIterator<Item = Pair<'a>>,
) -> SourceResult<Vec<HtmlNode>> {
let mut output = Vec::new();
for (child, styles) in children {
handle(engine, child, locator, styles, &mut output)?;
}
Ok(output)
}
/// Convert a child into HTML node(s).
fn handle(
engine: &mut Engine,
child: &Content,
locator: &mut SplitLocator,
styles: StyleChain,
output: &mut Vec<HtmlNode>,
) -> SourceResult<()> {
if let Some(elem) = child.to_packed::<TagElem>() {
output.push(HtmlNode::Tag(elem.tag.clone()));
} else if let Some(elem) = child.to_packed::<HtmlElem>() {
let mut children = vec![];
if let Some(body) = elem.body.get_ref(styles) {
children = html_fragment(engine, body, locator.next(&elem.span()), styles)?;
}
let element = HtmlElement {
tag: elem.tag,
attrs: elem.attrs.get_cloned(styles),
children,
span: elem.span(),
};
output.push(element.into());
} else if let Some(elem) = child.to_packed::<ParElem>() {
let children =
html_fragment(engine, &elem.body, locator.next(&elem.span()), styles)?;
output.push(
HtmlElement::new(tag::p)
.with_children(children)
.spanned(elem.span())
.into(),
);
} else if let Some(elem) = child.to_packed::<BoxElem>() {
// TODO: This is rather incomplete.
if let Some(body) = elem.body.get_ref(styles) {
let children =
html_fragment(engine, body, locator.next(&elem.span()), styles)?;
output.push(
HtmlElement::new(tag::span)
.with_attr(attr::style, "display: inline-block;")
.with_children(children)
.spanned(elem.span())
.into(),
)
}
} else if let Some((elem, body)) =
child
.to_packed::<BlockElem>()
.and_then(|elem| match elem.body.get_ref(styles) {
Some(BlockBody::Content(body)) => Some((elem, body)),
_ => None,
})
{
// TODO: This is rather incomplete.
let children = html_fragment(engine, body, locator.next(&elem.span()), styles)?;
output.push(
HtmlElement::new(tag::div)
.with_children(children)
.spanned(elem.span())
.into(),
);
} else if child.is::<SpaceElem>() {
output.push(HtmlNode::text(' ', child.span()));
} else if let Some(elem) = child.to_packed::<TextElem>() {
output.push(HtmlNode::text(elem.text.clone(), elem.span()));
} else if let Some(elem) = child.to_packed::<LinebreakElem>() {
output.push(HtmlElement::new(tag::br).spanned(elem.span()).into());
} else if let Some(elem) = child.to_packed::<SmartQuoteElem>() {
output.push(HtmlNode::text(
if elem.double.get(styles) { '"' } else { '\'' },
child.span(),
));
} else if let Some(elem) = child.to_packed::<FrameElem>() {
let locator = locator.next(&elem.span());
let style = TargetElem::target.set(Target::Paged).wrap();
let frame = (engine.routines.layout_frame)(
engine,
&elem.body,
locator,
styles.chain(&style),
Region::new(Size::splat(Abs::inf()), Axes::splat(false)),
)?;
output.push(HtmlNode::Frame(HtmlFrame {
inner: frame,
text_size: styles.resolve(TextElem::size),
}));
} else {
engine.sink.warn(warning!(
child.span(),
"{} was ignored during HTML export",
child.elem().name()
));
}
Ok(())
}
/// Wrap the nodes in `<html>` and `<body>` if they are not yet rooted,
/// supplying a suitable `<head>`.
fn root_element(output: Vec<HtmlNode>, info: &DocumentInfo) -> SourceResult<HtmlElement> {
let head = head_element(info);
let body = match classify_output(output)? {
OutputKind::Html(element) => return Ok(element),
OutputKind::Body(body) => body,
OutputKind::Leafs(leafs) => HtmlElement::new(tag::body).with_children(leafs),
};
Ok(HtmlElement::new(tag::html).with_children(vec![head.into(), body.into()]))
}
/// Generate a `<head>` element.
fn head_element(info: &DocumentInfo) -> HtmlElement {
let mut children = vec![];
children.push(HtmlElement::new(tag::meta).with_attr(attr::charset, "utf-8").into());
children.push(
HtmlElement::new(tag::meta)
.with_attr(attr::name, "viewport")
.with_attr(attr::content, "width=device-width, initial-scale=1")
.into(),
);
if let Some(title) = &info.title {
children.push(
HtmlElement::new(tag::title)
.with_children(vec![HtmlNode::Text(title.clone(), Span::detached())])
.into(),
);
impl HtmlElem {
/// Add an attribute to the element.
pub fn with_attr(mut self, attr: HtmlAttr, value: impl Into<EcoString>) -> Self {
self.attrs
.as_option_mut()
.get_or_insert_with(Default::default)
.push(attr, value);
self
}
if let Some(description) = &info.description {
children.push(
HtmlElement::new(tag::meta)
.with_attr(attr::name, "description")
.with_attr(attr::content, description.clone())
.into(),
);
}
if !info.author.is_empty() {
children.push(
HtmlElement::new(tag::meta)
.with_attr(attr::name, "authors")
.with_attr(attr::content, info.author.join(", "))
.into(),
)
}
if !info.keywords.is_empty() {
children.push(
HtmlElement::new(tag::meta)
.with_attr(attr::name, "keywords")
.with_attr(attr::content, info.keywords.join(", "))
.into(),
)
}
HtmlElement::new(tag::head).with_children(children)
}
/// Determine which kind of output the user generated.
fn classify_output(mut output: Vec<HtmlNode>) -> SourceResult<OutputKind> {
let count = output.iter().filter(|node| !matches!(node, HtmlNode::Tag(_))).count();
for node in &mut output {
let HtmlNode::Element(elem) = node else { continue };
let tag = elem.tag;
let mut take = || std::mem::replace(elem, HtmlElement::new(tag::html));
match (tag, count) {
(tag::html, 1) => return Ok(OutputKind::Html(take())),
(tag::body, 1) => return Ok(OutputKind::Body(take())),
(tag::html | tag::body, _) => bail!(
elem.span,
"`{}` element must be the only element in the document",
elem.tag,
),
_ => {}
/// Adds CSS styles to an element.
fn with_styles(self, properties: css::Properties) -> Self {
if let Some(value) = properties.into_inline_styles() {
self.with_attr(attr::style, value)
} else {
self
}
}
Ok(OutputKind::Leafs(output))
}
/// What kinds of output the user generated.
enum OutputKind {
/// The user generated their own `<html>` element. We do not need to supply
/// one.
Html(HtmlElement),
/// The user generate their own `<body>` element. We do not need to supply
/// one, but need supply the `<html>` element.
Body(HtmlElement),
/// The user generated leafs which we wrap in a `<body>` and `<html>`.
Leafs(Vec<HtmlNode>),
/// An element that lays out its content as an inline SVG.
///
/// Sometimes, converting Typst content to HTML is not desirable. This can be
/// the case for plots and other content that relies on positioning and styling
/// to convey its message.
///
/// This function allows you to use the Typst layout engine that would also be
/// used for PDF, SVG, and PNG export to render a part of your document exactly
/// how it would appear when exported in one of these formats. It embeds the
/// content as an inline SVG.
#[elem]
pub struct FrameElem {
/// The content that shall be laid out.
#[positional]
#[required]
pub body: Content,
}

View File

@ -5,7 +5,6 @@ use typst_library::diag::warning;
use typst_library::foundations::{
Content, NativeElement, NativeRuleMap, ShowFn, Smart, StyleChain, Target,
};
use typst_library::html::{attr, tag, HtmlAttrs, HtmlElem, HtmlTag};
use typst_library::introspection::{Counter, Locator};
use typst_library::layout::resolve::{table_to_cellgrid, Cell, CellGrid, Entry};
use typst_library::layout::{OuterVAlignment, Sizing};
@ -20,11 +19,11 @@ use typst_library::text::{
};
use typst_library::visualize::ImageElem;
use crate::css::{self, HtmlElemExt};
use crate::{attr, css, tag, FrameElem, HtmlAttrs, HtmlElem, HtmlTag};
/// Register show rules for the [HTML target](Target::Html).
/// Registers show rules for the [HTML target](Target::Html).
pub fn register(rules: &mut NativeRuleMap) {
use Target::Html;
use Target::{Html, Paged};
// Model.
rules.register(Html, STRONG_RULE);
@ -53,6 +52,11 @@ pub fn register(rules: &mut NativeRuleMap) {
// Visualize.
rules.register(Html, IMAGE_RULE);
// For the HTML target, `html.frame` is a primitive. In the laid-out target,
// it should be a no-op so that nested frames don't break (things like `show
// math.equation: html.frame` can result in nested ones).
rules.register::<FrameElem>(Paged, |elem, _, _| Ok(elem.body.clone()));
}
const STRONG_RULE: ShowFn<StrongElem> = |elem, _, _| {

View File

@ -0,0 +1,271 @@
//! Predefined constants for HTML tags.
#![allow(non_upper_case_globals)]
#![allow(dead_code)]
use crate::HtmlTag;
pub const a: HtmlTag = HtmlTag::constant("a");
pub const abbr: HtmlTag = HtmlTag::constant("abbr");
pub const address: HtmlTag = HtmlTag::constant("address");
pub const area: HtmlTag = HtmlTag::constant("area");
pub const article: HtmlTag = HtmlTag::constant("article");
pub const aside: HtmlTag = HtmlTag::constant("aside");
pub const audio: HtmlTag = HtmlTag::constant("audio");
pub const b: HtmlTag = HtmlTag::constant("b");
pub const base: HtmlTag = HtmlTag::constant("base");
pub const bdi: HtmlTag = HtmlTag::constant("bdi");
pub const bdo: HtmlTag = HtmlTag::constant("bdo");
pub const blockquote: HtmlTag = HtmlTag::constant("blockquote");
pub const body: HtmlTag = HtmlTag::constant("body");
pub const br: HtmlTag = HtmlTag::constant("br");
pub const button: HtmlTag = HtmlTag::constant("button");
pub const canvas: HtmlTag = HtmlTag::constant("canvas");
pub const caption: HtmlTag = HtmlTag::constant("caption");
pub const cite: HtmlTag = HtmlTag::constant("cite");
pub const code: HtmlTag = HtmlTag::constant("code");
pub const col: HtmlTag = HtmlTag::constant("col");
pub const colgroup: HtmlTag = HtmlTag::constant("colgroup");
pub const data: HtmlTag = HtmlTag::constant("data");
pub const datalist: HtmlTag = HtmlTag::constant("datalist");
pub const dd: HtmlTag = HtmlTag::constant("dd");
pub const del: HtmlTag = HtmlTag::constant("del");
pub const details: HtmlTag = HtmlTag::constant("details");
pub const dfn: HtmlTag = HtmlTag::constant("dfn");
pub const dialog: HtmlTag = HtmlTag::constant("dialog");
pub const div: HtmlTag = HtmlTag::constant("div");
pub const dl: HtmlTag = HtmlTag::constant("dl");
pub const dt: HtmlTag = HtmlTag::constant("dt");
pub const em: HtmlTag = HtmlTag::constant("em");
pub const embed: HtmlTag = HtmlTag::constant("embed");
pub const fieldset: HtmlTag = HtmlTag::constant("fieldset");
pub const figcaption: HtmlTag = HtmlTag::constant("figcaption");
pub const figure: HtmlTag = HtmlTag::constant("figure");
pub const footer: HtmlTag = HtmlTag::constant("footer");
pub const form: HtmlTag = HtmlTag::constant("form");
pub const h1: HtmlTag = HtmlTag::constant("h1");
pub const h2: HtmlTag = HtmlTag::constant("h2");
pub const h3: HtmlTag = HtmlTag::constant("h3");
pub const h4: HtmlTag = HtmlTag::constant("h4");
pub const h5: HtmlTag = HtmlTag::constant("h5");
pub const h6: HtmlTag = HtmlTag::constant("h6");
pub const head: HtmlTag = HtmlTag::constant("head");
pub const header: HtmlTag = HtmlTag::constant("header");
pub const hgroup: HtmlTag = HtmlTag::constant("hgroup");
pub const hr: HtmlTag = HtmlTag::constant("hr");
pub const html: HtmlTag = HtmlTag::constant("html");
pub const i: HtmlTag = HtmlTag::constant("i");
pub const iframe: HtmlTag = HtmlTag::constant("iframe");
pub const img: HtmlTag = HtmlTag::constant("img");
pub const input: HtmlTag = HtmlTag::constant("input");
pub const ins: HtmlTag = HtmlTag::constant("ins");
pub const kbd: HtmlTag = HtmlTag::constant("kbd");
pub const label: HtmlTag = HtmlTag::constant("label");
pub const legend: HtmlTag = HtmlTag::constant("legend");
pub const li: HtmlTag = HtmlTag::constant("li");
pub const link: HtmlTag = HtmlTag::constant("link");
pub const main: HtmlTag = HtmlTag::constant("main");
pub const map: HtmlTag = HtmlTag::constant("map");
pub const mark: HtmlTag = HtmlTag::constant("mark");
pub const menu: HtmlTag = HtmlTag::constant("menu");
pub const meta: HtmlTag = HtmlTag::constant("meta");
pub const meter: HtmlTag = HtmlTag::constant("meter");
pub const nav: HtmlTag = HtmlTag::constant("nav");
pub const noscript: HtmlTag = HtmlTag::constant("noscript");
pub const object: HtmlTag = HtmlTag::constant("object");
pub const ol: HtmlTag = HtmlTag::constant("ol");
pub const optgroup: HtmlTag = HtmlTag::constant("optgroup");
pub const option: HtmlTag = HtmlTag::constant("option");
pub const output: HtmlTag = HtmlTag::constant("output");
pub const p: HtmlTag = HtmlTag::constant("p");
pub const picture: HtmlTag = HtmlTag::constant("picture");
pub const pre: HtmlTag = HtmlTag::constant("pre");
pub const progress: HtmlTag = HtmlTag::constant("progress");
pub const q: HtmlTag = HtmlTag::constant("q");
pub const rp: HtmlTag = HtmlTag::constant("rp");
pub const rt: HtmlTag = HtmlTag::constant("rt");
pub const ruby: HtmlTag = HtmlTag::constant("ruby");
pub const s: HtmlTag = HtmlTag::constant("s");
pub const samp: HtmlTag = HtmlTag::constant("samp");
pub const script: HtmlTag = HtmlTag::constant("script");
pub const search: HtmlTag = HtmlTag::constant("search");
pub const section: HtmlTag = HtmlTag::constant("section");
pub const select: HtmlTag = HtmlTag::constant("select");
pub const slot: HtmlTag = HtmlTag::constant("slot");
pub const small: HtmlTag = HtmlTag::constant("small");
pub const source: HtmlTag = HtmlTag::constant("source");
pub const span: HtmlTag = HtmlTag::constant("span");
pub const strong: HtmlTag = HtmlTag::constant("strong");
pub const style: HtmlTag = HtmlTag::constant("style");
pub const sub: HtmlTag = HtmlTag::constant("sub");
pub const summary: HtmlTag = HtmlTag::constant("summary");
pub const sup: HtmlTag = HtmlTag::constant("sup");
pub const table: HtmlTag = HtmlTag::constant("table");
pub const tbody: HtmlTag = HtmlTag::constant("tbody");
pub const td: HtmlTag = HtmlTag::constant("td");
pub const template: HtmlTag = HtmlTag::constant("template");
pub const textarea: HtmlTag = HtmlTag::constant("textarea");
pub const tfoot: HtmlTag = HtmlTag::constant("tfoot");
pub const th: HtmlTag = HtmlTag::constant("th");
pub const thead: HtmlTag = HtmlTag::constant("thead");
pub const time: HtmlTag = HtmlTag::constant("time");
pub const title: HtmlTag = HtmlTag::constant("title");
pub const tr: HtmlTag = HtmlTag::constant("tr");
pub const track: HtmlTag = HtmlTag::constant("track");
pub const u: HtmlTag = HtmlTag::constant("u");
pub const ul: HtmlTag = HtmlTag::constant("ul");
pub const var: HtmlTag = HtmlTag::constant("var");
pub const video: HtmlTag = HtmlTag::constant("video");
pub const wbr: HtmlTag = HtmlTag::constant("wbr");
/// Whether this is a void tag whose associated element may not have
/// children.
pub fn is_void(tag: HtmlTag) -> bool {
matches!(
tag,
self::area
| self::base
| self::br
| self::col
| self::embed
| self::hr
| self::img
| self::input
| self::link
| self::meta
| self::source
| self::track
| self::wbr
)
}
/// Whether this is a tag containing raw text.
pub fn is_raw(tag: HtmlTag) -> bool {
matches!(tag, self::script | self::style)
}
/// Whether this is a tag containing escapable raw text.
pub fn is_escapable_raw(tag: HtmlTag) -> bool {
matches!(tag, self::textarea | self::title)
}
/// Whether an element is considered metadata.
pub fn is_metadata(tag: HtmlTag) -> bool {
matches!(
tag,
self::base
| self::link
| self::meta
| self::noscript
| self::script
| self::style
| self::template
| self::title
)
}
/// Whether nodes with the tag have the CSS property `display: block` by
/// default.
pub fn is_block_by_default(tag: HtmlTag) -> bool {
matches!(
tag,
self::html
| self::head
| self::body
| self::article
| self::aside
| self::h1
| self::h2
| self::h3
| self::h4
| self::h5
| self::h6
| self::hgroup
| self::nav
| self::section
| self::dd
| self::dl
| self::dt
| self::menu
| self::ol
| self::ul
| self::address
| self::blockquote
| self::dialog
| self::div
| self::fieldset
| self::figure
| self::figcaption
| self::footer
| self::form
| self::header
| self::hr
| self::legend
| self::main
| self::p
| self::pre
| self::search
)
}
/// Whether the element is inline-level as opposed to being block-level.
///
/// Not sure whether this distinction really makes sense. But we somehow
/// need to decide what to put into automatic paragraphs. A `<strong>`
/// should merged into a paragraph created by realization, but a `<div>`
/// shouldn't.
///
/// <https://www.w3.org/TR/html401/struct/global.html#block-inline>
/// <https://developer.mozilla.org/en-US/docs/Glossary/Inline-level_content>
/// <https://github.com/orgs/mdn/discussions/353>
pub fn is_inline_by_default(tag: HtmlTag) -> bool {
matches!(
tag,
self::abbr
| self::a
| self::bdi
| self::b
| self::br
| self::bdo
| self::code
| self::cite
| self::dfn
| self::data
| self::i
| self::em
| self::mark
| self::kbd
| self::rp
| self::q
| self::ruby
| self::rt
| self::samp
| self::s
| self::span
| self::small
| self::sub
| self::strong
| self::time
| self::sup
| self::var
| self::u
)
}
/// Whether nodes with the tag have the CSS property `display: table(-.*)?`
/// by default.
pub fn is_tabular_by_default(tag: HtmlTag) -> bool {
matches!(
tag,
self::table
| self::thead
| self::tbody
| self::tfoot
| self::tr
| self::th
| self::td
| self::caption
| self::col
| self::colgroup
)
}

View File

@ -18,13 +18,11 @@ use typst_library::foundations::{
FromValue, IntoValue, NativeFuncData, NativeFuncPtr, NoneValue, ParamInfo,
PositiveF64, Reflect, Scope, Str, Type, Value,
};
use typst_library::html::tag;
use typst_library::html::{HtmlAttr, HtmlAttrs, HtmlElem, HtmlTag};
use typst_library::layout::{Axes, Axis, Dir, Length};
use typst_library::visualize::Color;
use typst_macros::cast;
use crate::css;
use crate::{css, tag, HtmlAttr, HtmlAttrs, HtmlElem, HtmlTag};
/// Hook up all typed HTML definitions.
pub(super) fn define(html: &mut Scope) {

View File

@ -1,3 +1,5 @@
use std::collections::HashSet;
use comemo::Track;
use ecow::{eco_vec, EcoString, EcoVec};
use typst::foundations::{Label, Styles, Value};
@ -66,14 +68,22 @@ pub fn analyze_import(world: &dyn IdeWorld, source: &LinkedNode) -> Option<Value
/// - All labels and descriptions for them, if available
/// - A split offset: All labels before this offset belong to nodes, all after
/// belong to a bibliography.
///
/// Note: When multiple labels in the document have the same identifier,
/// this only returns the first one.
pub fn analyze_labels(
document: &PagedDocument,
) -> (Vec<(Label, Option<EcoString>)>, usize) {
let mut output = vec![];
let mut seen_labels = HashSet::new();
// Labels in the document.
for elem in document.introspector.all() {
let Some(label) = elem.label() else { continue };
if !seen_labels.insert(label) {
continue;
}
let details = elem
.to_packed::<FigureElem>()
.and_then(|figure| match figure.caption.as_option() {

View File

@ -76,7 +76,7 @@ pub struct Completion {
}
/// A kind of item that can be completed.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum CompletionKind {
/// A syntactical structure.
@ -1564,7 +1564,7 @@ mod tests {
use typst::layout::PagedDocument;
use super::{autocomplete, Completion};
use super::{autocomplete, Completion, CompletionKind};
use crate::tests::{FilePos, TestWorld, WorldLike};
/// Quote a string.
@ -1709,6 +1709,21 @@ mod tests {
.must_exclude(["bib"]);
}
#[test]
fn test_autocomplete_ref_identical_labels_returns_single_completion() {
let mut world = TestWorld::new("x<test> y<test>");
let doc = typst::compile(&world).output.ok();
let end = world.main.text().len();
world.main.edit(end..end, " @t");
let result = test_with_doc(&world, -1, doc.as_ref());
let completions = result.completions();
let label_count =
completions.iter().filter(|c| c.kind == CompletionKind::Label).count();
assert_eq!(label_count, 1);
}
/// Test what kind of brackets we autocomplete for function calls depending
/// on the function and existing parens.
#[test]

View File

@ -143,7 +143,7 @@ fn layout_fragment_impl(
let mut kind = FragmentKind::Block;
let arenas = Arenas::default();
let children = (engine.routines.realize)(
RealizationKind::LayoutFragment(&mut kind),
RealizationKind::LayoutFragment { kind: &mut kind },
&mut engine,
&mut locator,
&arenas,

View File

@ -4,14 +4,16 @@ mod collect;
mod finalize;
mod run;
use std::num::NonZeroUsize;
use comemo::{Tracked, TrackedMut};
use typst_library::diag::SourceResult;
use typst_library::engine::{Engine, Route, Sink, Traced};
use typst_library::foundations::{Content, StyleChain};
use typst_library::introspection::{
Introspector, Locator, ManualPageCounter, SplitLocator, TagElem,
Introspector, IntrospectorBuilder, Locator, ManualPageCounter, SplitLocator, TagElem,
};
use typst_library::layout::{FrameItem, Page, PagedDocument, Point};
use typst_library::layout::{FrameItem, Page, PagedDocument, Point, Transform};
use typst_library::model::DocumentInfo;
use typst_library::routines::{Arenas, Pair, RealizationKind, Routines};
use typst_library::World;
@ -75,7 +77,7 @@ fn layout_document_impl(
let arenas = Arenas::default();
let mut info = DocumentInfo::default();
let mut children = (engine.routines.realize)(
RealizationKind::LayoutDocument(&mut info),
RealizationKind::LayoutDocument { info: &mut info },
&mut engine,
&mut locator,
&arenas,
@ -84,7 +86,7 @@ fn layout_document_impl(
)?;
let pages = layout_pages(&mut engine, &mut children, &mut locator, styles)?;
let introspector = Introspector::paged(&pages);
let introspector = introspect_pages(&pages);
Ok(PagedDocument { pages, info, introspector })
}
@ -157,3 +159,27 @@ fn layout_pages<'a>(
Ok(pages)
}
/// Introspects pages.
#[typst_macros::time(name = "introspect pages")]
fn introspect_pages(pages: &[Page]) -> Introspector {
let mut builder = IntrospectorBuilder::new();
builder.pages = pages.len();
builder.page_numberings.reserve(pages.len());
builder.page_supplements.reserve(pages.len());
// Discover all elements.
let mut elems = Vec::new();
for (i, page) in pages.iter().enumerate() {
builder.page_numberings.push(page.numbering.clone());
builder.page_supplements.push(page.supplement.clone());
builder.discover_in_frame(
&mut elems,
&page.frame,
NonZeroUsize::new(1 + i).unwrap(),
Transform::identity(),
);
}
builder.finalize(elems)
}

View File

@ -1,828 +0,0 @@
use std::fmt::{self, Debug, Display, Formatter};
use ecow::{EcoString, EcoVec};
use typst_syntax::Span;
use typst_utils::{PicoStr, ResolvedPicoStr};
use crate::diag::{bail, HintedStrResult, StrResult};
use crate::foundations::{cast, Dict, Repr, Str};
use crate::introspection::{Introspector, Tag};
use crate::layout::{Abs, Frame};
use crate::model::DocumentInfo;
/// An HTML document.
#[derive(Debug, Clone)]
pub struct HtmlDocument {
/// The document's root HTML element.
pub root: HtmlElement,
/// Details about the document.
pub info: DocumentInfo,
/// Provides the ability to execute queries on the document.
pub introspector: Introspector,
}
/// A child of an HTML element.
#[derive(Debug, Clone, Hash)]
pub enum HtmlNode {
/// An introspectable element that produced something within this node.
Tag(Tag),
/// Plain text.
Text(EcoString, Span),
/// Another element.
Element(HtmlElement),
/// Layouted content that will be embedded into HTML as an SVG.
Frame(HtmlFrame),
}
impl HtmlNode {
/// Create a plain text node.
pub fn text(text: impl Into<EcoString>, span: Span) -> Self {
Self::Text(text.into(), span)
}
}
impl From<HtmlElement> for HtmlNode {
fn from(element: HtmlElement) -> Self {
Self::Element(element)
}
}
/// An HTML element.
#[derive(Debug, Clone, Hash)]
pub struct HtmlElement {
/// The HTML tag.
pub tag: HtmlTag,
/// The element's attributes.
pub attrs: HtmlAttrs,
/// The element's children.
pub children: Vec<HtmlNode>,
/// The span from which the element originated, if any.
pub span: Span,
}
impl HtmlElement {
/// Create a new, blank element without attributes or children.
pub fn new(tag: HtmlTag) -> Self {
Self {
tag,
attrs: HtmlAttrs::default(),
children: vec![],
span: Span::detached(),
}
}
/// Attach children to the element.
///
/// Note: This overwrites potential previous children.
pub fn with_children(mut self, children: Vec<HtmlNode>) -> Self {
self.children = children;
self
}
/// Add an atribute to the element.
pub fn with_attr(mut self, key: HtmlAttr, value: impl Into<EcoString>) -> Self {
self.attrs.push(key, value);
self
}
/// Attach a span to the element.
pub fn spanned(mut self, span: Span) -> Self {
self.span = span;
self
}
}
/// The tag of an HTML element.
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub struct HtmlTag(PicoStr);
impl HtmlTag {
/// Intern an HTML tag string at runtime.
pub fn intern(string: &str) -> StrResult<Self> {
if string.is_empty() {
bail!("tag name must not be empty");
}
if let Some(c) = string.chars().find(|&c| !charsets::is_valid_in_tag_name(c)) {
bail!("the character {} is not valid in a tag name", c.repr());
}
Ok(Self(PicoStr::intern(string)))
}
/// Creates a compile-time constant `HtmlTag`.
///
/// Should only be used in const contexts because it can panic.
#[track_caller]
pub const fn constant(string: &'static str) -> Self {
if string.is_empty() {
panic!("tag name must not be empty");
}
let bytes = string.as_bytes();
let mut i = 0;
while i < bytes.len() {
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;
}
Self(PicoStr::constant(string))
}
/// Resolves the tag to a string.
pub fn resolve(self) -> ResolvedPicoStr {
self.0.resolve()
}
/// Turns the tag into its inner interned string.
pub const fn into_inner(self) -> PicoStr {
self.0
}
}
impl Debug for HtmlTag {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Display::fmt(self, f)
}
}
impl Display for HtmlTag {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "<{}>", self.resolve())
}
}
cast! {
HtmlTag,
self => self.0.resolve().as_str().into_value(),
v: Str => Self::intern(&v)?,
}
/// Attributes of an HTML element.
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct HtmlAttrs(pub EcoVec<(HtmlAttr, EcoString)>);
impl HtmlAttrs {
/// Creates an empty attribute list.
pub fn new() -> Self {
Self::default()
}
/// Add an attribute.
pub fn push(&mut self, attr: HtmlAttr, value: impl Into<EcoString>) {
self.0.push((attr, value.into()));
}
}
cast! {
HtmlAttrs,
self => self.0
.into_iter()
.map(|(key, value)| (key.resolve().as_str().into(), value.into_value()))
.collect::<Dict>()
.into_value(),
values: Dict => Self(values
.into_iter()
.map(|(k, v)| {
let attr = HtmlAttr::intern(&k)?;
let value = v.cast::<EcoString>()?;
Ok((attr, value))
})
.collect::<HintedStrResult<_>>()?),
}
/// An attribute of an HTML element.
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub struct HtmlAttr(PicoStr);
impl HtmlAttr {
/// Intern an HTML attribute string at runtime.
pub fn intern(string: &str) -> StrResult<Self> {
if string.is_empty() {
bail!("attribute name must not be empty");
}
if let Some(c) =
string.chars().find(|&c| !charsets::is_valid_in_attribute_name(c))
{
bail!("the character {} is not valid in an attribute name", c.repr());
}
Ok(Self(PicoStr::intern(string)))
}
/// Creates a compile-time constant `HtmlAttr`.
///
/// Must only be used in const contexts (in a constant definition or
/// explicit `const { .. }` block) because otherwise a panic for a malformed
/// attribute or not auto-internible constant will only be caught at
/// runtime.
#[track_caller]
pub const fn constant(string: &'static str) -> Self {
if string.is_empty() {
panic!("attribute name must not be empty");
}
let bytes = string.as_bytes();
let mut i = 0;
while i < bytes.len() {
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;
}
Self(PicoStr::constant(string))
}
/// Resolves the attribute to a string.
pub fn resolve(self) -> ResolvedPicoStr {
self.0.resolve()
}
/// Turns the attribute into its inner interned string.
pub const fn into_inner(self) -> PicoStr {
self.0
}
}
impl Debug for HtmlAttr {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Display::fmt(self, f)
}
}
impl Display for HtmlAttr {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.resolve())
}
}
cast! {
HtmlAttr,
self => self.0.resolve().as_str().into_value(),
v: Str => Self::intern(&v)?,
}
/// Layouted content that will be embedded into HTML as an SVG.
#[derive(Debug, Clone, Hash)]
pub struct HtmlFrame {
/// The frame that will be displayed as an SVG.
pub inner: Frame,
/// The text size where the frame was defined. This is used to size the
/// frame with em units to make text in and outside of the frame sized
/// consistently.
pub text_size: Abs,
}
/// Defines syntactical properties of HTML tags, attributes, and text.
pub mod charsets {
/// Check whether a character is in a tag name.
pub const fn is_valid_in_tag_name(c: char) -> bool {
c.is_ascii_alphanumeric()
}
/// Check whether a character is valid in an attribute name.
pub const fn is_valid_in_attribute_name(c: char) -> bool {
match c {
// These are forbidden.
'\0' | ' ' | '"' | '\'' | '>' | '/' | '=' => false,
c if is_whatwg_control_char(c) => false,
c if is_whatwg_non_char(c) => false,
// _Everything_ else is allowed, including U+2029 paragraph
// separator. Go wild.
_ => true,
}
}
/// Check whether a character can be an used in an attribute value without
/// escaping.
///
/// See <https://html.spec.whatwg.org/multipage/syntax.html#attributes-2>
pub const fn is_valid_in_attribute_value(c: char) -> bool {
match c {
// Ampersands are sometimes legal (i.e. when they are not _ambiguous
// ampersands_) but it is not worth the trouble to check for that.
'&' => false,
// Quotation marks are not allowed in double-quote-delimited attribute
// values.
'"' => false,
// All other text characters are allowed.
c => is_w3c_text_char(c),
}
}
/// Check whether a character can be an used in normal text without
/// escaping.
pub const fn is_valid_in_normal_element_text(c: char) -> bool {
match c {
// Ampersands are sometimes legal (i.e. when they are not _ambiguous
// ampersands_) but it is not worth the trouble to check for that.
'&' => false,
// Less-than signs are not allowed in text.
'<' => false,
// All other text characters are allowed.
c => is_w3c_text_char(c),
}
}
/// Check if something is valid text in HTML.
pub const fn is_w3c_text_char(c: char) -> bool {
match c {
// Non-characters are obviously not text characters.
c if is_whatwg_non_char(c) => false,
// Control characters are disallowed, except for whitespace.
c if is_whatwg_control_char(c) => c.is_ascii_whitespace(),
// Everything else is allowed.
_ => true,
}
}
const fn is_whatwg_non_char(c: char) -> bool {
match c {
'\u{fdd0}'..='\u{fdef}' => true,
// Non-characters matching xxFFFE or xxFFFF up to x10FFFF (inclusive).
c if c as u32 & 0xfffe == 0xfffe && c as u32 <= 0x10ffff => true,
_ => false,
}
}
const fn is_whatwg_control_char(c: char) -> bool {
match c {
// C0 control characters.
'\u{00}'..='\u{1f}' => true,
// Other control characters.
'\u{7f}'..='\u{9f}' => true,
_ => false,
}
}
}
/// Predefined constants for HTML tags.
#[allow(non_upper_case_globals)]
pub mod tag {
use super::HtmlTag;
pub const a: HtmlTag = HtmlTag::constant("a");
pub const abbr: HtmlTag = HtmlTag::constant("abbr");
pub const address: HtmlTag = HtmlTag::constant("address");
pub const area: HtmlTag = HtmlTag::constant("area");
pub const article: HtmlTag = HtmlTag::constant("article");
pub const aside: HtmlTag = HtmlTag::constant("aside");
pub const audio: HtmlTag = HtmlTag::constant("audio");
pub const b: HtmlTag = HtmlTag::constant("b");
pub const base: HtmlTag = HtmlTag::constant("base");
pub const bdi: HtmlTag = HtmlTag::constant("bdi");
pub const bdo: HtmlTag = HtmlTag::constant("bdo");
pub const blockquote: HtmlTag = HtmlTag::constant("blockquote");
pub const body: HtmlTag = HtmlTag::constant("body");
pub const br: HtmlTag = HtmlTag::constant("br");
pub const button: HtmlTag = HtmlTag::constant("button");
pub const canvas: HtmlTag = HtmlTag::constant("canvas");
pub const caption: HtmlTag = HtmlTag::constant("caption");
pub const cite: HtmlTag = HtmlTag::constant("cite");
pub const code: HtmlTag = HtmlTag::constant("code");
pub const col: HtmlTag = HtmlTag::constant("col");
pub const colgroup: HtmlTag = HtmlTag::constant("colgroup");
pub const data: HtmlTag = HtmlTag::constant("data");
pub const datalist: HtmlTag = HtmlTag::constant("datalist");
pub const dd: HtmlTag = HtmlTag::constant("dd");
pub const del: HtmlTag = HtmlTag::constant("del");
pub const details: HtmlTag = HtmlTag::constant("details");
pub const dfn: HtmlTag = HtmlTag::constant("dfn");
pub const dialog: HtmlTag = HtmlTag::constant("dialog");
pub const div: HtmlTag = HtmlTag::constant("div");
pub const dl: HtmlTag = HtmlTag::constant("dl");
pub const dt: HtmlTag = HtmlTag::constant("dt");
pub const em: HtmlTag = HtmlTag::constant("em");
pub const embed: HtmlTag = HtmlTag::constant("embed");
pub const fieldset: HtmlTag = HtmlTag::constant("fieldset");
pub const figcaption: HtmlTag = HtmlTag::constant("figcaption");
pub const figure: HtmlTag = HtmlTag::constant("figure");
pub const footer: HtmlTag = HtmlTag::constant("footer");
pub const form: HtmlTag = HtmlTag::constant("form");
pub const h1: HtmlTag = HtmlTag::constant("h1");
pub const h2: HtmlTag = HtmlTag::constant("h2");
pub const h3: HtmlTag = HtmlTag::constant("h3");
pub const h4: HtmlTag = HtmlTag::constant("h4");
pub const h5: HtmlTag = HtmlTag::constant("h5");
pub const h6: HtmlTag = HtmlTag::constant("h6");
pub const head: HtmlTag = HtmlTag::constant("head");
pub const header: HtmlTag = HtmlTag::constant("header");
pub const hgroup: HtmlTag = HtmlTag::constant("hgroup");
pub const hr: HtmlTag = HtmlTag::constant("hr");
pub const html: HtmlTag = HtmlTag::constant("html");
pub const i: HtmlTag = HtmlTag::constant("i");
pub const iframe: HtmlTag = HtmlTag::constant("iframe");
pub const img: HtmlTag = HtmlTag::constant("img");
pub const input: HtmlTag = HtmlTag::constant("input");
pub const ins: HtmlTag = HtmlTag::constant("ins");
pub const kbd: HtmlTag = HtmlTag::constant("kbd");
pub const label: HtmlTag = HtmlTag::constant("label");
pub const legend: HtmlTag = HtmlTag::constant("legend");
pub const li: HtmlTag = HtmlTag::constant("li");
pub const link: HtmlTag = HtmlTag::constant("link");
pub const main: HtmlTag = HtmlTag::constant("main");
pub const map: HtmlTag = HtmlTag::constant("map");
pub const mark: HtmlTag = HtmlTag::constant("mark");
pub const menu: HtmlTag = HtmlTag::constant("menu");
pub const meta: HtmlTag = HtmlTag::constant("meta");
pub const meter: HtmlTag = HtmlTag::constant("meter");
pub const nav: HtmlTag = HtmlTag::constant("nav");
pub const noscript: HtmlTag = HtmlTag::constant("noscript");
pub const object: HtmlTag = HtmlTag::constant("object");
pub const ol: HtmlTag = HtmlTag::constant("ol");
pub const optgroup: HtmlTag = HtmlTag::constant("optgroup");
pub const option: HtmlTag = HtmlTag::constant("option");
pub const output: HtmlTag = HtmlTag::constant("output");
pub const p: HtmlTag = HtmlTag::constant("p");
pub const picture: HtmlTag = HtmlTag::constant("picture");
pub const pre: HtmlTag = HtmlTag::constant("pre");
pub const progress: HtmlTag = HtmlTag::constant("progress");
pub const q: HtmlTag = HtmlTag::constant("q");
pub const rp: HtmlTag = HtmlTag::constant("rp");
pub const rt: HtmlTag = HtmlTag::constant("rt");
pub const ruby: HtmlTag = HtmlTag::constant("ruby");
pub const s: HtmlTag = HtmlTag::constant("s");
pub const samp: HtmlTag = HtmlTag::constant("samp");
pub const script: HtmlTag = HtmlTag::constant("script");
pub const search: HtmlTag = HtmlTag::constant("search");
pub const section: HtmlTag = HtmlTag::constant("section");
pub const select: HtmlTag = HtmlTag::constant("select");
pub const slot: HtmlTag = HtmlTag::constant("slot");
pub const small: HtmlTag = HtmlTag::constant("small");
pub const source: HtmlTag = HtmlTag::constant("source");
pub const span: HtmlTag = HtmlTag::constant("span");
pub const strong: HtmlTag = HtmlTag::constant("strong");
pub const style: HtmlTag = HtmlTag::constant("style");
pub const sub: HtmlTag = HtmlTag::constant("sub");
pub const summary: HtmlTag = HtmlTag::constant("summary");
pub const sup: HtmlTag = HtmlTag::constant("sup");
pub const table: HtmlTag = HtmlTag::constant("table");
pub const tbody: HtmlTag = HtmlTag::constant("tbody");
pub const td: HtmlTag = HtmlTag::constant("td");
pub const template: HtmlTag = HtmlTag::constant("template");
pub const textarea: HtmlTag = HtmlTag::constant("textarea");
pub const tfoot: HtmlTag = HtmlTag::constant("tfoot");
pub const th: HtmlTag = HtmlTag::constant("th");
pub const thead: HtmlTag = HtmlTag::constant("thead");
pub const time: HtmlTag = HtmlTag::constant("time");
pub const title: HtmlTag = HtmlTag::constant("title");
pub const tr: HtmlTag = HtmlTag::constant("tr");
pub const track: HtmlTag = HtmlTag::constant("track");
pub const u: HtmlTag = HtmlTag::constant("u");
pub const ul: HtmlTag = HtmlTag::constant("ul");
pub const var: HtmlTag = HtmlTag::constant("var");
pub const video: HtmlTag = HtmlTag::constant("video");
pub const wbr: HtmlTag = HtmlTag::constant("wbr");
/// Whether this is a void tag whose associated element may not have
/// children.
pub fn is_void(tag: HtmlTag) -> bool {
matches!(
tag,
self::area
| self::base
| self::br
| self::col
| self::embed
| self::hr
| self::img
| self::input
| self::link
| self::meta
| self::source
| self::track
| self::wbr
)
}
/// Whether this is a tag containing raw text.
pub fn is_raw(tag: HtmlTag) -> bool {
matches!(tag, self::script | self::style)
}
/// Whether this is a tag containing escapable raw text.
pub fn is_escapable_raw(tag: HtmlTag) -> bool {
matches!(tag, self::textarea | self::title)
}
/// Whether an element is considered metadata.
pub fn is_metadata(tag: HtmlTag) -> bool {
matches!(
tag,
self::base
| self::link
| self::meta
| self::noscript
| self::script
| self::style
| self::template
| self::title
)
}
/// Whether nodes with the tag have the CSS property `display: block` by
/// default.
pub fn is_block_by_default(tag: HtmlTag) -> bool {
matches!(
tag,
self::html
| self::head
| self::body
| self::article
| self::aside
| self::h1
| self::h2
| self::h3
| self::h4
| self::h5
| self::h6
| self::hgroup
| self::nav
| self::section
| self::dd
| self::dl
| self::dt
| self::menu
| self::ol
| self::ul
| self::address
| self::blockquote
| self::dialog
| self::div
| self::fieldset
| self::figure
| self::figcaption
| self::footer
| self::form
| self::header
| self::hr
| self::legend
| self::main
| self::p
| self::pre
| self::search
)
}
/// Whether the element is inline-level as opposed to being block-level.
///
/// Not sure whether this distinction really makes sense. But we somehow
/// need to decide what to put into automatic paragraphs. A `<strong>`
/// should merged into a paragraph created by realization, but a `<div>`
/// shouldn't.
///
/// <https://www.w3.org/TR/html401/struct/global.html#block-inline>
/// <https://developer.mozilla.org/en-US/docs/Glossary/Inline-level_content>
/// <https://github.com/orgs/mdn/discussions/353>
pub fn is_inline_by_default(tag: HtmlTag) -> bool {
matches!(
tag,
self::abbr
| self::a
| self::bdi
| self::b
| self::br
| self::bdo
| self::code
| self::cite
| self::dfn
| self::data
| self::i
| self::em
| self::mark
| self::kbd
| self::rp
| self::q
| self::ruby
| self::rt
| self::samp
| self::s
| self::span
| self::small
| self::sub
| self::strong
| self::time
| self::sup
| self::var
| self::u
)
}
/// Whether nodes with the tag have the CSS property `display: table(-.*)?`
/// by default.
pub fn is_tabular_by_default(tag: HtmlTag) -> bool {
matches!(
tag,
self::table
| self::thead
| self::tbody
| self::tfoot
| self::tr
| self::th
| self::td
| self::caption
| self::col
| self::colgroup
)
}
}
#[allow(non_upper_case_globals)]
#[rustfmt::skip]
pub mod attr {
use crate::html::HtmlAttr;
pub const abbr: HtmlAttr = HtmlAttr::constant("abbr");
pub const accept: HtmlAttr = HtmlAttr::constant("accept");
pub const accept_charset: HtmlAttr = HtmlAttr::constant("accept-charset");
pub const accesskey: HtmlAttr = HtmlAttr::constant("accesskey");
pub const action: HtmlAttr = HtmlAttr::constant("action");
pub const allow: HtmlAttr = HtmlAttr::constant("allow");
pub const allowfullscreen: HtmlAttr = HtmlAttr::constant("allowfullscreen");
pub const alpha: HtmlAttr = HtmlAttr::constant("alpha");
pub const alt: HtmlAttr = HtmlAttr::constant("alt");
pub const aria_activedescendant: HtmlAttr = HtmlAttr::constant("aria-activedescendant");
pub const aria_atomic: HtmlAttr = HtmlAttr::constant("aria-atomic");
pub const aria_autocomplete: HtmlAttr = HtmlAttr::constant("aria-autocomplete");
pub const aria_busy: HtmlAttr = HtmlAttr::constant("aria-busy");
pub const aria_checked: HtmlAttr = HtmlAttr::constant("aria-checked");
pub const aria_colcount: HtmlAttr = HtmlAttr::constant("aria-colcount");
pub const aria_colindex: HtmlAttr = HtmlAttr::constant("aria-colindex");
pub const aria_colspan: HtmlAttr = HtmlAttr::constant("aria-colspan");
pub const aria_controls: HtmlAttr = HtmlAttr::constant("aria-controls");
pub const aria_current: HtmlAttr = HtmlAttr::constant("aria-current");
pub const aria_describedby: HtmlAttr = HtmlAttr::constant("aria-describedby");
pub const aria_details: HtmlAttr = HtmlAttr::constant("aria-details");
pub const aria_disabled: HtmlAttr = HtmlAttr::constant("aria-disabled");
pub const aria_errormessage: HtmlAttr = HtmlAttr::constant("aria-errormessage");
pub const aria_expanded: HtmlAttr = HtmlAttr::constant("aria-expanded");
pub const aria_flowto: HtmlAttr = HtmlAttr::constant("aria-flowto");
pub const aria_haspopup: HtmlAttr = HtmlAttr::constant("aria-haspopup");
pub const aria_hidden: HtmlAttr = HtmlAttr::constant("aria-hidden");
pub const aria_invalid: HtmlAttr = HtmlAttr::constant("aria-invalid");
pub const aria_keyshortcuts: HtmlAttr = HtmlAttr::constant("aria-keyshortcuts");
pub const aria_label: HtmlAttr = HtmlAttr::constant("aria-label");
pub const aria_labelledby: HtmlAttr = HtmlAttr::constant("aria-labelledby");
pub const aria_level: HtmlAttr = HtmlAttr::constant("aria-level");
pub const aria_live: HtmlAttr = HtmlAttr::constant("aria-live");
pub const aria_modal: HtmlAttr = HtmlAttr::constant("aria-modal");
pub const aria_multiline: HtmlAttr = HtmlAttr::constant("aria-multiline");
pub const aria_multiselectable: HtmlAttr = HtmlAttr::constant("aria-multiselectable");
pub const aria_orientation: HtmlAttr = HtmlAttr::constant("aria-orientation");
pub const aria_owns: HtmlAttr = HtmlAttr::constant("aria-owns");
pub const aria_placeholder: HtmlAttr = HtmlAttr::constant("aria-placeholder");
pub const aria_posinset: HtmlAttr = HtmlAttr::constant("aria-posinset");
pub const aria_pressed: HtmlAttr = HtmlAttr::constant("aria-pressed");
pub const aria_readonly: HtmlAttr = HtmlAttr::constant("aria-readonly");
pub const aria_relevant: HtmlAttr = HtmlAttr::constant("aria-relevant");
pub const aria_required: HtmlAttr = HtmlAttr::constant("aria-required");
pub const aria_roledescription: HtmlAttr = HtmlAttr::constant("aria-roledescription");
pub const aria_rowcount: HtmlAttr = HtmlAttr::constant("aria-rowcount");
pub const aria_rowindex: HtmlAttr = HtmlAttr::constant("aria-rowindex");
pub const aria_rowspan: HtmlAttr = HtmlAttr::constant("aria-rowspan");
pub const aria_selected: HtmlAttr = HtmlAttr::constant("aria-selected");
pub const aria_setsize: HtmlAttr = HtmlAttr::constant("aria-setsize");
pub const aria_sort: HtmlAttr = HtmlAttr::constant("aria-sort");
pub const aria_valuemax: HtmlAttr = HtmlAttr::constant("aria-valuemax");
pub const aria_valuemin: HtmlAttr = HtmlAttr::constant("aria-valuemin");
pub const aria_valuenow: HtmlAttr = HtmlAttr::constant("aria-valuenow");
pub const aria_valuetext: HtmlAttr = HtmlAttr::constant("aria-valuetext");
pub const r#as: HtmlAttr = HtmlAttr::constant("as");
pub const r#async: HtmlAttr = HtmlAttr::constant("async");
pub const autocapitalize: HtmlAttr = HtmlAttr::constant("autocapitalize");
pub const autocomplete: HtmlAttr = HtmlAttr::constant("autocomplete");
pub const autocorrect: HtmlAttr = HtmlAttr::constant("autocorrect");
pub const autofocus: HtmlAttr = HtmlAttr::constant("autofocus");
pub const autoplay: HtmlAttr = HtmlAttr::constant("autoplay");
pub const blocking: HtmlAttr = HtmlAttr::constant("blocking");
pub const charset: HtmlAttr = HtmlAttr::constant("charset");
pub const checked: HtmlAttr = HtmlAttr::constant("checked");
pub const cite: HtmlAttr = HtmlAttr::constant("cite");
pub const class: HtmlAttr = HtmlAttr::constant("class");
pub const closedby: HtmlAttr = HtmlAttr::constant("closedby");
pub const color: HtmlAttr = HtmlAttr::constant("color");
pub const colorspace: HtmlAttr = HtmlAttr::constant("colorspace");
pub const cols: HtmlAttr = HtmlAttr::constant("cols");
pub const colspan: HtmlAttr = HtmlAttr::constant("colspan");
pub const command: HtmlAttr = HtmlAttr::constant("command");
pub const commandfor: HtmlAttr = HtmlAttr::constant("commandfor");
pub const content: HtmlAttr = HtmlAttr::constant("content");
pub const contenteditable: HtmlAttr = HtmlAttr::constant("contenteditable");
pub const controls: HtmlAttr = HtmlAttr::constant("controls");
pub const coords: HtmlAttr = HtmlAttr::constant("coords");
pub const crossorigin: HtmlAttr = HtmlAttr::constant("crossorigin");
pub const data: HtmlAttr = HtmlAttr::constant("data");
pub const datetime: HtmlAttr = HtmlAttr::constant("datetime");
pub const decoding: HtmlAttr = HtmlAttr::constant("decoding");
pub const default: HtmlAttr = HtmlAttr::constant("default");
pub const defer: HtmlAttr = HtmlAttr::constant("defer");
pub const dir: HtmlAttr = HtmlAttr::constant("dir");
pub const dirname: HtmlAttr = HtmlAttr::constant("dirname");
pub const disabled: HtmlAttr = HtmlAttr::constant("disabled");
pub const download: HtmlAttr = HtmlAttr::constant("download");
pub const draggable: HtmlAttr = HtmlAttr::constant("draggable");
pub const enctype: HtmlAttr = HtmlAttr::constant("enctype");
pub const enterkeyhint: HtmlAttr = HtmlAttr::constant("enterkeyhint");
pub const fetchpriority: HtmlAttr = HtmlAttr::constant("fetchpriority");
pub const r#for: HtmlAttr = HtmlAttr::constant("for");
pub const form: HtmlAttr = HtmlAttr::constant("form");
pub const formaction: HtmlAttr = HtmlAttr::constant("formaction");
pub const formenctype: HtmlAttr = HtmlAttr::constant("formenctype");
pub const formmethod: HtmlAttr = HtmlAttr::constant("formmethod");
pub const formnovalidate: HtmlAttr = HtmlAttr::constant("formnovalidate");
pub const formtarget: HtmlAttr = HtmlAttr::constant("formtarget");
pub const headers: HtmlAttr = HtmlAttr::constant("headers");
pub const height: HtmlAttr = HtmlAttr::constant("height");
pub const hidden: HtmlAttr = HtmlAttr::constant("hidden");
pub const high: HtmlAttr = HtmlAttr::constant("high");
pub const href: HtmlAttr = HtmlAttr::constant("href");
pub const hreflang: HtmlAttr = HtmlAttr::constant("hreflang");
pub const http_equiv: HtmlAttr = HtmlAttr::constant("http-equiv");
pub const id: HtmlAttr = HtmlAttr::constant("id");
pub const imagesizes: HtmlAttr = HtmlAttr::constant("imagesizes");
pub const imagesrcset: HtmlAttr = HtmlAttr::constant("imagesrcset");
pub const inert: HtmlAttr = HtmlAttr::constant("inert");
pub const inputmode: HtmlAttr = HtmlAttr::constant("inputmode");
pub const integrity: HtmlAttr = HtmlAttr::constant("integrity");
pub const is: HtmlAttr = HtmlAttr::constant("is");
pub const ismap: HtmlAttr = HtmlAttr::constant("ismap");
pub const itemid: HtmlAttr = HtmlAttr::constant("itemid");
pub const itemprop: HtmlAttr = HtmlAttr::constant("itemprop");
pub const itemref: HtmlAttr = HtmlAttr::constant("itemref");
pub const itemscope: HtmlAttr = HtmlAttr::constant("itemscope");
pub const itemtype: HtmlAttr = HtmlAttr::constant("itemtype");
pub const kind: HtmlAttr = HtmlAttr::constant("kind");
pub const label: HtmlAttr = HtmlAttr::constant("label");
pub const lang: HtmlAttr = HtmlAttr::constant("lang");
pub const list: HtmlAttr = HtmlAttr::constant("list");
pub const loading: HtmlAttr = HtmlAttr::constant("loading");
pub const r#loop: HtmlAttr = HtmlAttr::constant("loop");
pub const low: HtmlAttr = HtmlAttr::constant("low");
pub const max: HtmlAttr = HtmlAttr::constant("max");
pub const maxlength: HtmlAttr = HtmlAttr::constant("maxlength");
pub const media: HtmlAttr = HtmlAttr::constant("media");
pub const method: HtmlAttr = HtmlAttr::constant("method");
pub const min: HtmlAttr = HtmlAttr::constant("min");
pub const minlength: HtmlAttr = HtmlAttr::constant("minlength");
pub const multiple: HtmlAttr = HtmlAttr::constant("multiple");
pub const muted: HtmlAttr = HtmlAttr::constant("muted");
pub const name: HtmlAttr = HtmlAttr::constant("name");
pub const nomodule: HtmlAttr = HtmlAttr::constant("nomodule");
pub const nonce: HtmlAttr = HtmlAttr::constant("nonce");
pub const novalidate: HtmlAttr = HtmlAttr::constant("novalidate");
pub const open: HtmlAttr = HtmlAttr::constant("open");
pub const optimum: HtmlAttr = HtmlAttr::constant("optimum");
pub const pattern: HtmlAttr = HtmlAttr::constant("pattern");
pub const ping: HtmlAttr = HtmlAttr::constant("ping");
pub const placeholder: HtmlAttr = HtmlAttr::constant("placeholder");
pub const playsinline: HtmlAttr = HtmlAttr::constant("playsinline");
pub const popover: HtmlAttr = HtmlAttr::constant("popover");
pub const popovertarget: HtmlAttr = HtmlAttr::constant("popovertarget");
pub const popovertargetaction: HtmlAttr = HtmlAttr::constant("popovertargetaction");
pub const poster: HtmlAttr = HtmlAttr::constant("poster");
pub const preload: HtmlAttr = HtmlAttr::constant("preload");
pub const readonly: HtmlAttr = HtmlAttr::constant("readonly");
pub const referrerpolicy: HtmlAttr = HtmlAttr::constant("referrerpolicy");
pub const rel: HtmlAttr = HtmlAttr::constant("rel");
pub const required: HtmlAttr = HtmlAttr::constant("required");
pub const reversed: HtmlAttr = HtmlAttr::constant("reversed");
pub const role: HtmlAttr = HtmlAttr::constant("role");
pub const rows: HtmlAttr = HtmlAttr::constant("rows");
pub const rowspan: HtmlAttr = HtmlAttr::constant("rowspan");
pub const sandbox: HtmlAttr = HtmlAttr::constant("sandbox");
pub const scope: HtmlAttr = HtmlAttr::constant("scope");
pub const selected: HtmlAttr = HtmlAttr::constant("selected");
pub const shadowrootclonable: HtmlAttr = HtmlAttr::constant("shadowrootclonable");
pub const shadowrootcustomelementregistry: HtmlAttr = HtmlAttr::constant("shadowrootcustomelementregistry");
pub const shadowrootdelegatesfocus: HtmlAttr = HtmlAttr::constant("shadowrootdelegatesfocus");
pub const shadowrootmode: HtmlAttr = HtmlAttr::constant("shadowrootmode");
pub const shadowrootserializable: HtmlAttr = HtmlAttr::constant("shadowrootserializable");
pub const shape: HtmlAttr = HtmlAttr::constant("shape");
pub const size: HtmlAttr = HtmlAttr::constant("size");
pub const sizes: HtmlAttr = HtmlAttr::constant("sizes");
pub const slot: HtmlAttr = HtmlAttr::constant("slot");
pub const span: HtmlAttr = HtmlAttr::constant("span");
pub const spellcheck: HtmlAttr = HtmlAttr::constant("spellcheck");
pub const src: HtmlAttr = HtmlAttr::constant("src");
pub const srcdoc: HtmlAttr = HtmlAttr::constant("srcdoc");
pub const srclang: HtmlAttr = HtmlAttr::constant("srclang");
pub const srcset: HtmlAttr = HtmlAttr::constant("srcset");
pub const start: HtmlAttr = HtmlAttr::constant("start");
pub const step: HtmlAttr = HtmlAttr::constant("step");
pub const style: HtmlAttr = HtmlAttr::constant("style");
pub const tabindex: HtmlAttr = HtmlAttr::constant("tabindex");
pub const target: HtmlAttr = HtmlAttr::constant("target");
pub const title: HtmlAttr = HtmlAttr::constant("title");
pub const translate: HtmlAttr = HtmlAttr::constant("translate");
pub const r#type: HtmlAttr = HtmlAttr::constant("type");
pub const usemap: HtmlAttr = HtmlAttr::constant("usemap");
pub const value: HtmlAttr = HtmlAttr::constant("value");
pub const width: HtmlAttr = HtmlAttr::constant("width");
pub const wrap: HtmlAttr = HtmlAttr::constant("wrap");
pub const writingsuggestions: HtmlAttr = HtmlAttr::constant("writingsuggestions");
}

View File

@ -1,75 +0,0 @@
//! HTML output.
mod dom;
pub use self::dom::*;
use ecow::EcoString;
use crate::foundations::{elem, Content};
/// An HTML element that can contain Typst content.
///
/// Typst's HTML export automatically generates the appropriate tags for most
/// elements. However, sometimes, it is desirable to retain more control. For
/// example, when using Typst to generate your blog, you could use this function
/// to wrap each article in an `<article>` tag.
///
/// Typst is aware of what is valid HTML. A tag and its attributes must form
/// syntactically valid HTML. Some tags, like `meta` do not accept content.
/// Hence, you must not provide a body for them. We may add more checks in the
/// future, so be sure that you are generating valid HTML when using this
/// function.
///
/// Normally, Typst will generate `html`, `head`, and `body` tags for you. If
/// you instead create them with this function, Typst will omit its own tags.
///
/// ```typ
/// #html.elem("div", attrs: (style: "background: aqua"))[
/// A div with _Typst content_ inside!
/// ]
/// ```
#[elem(name = "elem")]
pub struct HtmlElem {
/// The element's tag.
#[required]
pub tag: HtmlTag,
/// The element's HTML attributes.
pub attrs: HtmlAttrs,
/// The contents of the HTML element.
///
/// The body can be arbitrary Typst content.
#[positional]
pub body: Option<Content>,
}
impl HtmlElem {
/// Add an attribute to the element.
pub fn with_attr(mut self, attr: HtmlAttr, value: impl Into<EcoString>) -> Self {
self.attrs
.as_option_mut()
.get_or_insert_with(Default::default)
.push(attr, value);
self
}
}
/// An element that lays out its content as an inline SVG.
///
/// Sometimes, converting Typst content to HTML is not desirable. This can be
/// the case for plots and other content that relies on positioning and styling
/// to convey its message.
///
/// This function allows you to use the Typst layout engine that would also be
/// used for PDF, SVG, and PNG export to render a part of your document exactly
/// how it would appear when exported in one of these formats. It embeds the
/// content as an inline SVG.
#[elem]
pub struct FrameElem {
/// The content that shall be laid out.
#[positional]
#[required]
pub body: Content,
}

View File

@ -10,9 +10,8 @@ use typst_utils::NonZeroExt;
use crate::diag::{bail, StrResult};
use crate::foundations::{Content, Label, Repr, Selector};
use crate::html::HtmlNode;
use crate::introspection::{Location, Tag};
use crate::layout::{Frame, FrameItem, Page, Point, Position, Transform};
use crate::layout::{Frame, FrameItem, Point, Position, Transform};
use crate::model::Numbering;
/// Can be queried for elements and their positions.
@ -47,18 +46,6 @@ pub struct Introspector {
type Pair = (Content, Position);
impl Introspector {
/// Creates an introspector for a page list.
#[typst_macros::time(name = "introspect pages")]
pub fn paged(pages: &[Page]) -> Self {
IntrospectorBuilder::new().build_paged(pages)
}
/// Creates an introspector for HTML.
#[typst_macros::time(name = "introspect html")]
pub fn html(output: &[HtmlNode]) -> Self {
IntrospectorBuilder::new().build_html(output)
}
/// Iterates over all locatable elements.
pub fn all(&self) -> impl Iterator<Item = &Content> + '_ {
self.elems.iter().map(|(c, _)| c)
@ -352,10 +339,10 @@ impl Clone for QueryCache {
/// Builds the introspector.
#[derive(Default)]
struct IntrospectorBuilder {
pages: usize,
page_numberings: Vec<Option<Numbering>>,
page_supplements: Vec<Content>,
pub struct IntrospectorBuilder {
pub pages: usize,
pub page_numberings: Vec<Option<Numbering>>,
pub page_supplements: Vec<Content>,
seen: HashSet<Location>,
insertions: MultiMap<Location, Vec<Pair>>,
keys: MultiMap<u128, Location>,
@ -365,41 +352,12 @@ struct IntrospectorBuilder {
impl IntrospectorBuilder {
/// Create an empty builder.
fn new() -> Self {
pub fn new() -> Self {
Self::default()
}
/// Build an introspector for a page list.
fn build_paged(mut self, pages: &[Page]) -> Introspector {
self.pages = pages.len();
self.page_numberings.reserve(pages.len());
self.page_supplements.reserve(pages.len());
// Discover all elements.
let mut elems = Vec::new();
for (i, page) in pages.iter().enumerate() {
self.page_numberings.push(page.numbering.clone());
self.page_supplements.push(page.supplement.clone());
self.discover_in_frame(
&mut elems,
&page.frame,
NonZeroUsize::new(1 + i).unwrap(),
Transform::identity(),
);
}
self.finalize(elems)
}
/// Build an introspector for an HTML document.
fn build_html(mut self, output: &[HtmlNode]) -> Introspector {
let mut elems = Vec::new();
self.discover_in_html(&mut elems, output);
self.finalize(elems)
}
/// Processes the tags in the frame.
fn discover_in_frame(
pub fn discover_in_frame(
&mut self,
sink: &mut Vec<Pair>,
frame: &Frame,
@ -433,29 +391,13 @@ impl IntrospectorBuilder {
}
}
/// Processes the tags in the HTML element.
fn discover_in_html(&mut self, sink: &mut Vec<Pair>, nodes: &[HtmlNode]) {
for node in nodes {
match node {
HtmlNode::Tag(tag) => self.discover_in_tag(
sink,
tag,
Position { page: NonZeroUsize::ONE, point: Point::zero() },
),
HtmlNode::Text(_, _) => {}
HtmlNode::Element(elem) => self.discover_in_html(sink, &elem.children),
HtmlNode::Frame(frame) => self.discover_in_frame(
sink,
&frame.inner,
NonZeroUsize::ONE,
Transform::identity(),
),
}
}
}
/// Handle a tag.
fn discover_in_tag(&mut self, sink: &mut Vec<Pair>, tag: &Tag, position: Position) {
pub fn discover_in_tag(
&mut self,
sink: &mut Vec<Pair>,
tag: &Tag,
position: Position,
) {
match tag {
Tag::Start(elem) => {
let loc = elem.location().unwrap();
@ -471,7 +413,7 @@ impl IntrospectorBuilder {
/// Build a complete introspector with all acceleration structures from a
/// list of top-level pairs.
fn finalize(mut self, root: Vec<Pair>) -> Introspector {
pub fn finalize(mut self, root: Vec<Pair>) -> Introspector {
self.locations.reserve(self.seen.len());
// Save all pairs and their descendants in the correct order.

View File

@ -15,7 +15,6 @@ extern crate self as typst_library;
pub mod diag;
pub mod engine;
pub mod foundations;
pub mod html;
pub mod introspection;
pub mod layout;
pub mod loading;

View File

@ -101,20 +101,25 @@ routines! {
pub enum RealizationKind<'a> {
/// This the root realization for layout. Requires a mutable reference
/// to document metadata that will be filled from `set document` rules.
LayoutDocument(&'a mut DocumentInfo),
LayoutDocument { info: &'a mut DocumentInfo },
/// A nested realization in a container (e.g. a `block`). Requires a mutable
/// reference to an enum that will be set to `FragmentKind::Inline` if the
/// fragment's content was fully inline.
LayoutFragment(&'a mut FragmentKind),
LayoutFragment { kind: &'a mut FragmentKind },
/// A nested realization in a paragraph (i.e. a `par`)
LayoutPar,
/// This the root realization for HTML. Requires a mutable reference
/// to document metadata that will be filled from `set document` rules.
HtmlDocument(&'a mut DocumentInfo),
/// This the root realization for HTML. Requires a mutable reference to
/// document metadata that will be filled from `set document` rules.
///
/// The `is_inline` function checks whether content consists of an inline
/// HTML element. It's used by the `PAR` grouping rules. This is slightly
/// hacky and might be replaced by a mechanism to supply the grouping rules
/// as a realization user.
HtmlDocument { info: &'a mut DocumentInfo, is_inline: fn(&Content) -> bool },
/// A nested realization in a container (e.g. a `block`). Requires a mutable
/// reference to an enum that will be set to `FragmentKind::Inline` if the
/// fragment's content was fully inline.
HtmlFragment(&'a mut FragmentKind),
HtmlFragment { kind: &'a mut FragmentKind, is_inline: fn(&Content) -> bool },
/// A realization within math.
Math,
}
@ -122,18 +127,20 @@ pub enum RealizationKind<'a> {
impl RealizationKind<'_> {
/// It this a realization for HTML export?
pub fn is_html(&self) -> bool {
matches!(self, Self::HtmlDocument(_) | Self::HtmlFragment(_))
matches!(self, Self::HtmlDocument { .. } | Self::HtmlFragment { .. })
}
/// It this a realization for a container?
pub fn is_fragment(&self) -> bool {
matches!(self, Self::LayoutFragment(_) | Self::HtmlFragment(_))
matches!(self, Self::LayoutFragment { .. } | Self::HtmlFragment { .. })
}
/// If this is a document-level realization, accesses the document info.
pub fn as_document_mut(&mut self) -> Option<&mut DocumentInfo> {
match self {
Self::LayoutDocument(info) | Self::HtmlDocument(info) => Some(*info),
Self::LayoutDocument { info } | Self::HtmlDocument { info, .. } => {
Some(*info)
}
_ => None,
}
}
@ -141,7 +148,9 @@ impl RealizationKind<'_> {
/// If this is a container-level realization, accesses the fragment kind.
pub fn as_fragment_mut(&mut self) -> Option<&mut FragmentKind> {
match self {
Self::LayoutFragment(kind) | Self::HtmlFragment(kind) => Some(*kind),
Self::LayoutFragment { kind } | Self::HtmlFragment { kind, .. } => {
Some(*kind)
}
_ => None,
}
}

View File

@ -18,7 +18,6 @@ use typst_library::foundations::{
RecipeIndex, Selector, SequenceElem, ShowSet, Style, StyleChain, StyledElem, Styles,
SymbolElem, Synthesize, TargetElem, Transformation,
};
use typst_library::html::{tag, FrameElem, HtmlElem};
use typst_library::introspection::{Locatable, SplitLocator, Tag, TagElem};
use typst_library::layout::{
AlignElem, BoxElem, HElem, InlineElem, PageElem, PagebreakElem, VElem,
@ -48,16 +47,16 @@ pub fn realize<'a>(
locator,
arenas,
rules: match kind {
RealizationKind::LayoutDocument(_) => LAYOUT_RULES,
RealizationKind::LayoutFragment(_) => LAYOUT_RULES,
RealizationKind::LayoutDocument { .. } => LAYOUT_RULES,
RealizationKind::LayoutFragment { .. } => LAYOUT_RULES,
RealizationKind::LayoutPar => LAYOUT_PAR_RULES,
RealizationKind::HtmlDocument(_) => HTML_DOCUMENT_RULES,
RealizationKind::HtmlFragment(_) => HTML_FRAGMENT_RULES,
RealizationKind::HtmlDocument { .. } => HTML_DOCUMENT_RULES,
RealizationKind::HtmlFragment { .. } => HTML_FRAGMENT_RULES,
RealizationKind::Math => MATH_RULES,
},
sink: vec![],
groupings: ArrayVec::new(),
outside: matches!(kind, RealizationKind::LayoutDocument(_)),
outside: matches!(kind, RealizationKind::LayoutDocument { .. }),
may_attach: false,
saw_parbreak: false,
kind,
@ -113,7 +112,7 @@ struct GroupingRule {
/// be visible to `finish`.
tags: bool,
/// Defines which kinds of elements start and make up this kind of grouping.
trigger: fn(&Content, &RealizationKind) -> bool,
trigger: fn(&Content, &State) -> bool,
/// Defines elements that may appear in the interior of the grouping, but
/// not at the edges.
inner: fn(&Content) -> bool,
@ -334,13 +333,6 @@ fn visit_kind_rules<'a>(
}
}
if !s.kind.is_html() {
if let Some(elem) = content.to_packed::<FrameElem>() {
visit(s, &elem.body, styles)?;
return Ok(true);
}
}
Ok(false)
}
@ -601,7 +593,7 @@ fn visit_styled<'a>(
);
}
} else if elem == PageElem::ELEM {
if !matches!(s.kind, RealizationKind::LayoutDocument(_)) {
if !matches!(s.kind, RealizationKind::LayoutDocument { .. }) {
bail!(
style.span(),
"page configuration is not allowed inside of containers"
@ -659,7 +651,7 @@ fn visit_grouping_rules<'a>(
content: &'a Content,
styles: StyleChain<'a>,
) -> SourceResult<bool> {
let matching = s.rules.iter().find(|&rule| (rule.trigger)(content, &s.kind));
let matching = s.rules.iter().find(|&rule| (rule.trigger)(content, s));
// Try to continue or finish an existing grouping.
let mut i = 0;
@ -671,7 +663,7 @@ fn visit_grouping_rules<'a>(
// If the element can be added to the active grouping, do it.
if !active.interrupted
&& ((active.rule.trigger)(content, &s.kind) || (active.rule.inner)(content))
&& ((active.rule.trigger)(content, s) || (active.rule.inner)(content))
{
s.sink.push((content, styles));
return Ok(true);
@ -806,7 +798,7 @@ fn finish_innermost_grouping(s: &mut State) -> SourceResult<()> {
let Grouping { start, rule, .. } = s.groupings.pop().unwrap();
// Trim trailing non-trigger elements.
let trimmed = s.sink[start..].trim_end_matches(|(c, _)| !(rule.trigger)(c, &s.kind));
let trimmed = s.sink[start..].trim_end_matches(|(c, _)| !(rule.trigger)(c, s));
let end = start + trimmed.len();
let tail = s.store_slice(&s.sink[end..]);
s.sink.truncate(end);
@ -885,7 +877,7 @@ static TEXTUAL: GroupingRule = GroupingRule {
static PAR: GroupingRule = GroupingRule {
priority: 1,
tags: true,
trigger: |content, kind| {
trigger: |content, state| {
let elem = content.elem();
elem == TextElem::ELEM
|| elem == HElem::ELEM
@ -893,10 +885,11 @@ static PAR: GroupingRule = GroupingRule {
|| elem == SmartQuoteElem::ELEM
|| elem == InlineElem::ELEM
|| elem == BoxElem::ELEM
|| (kind.is_html()
&& content
.to_packed::<HtmlElem>()
.is_some_and(|elem| tag::is_inline_by_default(elem.tag)))
|| match state.kind {
RealizationKind::HtmlDocument { is_inline, .. }
| RealizationKind::HtmlFragment { is_inline, .. } => is_inline(content),
_ => false,
}
},
inner: |content| content.elem() == SpaceElem::ELEM,
interrupt: |elem| elem == ParElem::ELEM || elem == AlignElem::ELEM,

View File

@ -43,12 +43,12 @@ use std::sync::LazyLock;
use comemo::{Track, Tracked, Validate};
use ecow::{eco_format, eco_vec, EcoString, EcoVec};
use typst_html::HtmlDocument;
use typst_library::diag::{
bail, warning, FileError, SourceDiagnostic, SourceResult, Warned,
};
use typst_library::engine::{Engine, Route, Sink, Traced};
use typst_library::foundations::{NativeRuleMap, StyleChain, Styles, Value};
use typst_library::html::HtmlDocument;
use typst_library::introspection::Introspector;
use typst_library::layout::PagedDocument;
use typst_library::routines::Routines;

View File

@ -84,8 +84,9 @@ meaning in Typst. We can use `=`, `-`, `+`, and `_` to create headings, lists
and emphasized text, respectively. However, having a special symbol for
everything we want to insert into our document would soon become cryptic and
unwieldy. For this reason, Typst reserves markup symbols only for the most
common things. Everything else is inserted with _functions._ For our image to
show up on the page, we use Typst's [`image`] function.
common things. Everything else is inserted with _functions._ For
[our image](https://github.com/typst/typst-dev-assets/blob/main/files/images/glacier.jpg)
to show up on the page, we use Typst's [`image`] function.
```example
#image("glacier.jpg")
@ -125,19 +126,38 @@ mode. This means, you now have to remove the hash before the image function call
The hash is only needed directly in markup (to disambiguate text from function
calls).
The caption consists of arbitrary markup. To give markup to a function, we
enclose it in square brackets. This construct is called a _content block._
The caption consists of arbitrary markup, and can also be a string. To give
markup to a function, we enclose it in square brackets. This construct is called
a _content block._
```example
#figure(
image("glacier.jpg", width: 70%),
caption: [
caption: box[
_Glaciers_ form an important part
of the earth's climate system.
],
)
```
**Be careful** about putting the square brackets by themselves on separate
lines. This will introduce leading and trailing space around inline text inside
the brackets, that is hard to notice. Below are several caption examples: one
with extra undesired space, and 3 correct ones.
```example
#show rect: none
#figure(rect(), caption: [
Caption text
])
#figure(rect(), caption: box[
Caption text
])
#figure(rect(), caption: [Caption
text]) // Many spaces in markup counts as one.
#figure(rect(), caption: "Caption text") // Spaces in strings are displayed verbatim.
```
You continue to write your report and now want to reference the figure. To do
that, first attach a label to figure. A label uniquely identifies an element in
your document. Add one after the figure by enclosing some name in angle

View File

@ -110,8 +110,8 @@ font. For the purposes of the example, we'll also set another page size.
margin: (x: 1.8cm, y: 1.5cm),
)
#set text(
size: 10pt,
font: "New Computer Modern",
size: 10pt
)
#set par(
justify: true,
@ -235,19 +235,27 @@ Instead, you could maybe
[define a custom function]($function/#defining-functions) that always yields the
logo with its image. However, there is an even easier way:
With show rules, you can redefine how Typst displays certain elements. You
With show rules, you can redefine how Typst displays certain elements. You can
specify which elements Typst should show differently and how they should look.
Show rules can be applied to instances of text, many functions, and even the
whole document.
```example
#show "ArtosFlow": name => box[
#box(image(
// #show "ArtosFlow": name => {
// let logo = box(image(
// "logo.svg",
// height: 0.7em,
// ))
// [#logo #name]
// }
#show "ArtosFlow": name => {
box(image(
"logo.svg",
height: 0.7em,
))
#name
]
" "
name
}
This report is embedded in the
ArtosFlow project. ArtosFlow is a
@ -256,18 +264,18 @@ project of the Artos Institute.
There is a lot of new syntax in this example: We write the `{show}` keyword,
followed by a string of text we want to show differently and a colon. Then, we
write a function that takes the content that shall be shown as an argument.
Here, we called that argument `name`. We can now use the `name` variable in the
function's body to print the ArtosFlow name. Our show rule adds the logo image
in front of the name and puts the result into a box to prevent linebreaks from
occurring between logo and name. The image is also put inside of a box, so that
it does not appear in its own paragraph.
write a function that takes the content as an argument that shall be shown.
Here, we called that argument `name`. We can now use the `name` variable in
the function's body to display the ArtosFlow name. Our show rule adds the logo
image to the left of the name and inserts a single space between the two.
The image is put inside of a `box`, so that it does not appear in its own
paragraph, because `image` is a block-level element.
The calls to the first box function and the image function did not require a
leading `#` because they were not embedded directly in markup. When Typst
expects code instead of markup, the leading `#` is not needed to access
functions, keywords, and variables. This can be observed in parameter lists,
function definitions, and [code blocks]($scripting).
<!-- The calls to the first box function and the image function did not require a -->
<!-- leading `#` because they were not embedded directly in markup. When Typst -->
<!-- expects code instead of markup, the leading `#` is not needed to access -->
<!-- functions, keywords, and variables. This can be observed in parameter lists, -->
<!-- function definitions, and [code blocks]($scripting). -->
## Review
You now know how to apply basic formatting to your Typst documents. You learned

View File

@ -6,7 +6,7 @@ description: Typst's tutorial.
In the previous two chapters of this tutorial, you have learned how to write a
document in Typst and how to change its formatting. The report you wrote
throughout the last two chapters got a straight A and your supervisor wants to
base a conference paper on it! The report will of course have to comply with the
base a conference paper on it! The paper will of course have to comply with the
conference's style guide. Let's see how we can achieve that.
Before we start, let's create a team, invite your supervisor and add them to the
@ -30,12 +30,12 @@ to find other users and try teams with them!
The layout guidelines are available on the conference website. Let's take a look
at them:
- The font should be an 11pt serif font
- The title should be in 17pt and bold
- The font should be an 11 pt serif font
- The title should be in 17 pt and bold
- The paper contains a single-column abstract and two-column main text
- The abstract should be centered
- The main text should be justified
- First level section headings should be 13pt, centered, and rendered in small
- First level section headings should be 13 pt, centered, and rendered in small
capitals
- Second level headings are run-ins, italicized and have the same size as the
body text
@ -51,7 +51,6 @@ Let's start by writing some set rules for the document.
```example
#set page(
>>> margin: auto,
paper: "us-letter",
header: align(right)[
A fluid dynamic model for
@ -61,8 +60,8 @@ Let's start by writing some set rules for the document.
)
#set par(justify: true)
#set text(
font: "Libertinus Serif",
size: 11pt,
font: "Libertinus Serif",
)
#lorem(600)
@ -72,12 +71,12 @@ You are already familiar with most of what is going on here. We set the text
size to `{11pt}` and the font to Libertinus Serif. We also enable paragraph
justification and set the page size to US letter.
The `header` argument is new: With it, we can provide content to fill the top
The `header` field is new: with it, we can provide content to fill the top
margin of every page. In the header, we specify our paper's title as requested
by the conference style guide. We use the `align` function to align the text to
the right.
Last but not least is the `numbering` argument. Here, we can provide a
Last but not least is the `numbering` field. Here, we can provide a
[numbering pattern]($numbering) that defines how to number the pages. By
setting it to `{"1"}`, Typst only displays the bare page number. Setting it to
`{"(1/1)"}` would have displayed the current page and total number of pages
@ -89,35 +88,65 @@ Now, let's add a title and an abstract. We'll start with the title. We center
align it and increase its font weight by enclosing it in `[*stars*]`.
```example
>>> #set page(width: 300pt, margin: 30pt)
>>> #set text(font: "Libertinus Serif", 11pt)
#align(center, text(17pt)[
>>> #set page(
>>> // paper: "us-letter",
>>> width: 300pt,
>>> margin: 30pt,
>>> header: align(right)[
>>> A fluid dynamic model for
>>> glacier flow
>>> ],
>>> // numbering: "1",
>>> )
>>> #set par(justify: true)
>>> #set text(11pt, font: "Libertinus Serif")
<<< ...
#align(center, block(text(
17pt,
hyphenate: false
)[
*A fluid dynamic model
for glacier flow*
])
]))
```
This looks right. We used the `text` function to override the previous text
set rule locally, increasing the size to 17pt for the function's argument. Let's
also add the author list: Since we are writing this paper together with our
supervisor, we'll add our own and their name.
set rule locally, increasing the size to 17 pt.
Add explanation about block+hyphenate, which is pretty convoluted.
Let's also add the author list: Since we are writing this paper together with
our supervisor, we'll add our own and their name.
```example
>>> #set page(width: 300pt, margin: 30pt)
>>> #set text(font: "Libertinus Serif", 11pt)
>>> #set page(
>>> // paper: "us-letter",
>>> width: 300pt,
>>> margin: 30pt,
>>> header: align(right)[
>>> A fluid dynamic model for
>>> glacier flow
>>> ],
>>> // numbering: "1",
>>> )
>>> #set par(justify: true)
>>> #set text(11pt, font: "Libertinus Serif")
>>>
>>> #align(center, text(17pt)[
>>> #align(center, block(text(
>>> 17pt,
>>> hyphenate: false
>>> )[
>>> *A fluid dynamic model
>>> for glacier flow*
>>> ])
>>> ]))
<<< ...
#grid(
columns: (1fr, 1fr),
align(center)[
align: center,
[
Therese Tungsten \
Artos Institute \
#link("mailto:tung@artos.edu")
],
align(center)[
[
Dr. John Doe \
Artos Institute \
#link("mailto:doe@artos.edu")
@ -127,45 +156,49 @@ supervisor, we'll add our own and their name.
The two author blocks are laid out next to each other. We use the [`grid`]
function to create this layout. With a grid, we can control exactly how large
each column is and which content goes into which cell. The `columns` argument
takes an array of [relative lengths]($relative) or [fractions]($fraction). In
this case, we passed it two equal fractional sizes, telling it to split the
available space into two equal columns. We then passed two content arguments to
the grid function. The first with our own details, and the second with our
supervisors'. We again use the `align` function to center the content within the
column. The grid takes an arbitrary number of content arguments specifying the
cells. Rows are added automatically, but they can also be manually sized with
the `rows` argument.
each column is and which content goes into which cell. The `columns` field
takes the number of columns, or an array of [relative lengths]($relative) or
[fractions]($fraction). In this case, we passed it two equal fractional sizes,
telling it to split the available space into two equal columns. We then passed
two content arguments to the grid function --- the first with our own details,
and the second with our supervisor's. With grid, we can avoid using `align` on
each cell content to center them, and instead use the `align` field to do this
for all cells automatically. The grid takes an arbitrary number of content
arguments specifying the cells. Rows are added automatically, but they can also
be manually sized with the `rows` field.
Now, let's add the abstract. Remember that the conference wants the abstract to
be set ragged and centered.
```example:0,0,612,317.5
>>> #set text(font: "Libertinus Serif", 11pt)
>>> #set par(justify: true)
>>> #set page(
>>> "us-letter",
>>> margin: auto,
>>> header: align(right + horizon)[
>>> paper: "us-letter",
>>> header: align(right)[
>>> A fluid dynamic model for
>>> glacier flow
>>> ],
>>> numbering: "1",
>>> )
>>> #set par(justify: true)
>>> #set text(11pt, font: "Libertinus Serif")
>>>
>>> #align(center, text(17pt)[
>>> #align(center, block(text(
>>> 17pt,
>>> hyphenate: false
>>> )[
>>> *A fluid dynamic model
>>> for glacier flow*
>>> ])
>>> ]))
>>>
>>> #grid(
>>> columns: (1fr, 1fr),
>>> align(center)[
>>> align: center,
>>> [
>>> Therese Tungsten \
>>> Artos Institute \
>>> #link("mailto:tung@artos.edu")
>>> ],
>>> align(center)[
>>> [
>>> Dr. John Doe \
>>> Artos Institute \
>>> #link("mailto:doe@artos.edu")
@ -198,35 +231,33 @@ keyword:
for glacier flow
]
<<< ...
>>> #set text(font: "Libertinus Serif", 11pt)
>>> #set par(justify: true)
#set page(
>>> "us-letter",
>>> margin: auto,
header: align(
right + horizon,
title
),
>>> paper: "us-letter",
<<< ...
header: align(right, title),
<<< ...
>>> numbering: "1",
)
#align(center, text(17pt)[
*#title*
])
>>> #set par(justify: true)
>>> #set text(11pt, font: "Libertinus Serif")
<<< ...
#align(center, block(text(
17pt,
hyphenate: false,
strong(title),
)))
<<< ...
>>> #grid(
>>> columns: (1fr, 1fr),
>>> align(center)[
>>> align: center,
>>> [
>>> Therese Tungsten \
>>> Artos Institute \
>>> #link("mailto:tung@artos.edu")
>>> ],
>>> align(center)[
>>> [
>>> Dr. John Doe \
>>> Artos Institute \
>>> #link("mailto:doe@artos.edu")
@ -247,55 +278,48 @@ and also within markup (prefixed by `#`, like functions). This way, if we decide
on another title, we can easily change it in one place.
## Adding columns and headings { #columns-and-headings }
The paper above unfortunately looks like a wall of lead. To fix that, let's add
The paper above unfortunately looks like a wall of lead(?). To fix that, let's add
some headings and switch our paper to a two-column layout. Fortunately, that's
easy to do: We just need to amend our `page` set rule with the `columns`
argument.
easy to do: we just need to amend our `page` set rule with the `columns` field.
By adding `{columns: 2}` to the argument list, we have wrapped the whole
document in two columns. However, that would also affect the title and authors
overview. To keep them spanning the whole page, we can wrap them in a function
call to [`{place}`]($place). Place expects an alignment and the content it
should place as positional arguments. Using the named `{scope}` argument, we can
call to [`{place}`]($place). The `place` expects an alignment and the content it
should place as positional arguments. Using the named `scope` field, we can
decide if the items should be placed relative to the current column or its
parent (the page). There is one more thing to configure: If no other arguments
are provided, `{place}` takes its content out of the flow of the document and
are provided, `place` takes its content out of the flow of the document and
positions it over the other content without affecting the layout of other
content in its container:
```example
#place(
top + center,
rect(fill: black),
)
#place(top + center, rect())
#lorem(30)
```
If we hadn't used `{place}` here, the square would be in its own line, but here
it overlaps the few lines of text following it. Likewise, that text acts like as
if there was no square. To change this behavior, we can pass the argument
`{float: true}` to ensure that the space taken up by the placed item at the top
or bottom of the page is not occupied by any other content.
If we hadn't used `place` here, the rectangle would be in its own line, but
here it overlaps the few lines of text following it. Likewise, that text acts
like as if there was no rectangle. To change this behavior, we can pass the
argument `{float: true}` to ensure that the space taken up by the placed item
at the top or bottom of the page is not occupied by any other content.
```example:single
>>> #let title = [
>>> A fluid dynamic model
>>> for glacier flow
>>> ]
>>>
>>> #set text(font: "Libertinus Serif", 11pt)
>>> #set par(justify: true)
>>>
<<< ...
#set page(
>>> margin: auto,
paper: "us-letter",
header: align(
right + horizon,
title
),
header: align(right, title),
numbering: "1",
columns: 2,
)
>>> #set par(justify: true)
>>> #set text(11pt, font: "Libertinus Serif")
<<< ...
#place(
top + center,
@ -303,11 +327,7 @@ or bottom of the page is not occupied by any other content.
scope: "parent",
clearance: 2em,
)[
>>> #text(
>>> 17pt,
>>> weight: "bold",
>>> title,
>>> )
>>> #block(text(17pt, hyphenate: false, strong(title)))
>>>
>>> #grid(
>>> columns: (1fr, 1fr),
@ -324,10 +344,9 @@ or bottom of the page is not occupied by any other content.
>>> )
<<< ...
#par(justify: false)[
*Abstract* \
#lorem(80)
]
#set par(justify: false) // Put it above to remove hyphenate?
*Abstract* \
#lorem(80)
]
= Introduction
@ -337,14 +356,14 @@ or bottom of the page is not occupied by any other content.
#lorem(200)
```
In this example, we also used the `clearance` argument of the `{place}` function
to provide the space between it and the body instead of using the [`{v}`]($v)
In this example, we also used the `clearance` argument of the `place` function
to provide the space between it and the body instead of using the [`v`]
function. We can also remove the explicit `{align(center, ..)}` calls around the
various parts since they inherit the center alignment from the placement.
Now there is only one thing left to do: Style our headings. We need to make them
centered and use small capitals. Because the `heading` function does not offer
a way to set any of that, we need to write our own heading show rule.
Now there is only one thing left to do: style our headings. We need to make them
centered and use small capitals. For centering we can use a show-set rule, but
to use small capitals we need to write our own heading show rule.
```example:50,250,265,270
>>> #let title = [
@ -352,37 +371,27 @@ a way to set any of that, we need to write our own heading show rule.
>>> for glacier flow
>>> ]
>>>
>>> #set text(font: "Libertinus Serif", 11pt)
>>> #set par(justify: true)
>>> #set page(
>>> "us-letter",
>>> margin: auto,
>>> header: align(
>>> right + horizon,
>>> title
>>> ),
>>> paper: "us-letter",
>>> header: align(right, title),
>>> numbering: "1",
>>> columns: 2,
>>> )
#show heading: it => [
#set align(center)
#set text(13pt, weight: "regular")
#block(smallcaps(it.body))
]
>>> #set par(justify: true)
>>> #set text(11pt, font: "Libertinus Serif")
<<< ...
#show heading: set align(center)
#show heading: set text(13pt, weight: "regular")
#show heading: it => block(smallcaps(it.body))
<<< ...
>>>
>>> #place(
>>> top + center,
>>> float: true,
>>> scope: "parent",
>>> clearance: 2em,
>>> )[
>>> #text(
>>> 17pt,
>>> weight: "bold",
>>> title,
>>> )
>>> #block(text(17pt, hyphenate: false, strong(title)))
>>>
>>> #grid(
>>> columns: (1fr, 1fr),
@ -398,10 +407,9 @@ a way to set any of that, we need to write our own heading show rule.
>>> ]
>>> )
>>>
>>> #par(justify: false)[
>>> *Abstract* \
>>> #lorem(80)
>>> ]
>>> #set par(justify: false)
>>> *Abstract* \
>>> #lorem(80)
>>> ]
>>>
>>> = Introduction
@ -411,19 +419,26 @@ a way to set any of that, we need to write our own heading show rule.
>>> #lorem(45)
```
This looks great! We used a show rule that applies to all headings. We give it a
function that gets passed the heading as a parameter. That parameter can be used
as content but it also has some fields like `title`, `numbers`, and `level` from
which we can compose a custom look. Here, we are center-aligning, setting the
font weight to `{"regular"}` because headings are bold by default, and use the
[`smallcaps`] function to render the heading's title in small capitals.
This looks great! We used a few rules that apply to all headings. First, we
made headings centered, then we set font size to 13 pt and removed default
heading boldness by setting `weight` to `{"regular"}`. Lastly, there is a show
rule with a closure, i.e., a callback function. We gave it a function that
passes the heading as argument. That argument can be used as content but it
also has some fields like `title`, `numbers`, and `level`, from which we can
compose a custom look. Here, we use the [`smallcaps`] function to render the
heading's title in small capitals. Note that heading itself is wrapped in a
block by default, as it's a block-level element, and by using `{it.body}` we
are destroying the default structure of the heading. This means we strip away
not only the `block` "shell", but also any potential numbering and other
heading features. To restore it's semantic structure, we wrap
`{smallcaps(it.body)}` in a `block`. This way it will behave like usual.
The only remaining problem is that all headings look the same now. The
"Motivation" and "Problem Statement" subsections ought to be italic run in
headers, but right now, they look indistinguishable from the section headings. We
can fix that by using a `where` selector on our set rule: This is a
[method]($scripting/#methods) we can call on headings (and other
elements) that allows us to filter them by their level. We can use it to
The only remaining problem is that all headings now look the same. The
"Motivation" and "Problem Statement" subsections ought to be italic run-in
headings, but right now, they look indistinguishable from the section headings.
We can fix that by using a `where` selector on our show rules: this is a
[method]($scripting/#methods) we can call on headings (and other elements) that
allows us to filter them by their level (and other fields). We can use it to
differentiate between section and subsection headings:
```example:50,250,265,245
@ -432,66 +447,49 @@ differentiate between section and subsection headings:
>>> for glacier flow
>>> ]
>>>
>>> #set text(font: "Libertinus Serif", 11pt)
>>> #set par(justify: true)
>>> #set page(
>>> "us-letter",
>>> margin: auto,
>>> header: align(
>>> right + horizon,
>>> title
>>> ),
>>> paper: "us-letter",
>>> header: align(right, title),
>>> numbering: "1",
>>> columns: 2,
>>> )
>>>
#show heading.where(
level: 1
): it => block(width: 100%)[
#set align(center)
#set text(13pt, weight: "regular")
#smallcaps(it.body)
]
>>> #set par(justify: true)
>>> #set text(11pt, font: "Libertinus Serif")
<<< ...
#show heading.where(
level: 2
): it => text(
size: 11pt,
weight: "regular",
style: "italic",
it.body + [.],
)
>>>
#show heading.where(level: 1): set align(center)
#show heading.where(level: 1): set text(13pt, weight: "regular")
#show heading.where(level: 1): it => block(smallcaps(it.body))
#show heading.where(level: 2): set text(11pt, weight: "regular", style: "italic")
#show heading.where(level: 2): it => [#it.body.]
<<< ...
>>> #place(
>>> top + center,
>>> float: true,
>>> scope: "parent",
>>> clearance: 2em,
>>> )[
>>> #text(
>>> 17pt,
>>> weight: "bold",
>>> title,
>>> #block(text(17pt, hyphenate: false, strong(title)))
>>>
>>> #grid(
>>> columns: (1fr, 1fr),
>>> [
>>> Therese Tungsten \
>>> Artos Institute \
>>> #link("mailto:tung@artos.edu")
>>> ],
>>> [
>>> Dr. John Doe \
>>> Artos Institute \
>>> #link("mailto:doe@artos.edu")
>>> ]
>>> )
>>>
>>> #grid(
>>> columns: (1fr, 1fr),
>>> [
>>> Therese Tungsten \
>>> Artos Institute \
>>> #link("mailto:tung@artos.edu")
>>> ],
>>> [
>>> Dr. John Doe \
>>> Artos Institute \
>>> #link("mailto:doe@artos.edu")
>>> ]
>>> )
>>>
>>> #par(justify: false)[
>>> *Abstract* \
>>> #lorem(80)
>>> ]
>>> #set par(justify: false)
>>> *Abstract* \
>>> #lorem(80)
>>> ]
>>>
>>> = Introduction
@ -501,23 +499,26 @@ differentiate between section and subsection headings:
>>> #lorem(45)
```
This looks great! We wrote two show rules that each selectively apply to the
first and second level headings. We used a `where` selector to filter the
headings by their level. We then rendered the subsection headings as run-ins. We
also automatically add a period to the end of the subsection headings.
Excellent! We wrote several rules that selectively apply to the first and second
level headings. We used a `where` selector to filter the headings by their
level. We then rendered the subsection headings as run-ins. We also
automatically added a period to the end of the subsection headings. This time
we did not wrap result in a `block`, because we need the heading to be inline
with the following text.
Let's review the conference's style guide:
- The font should be an 11pt serif font ✓
- The title should be in 17pt and bold ✓
- The font should be an 11 pt serif font ✓
- The title should be in 17 pt and bold ✓
- The paper contains a single-column abstract and two-column main text ✓
- The abstract should be centered ✓
- The main text should be justified ✓
- First level section headings should be centered, rendered in small caps and in
13pt ✓
13 pt ✓
- Second level headings are run-ins, italicized and have the same size as the
body text ✓
- Finally, the pages should be US letter sized, numbered in the center and the
top right corner of each page should contain the title of the paper ✓
- Finally, the pages should be US letter sized, numbered in the center of the
footer and the top right corner of each page should contain the title of the
paper ✓
We are now in compliance with all of these styles and can submit the paper to
the conference! The finished paper looks like this:
@ -528,6 +529,62 @@ the conference! The finished paper looks like this:
style="box-shadow: 0 4px 12px rgb(89 85 101 / 20%); width: 500px; max-width: 100%; display: block; margin: 24px auto;"
>
Here is a full listing of the finished paper:
```example
#let title = [A fluid dynamic model for glacier flow]
#set page(
paper: "us-letter",
header: align(right, title),
numbering: "1",
columns: 2,
)
#set par(justify: true)
#set text(11pt, font: "Libertinus Serif")
#show heading.where(level: 1): set align(center)
#show heading.where(level: 1): set text(13pt, weight: "regular")
#show heading.where(level: 1): it => block(smallcaps(it.body))
#show heading.where(level: 2): set text(11pt, weight: "regular", style: "italic")
#show heading.where(level: 2): it => [#it.body.]
#place(top + center, float: true, scope: "parent", clearance: 2em)[
#block(text(17pt, hyphenate: false, strong(title)))
#grid(
columns: (1fr, 1fr),
[
Therese Tungsten \
Artos Institute \
#link("mailto:tung@artos.edu")
],
[
Dr. John Doe \
Artos Institute \
#link("mailto:doe@artos.edu")
]
)
#set par(justify: false)
*Abstract* \
#lorem(80)
]
= Introduction
#lorem(90)
== Motivation
#lorem(140)
== Problem Statement
#lorem(50)
= Related Work
#lorem(200)
```
## Review
You have now learned how to create headers and footers, how to use functions and
scopes to locally override styles, how to create more complex layouts with the

View File

@ -25,7 +25,10 @@ You are #amazed[beautiful]!
This function takes a single argument, `term`, and returns a content block with
the `term` surrounded by sparkles. We also put the whole thing in a box so that
the term we are amazed by cannot be separated from its sparkles by a line break.
the term we are amazed by cannot be separated from its sparkles by a line
break. Alternatively, you can use a
[shorthand](https://typst.app/docs/reference/symbols/#shorthands)
for a no-break space and write `{[✨~#term~✨]}`.
Many functions that come with Typst have optional named parameters. Our
functions can also have them. Let's add a parameter to our function that lets us
@ -34,7 +37,7 @@ parameter isn't given.
```example
#let amazed(term, color: blue) = {
text(color, box[✨ #term ✨])
text(color)[✨~#term~✨]
}
You are #amazed[beautiful]!
@ -43,7 +46,7 @@ I am #amazed(color: purple)[amazed]!
Templates now work by wrapping our whole document in a custom function like
`amazed`. But wrapping a whole document in a giant function call would be
cumbersome! Instead, we can use an "everything" show rule to achieve the same
cumbersome! Instead, we can use an "global" show rule to achieve the same
with cleaner code. To write such a show rule, put a colon directly after the
show keyword and then provide a function. This function is given the rest of the
document as a parameter. The function can then do anything with this content.
@ -52,7 +55,7 @@ just pass it by name to the show rule. Let's try it:
```example
>>> #let amazed(term, color: blue) = {
>>> text(color, box[✨ #term ✨])
>>> text(color)[✨~#term~✨]
>>> }
#show: amazed
I choose to focus on the good
@ -68,69 +71,56 @@ powerful.
## Embedding set and show rules { #set-and-show-rules }
To apply some set and show rules to our template, we can use `set` and `show`
within a content block in our function and then insert the document into
that content block.
within a code block in our function and then insert the document into
that code block.
```example
#let template(doc) = [
#set text(font: "Inria Serif")
#show "something cool": [Typst]
#doc
]
#let template(doc) = {
set text(font: "Inria Serif")
show "something cool": [Typst]
doc
}
#show: template
I am learning something cool today.
It's going great so far!
```
Just like we already discovered in the previous chapter, set rules will apply to
everything within their content block. Since the everything show rule passes our
whole document to the `template` function, the text set rule and string show
rule in our template will apply to the whole document. Let's use this knowledge
to create a template that reproduces the body style of the paper we wrote in the
previous chapter.
Just like we already discovered in the previous chapter, set rules will apply
to everything within their scope. Since the global show rule passes our whole
document to the `template` function, the text set rule and string show rule in
our template will apply to the whole document.
We used a curly-braced code block instead of a content block. This way, we
don't need to prefix all set rules and function calls with a `#`. This also
removes the implicit spaces that are naturally introduced in the markup mode.
In exchange, we cannot write markup directly in the code block anymore.
Let's use this knowledge to create a template that reproduces the body style of
the paper we wrote in the previous chapter.
```example
#let conf(title, doc) = {
set page(
paper: "us-letter",
>>> margin: auto,
header: align(
right + horizon,
title
),
header: align(right, title),
columns: 2,
<<< ...
)
set par(justify: true)
set text(
11pt,
font: "Libertinus Serif",
size: 11pt,
)
// Heading show rules.
<<< ...
>>> show heading.where(
>>> level: 1
>>> ): it => block(
>>> align(center,
>>> text(
>>> 13pt,
>>> weight: "regular",
>>> smallcaps(it.body),
>>> )
>>> ),
>>> )
>>> show heading.where(
>>> level: 2
>>> ): it => box(
>>> text(
>>> 11pt,
>>> weight: "regular",
>>> style: "italic",
>>> it.body + [.],
>>> )
>>> )
>>> show heading.where(level: 1): set align(center)
>>> show heading.where(level: 1): set text(13pt, weight: "regular")
>>> show heading.where(level: 1): it => block(smallcaps(it.body))
>>>
>>> show heading.where(level: 2): set text(11pt, weight: "regular", style: "italic")
>>> show heading.where(level: 2): it => [#it.body.]
doc
}
@ -154,24 +144,17 @@ previous chapter.
>>> #lorem(200)
```
We copy-pasted most of that code from the previous chapter. The two differences
are this:
We copied most of that code from the previous chapter. However, now we wrapped
everything in the function `conf` using a global show rule. The function applies
a few set and show rules and echoes the content it has been passed at the end.
1. We wrapped everything in the function `conf` using an everything show rule.
The function applies a few set and show rules and echoes the content it has
been passed at the end.
2. Moreover, we used a curly-braced code block instead of a content block. This
way, we don't need to prefix all set rules and function calls with a `#`. In
exchange, we cannot write markup directly in the code block anymore.
Also note where the title comes from: We previously had it inside of a variable.
Also note where the title comes from: we previously had it inside of a variable.
Now, we are receiving it as the first parameter of the template function. To do
so, we passed a closure (that's a function without a name that is used right
away) to the everything show rule. We did that because the `conf` function
expects two positional arguments, the title and the body, but the show rule will
only pass the body. Therefore, we add a new function definition that allows us
to set a paper title and use the single parameter from the show rule.
away) to the global show rule. We did that because the `conf` function expects
two positional arguments: the title and the body, but the show rule will only
pass the body. Therefore, we add a new function definition that allows us to set
a paper title and use the single parameter from the show rule.
## Templates with named arguments { #named-arguments }
Our paper in the previous chapter had a title and an author list. Let's add
@ -230,6 +213,9 @@ multiple arguments for the grid. We can do that by using the
[`spread` operator]($arguments). It takes an array and applies each of its items
as a separate argument to the function.
Let's also include some PDF metadata. We can achieve this by using
the [`document`] function and specifying fields such as `title` and `author`.
The resulting template function looks like this:
```typ
@ -239,31 +225,33 @@ The resulting template function looks like this:
abstract: [],
doc,
) = {
set document(title: title, author: authors.map(author => author.name))
// Set and show rules from before.
>>> #set page(columns: 2)
<<< ...
set align(center)
text(17pt, title)
{
set align(center)
set par(justify: false)
let count = authors.len()
let ncols = calc.min(count, 3)
grid(
columns: (1fr,) * ncols,
row-gutter: 24pt,
..authors.map(author => [
#author.name \
#author.affiliation \
#link("mailto:" + author.email)
]),
)
block(text(17pt, strong(title)))
par(justify: false)[
*Abstract* \
#abstract
]
let count = authors.len()
let ncols = calc.min(count, 3)
grid(
columns: (1fr,) * ncols,
row-gutter: 24pt,
..authors.map(author => [
#author.name \
#author.affiliation \
#link("mailto:" + author.email)
]),
)
strong[Abstract]
linebreak()
abstract
}
set align(left)
doc
}
```
@ -291,72 +279,45 @@ call.
>>> abstract: [],
>>> doc,
>>> ) = {
>>> set text(font: "Libertinus Serif", 11pt)
>>> set par(justify: true)
>>> set page(
>>> "us-letter",
>>> margin: auto,
>>> header: align(
>>> right + horizon,
>>> title
>>> ),
>>> numbering: "1",
>>> columns: 2,
>>> )
>>> set document(title: title, author: authors.map(author => author.name))
>>> set page(
>>> "us-letter",
>>> header: align(right, title),
>>> numbering: "1",
>>> columns: 2,
>>> )
>>> set par(justify: true)
>>> set text(11pt, font: "Libertinus Serif")
>>>
>>> show heading.where(
>>> level: 1
>>> ): it => block(
>>> align(center,
>>> text(
>>> 13pt,
>>> weight: "regular",
>>> smallcaps(it.body),
>>> )
>>> ),
>>> )
>>> show heading.where(
>>> level: 2
>>> ): it => box(
>>> text(
>>> 11pt,
>>> weight: "regular",
>>> style: "italic",
>>> it.body + [.],
>>> )
>>> )
>>> show heading.where(level: 1): set align(center)
>>> show heading.where(level: 1): set text(13pt, weight: "regular")
>>> show heading.where(level: 1): it => block(smallcaps(it.body))
>>>
>>> place(
>>> top,
>>> float: true,
>>> scope: "parent",
>>> clearance: 2em,
>>> {
>>> set align(center)
>>> text(17pt, title)
>>> let count = calc.min(authors.len(), 3)
>>> grid(
>>> columns: (1fr,) * count,
>>> row-gutter: 24pt,
>>> ..authors.map(author => [
>>> #author.name \
>>> #author.affiliation \
>>> #link("mailto:" + author.email)
>>> ]),
>>> )
>>> par(justify: false)[
>>> *Abstract* \
>>> #abstract
>>> ]
>>> },
>>> )
>>> doc
>>>}
>>> show heading.where(level: 2): set text(11pt, weight: "regular", style: "italic")
>>> show heading.where(level: 2): it => [#it.body.]
>>>
>>> place(top + center, float: true, scope: "parent", clearance: 2em, {
>>> set par(justify: false)
>>> block(text(17pt, title))
>>> let count = calc.min(authors.len(), 3)
>>> grid(
>>> columns: (1fr,) * count,
>>> row-gutter: 24pt,
>>> ..authors.map(author => [
>>> #author.name \
>>> #author.affiliation \
>>> #link("mailto:" + author.email)
>>> ]),
>>> )
>>> strong[Abstract]
>>> linebreak()
>>> abstract
>>> })
>>> doc
>>> }
<<< #import "conf.typ": conf
#show: conf.with(
title: [
Towards Improved Modelling
],
title: [Towards Improved Modelling],
authors: (
(
name: "Theresa Tungsten",
@ -397,7 +358,7 @@ that define reusable document styles. You've made it far and learned a lot. You
can now use Typst to write your own documents and share them with others.
We are still a super young project and are looking for feedback. If you have any
questions, suggestions or you found a bug, please let us know
questions, suggestions, or you found a bug, please let us know
in the [Forum](https://forum.typst.app/),
on our [Discord server](https://discord.gg/2uDybryKPe),
on [GitHub](https://github.com/typst/typst/),

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<p>my <strong>lower</strong> a<br>MY <strong>UPPER</strong> A<br></p>
</body>
</html>

View File

@ -5,10 +5,10 @@ use std::path::PathBuf;
use ecow::eco_vec;
use tiny_skia as sk;
use typst::diag::{SourceDiagnostic, SourceResult, Warned};
use typst::html::HtmlDocument;
use typst::layout::{Abs, Frame, FrameItem, PagedDocument, Transform};
use typst::visualize::Color;
use typst::{Document, WorldExt};
use typst_html::HtmlDocument;
use typst_pdf::PdfOptions;
use typst_syntax::{FileId, Lines};

View File

@ -14,6 +14,10 @@
// Check that cases are applied to symbols nested in content
#lower($H I !$.body)
--- cases-content-html html ---
#lower[MY #html.strong[Lower] #symbol("A")] \
#upper[my #html.strong[Upper] #symbol("a")] \
--- upper-bad-type ---
// Error: 8-9 expected string or content, found integer
#upper(1)