mirror of
https://github.com/typst/typst
synced 2025-08-18 17:08:33 +08:00
Compare commits
8 Commits
1b92c15869
...
268aa26734
Author | SHA1 | Date | |
---|---|---|---|
|
268aa26734 | ||
|
70710deb2b | ||
|
275012d7c6 | ||
|
98802dde7e | ||
|
6daae2e292 | ||
|
7f24cd9253 | ||
|
fea153a6fc | ||
|
cdb8a42c68 |
@ -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::{
|
||||
|
195
crates/typst-html/src/attr.rs
Normal file
195
crates/typst-html/src/attr.rs
Normal 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");
|
81
crates/typst-html/src/charsets.rs
Normal file
81
crates/typst-html/src/charsets.rs
Normal 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,
|
||||
}
|
||||
}
|
130
crates/typst-html/src/convert.rs
Normal file
130
crates/typst-html/src/convert.rs
Normal 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))
|
||||
}
|
@ -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);
|
||||
|
219
crates/typst-html/src/document.rs
Normal file
219
crates/typst-html/src/document.rs
Normal 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>),
|
||||
}
|
281
crates/typst-html/src/dom.rs
Normal file
281
crates/typst-html/src/dom.rs
Normal 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,
|
||||
}
|
@ -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> {
|
||||
|
76
crates/typst-html/src/fragment.rs
Normal file
76
crates/typst-html/src/fragment.rs
Normal 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())
|
||||
}
|
@ -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)?;
|
||||
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
|
||||
}
|
||||
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),
|
||||
}));
|
||||
/// 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 {
|
||||
engine.sink.warn(warning!(
|
||||
child.span(),
|
||||
"{} was ignored during HTML export",
|
||||
child.elem().name()
|
||||
));
|
||||
self
|
||||
}
|
||||
}
|
||||
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(),
|
||||
);
|
||||
}
|
||||
|
||||
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>),
|
||||
/// 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,
|
||||
}
|
||||
|
@ -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, _, _| {
|
||||
|
271
crates/typst-html/src/tag.rs
Normal file
271
crates/typst-html/src/tag.rs
Normal 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
|
||||
)
|
||||
}
|
@ -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) {
|
||||
|
@ -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() {
|
||||
|
@ -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]
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
@ -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,
|
||||
}
|
@ -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.
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)[
|
||||
#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,11 +407,10 @@ a way to set any of that, we need to write our own heading show rule.
|
||||
>>> ]
|
||||
>>> )
|
||||
>>>
|
||||
>>> #par(justify: false)[
|
||||
>>> #set par(justify: false)
|
||||
>>> *Abstract* \
|
||||
>>> #lorem(80)
|
||||
>>> ]
|
||||
>>> ]
|
||||
>>>
|
||||
>>> = Introduction
|
||||
>>> #lorem(35)
|
||||
@ -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,47 +447,31 @@ 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),
|
||||
@ -488,11 +487,10 @@ differentiate between section and subsection headings:
|
||||
>>> ]
|
||||
>>> )
|
||||
>>>
|
||||
>>> #par(justify: false)[
|
||||
>>> #set par(justify: false)
|
||||
>>> *Abstract* \
|
||||
>>> #lorem(80)
|
||||
>>> ]
|
||||
>>> ]
|
||||
>>>
|
||||
>>> = Introduction
|
||||
>>> #lorem(35)
|
||||
@ -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
|
||||
|
@ -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,12 +225,15 @@ 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 par(justify: false)
|
||||
|
||||
block(text(17pt, strong(title)))
|
||||
|
||||
let count = authors.len()
|
||||
let ncols = calc.min(count, 3)
|
||||
@ -258,12 +247,11 @@ The resulting template function looks like this:
|
||||
]),
|
||||
)
|
||||
|
||||
par(justify: false)[
|
||||
*Abstract* \
|
||||
#abstract
|
||||
]
|
||||
strong[Abstract]
|
||||
linebreak()
|
||||
abstract
|
||||
}
|
||||
|
||||
set align(left)
|
||||
doc
|
||||
}
|
||||
```
|
||||
@ -291,49 +279,26 @@ call.
|
||||
>>> abstract: [],
|
||||
>>> doc,
|
||||
>>> ) = {
|
||||
>>> set text(font: "Libertinus Serif", 11pt)
|
||||
>>> set par(justify: true)
|
||||
>>> set document(title: title, author: authors.map(author => author.name))
|
||||
>>> set page(
|
||||
>>> "us-letter",
|
||||
>>> margin: auto,
|
||||
>>> header: align(
|
||||
>>> right + horizon,
|
||||
>>> title
|
||||
>>> ),
|
||||
>>> 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)
|
||||
>>> 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,
|
||||
@ -344,19 +309,15 @@ call.
|
||||
>>> #link("mailto:" + author.email)
|
||||
>>> ]),
|
||||
>>> )
|
||||
>>> par(justify: false)[
|
||||
>>> *Abstract* \
|
||||
>>> #abstract
|
||||
>>> ]
|
||||
>>> },
|
||||
>>> )
|
||||
>>> 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/),
|
||||
|
10
tests/ref/html/cases-content-html.html
Normal file
10
tests/ref/html/cases-content-html.html
Normal 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>
|
@ -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};
|
||||
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user