mirror of
https://github.com/typst/typst
synced 2025-08-19 01:18:32 +08:00
Compare commits
7 Commits
708eea9015
...
82a5cbb0b1
Author | SHA1 | Date | |
---|---|---|---|
|
82a5cbb0b1 | ||
|
275012d7c6 | ||
|
98802dde7e | ||
|
ac77fdbb6e | ||
|
3aa7e861e7 | ||
|
a45c3388a6 | ||
|
f9b01f595d |
5
Cargo.lock
generated
5
Cargo.lock
generated
@ -413,7 +413,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "codex"
|
name = "codex"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
source = "git+https://github.com/typst/codex?rev=a5428cb#a5428cb9c81a41354d44b44dbd5a16a710bbd928"
|
source = "git+https://github.com/typst/codex?rev=9ac86f9#9ac86f96af5b89fce555e6bba8b6d1ac7b44ef00"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "color-print"
|
name = "color-print"
|
||||||
@ -2861,7 +2861,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "typst-assets"
|
name = "typst-assets"
|
||||||
version = "0.13.1"
|
version = "0.13.1"
|
||||||
source = "git+https://github.com/typst/typst-assets?rev=c1089b4#c1089b46c461bdde579c55caa941a3cc7dec3e8a"
|
source = "git+https://github.com/typst/typst-assets?rev=edf0d64#edf0d648376e29738a05a933af9ea99bb81557b1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typst-cli"
|
name = "typst-cli"
|
||||||
@ -3032,6 +3032,7 @@ version = "0.13.1"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"az",
|
"az",
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
|
"codex",
|
||||||
"comemo",
|
"comemo",
|
||||||
"ecow",
|
"ecow",
|
||||||
"hypher",
|
"hypher",
|
||||||
|
@ -32,7 +32,7 @@ typst-svg = { path = "crates/typst-svg", version = "0.13.1" }
|
|||||||
typst-syntax = { path = "crates/typst-syntax", version = "0.13.1" }
|
typst-syntax = { path = "crates/typst-syntax", version = "0.13.1" }
|
||||||
typst-timing = { path = "crates/typst-timing", version = "0.13.1" }
|
typst-timing = { path = "crates/typst-timing", version = "0.13.1" }
|
||||||
typst-utils = { path = "crates/typst-utils", version = "0.13.1" }
|
typst-utils = { path = "crates/typst-utils", version = "0.13.1" }
|
||||||
typst-assets = { git = "https://github.com/typst/typst-assets", rev = "c1089b4" }
|
typst-assets = { git = "https://github.com/typst/typst-assets", rev = "edf0d64" }
|
||||||
typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "bfa947f" }
|
typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "bfa947f" }
|
||||||
arrayvec = "0.7.4"
|
arrayvec = "0.7.4"
|
||||||
az = "1.2"
|
az = "1.2"
|
||||||
@ -47,7 +47,7 @@ clap = { version = "4.4", features = ["derive", "env", "wrap_help"] }
|
|||||||
clap_complete = "4.2.1"
|
clap_complete = "4.2.1"
|
||||||
clap_mangen = "0.2.10"
|
clap_mangen = "0.2.10"
|
||||||
codespan-reporting = "0.11"
|
codespan-reporting = "0.11"
|
||||||
codex = { git = "https://github.com/typst/codex", rev = "a5428cb" }
|
codex = { git = "https://github.com/typst/codex", rev = "9ac86f9" }
|
||||||
color-print = "0.3.6"
|
color-print = "0.3.6"
|
||||||
comemo = "0.4"
|
comemo = "0.4"
|
||||||
csv = "1"
|
csv = "1"
|
||||||
|
@ -14,10 +14,10 @@ use typst::diag::{
|
|||||||
bail, At, Severity, SourceDiagnostic, SourceResult, StrResult, Warned,
|
bail, At, Severity, SourceDiagnostic, SourceResult, StrResult, Warned,
|
||||||
};
|
};
|
||||||
use typst::foundations::{Datetime, Smart};
|
use typst::foundations::{Datetime, Smart};
|
||||||
use typst::html::HtmlDocument;
|
|
||||||
use typst::layout::{Frame, Page, PageRanges, PagedDocument};
|
use typst::layout::{Frame, Page, PageRanges, PagedDocument};
|
||||||
use typst::syntax::{FileId, Lines, Span};
|
use typst::syntax::{FileId, Lines, Span};
|
||||||
use typst::WorldExt;
|
use typst::WorldExt;
|
||||||
|
use typst_html::HtmlDocument;
|
||||||
use typst_pdf::{PdfOptions, PdfStandards, Timestamp};
|
use typst_pdf::{PdfOptions, PdfStandards, Timestamp};
|
||||||
|
|
||||||
use crate::args::{
|
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))
|
||||||
|
}
|
@ -1,11 +1,54 @@
|
|||||||
//! Conversion from Typst data types into CSS data types.
|
//! Conversion from Typst data types into CSS data types.
|
||||||
|
|
||||||
use std::fmt::{self, Display};
|
use std::fmt::{self, Display, Write};
|
||||||
|
|
||||||
use typst_library::layout::Length;
|
use ecow::EcoString;
|
||||||
|
use typst_library::layout::{Length, Rel};
|
||||||
use typst_library::visualize::{Color, Hsl, LinearRgb, Oklab, Oklch, Rgb};
|
use typst_library::visualize::{Color, Hsl, LinearRgb, Oklab, Oklch, Rgb};
|
||||||
use typst_utils::Numeric;
|
use typst_utils::Numeric;
|
||||||
|
|
||||||
|
/// A list of CSS properties with values.
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct Properties(EcoString);
|
||||||
|
|
||||||
|
impl Properties {
|
||||||
|
/// Creates an empty list.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a new property to the list.
|
||||||
|
pub fn push(&mut self, property: &str, value: impl Display) {
|
||||||
|
if !self.0.is_empty() {
|
||||||
|
self.0.push_str("; ");
|
||||||
|
}
|
||||||
|
write!(&mut self.0, "{property}: {value}").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a new property in builder-style.
|
||||||
|
#[expect(unused)]
|
||||||
|
pub fn with(mut self, property: &str, value: impl Display) -> Self {
|
||||||
|
self.push(property, value);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Turns this into a string suitable for use as an inline `style`
|
||||||
|
/// attribute.
|
||||||
|
pub fn into_inline_styles(self) -> Option<EcoString> {
|
||||||
|
(!self.0.is_empty()).then_some(self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rel(rel: Rel) -> impl Display {
|
||||||
|
typst_utils::display(move |f| match (rel.abs.is_zero(), rel.rel.is_zero()) {
|
||||||
|
(false, false) => {
|
||||||
|
write!(f, "calc({}% + {})", rel.rel.get(), length(rel.abs))
|
||||||
|
}
|
||||||
|
(true, false) => write!(f, "{}%", rel.rel.get()),
|
||||||
|
(_, true) => write!(f, "{}", length(rel.abs)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn length(length: Length) -> impl Display {
|
pub fn length(length: Length) -> impl Display {
|
||||||
typst_utils::display(move |f| match (length.abs.is_zero(), length.em.is_zero()) {
|
typst_utils::display(move |f| match (length.abs.is_zero(), length.em.is_zero()) {
|
||||||
(false, false) => {
|
(false, false) => {
|
||||||
|
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::diag::{bail, At, SourceResult, StrResult};
|
||||||
use typst_library::foundations::Repr;
|
use typst_library::foundations::Repr;
|
||||||
use typst_library::html::{
|
use typst_syntax::Span;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
attr, charsets, tag, HtmlDocument, HtmlElement, HtmlFrame, HtmlNode, HtmlTag,
|
attr, charsets, tag, HtmlDocument, HtmlElement, HtmlFrame, HtmlNode, HtmlTag,
|
||||||
};
|
};
|
||||||
use typst_syntax::Span;
|
|
||||||
|
|
||||||
/// Encodes an HTML document into a string.
|
/// Encodes an HTML document into a string.
|
||||||
pub fn html(document: &HtmlDocument) -> SourceResult<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.
|
//! Typst's HTML exporter.
|
||||||
|
|
||||||
|
mod attr;
|
||||||
|
mod charsets;
|
||||||
|
mod convert;
|
||||||
mod css;
|
mod css;
|
||||||
|
mod document;
|
||||||
|
mod dom;
|
||||||
mod encode;
|
mod encode;
|
||||||
|
mod fragment;
|
||||||
mod rules;
|
mod rules;
|
||||||
|
mod tag;
|
||||||
mod typed;
|
mod typed;
|
||||||
|
|
||||||
|
pub use self::document::html_document;
|
||||||
|
pub use self::dom::*;
|
||||||
pub use self::encode::html;
|
pub use self::encode::html;
|
||||||
pub use self::rules::register;
|
pub use self::rules::register;
|
||||||
|
|
||||||
use comemo::{Track, Tracked, TrackedMut};
|
use ecow::EcoString;
|
||||||
use typst_library::diag::{bail, warning, At, SourceResult};
|
use typst_library::foundations::{Content, Module, Scope};
|
||||||
use typst_library::engine::{Engine, Route, Sink, Traced};
|
use typst_library::Category;
|
||||||
use typst_library::foundations::{
|
use typst_macros::elem;
|
||||||
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;
|
|
||||||
|
|
||||||
/// Create a module with all HTML definitions.
|
/// Creates the module with all HTML definitions.
|
||||||
pub fn module() -> Module {
|
pub fn module() -> Module {
|
||||||
let mut html = Scope::deduplicating();
|
let mut html = Scope::deduplicating();
|
||||||
html.start_category(Category::Html);
|
html.start_category(Category::Html);
|
||||||
@ -37,337 +32,77 @@ pub fn module() -> Module {
|
|||||||
Module::new("html", html)
|
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
|
/// Typst's HTML export automatically generates the appropriate tags for most
|
||||||
/// elements into HTML.
|
/// elements. However, sometimes, it is desirable to retain more control. For
|
||||||
#[typst_macros::time(name = "html document")]
|
/// example, when using Typst to generate your blog, you could use this function
|
||||||
pub fn html_document(
|
/// to wrap each article in an `<article>` tag.
|
||||||
engine: &mut Engine,
|
///
|
||||||
content: &Content,
|
/// Typst is aware of what is valid HTML. A tag and its attributes must form
|
||||||
styles: StyleChain,
|
/// syntactically valid HTML. Some tags, like `meta` do not accept content.
|
||||||
) -> SourceResult<HtmlDocument> {
|
/// Hence, you must not provide a body for them. We may add more checks in the
|
||||||
html_document_impl(
|
/// future, so be sure that you are generating valid HTML when using this
|
||||||
engine.routines,
|
/// function.
|
||||||
engine.world,
|
///
|
||||||
engine.introspector,
|
/// Normally, Typst will generate `html`, `head`, and `body` tags for you. If
|
||||||
engine.traced,
|
/// you instead create them with this function, Typst will omit its own tags.
|
||||||
TrackedMut::reborrow_mut(&mut engine.sink),
|
///
|
||||||
engine.route.track(),
|
/// ```typ
|
||||||
content,
|
/// #html.elem("div", attrs: (style: "background: aqua"))[
|
||||||
styles,
|
/// 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`.
|
impl HtmlElem {
|
||||||
#[comemo::memoize]
|
/// Add an attribute to the element.
|
||||||
#[allow(clippy::too_many_arguments)]
|
pub fn with_attr(mut self, attr: HtmlAttr, value: impl Into<EcoString>) -> Self {
|
||||||
fn html_document_impl(
|
self.attrs
|
||||||
routines: &Routines,
|
.as_option_mut()
|
||||||
world: Tracked<dyn World + '_>,
|
.get_or_insert_with(Default::default)
|
||||||
introspector: Tracked<Introspector>,
|
.push(attr, value);
|
||||||
traced: Tracked<Traced>,
|
self
|
||||||
sink: TrackedMut<Sink>,
|
|
||||||
route: Tracked<Route>,
|
|
||||||
content: &Content,
|
|
||||||
styles: StyleChain,
|
|
||||||
) -> SourceResult<HtmlDocument> {
|
|
||||||
let mut locator = Locator::root().split();
|
|
||||||
let mut engine = Engine {
|
|
||||||
routines,
|
|
||||||
world,
|
|
||||||
introspector,
|
|
||||||
traced,
|
|
||||||
sink,
|
|
||||||
route: Route::extend(route).unnested(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Mark the external styles as "outside" so that they are valid at the page
|
|
||||||
// level.
|
|
||||||
let styles = styles.to_map().outside();
|
|
||||||
let styles = StyleChain::new(&styles);
|
|
||||||
|
|
||||||
let arenas = Arenas::default();
|
|
||||||
let mut info = DocumentInfo::default();
|
|
||||||
let children = (engine.routines.realize)(
|
|
||||||
RealizationKind::HtmlDocument(&mut info),
|
|
||||||
&mut engine,
|
|
||||||
&mut locator,
|
|
||||||
&arenas,
|
|
||||||
content,
|
|
||||||
styles,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let output = handle_list(&mut engine, &mut locator, children.iter().copied())?;
|
|
||||||
let introspector = Introspector::html(&output);
|
|
||||||
let root = root_element(output, &info)?;
|
|
||||||
|
|
||||||
Ok(HtmlDocument { info, root, introspector })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Produce HTML nodes from content.
|
|
||||||
#[typst_macros::time(name = "html fragment")]
|
|
||||||
pub fn html_fragment(
|
|
||||||
engine: &mut Engine,
|
|
||||||
content: &Content,
|
|
||||||
locator: Locator,
|
|
||||||
styles: StyleChain,
|
|
||||||
) -> SourceResult<Vec<HtmlNode>> {
|
|
||||||
html_fragment_impl(
|
|
||||||
engine.routines,
|
|
||||||
engine.world,
|
|
||||||
engine.introspector,
|
|
||||||
engine.traced,
|
|
||||||
TrackedMut::reborrow_mut(&mut engine.sink),
|
|
||||||
engine.route.track(),
|
|
||||||
content,
|
|
||||||
locator.track(),
|
|
||||||
styles,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The cached, internal implementation of [`html_fragment`].
|
|
||||||
#[comemo::memoize]
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn html_fragment_impl(
|
|
||||||
routines: &Routines,
|
|
||||||
world: Tracked<dyn World + '_>,
|
|
||||||
introspector: Tracked<Introspector>,
|
|
||||||
traced: Tracked<Traced>,
|
|
||||||
sink: TrackedMut<Sink>,
|
|
||||||
route: Tracked<Route>,
|
|
||||||
content: &Content,
|
|
||||||
locator: Tracked<Locator>,
|
|
||||||
styles: StyleChain,
|
|
||||||
) -> SourceResult<Vec<HtmlNode>> {
|
|
||||||
let link = LocatorLink::new(locator);
|
|
||||||
let mut locator = Locator::link(&link).split();
|
|
||||||
let mut engine = Engine {
|
|
||||||
routines,
|
|
||||||
world,
|
|
||||||
introspector,
|
|
||||||
traced,
|
|
||||||
sink,
|
|
||||||
route: Route::extend(route),
|
|
||||||
};
|
|
||||||
|
|
||||||
engine.route.check_html_depth().at(content.span())?;
|
|
||||||
|
|
||||||
let arenas = Arenas::default();
|
|
||||||
let children = (engine.routines.realize)(
|
|
||||||
// No need to know about the `FragmentKind` because we handle both
|
|
||||||
// uniformly.
|
|
||||||
RealizationKind::HtmlFragment(&mut FragmentKind::Block),
|
|
||||||
&mut engine,
|
|
||||||
&mut locator,
|
|
||||||
&arenas,
|
|
||||||
content,
|
|
||||||
styles,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
handle_list(&mut engine, &mut locator, children.iter().copied())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert children into HTML nodes.
|
|
||||||
fn handle_list<'a>(
|
|
||||||
engine: &mut Engine,
|
|
||||||
locator: &mut SplitLocator,
|
|
||||||
children: impl IntoIterator<Item = Pair<'a>>,
|
|
||||||
) -> SourceResult<Vec<HtmlNode>> {
|
|
||||||
let mut output = Vec::new();
|
|
||||||
for (child, styles) in children {
|
|
||||||
handle(engine, child, locator, styles, &mut output)?;
|
|
||||||
}
|
|
||||||
Ok(output)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert a child into HTML node(s).
|
|
||||||
fn handle(
|
|
||||||
engine: &mut Engine,
|
|
||||||
child: &Content,
|
|
||||||
locator: &mut SplitLocator,
|
|
||||||
styles: StyleChain,
|
|
||||||
output: &mut Vec<HtmlNode>,
|
|
||||||
) -> SourceResult<()> {
|
|
||||||
if let Some(elem) = child.to_packed::<TagElem>() {
|
|
||||||
output.push(HtmlNode::Tag(elem.tag.clone()));
|
|
||||||
} else if let Some(elem) = child.to_packed::<HtmlElem>() {
|
|
||||||
let mut children = vec![];
|
|
||||||
if let Some(body) = elem.body.get_ref(styles) {
|
|
||||||
children = html_fragment(engine, body, locator.next(&elem.span()), styles)?;
|
|
||||||
}
|
|
||||||
let element = HtmlElement {
|
|
||||||
tag: elem.tag,
|
|
||||||
attrs: elem.attrs.get_cloned(styles),
|
|
||||||
children,
|
|
||||||
span: elem.span(),
|
|
||||||
};
|
|
||||||
output.push(element.into());
|
|
||||||
} else if let Some(elem) = child.to_packed::<ParElem>() {
|
|
||||||
let children =
|
|
||||||
html_fragment(engine, &elem.body, locator.next(&elem.span()), styles)?;
|
|
||||||
output.push(
|
|
||||||
HtmlElement::new(tag::p)
|
|
||||||
.with_children(children)
|
|
||||||
.spanned(elem.span())
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
} else if let Some(elem) = child.to_packed::<BoxElem>() {
|
|
||||||
// TODO: This is rather incomplete.
|
|
||||||
if let Some(body) = elem.body.get_ref(styles) {
|
|
||||||
let children =
|
|
||||||
html_fragment(engine, body, locator.next(&elem.span()), styles)?;
|
|
||||||
output.push(
|
|
||||||
HtmlElement::new(tag::span)
|
|
||||||
.with_attr(attr::style, "display: inline-block;")
|
|
||||||
.with_children(children)
|
|
||||||
.spanned(elem.span())
|
|
||||||
.into(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if let Some((elem, body)) =
|
|
||||||
child
|
|
||||||
.to_packed::<BlockElem>()
|
|
||||||
.and_then(|elem| match elem.body.get_ref(styles) {
|
|
||||||
Some(BlockBody::Content(body)) => Some((elem, body)),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
{
|
|
||||||
// TODO: This is rather incomplete.
|
|
||||||
let children = html_fragment(engine, body, locator.next(&elem.span()), styles)?;
|
|
||||||
output.push(
|
|
||||||
HtmlElement::new(tag::div)
|
|
||||||
.with_children(children)
|
|
||||||
.spanned(elem.span())
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
} else if child.is::<SpaceElem>() {
|
|
||||||
output.push(HtmlNode::text(' ', child.span()));
|
|
||||||
} else if let Some(elem) = child.to_packed::<TextElem>() {
|
|
||||||
output.push(HtmlNode::text(elem.text.clone(), elem.span()));
|
|
||||||
} else if let Some(elem) = child.to_packed::<LinebreakElem>() {
|
|
||||||
output.push(HtmlElement::new(tag::br).spanned(elem.span()).into());
|
|
||||||
} else if let Some(elem) = child.to_packed::<SmartQuoteElem>() {
|
|
||||||
output.push(HtmlNode::text(
|
|
||||||
if elem.double.get(styles) { '"' } else { '\'' },
|
|
||||||
child.span(),
|
|
||||||
));
|
|
||||||
} else if let Some(elem) = child.to_packed::<FrameElem>() {
|
|
||||||
let locator = locator.next(&elem.span());
|
|
||||||
let style = TargetElem::target.set(Target::Paged).wrap();
|
|
||||||
let frame = (engine.routines.layout_frame)(
|
|
||||||
engine,
|
|
||||||
&elem.body,
|
|
||||||
locator,
|
|
||||||
styles.chain(&style),
|
|
||||||
Region::new(Size::splat(Abs::inf()), Axes::splat(false)),
|
|
||||||
)?;
|
|
||||||
output.push(HtmlNode::Frame(HtmlFrame {
|
|
||||||
inner: frame,
|
|
||||||
text_size: styles.resolve(TextElem::size),
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
engine.sink.warn(warning!(
|
|
||||||
child.span(),
|
|
||||||
"{} was ignored during HTML export",
|
|
||||||
child.elem().name()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Wrap the nodes in `<html>` and `<body>` if they are not yet rooted,
|
|
||||||
/// supplying a suitable `<head>`.
|
|
||||||
fn root_element(output: Vec<HtmlNode>, info: &DocumentInfo) -> SourceResult<HtmlElement> {
|
|
||||||
let head = head_element(info);
|
|
||||||
let body = match classify_output(output)? {
|
|
||||||
OutputKind::Html(element) => return Ok(element),
|
|
||||||
OutputKind::Body(body) => body,
|
|
||||||
OutputKind::Leafs(leafs) => HtmlElement::new(tag::body).with_children(leafs),
|
|
||||||
};
|
|
||||||
Ok(HtmlElement::new(tag::html).with_children(vec![head.into(), body.into()]))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generate a `<head>` element.
|
|
||||||
fn head_element(info: &DocumentInfo) -> HtmlElement {
|
|
||||||
let mut children = vec![];
|
|
||||||
|
|
||||||
children.push(HtmlElement::new(tag::meta).with_attr(attr::charset, "utf-8").into());
|
|
||||||
|
|
||||||
children.push(
|
|
||||||
HtmlElement::new(tag::meta)
|
|
||||||
.with_attr(attr::name, "viewport")
|
|
||||||
.with_attr(attr::content, "width=device-width, initial-scale=1")
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(title) = &info.title {
|
|
||||||
children.push(
|
|
||||||
HtmlElement::new(tag::title)
|
|
||||||
.with_children(vec![HtmlNode::Text(title.clone(), Span::detached())])
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(description) = &info.description {
|
/// Adds CSS styles to an element.
|
||||||
children.push(
|
fn with_styles(self, properties: css::Properties) -> Self {
|
||||||
HtmlElement::new(tag::meta)
|
if let Some(value) = properties.into_inline_styles() {
|
||||||
.with_attr(attr::name, "description")
|
self.with_attr(attr::style, value)
|
||||||
.with_attr(attr::content, description.clone())
|
} else {
|
||||||
.into(),
|
self
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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.
|
/// An element that lays out its content as an inline SVG.
|
||||||
enum OutputKind {
|
///
|
||||||
/// The user generated their own `<html>` element. We do not need to supply
|
/// Sometimes, converting Typst content to HTML is not desirable. This can be
|
||||||
/// one.
|
/// the case for plots and other content that relies on positioning and styling
|
||||||
Html(HtmlElement),
|
/// to convey its message.
|
||||||
/// The user generate their own `<body>` element. We do not need to supply
|
///
|
||||||
/// one, but need supply the `<html>` element.
|
/// This function allows you to use the Typst layout engine that would also be
|
||||||
Body(HtmlElement),
|
/// used for PDF, SVG, and PNG export to render a part of your document exactly
|
||||||
/// The user generated leafs which we wrap in a `<body>` and `<html>`.
|
/// how it would appear when exported in one of these formats. It embeds the
|
||||||
Leafs(Vec<HtmlNode>),
|
/// content as an inline SVG.
|
||||||
|
#[elem]
|
||||||
|
pub struct FrameElem {
|
||||||
|
/// The content that shall be laid out.
|
||||||
|
#[positional]
|
||||||
|
#[required]
|
||||||
|
pub body: Content,
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,11 @@ use std::num::NonZeroUsize;
|
|||||||
use ecow::{eco_format, EcoVec};
|
use ecow::{eco_format, EcoVec};
|
||||||
use typst_library::diag::warning;
|
use typst_library::diag::warning;
|
||||||
use typst_library::foundations::{
|
use typst_library::foundations::{
|
||||||
Content, NativeElement, NativeRuleMap, ShowFn, StyleChain, Target,
|
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::introspection::{Counter, Locator};
|
||||||
use typst_library::layout::resolve::{table_to_cellgrid, Cell, CellGrid, Entry};
|
use typst_library::layout::resolve::{table_to_cellgrid, Cell, CellGrid, Entry};
|
||||||
use typst_library::layout::OuterVAlignment;
|
use typst_library::layout::{OuterVAlignment, Sizing};
|
||||||
use typst_library::model::{
|
use typst_library::model::{
|
||||||
Attribution, CiteElem, CiteGroup, Destination, EmphElem, EnumElem, FigureCaption,
|
Attribution, CiteElem, CiteGroup, Destination, EmphElem, EnumElem, FigureCaption,
|
||||||
FigureElem, HeadingElem, LinkElem, LinkTarget, ListElem, ParbreakElem, QuoteElem,
|
FigureElem, HeadingElem, LinkElem, LinkTarget, ListElem, ParbreakElem, QuoteElem,
|
||||||
@ -18,10 +17,13 @@ use typst_library::text::{
|
|||||||
HighlightElem, LinebreakElem, OverlineElem, RawElem, RawLine, SpaceElem, StrikeElem,
|
HighlightElem, LinebreakElem, OverlineElem, RawElem, RawLine, SpaceElem, StrikeElem,
|
||||||
SubElem, SuperElem, UnderlineElem,
|
SubElem, SuperElem, UnderlineElem,
|
||||||
};
|
};
|
||||||
|
use typst_library::visualize::ImageElem;
|
||||||
|
|
||||||
/// Register show rules for the [HTML target](Target::Html).
|
use crate::{attr, css, tag, FrameElem, HtmlAttrs, HtmlElem, HtmlTag};
|
||||||
|
|
||||||
|
/// Registers show rules for the [HTML target](Target::Html).
|
||||||
pub fn register(rules: &mut NativeRuleMap) {
|
pub fn register(rules: &mut NativeRuleMap) {
|
||||||
use Target::Html;
|
use Target::{Html, Paged};
|
||||||
|
|
||||||
// Model.
|
// Model.
|
||||||
rules.register(Html, STRONG_RULE);
|
rules.register(Html, STRONG_RULE);
|
||||||
@ -47,6 +49,14 @@ pub fn register(rules: &mut NativeRuleMap) {
|
|||||||
rules.register(Html, HIGHLIGHT_RULE);
|
rules.register(Html, HIGHLIGHT_RULE);
|
||||||
rules.register(Html, RAW_RULE);
|
rules.register(Html, RAW_RULE);
|
||||||
rules.register(Html, RAW_LINE_RULE);
|
rules.register(Html, RAW_LINE_RULE);
|
||||||
|
|
||||||
|
// 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, _, _| {
|
const STRONG_RULE: ShowFn<StrongElem> = |elem, _, _| {
|
||||||
@ -338,7 +348,7 @@ fn show_cellgrid(grid: CellGrid, styles: StyleChain) -> Content {
|
|||||||
fn show_cell(tag: HtmlTag, cell: &Cell, styles: StyleChain) -> Content {
|
fn show_cell(tag: HtmlTag, cell: &Cell, styles: StyleChain) -> Content {
|
||||||
let cell = cell.body.clone();
|
let cell = cell.body.clone();
|
||||||
let Some(cell) = cell.to_packed::<TableCell>() else { return cell };
|
let Some(cell) = cell.to_packed::<TableCell>() else { return cell };
|
||||||
let mut attrs = HtmlAttrs::default();
|
let mut attrs = HtmlAttrs::new();
|
||||||
let span = |n: NonZeroUsize| (n != NonZeroUsize::MIN).then(|| n.to_string());
|
let span = |n: NonZeroUsize| (n != NonZeroUsize::MIN).then(|| n.to_string());
|
||||||
if let Some(colspan) = span(cell.colspan.get(styles)) {
|
if let Some(colspan) = span(cell.colspan.get(styles)) {
|
||||||
attrs.push(attr::colspan, colspan);
|
attrs.push(attr::colspan, colspan);
|
||||||
@ -409,3 +419,36 @@ const RAW_RULE: ShowFn<RawElem> = |elem, _, styles| {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const RAW_LINE_RULE: ShowFn<RawLine> = |elem, _, _| Ok(elem.body.clone());
|
const RAW_LINE_RULE: ShowFn<RawLine> = |elem, _, _| Ok(elem.body.clone());
|
||||||
|
|
||||||
|
const IMAGE_RULE: ShowFn<ImageElem> = |elem, engine, styles| {
|
||||||
|
let image = elem.decode(engine, styles)?;
|
||||||
|
|
||||||
|
let mut attrs = HtmlAttrs::new();
|
||||||
|
attrs.push(attr::src, typst_svg::convert_image_to_base64_url(&image));
|
||||||
|
|
||||||
|
if let Some(alt) = elem.alt.get_cloned(styles) {
|
||||||
|
attrs.push(attr::alt, alt);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut inline = css::Properties::new();
|
||||||
|
|
||||||
|
// TODO: Exclude in semantic profile.
|
||||||
|
if let Some(value) = typst_svg::convert_image_scaling(image.scaling()) {
|
||||||
|
inline.push("image-rendering", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Exclude in semantic profile?
|
||||||
|
match elem.width.get(styles) {
|
||||||
|
Smart::Auto => {}
|
||||||
|
Smart::Custom(rel) => inline.push("width", css::rel(rel)),
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Exclude in semantic profile?
|
||||||
|
match elem.height.get(styles) {
|
||||||
|
Sizing::Auto => {}
|
||||||
|
Sizing::Rel(rel) => inline.push("height", css::rel(rel)),
|
||||||
|
Sizing::Fr(_) => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(HtmlElem::new(tag::img).with_attrs(attrs).with_styles(inline).pack())
|
||||||
|
};
|
||||||
|
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,
|
FromValue, IntoValue, NativeFuncData, NativeFuncPtr, NoneValue, ParamInfo,
|
||||||
PositiveF64, Reflect, Scope, Str, Type, Value,
|
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::layout::{Axes, Axis, Dir, Length};
|
||||||
use typst_library::visualize::Color;
|
use typst_library::visualize::Color;
|
||||||
use typst_macros::cast;
|
use typst_macros::cast;
|
||||||
|
|
||||||
use crate::css;
|
use crate::{css, tag, HtmlAttr, HtmlAttrs, HtmlElem, HtmlTag};
|
||||||
|
|
||||||
/// Hook up all typed HTML definitions.
|
/// Hook up all typed HTML definitions.
|
||||||
pub(super) fn define(html: &mut Scope) {
|
pub(super) fn define(html: &mut Scope) {
|
||||||
|
@ -2,7 +2,7 @@ use comemo::Track;
|
|||||||
use ecow::{eco_vec, EcoString, EcoVec};
|
use ecow::{eco_vec, EcoString, EcoVec};
|
||||||
use typst::foundations::{Label, Styles, Value};
|
use typst::foundations::{Label, Styles, Value};
|
||||||
use typst::layout::PagedDocument;
|
use typst::layout::PagedDocument;
|
||||||
use typst::model::BibliographyElem;
|
use typst::model::{BibliographyElem, FigureElem};
|
||||||
use typst::syntax::{ast, LinkedNode, SyntaxKind};
|
use typst::syntax::{ast, LinkedNode, SyntaxKind};
|
||||||
|
|
||||||
use crate::IdeWorld;
|
use crate::IdeWorld;
|
||||||
@ -75,8 +75,13 @@ pub fn analyze_labels(
|
|||||||
for elem in document.introspector.all() {
|
for elem in document.introspector.all() {
|
||||||
let Some(label) = elem.label() else { continue };
|
let Some(label) = elem.label() else { continue };
|
||||||
let details = elem
|
let details = elem
|
||||||
.get_by_name("caption")
|
.to_packed::<FigureElem>()
|
||||||
.or_else(|_| elem.get_by_name("body"))
|
.and_then(|figure| match figure.caption.as_option() {
|
||||||
|
Some(Some(caption)) => Some(caption.pack_ref()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.unwrap_or(elem)
|
||||||
|
.get_by_name("body")
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|field| match field {
|
.and_then(|field| match field {
|
||||||
Value::Content(content) => Some(content),
|
Value::Content(content) => Some(content),
|
||||||
|
@ -378,4 +378,9 @@ mod tests {
|
|||||||
.with_source("other.typ", "#let f = (x) => 1");
|
.with_source("other.typ", "#let f = (x) => 1");
|
||||||
test(&world, -4, Side::After).must_be_code("(..) => ..");
|
test(&world, -4, Side::After).must_be_code("(..) => ..");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tooltip_reference() {
|
||||||
|
test("#figure(caption: [Hi])[]<f> @f", -1, Side::Before).must_be_text("Hi");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ typst-timing = { workspace = true }
|
|||||||
typst-utils = { workspace = true }
|
typst-utils = { workspace = true }
|
||||||
az = { workspace = true }
|
az = { workspace = true }
|
||||||
bumpalo = { workspace = true }
|
bumpalo = { workspace = true }
|
||||||
|
codex = { workspace = true }
|
||||||
comemo = { workspace = true }
|
comemo = { workspace = true }
|
||||||
ecow = { workspace = true }
|
ecow = { workspace = true }
|
||||||
hypher = { workspace = true }
|
hypher = { workspace = true }
|
||||||
|
@ -143,7 +143,7 @@ fn layout_fragment_impl(
|
|||||||
let mut kind = FragmentKind::Block;
|
let mut kind = FragmentKind::Block;
|
||||||
let arenas = Arenas::default();
|
let arenas = Arenas::default();
|
||||||
let children = (engine.routines.realize)(
|
let children = (engine.routines.realize)(
|
||||||
RealizationKind::LayoutFragment(&mut kind),
|
RealizationKind::LayoutFragment { kind: &mut kind },
|
||||||
&mut engine,
|
&mut engine,
|
||||||
&mut locator,
|
&mut locator,
|
||||||
&arenas,
|
&arenas,
|
||||||
|
@ -1,18 +1,11 @@
|
|||||||
use std::ffi::OsStr;
|
use typst_library::diag::SourceResult;
|
||||||
|
|
||||||
use typst_library::diag::{warning, At, LoadedWithin, SourceResult, StrResult};
|
|
||||||
use typst_library::engine::Engine;
|
use typst_library::engine::Engine;
|
||||||
use typst_library::foundations::{Bytes, Derived, Packed, Smart, StyleChain};
|
use typst_library::foundations::{Packed, StyleChain};
|
||||||
use typst_library::introspection::Locator;
|
use typst_library::introspection::Locator;
|
||||||
use typst_library::layout::{
|
use typst_library::layout::{
|
||||||
Abs, Axes, FixedAlignment, Frame, FrameItem, Point, Region, Size,
|
Abs, Axes, FixedAlignment, Frame, FrameItem, Point, Region, Size,
|
||||||
};
|
};
|
||||||
use typst_library::loading::DataSource;
|
use typst_library::visualize::{Curve, Image, ImageElem, ImageFit};
|
||||||
use typst_library::text::families;
|
|
||||||
use typst_library::visualize::{
|
|
||||||
Curve, ExchangeFormat, Image, ImageElem, ImageFit, ImageFormat, ImageKind,
|
|
||||||
RasterImage, SvgImage, VectorFormat,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Layout the image.
|
/// Layout the image.
|
||||||
#[typst_macros::time(span = elem.span())]
|
#[typst_macros::time(span = elem.span())]
|
||||||
@ -23,53 +16,7 @@ pub fn layout_image(
|
|||||||
styles: StyleChain,
|
styles: StyleChain,
|
||||||
region: Region,
|
region: Region,
|
||||||
) -> SourceResult<Frame> {
|
) -> SourceResult<Frame> {
|
||||||
let span = elem.span();
|
let image = elem.decode(engine, styles)?;
|
||||||
|
|
||||||
// Take the format that was explicitly defined, or parse the extension,
|
|
||||||
// or try to detect the format.
|
|
||||||
let Derived { source, derived: loaded } = &elem.source;
|
|
||||||
let format = match elem.format.get(styles) {
|
|
||||||
Smart::Custom(v) => v,
|
|
||||||
Smart::Auto => determine_format(source, &loaded.data).at(span)?,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Warn the user if the image contains a foreign object. Not perfect
|
|
||||||
// because the svg could also be encoded, but that's an edge case.
|
|
||||||
if format == ImageFormat::Vector(VectorFormat::Svg) {
|
|
||||||
let has_foreign_object =
|
|
||||||
memchr::memmem::find(&loaded.data, b"<foreignObject").is_some();
|
|
||||||
|
|
||||||
if has_foreign_object {
|
|
||||||
engine.sink.warn(warning!(
|
|
||||||
span,
|
|
||||||
"image contains foreign object";
|
|
||||||
hint: "SVG images with foreign objects might render incorrectly in typst";
|
|
||||||
hint: "see https://github.com/typst/typst/issues/1421 for more information"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct the image itself.
|
|
||||||
let kind = match format {
|
|
||||||
ImageFormat::Raster(format) => ImageKind::Raster(
|
|
||||||
RasterImage::new(
|
|
||||||
loaded.data.clone(),
|
|
||||||
format,
|
|
||||||
elem.icc.get_ref(styles).as_ref().map(|icc| icc.derived.clone()),
|
|
||||||
)
|
|
||||||
.at(span)?,
|
|
||||||
),
|
|
||||||
ImageFormat::Vector(VectorFormat::Svg) => ImageKind::Svg(
|
|
||||||
SvgImage::with_fonts(
|
|
||||||
loaded.data.clone(),
|
|
||||||
engine.world,
|
|
||||||
&families(styles).map(|f| f.as_str()).collect::<Vec<_>>(),
|
|
||||||
)
|
|
||||||
.within(loaded)?,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
let image = Image::new(kind, elem.alt.get_cloned(styles), elem.scaling.get(styles));
|
|
||||||
|
|
||||||
// Determine the image's pixel aspect ratio.
|
// Determine the image's pixel aspect ratio.
|
||||||
let pxw = image.width();
|
let pxw = image.width();
|
||||||
@ -122,7 +69,7 @@ pub fn layout_image(
|
|||||||
// the frame to the target size, center aligning the image in the
|
// the frame to the target size, center aligning the image in the
|
||||||
// process.
|
// process.
|
||||||
let mut frame = Frame::soft(fitted);
|
let mut frame = Frame::soft(fitted);
|
||||||
frame.push(Point::zero(), FrameItem::Image(image, fitted, span));
|
frame.push(Point::zero(), FrameItem::Image(image, fitted, elem.span()));
|
||||||
frame.resize(target, Axes::splat(FixedAlignment::Center));
|
frame.resize(target, Axes::splat(FixedAlignment::Center));
|
||||||
|
|
||||||
// Create a clipping group if only part of the image should be visible.
|
// Create a clipping group if only part of the image should be visible.
|
||||||
@ -132,25 +79,3 @@ pub fn layout_image(
|
|||||||
|
|
||||||
Ok(frame)
|
Ok(frame)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Try to determine the image format based on the data.
|
|
||||||
fn determine_format(source: &DataSource, data: &Bytes) -> StrResult<ImageFormat> {
|
|
||||||
if let DataSource::Path(path) = source {
|
|
||||||
let ext = std::path::Path::new(path.as_str())
|
|
||||||
.extension()
|
|
||||||
.and_then(OsStr::to_str)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.to_lowercase();
|
|
||||||
|
|
||||||
match ext.as_str() {
|
|
||||||
"png" => return Ok(ExchangeFormat::Png.into()),
|
|
||||||
"jpg" | "jpeg" => return Ok(ExchangeFormat::Jpg.into()),
|
|
||||||
"gif" => return Ok(ExchangeFormat::Gif.into()),
|
|
||||||
"svg" | "svgz" => return Ok(VectorFormat::Svg.into()),
|
|
||||||
"webp" => return Ok(ExchangeFormat::Webp.into()),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(ImageFormat::detect(data).ok_or("unknown image format")?)
|
|
||||||
}
|
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
use std::f64::consts::SQRT_2;
|
use std::f64::consts::SQRT_2;
|
||||||
|
|
||||||
|
use codex::styling::{to_style, MathStyle};
|
||||||
use ecow::EcoString;
|
use ecow::EcoString;
|
||||||
use typst_library::diag::SourceResult;
|
use typst_library::diag::SourceResult;
|
||||||
use typst_library::foundations::{Packed, StyleChain, SymbolElem};
|
use typst_library::foundations::{Packed, StyleChain, SymbolElem};
|
||||||
use typst_library::layout::{Abs, Size};
|
use typst_library::layout::{Abs, Size};
|
||||||
use typst_library::math::{EquationElem, MathSize, MathVariant};
|
use typst_library::math::{EquationElem, MathSize};
|
||||||
use typst_library::text::{
|
use typst_library::text::{
|
||||||
BottomEdge, BottomEdgeMetric, TextElem, TopEdge, TopEdgeMetric,
|
BottomEdge, BottomEdgeMetric, TextElem, TopEdge, TopEdgeMetric,
|
||||||
};
|
};
|
||||||
@ -64,12 +65,21 @@ fn layout_inline_text(
|
|||||||
ctx: &mut MathContext,
|
ctx: &mut MathContext,
|
||||||
styles: StyleChain,
|
styles: StyleChain,
|
||||||
) -> SourceResult<FrameFragment> {
|
) -> SourceResult<FrameFragment> {
|
||||||
|
let variant = styles.get(EquationElem::variant);
|
||||||
|
let bold = styles.get(EquationElem::bold);
|
||||||
|
// Disable auto-italic.
|
||||||
|
let italic = styles.get(EquationElem::italic).or(Some(false));
|
||||||
|
|
||||||
if text.chars().all(|c| c.is_ascii_digit() || c == '.') {
|
if text.chars().all(|c| c.is_ascii_digit() || c == '.') {
|
||||||
// Small optimization for numbers. Note that this lays out slightly
|
// Small optimization for numbers. Note that this lays out slightly
|
||||||
// differently to normal text and is worth re-evaluating in the future.
|
// differently to normal text and is worth re-evaluating in the future.
|
||||||
let mut fragments = vec![];
|
let mut fragments = vec![];
|
||||||
for unstyled_c in text.chars() {
|
for unstyled_c in text.chars() {
|
||||||
let c = styled_char(styles, unstyled_c, false);
|
// This is fine as ascii digits and '.' can never end up as more
|
||||||
|
// than a single char after styling.
|
||||||
|
let style = MathStyle::select(unstyled_c, variant, bold, italic);
|
||||||
|
let c = to_style(unstyled_c, style).next().unwrap();
|
||||||
|
|
||||||
let glyph = GlyphFragment::new_char(ctx.font, styles, c, span)?;
|
let glyph = GlyphFragment::new_char(ctx.font, styles, c, span)?;
|
||||||
fragments.push(glyph.into());
|
fragments.push(glyph.into());
|
||||||
}
|
}
|
||||||
@ -83,8 +93,10 @@ fn layout_inline_text(
|
|||||||
.map(|p| p.wrap());
|
.map(|p| p.wrap());
|
||||||
|
|
||||||
let styles = styles.chain(&local);
|
let styles = styles.chain(&local);
|
||||||
let styled_text: EcoString =
|
let styled_text: EcoString = text
|
||||||
text.chars().map(|c| styled_char(styles, c, false)).collect();
|
.chars()
|
||||||
|
.flat_map(|c| to_style(c, MathStyle::select(c, variant, bold, italic)))
|
||||||
|
.collect();
|
||||||
|
|
||||||
let spaced = styled_text.graphemes(true).nth(1).is_some();
|
let spaced = styled_text.graphemes(true).nth(1).is_some();
|
||||||
let elem = TextElem::packed(styled_text).spanned(span);
|
let elem = TextElem::packed(styled_text).spanned(span);
|
||||||
@ -124,9 +136,16 @@ pub fn layout_symbol(
|
|||||||
Some(c) if has_dtls_feat(ctx.font) => (c, styles.chain(&dtls)),
|
Some(c) if has_dtls_feat(ctx.font) => (c, styles.chain(&dtls)),
|
||||||
_ => (elem.text, styles),
|
_ => (elem.text, styles),
|
||||||
};
|
};
|
||||||
let c = styled_char(styles, unstyled_c, true);
|
|
||||||
|
let variant = styles.get(EquationElem::variant);
|
||||||
|
let bold = styles.get(EquationElem::bold);
|
||||||
|
let italic = styles.get(EquationElem::italic);
|
||||||
|
|
||||||
|
let style = MathStyle::select(unstyled_c, variant, bold, italic);
|
||||||
|
let text: EcoString = to_style(unstyled_c, style).collect();
|
||||||
|
|
||||||
let fragment: MathFragment =
|
let fragment: MathFragment =
|
||||||
match GlyphFragment::new_char(ctx.font, symbol_styles, c, elem.span()) {
|
match GlyphFragment::new(ctx.font, symbol_styles, &text, elem.span()) {
|
||||||
Ok(mut glyph) => {
|
Ok(mut glyph) => {
|
||||||
adjust_glyph_layout(&mut glyph, ctx, styles);
|
adjust_glyph_layout(&mut glyph, ctx, styles);
|
||||||
glyph.into()
|
glyph.into()
|
||||||
@ -134,8 +153,7 @@ pub fn layout_symbol(
|
|||||||
Err(_) => {
|
Err(_) => {
|
||||||
// Not in the math font, fallback to normal inline text layout.
|
// Not in the math font, fallback to normal inline text layout.
|
||||||
// TODO: Should replace this with proper fallback in [`GlyphFragment::new`].
|
// TODO: Should replace this with proper fallback in [`GlyphFragment::new`].
|
||||||
layout_inline_text(c.encode_utf8(&mut [0; 4]), elem.span(), ctx, styles)?
|
layout_inline_text(&text, elem.span(), ctx, styles)?.into()
|
||||||
.into()
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
ctx.push(fragment);
|
ctx.push(fragment);
|
||||||
@ -161,226 +179,6 @@ fn adjust_glyph_layout(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Style the character by selecting the unicode codepoint for italic, bold,
|
|
||||||
/// caligraphic, etc.
|
|
||||||
///
|
|
||||||
/// <https://www.w3.org/TR/mathml-core/#new-text-transform-mappings>
|
|
||||||
/// <https://en.wikipedia.org/wiki/Mathematical_Alphanumeric_Symbols>
|
|
||||||
fn styled_char(styles: StyleChain, c: char, auto_italic: bool) -> char {
|
|
||||||
use MathVariant::*;
|
|
||||||
|
|
||||||
let variant = styles.get(EquationElem::variant);
|
|
||||||
let bold = styles.get(EquationElem::bold);
|
|
||||||
let italic = styles.get(EquationElem::italic).unwrap_or(
|
|
||||||
auto_italic
|
|
||||||
&& matches!(
|
|
||||||
c,
|
|
||||||
'a'..='z' | 'ħ' | 'ı' | 'ȷ' | 'A'..='Z' |
|
|
||||||
'α'..='ω' | '∂' | 'ϵ' | 'ϑ' | 'ϰ' | 'ϕ' | 'ϱ' | 'ϖ'
|
|
||||||
)
|
|
||||||
&& matches!(variant, Sans | Serif),
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(c) = basic_exception(c) {
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(c) = latin_exception(c, variant, bold, italic) {
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(c) = greek_exception(c, variant, bold, italic) {
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
|
|
||||||
let base = match c {
|
|
||||||
'A'..='Z' => 'A',
|
|
||||||
'a'..='z' => 'a',
|
|
||||||
'Α'..='Ω' => 'Α',
|
|
||||||
'α'..='ω' => 'α',
|
|
||||||
'0'..='9' => '0',
|
|
||||||
// Hebrew Alef -> Dalet.
|
|
||||||
'\u{05D0}'..='\u{05D3}' => '\u{05D0}',
|
|
||||||
_ => return c,
|
|
||||||
};
|
|
||||||
|
|
||||||
let tuple = (variant, bold, italic);
|
|
||||||
let start = match c {
|
|
||||||
// Latin upper.
|
|
||||||
'A'..='Z' => match tuple {
|
|
||||||
(Serif, false, false) => 0x0041,
|
|
||||||
(Serif, true, false) => 0x1D400,
|
|
||||||
(Serif, false, true) => 0x1D434,
|
|
||||||
(Serif, true, true) => 0x1D468,
|
|
||||||
(Sans, false, false) => 0x1D5A0,
|
|
||||||
(Sans, true, false) => 0x1D5D4,
|
|
||||||
(Sans, false, true) => 0x1D608,
|
|
||||||
(Sans, true, true) => 0x1D63C,
|
|
||||||
(Cal, false, _) => 0x1D49C,
|
|
||||||
(Cal, true, _) => 0x1D4D0,
|
|
||||||
(Frak, false, _) => 0x1D504,
|
|
||||||
(Frak, true, _) => 0x1D56C,
|
|
||||||
(Mono, _, _) => 0x1D670,
|
|
||||||
(Bb, _, _) => 0x1D538,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Latin lower.
|
|
||||||
'a'..='z' => match tuple {
|
|
||||||
(Serif, false, false) => 0x0061,
|
|
||||||
(Serif, true, false) => 0x1D41A,
|
|
||||||
(Serif, false, true) => 0x1D44E,
|
|
||||||
(Serif, true, true) => 0x1D482,
|
|
||||||
(Sans, false, false) => 0x1D5BA,
|
|
||||||
(Sans, true, false) => 0x1D5EE,
|
|
||||||
(Sans, false, true) => 0x1D622,
|
|
||||||
(Sans, true, true) => 0x1D656,
|
|
||||||
(Cal, false, _) => 0x1D4B6,
|
|
||||||
(Cal, true, _) => 0x1D4EA,
|
|
||||||
(Frak, false, _) => 0x1D51E,
|
|
||||||
(Frak, true, _) => 0x1D586,
|
|
||||||
(Mono, _, _) => 0x1D68A,
|
|
||||||
(Bb, _, _) => 0x1D552,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Greek upper.
|
|
||||||
'Α'..='Ω' => match tuple {
|
|
||||||
(Serif, false, false) => 0x0391,
|
|
||||||
(Serif, true, false) => 0x1D6A8,
|
|
||||||
(Serif, false, true) => 0x1D6E2,
|
|
||||||
(Serif, true, true) => 0x1D71C,
|
|
||||||
(Sans, _, false) => 0x1D756,
|
|
||||||
(Sans, _, true) => 0x1D790,
|
|
||||||
(Cal | Frak | Mono | Bb, _, _) => return c,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Greek lower.
|
|
||||||
'α'..='ω' => match tuple {
|
|
||||||
(Serif, false, false) => 0x03B1,
|
|
||||||
(Serif, true, false) => 0x1D6C2,
|
|
||||||
(Serif, false, true) => 0x1D6FC,
|
|
||||||
(Serif, true, true) => 0x1D736,
|
|
||||||
(Sans, _, false) => 0x1D770,
|
|
||||||
(Sans, _, true) => 0x1D7AA,
|
|
||||||
(Cal | Frak | Mono | Bb, _, _) => return c,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Hebrew Alef -> Dalet.
|
|
||||||
'\u{05D0}'..='\u{05D3}' => 0x2135,
|
|
||||||
|
|
||||||
// Numbers.
|
|
||||||
'0'..='9' => match tuple {
|
|
||||||
(Serif, false, _) => 0x0030,
|
|
||||||
(Serif, true, _) => 0x1D7CE,
|
|
||||||
(Bb, _, _) => 0x1D7D8,
|
|
||||||
(Sans, false, _) => 0x1D7E2,
|
|
||||||
(Sans, true, _) => 0x1D7EC,
|
|
||||||
(Mono, _, _) => 0x1D7F6,
|
|
||||||
(Cal | Frak, _, _) => return c,
|
|
||||||
},
|
|
||||||
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
std::char::from_u32(start + (c as u32 - base as u32)).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn basic_exception(c: char) -> Option<char> {
|
|
||||||
Some(match c {
|
|
||||||
'〈' => '⟨',
|
|
||||||
'〉' => '⟩',
|
|
||||||
'《' => '⟪',
|
|
||||||
'》' => '⟫',
|
|
||||||
_ => return None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn latin_exception(
|
|
||||||
c: char,
|
|
||||||
variant: MathVariant,
|
|
||||||
bold: bool,
|
|
||||||
italic: bool,
|
|
||||||
) -> Option<char> {
|
|
||||||
use MathVariant::*;
|
|
||||||
Some(match (c, variant, bold, italic) {
|
|
||||||
('B', Cal, false, _) => 'ℬ',
|
|
||||||
('E', Cal, false, _) => 'ℰ',
|
|
||||||
('F', Cal, false, _) => 'ℱ',
|
|
||||||
('H', Cal, false, _) => 'ℋ',
|
|
||||||
('I', Cal, false, _) => 'ℐ',
|
|
||||||
('L', Cal, false, _) => 'ℒ',
|
|
||||||
('M', Cal, false, _) => 'ℳ',
|
|
||||||
('R', Cal, false, _) => 'ℛ',
|
|
||||||
('C', Frak, false, _) => 'ℭ',
|
|
||||||
('H', Frak, false, _) => 'ℌ',
|
|
||||||
('I', Frak, false, _) => 'ℑ',
|
|
||||||
('R', Frak, false, _) => 'ℜ',
|
|
||||||
('Z', Frak, false, _) => 'ℨ',
|
|
||||||
('C', Bb, ..) => 'ℂ',
|
|
||||||
('H', Bb, ..) => 'ℍ',
|
|
||||||
('N', Bb, ..) => 'ℕ',
|
|
||||||
('P', Bb, ..) => 'ℙ',
|
|
||||||
('Q', Bb, ..) => 'ℚ',
|
|
||||||
('R', Bb, ..) => 'ℝ',
|
|
||||||
('Z', Bb, ..) => 'ℤ',
|
|
||||||
('D', Bb, _, true) => 'ⅅ',
|
|
||||||
('d', Bb, _, true) => 'ⅆ',
|
|
||||||
('e', Bb, _, true) => 'ⅇ',
|
|
||||||
('i', Bb, _, true) => 'ⅈ',
|
|
||||||
('j', Bb, _, true) => 'ⅉ',
|
|
||||||
('h', Serif, false, true) => 'ℎ',
|
|
||||||
('e', Cal, false, _) => 'ℯ',
|
|
||||||
('g', Cal, false, _) => 'ℊ',
|
|
||||||
('o', Cal, false, _) => 'ℴ',
|
|
||||||
('ħ', Serif, .., true) => 'ℏ',
|
|
||||||
('ı', Serif, .., true) => '𝚤',
|
|
||||||
('ȷ', Serif, .., true) => '𝚥',
|
|
||||||
_ => return None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn greek_exception(
|
|
||||||
c: char,
|
|
||||||
variant: MathVariant,
|
|
||||||
bold: bool,
|
|
||||||
italic: bool,
|
|
||||||
) -> Option<char> {
|
|
||||||
use MathVariant::*;
|
|
||||||
if c == 'Ϝ' && variant == Serif && bold {
|
|
||||||
return Some('𝟊');
|
|
||||||
}
|
|
||||||
if c == 'ϝ' && variant == Serif && bold {
|
|
||||||
return Some('𝟋');
|
|
||||||
}
|
|
||||||
|
|
||||||
let list = match c {
|
|
||||||
'ϴ' => ['𝚹', '𝛳', '𝜭', '𝝧', '𝞡', 'ϴ'],
|
|
||||||
'∇' => ['𝛁', '𝛻', '𝜵', '𝝯', '𝞩', '∇'],
|
|
||||||
'∂' => ['𝛛', '𝜕', '𝝏', '𝞉', '𝟃', '∂'],
|
|
||||||
'ϵ' => ['𝛜', '𝜖', '𝝐', '𝞊', '𝟄', 'ϵ'],
|
|
||||||
'ϑ' => ['𝛝', '𝜗', '𝝑', '𝞋', '𝟅', 'ϑ'],
|
|
||||||
'ϰ' => ['𝛞', '𝜘', '𝝒', '𝞌', '𝟆', 'ϰ'],
|
|
||||||
'ϕ' => ['𝛟', '𝜙', '𝝓', '𝞍', '𝟇', 'ϕ'],
|
|
||||||
'ϱ' => ['𝛠', '𝜚', '𝝔', '𝞎', '𝟈', 'ϱ'],
|
|
||||||
'ϖ' => ['𝛡', '𝜛', '𝝕', '𝞏', '𝟉', 'ϖ'],
|
|
||||||
'Γ' => ['𝚪', '𝛤', '𝜞', '𝝘', '𝞒', 'ℾ'],
|
|
||||||
'γ' => ['𝛄', '𝛾', '𝜸', '𝝲', '𝞬', 'ℽ'],
|
|
||||||
'Π' => ['𝚷', '𝛱', '𝜫', '𝝥', '𝞟', 'ℿ'],
|
|
||||||
'π' => ['𝛑', '𝜋', '𝝅', '𝝿', '𝞹', 'ℼ'],
|
|
||||||
'∑' => ['∑', '∑', '∑', '∑', '∑', '⅀'],
|
|
||||||
_ => return None,
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(match (variant, bold, italic) {
|
|
||||||
(Serif, true, false) => list[0],
|
|
||||||
(Serif, false, true) => list[1],
|
|
||||||
(Serif, true, true) => list[2],
|
|
||||||
(Sans, _, false) => list[3],
|
|
||||||
(Sans, _, true) => list[4],
|
|
||||||
(Bb, ..) => list[5],
|
|
||||||
_ => return None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The non-dotless version of a dotless character that can be used with the
|
/// The non-dotless version of a dotless character that can be used with the
|
||||||
/// `dtls` OpenType feature.
|
/// `dtls` OpenType feature.
|
||||||
pub fn try_dotless(c: char) -> Option<char> {
|
pub fn try_dotless(c: char) -> Option<char> {
|
||||||
|
@ -4,14 +4,16 @@ mod collect;
|
|||||||
mod finalize;
|
mod finalize;
|
||||||
mod run;
|
mod run;
|
||||||
|
|
||||||
|
use std::num::NonZeroUsize;
|
||||||
|
|
||||||
use comemo::{Tracked, TrackedMut};
|
use comemo::{Tracked, TrackedMut};
|
||||||
use typst_library::diag::SourceResult;
|
use typst_library::diag::SourceResult;
|
||||||
use typst_library::engine::{Engine, Route, Sink, Traced};
|
use typst_library::engine::{Engine, Route, Sink, Traced};
|
||||||
use typst_library::foundations::{Content, StyleChain};
|
use typst_library::foundations::{Content, StyleChain};
|
||||||
use typst_library::introspection::{
|
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::model::DocumentInfo;
|
||||||
use typst_library::routines::{Arenas, Pair, RealizationKind, Routines};
|
use typst_library::routines::{Arenas, Pair, RealizationKind, Routines};
|
||||||
use typst_library::World;
|
use typst_library::World;
|
||||||
@ -75,7 +77,7 @@ fn layout_document_impl(
|
|||||||
let arenas = Arenas::default();
|
let arenas = Arenas::default();
|
||||||
let mut info = DocumentInfo::default();
|
let mut info = DocumentInfo::default();
|
||||||
let mut children = (engine.routines.realize)(
|
let mut children = (engine.routines.realize)(
|
||||||
RealizationKind::LayoutDocument(&mut info),
|
RealizationKind::LayoutDocument { info: &mut info },
|
||||||
&mut engine,
|
&mut engine,
|
||||||
&mut locator,
|
&mut locator,
|
||||||
&arenas,
|
&arenas,
|
||||||
@ -84,7 +86,7 @@ fn layout_document_impl(
|
|||||||
)?;
|
)?;
|
||||||
|
|
||||||
let pages = layout_pages(&mut engine, &mut children, &mut locator, styles)?;
|
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 })
|
Ok(PagedDocument { pages, info, introspector })
|
||||||
}
|
}
|
||||||
@ -157,3 +159,27 @@ fn layout_pages<'a>(
|
|||||||
|
|
||||||
Ok(pages)
|
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)
|
||||||
|
}
|
||||||
|
@ -64,6 +64,16 @@ impl<T: NativeElement> Packed<T> {
|
|||||||
self.0
|
self.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pack back into a reference to content.
|
||||||
|
pub fn pack_ref(&self) -> &Content {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pack back into a mutable reference to content.
|
||||||
|
pub fn pack_mut(&mut self) -> &mut Content {
|
||||||
|
&mut self.0
|
||||||
|
}
|
||||||
|
|
||||||
/// Extract the raw underlying element.
|
/// Extract the raw underlying element.
|
||||||
pub fn unpack(self) -> T {
|
pub fn unpack(self) -> T {
|
||||||
// This function doesn't yet need owned self, but might in the future.
|
// This function doesn't yet need owned self, but might in the future.
|
||||||
@ -94,10 +104,6 @@ impl<T: NativeElement> Packed<T> {
|
|||||||
pub fn set_location(&mut self, location: Location) {
|
pub fn set_location(&mut self, location: Location) {
|
||||||
self.0.set_location(location);
|
self.0.set_location(location);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn as_content(&self) -> &Content {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: NativeElement> AsRef<T> for Packed<T> {
|
impl<T: NativeElement> AsRef<T> for Packed<T> {
|
||||||
|
@ -141,7 +141,7 @@ impl RawContent {
|
|||||||
|
|
||||||
/// Clones a packed element into new raw content.
|
/// Clones a packed element into new raw content.
|
||||||
pub(super) fn clone_impl<E: NativeElement>(elem: &Packed<E>) -> Self {
|
pub(super) fn clone_impl<E: NativeElement>(elem: &Packed<E>) -> Self {
|
||||||
let raw = &elem.as_content().0;
|
let raw = &elem.pack_ref().0;
|
||||||
let header = raw.header();
|
let header = raw.header();
|
||||||
RawContent::create(
|
RawContent::create(
|
||||||
elem.as_ref().clone(),
|
elem.as_ref().clone(),
|
||||||
|
@ -1,823 +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 {
|
|
||||||
/// 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::diag::{bail, StrResult};
|
||||||
use crate::foundations::{Content, Label, Repr, Selector};
|
use crate::foundations::{Content, Label, Repr, Selector};
|
||||||
use crate::html::HtmlNode;
|
|
||||||
use crate::introspection::{Location, Tag};
|
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;
|
use crate::model::Numbering;
|
||||||
|
|
||||||
/// Can be queried for elements and their positions.
|
/// Can be queried for elements and their positions.
|
||||||
@ -47,18 +46,6 @@ pub struct Introspector {
|
|||||||
type Pair = (Content, Position);
|
type Pair = (Content, Position);
|
||||||
|
|
||||||
impl Introspector {
|
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.
|
/// Iterates over all locatable elements.
|
||||||
pub fn all(&self) -> impl Iterator<Item = &Content> + '_ {
|
pub fn all(&self) -> impl Iterator<Item = &Content> + '_ {
|
||||||
self.elems.iter().map(|(c, _)| c)
|
self.elems.iter().map(|(c, _)| c)
|
||||||
@ -352,10 +339,10 @@ impl Clone for QueryCache {
|
|||||||
|
|
||||||
/// Builds the introspector.
|
/// Builds the introspector.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct IntrospectorBuilder {
|
pub struct IntrospectorBuilder {
|
||||||
pages: usize,
|
pub pages: usize,
|
||||||
page_numberings: Vec<Option<Numbering>>,
|
pub page_numberings: Vec<Option<Numbering>>,
|
||||||
page_supplements: Vec<Content>,
|
pub page_supplements: Vec<Content>,
|
||||||
seen: HashSet<Location>,
|
seen: HashSet<Location>,
|
||||||
insertions: MultiMap<Location, Vec<Pair>>,
|
insertions: MultiMap<Location, Vec<Pair>>,
|
||||||
keys: MultiMap<u128, Location>,
|
keys: MultiMap<u128, Location>,
|
||||||
@ -365,41 +352,12 @@ struct IntrospectorBuilder {
|
|||||||
|
|
||||||
impl IntrospectorBuilder {
|
impl IntrospectorBuilder {
|
||||||
/// Create an empty builder.
|
/// Create an empty builder.
|
||||||
fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
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.
|
/// Processes the tags in the frame.
|
||||||
fn discover_in_frame(
|
pub fn discover_in_frame(
|
||||||
&mut self,
|
&mut self,
|
||||||
sink: &mut Vec<Pair>,
|
sink: &mut Vec<Pair>,
|
||||||
frame: &Frame,
|
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.
|
/// 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 {
|
match tag {
|
||||||
Tag::Start(elem) => {
|
Tag::Start(elem) => {
|
||||||
let loc = elem.location().unwrap();
|
let loc = elem.location().unwrap();
|
||||||
@ -471,7 +413,7 @@ impl IntrospectorBuilder {
|
|||||||
|
|
||||||
/// Build a complete introspector with all acceleration structures from a
|
/// Build a complete introspector with all acceleration structures from a
|
||||||
/// list of top-level pairs.
|
/// 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());
|
self.locations.reserve(self.seen.len());
|
||||||
|
|
||||||
// Save all pairs and their descendants in the correct order.
|
// 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 diag;
|
||||||
pub mod engine;
|
pub mod engine;
|
||||||
pub mod foundations;
|
pub mod foundations;
|
||||||
pub mod html;
|
|
||||||
pub mod introspection;
|
pub mod introspection;
|
||||||
pub mod layout;
|
pub mod layout;
|
||||||
pub mod loading;
|
pub mod loading;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use std::num::NonZeroUsize;
|
use std::num::NonZeroUsize;
|
||||||
|
|
||||||
|
use codex::styling::MathVariant;
|
||||||
use typst_utils::NonZeroExt;
|
use typst_utils::NonZeroExt;
|
||||||
use unicode_math_class::MathClass;
|
use unicode_math_class::MathClass;
|
||||||
|
|
||||||
@ -12,7 +13,7 @@ use crate::introspection::{Count, Counter, CounterUpdate, Locatable};
|
|||||||
use crate::layout::{
|
use crate::layout::{
|
||||||
AlignElem, Alignment, BlockElem, OuterHAlignment, SpecificAlignment, VAlignment,
|
AlignElem, Alignment, BlockElem, OuterHAlignment, SpecificAlignment, VAlignment,
|
||||||
};
|
};
|
||||||
use crate::math::{MathSize, MathVariant};
|
use crate::math::MathSize;
|
||||||
use crate::model::{Numbering, Outlinable, ParLine, Refable, Supplement};
|
use crate::model::{Numbering, Outlinable, ParLine, Refable, Supplement};
|
||||||
use crate::text::{FontFamily, FontList, FontWeight, LocalName, TextElem};
|
use crate::text::{FontFamily, FontList, FontWeight, LocalName, TextElem};
|
||||||
|
|
||||||
@ -111,7 +112,7 @@ pub struct EquationElem {
|
|||||||
/// The style variant to select.
|
/// The style variant to select.
|
||||||
#[internal]
|
#[internal]
|
||||||
#[ghost]
|
#[ghost]
|
||||||
pub variant: MathVariant,
|
pub variant: Option<MathVariant>,
|
||||||
|
|
||||||
/// Affects the height of exponents.
|
/// Affects the height of exponents.
|
||||||
#[internal]
|
#[internal]
|
||||||
@ -128,7 +129,7 @@ pub struct EquationElem {
|
|||||||
/// Whether to use italic glyphs.
|
/// Whether to use italic glyphs.
|
||||||
#[internal]
|
#[internal]
|
||||||
#[ghost]
|
#[ghost]
|
||||||
pub italic: Smart<bool>,
|
pub italic: Option<bool>,
|
||||||
|
|
||||||
/// A forced class to use for all fragment.
|
/// A forced class to use for all fragment.
|
||||||
#[internal]
|
#[internal]
|
||||||
|
@ -80,6 +80,7 @@ pub fn module() -> Module {
|
|||||||
math.define_func::<italic>();
|
math.define_func::<italic>();
|
||||||
math.define_func::<serif>();
|
math.define_func::<serif>();
|
||||||
math.define_func::<sans>();
|
math.define_func::<sans>();
|
||||||
|
math.define_func::<scr>();
|
||||||
math.define_func::<cal>();
|
math.define_func::<cal>();
|
||||||
math.define_func::<frak>();
|
math.define_func::<frak>();
|
||||||
math.define_func::<mono>();
|
math.define_func::<mono>();
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
use crate::foundations::{func, Cast, Content, Smart};
|
use codex::styling::MathVariant;
|
||||||
|
|
||||||
|
use crate::foundations::{func, Cast, Content};
|
||||||
use crate::math::EquationElem;
|
use crate::math::EquationElem;
|
||||||
|
|
||||||
/// Bold font style in math.
|
/// Bold font style in math.
|
||||||
@ -24,7 +26,7 @@ pub fn upright(
|
|||||||
/// The content to style.
|
/// The content to style.
|
||||||
body: Content,
|
body: Content,
|
||||||
) -> Content {
|
) -> Content {
|
||||||
body.set(EquationElem::italic, Smart::Custom(false))
|
body.set(EquationElem::italic, Some(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Italic font style in math.
|
/// Italic font style in math.
|
||||||
@ -35,7 +37,7 @@ pub fn italic(
|
|||||||
/// The content to style.
|
/// The content to style.
|
||||||
body: Content,
|
body: Content,
|
||||||
) -> Content {
|
) -> Content {
|
||||||
body.set(EquationElem::italic, Smart::Custom(true))
|
body.set(EquationElem::italic, Some(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Serif (roman) font style in math.
|
/// Serif (roman) font style in math.
|
||||||
@ -46,7 +48,7 @@ pub fn serif(
|
|||||||
/// The content to style.
|
/// The content to style.
|
||||||
body: Content,
|
body: Content,
|
||||||
) -> Content {
|
) -> Content {
|
||||||
body.set(EquationElem::variant, MathVariant::Serif)
|
body.set(EquationElem::variant, Some(MathVariant::Plain))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sans-serif font style in math.
|
/// Sans-serif font style in math.
|
||||||
@ -59,23 +61,39 @@ pub fn sans(
|
|||||||
/// The content to style.
|
/// The content to style.
|
||||||
body: Content,
|
body: Content,
|
||||||
) -> Content {
|
) -> Content {
|
||||||
body.set(EquationElem::variant, MathVariant::Sans)
|
body.set(EquationElem::variant, Some(MathVariant::SansSerif))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calligraphic font style in math.
|
/// Calligraphic (chancery) font style in math.
|
||||||
///
|
///
|
||||||
/// ```example
|
/// ```example
|
||||||
/// Let $cal(P)$ be the set of ...
|
/// Let $cal(P)$ be the set of ...
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// This corresponds both to LaTeX's `\mathcal` and `\mathscr` as both of these
|
/// This is the default calligraphic/script style for most math fonts. See
|
||||||
/// styles share the same Unicode codepoints. Switching between the styles is
|
/// [`scr`]($math.scr) for more on how to get the other style (roundhand).
|
||||||
/// thus only possible if supported by the font via
|
#[func(title = "Calligraphic", keywords = ["mathcal", "chancery"])]
|
||||||
/// [font features]($text.features).
|
pub fn cal(
|
||||||
|
/// The content to style.
|
||||||
|
body: Content,
|
||||||
|
) -> Content {
|
||||||
|
body.set(EquationElem::variant, Some(MathVariant::Chancery))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Script (roundhand) font style in math.
|
||||||
///
|
///
|
||||||
/// For the default math font, the roundhand style is available through the
|
/// ```example
|
||||||
/// `ss01` feature. Therefore, you could define your own version of `\mathscr`
|
/// $ scr(S) $
|
||||||
/// like this:
|
/// ```
|
||||||
|
///
|
||||||
|
/// There are two ways that fonts can support differentiating `cal` and `scr`.
|
||||||
|
/// The first is using Unicode variation sequences. This works out of the box
|
||||||
|
/// in Typst, however only a few math fonts currently support this.
|
||||||
|
///
|
||||||
|
/// The other way is using [font features]($text.features). For example, the
|
||||||
|
/// roundhand style might be available in a font through the `ss01` feature.
|
||||||
|
/// To use it in Typst, you could then define your own version of `scr` like
|
||||||
|
/// this:
|
||||||
///
|
///
|
||||||
/// ```example
|
/// ```example
|
||||||
/// #let scr(it) = text(
|
/// #let scr(it) = text(
|
||||||
@ -88,12 +106,12 @@ pub fn sans(
|
|||||||
///
|
///
|
||||||
/// (The box is not conceptually necessary, but unfortunately currently needed
|
/// (The box is not conceptually necessary, but unfortunately currently needed
|
||||||
/// due to limitations in Typst's text style handling in math.)
|
/// due to limitations in Typst's text style handling in math.)
|
||||||
#[func(title = "Calligraphic", keywords = ["mathcal", "mathscr"])]
|
#[func(title = "Script Style", keywords = ["mathscr", "roundhand"])]
|
||||||
pub fn cal(
|
pub fn scr(
|
||||||
/// The content to style.
|
/// The content to style.
|
||||||
body: Content,
|
body: Content,
|
||||||
) -> Content {
|
) -> Content {
|
||||||
body.set(EquationElem::variant, MathVariant::Cal)
|
body.set(EquationElem::variant, Some(MathVariant::Roundhand))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fraktur font style in math.
|
/// Fraktur font style in math.
|
||||||
@ -106,7 +124,7 @@ pub fn frak(
|
|||||||
/// The content to style.
|
/// The content to style.
|
||||||
body: Content,
|
body: Content,
|
||||||
) -> Content {
|
) -> Content {
|
||||||
body.set(EquationElem::variant, MathVariant::Frak)
|
body.set(EquationElem::variant, Some(MathVariant::Fraktur))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Monospace font style in math.
|
/// Monospace font style in math.
|
||||||
@ -119,7 +137,7 @@ pub fn mono(
|
|||||||
/// The content to style.
|
/// The content to style.
|
||||||
body: Content,
|
body: Content,
|
||||||
) -> Content {
|
) -> Content {
|
||||||
body.set(EquationElem::variant, MathVariant::Mono)
|
body.set(EquationElem::variant, Some(MathVariant::Monospace))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Blackboard bold (double-struck) font style in math.
|
/// Blackboard bold (double-struck) font style in math.
|
||||||
@ -137,7 +155,7 @@ pub fn bb(
|
|||||||
/// The content to style.
|
/// The content to style.
|
||||||
body: Content,
|
body: Content,
|
||||||
) -> Content {
|
) -> Content {
|
||||||
body.set(EquationElem::variant, MathVariant::Bb)
|
body.set(EquationElem::variant, Some(MathVariant::DoubleStruck))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Forced display style in math.
|
/// Forced display style in math.
|
||||||
@ -240,15 +258,3 @@ pub enum MathSize {
|
|||||||
/// Math on its own line.
|
/// Math on its own line.
|
||||||
Display,
|
Display,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A mathematical style variant, as defined by Unicode.
|
|
||||||
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Cast, Hash)]
|
|
||||||
pub enum MathVariant {
|
|
||||||
#[default]
|
|
||||||
Serif,
|
|
||||||
Sans,
|
|
||||||
Cal,
|
|
||||||
Frak,
|
|
||||||
Mono,
|
|
||||||
Bb,
|
|
||||||
}
|
|
||||||
|
@ -101,20 +101,25 @@ routines! {
|
|||||||
pub enum RealizationKind<'a> {
|
pub enum RealizationKind<'a> {
|
||||||
/// This the root realization for layout. Requires a mutable reference
|
/// This the root realization for layout. Requires a mutable reference
|
||||||
/// to document metadata that will be filled from `set document` rules.
|
/// 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
|
/// 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
|
/// reference to an enum that will be set to `FragmentKind::Inline` if the
|
||||||
/// fragment's content was fully inline.
|
/// 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`)
|
/// A nested realization in a paragraph (i.e. a `par`)
|
||||||
LayoutPar,
|
LayoutPar,
|
||||||
/// This the root realization for HTML. Requires a mutable reference
|
/// This the root realization for HTML. Requires a mutable reference to
|
||||||
/// to document metadata that will be filled from `set document` rules.
|
/// document metadata that will be filled from `set document` rules.
|
||||||
HtmlDocument(&'a mut DocumentInfo),
|
///
|
||||||
|
/// 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
|
/// 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
|
/// reference to an enum that will be set to `FragmentKind::Inline` if the
|
||||||
/// fragment's content was fully inline.
|
/// fragment's content was fully inline.
|
||||||
HtmlFragment(&'a mut FragmentKind),
|
HtmlFragment { kind: &'a mut FragmentKind, is_inline: fn(&Content) -> bool },
|
||||||
/// A realization within math.
|
/// A realization within math.
|
||||||
Math,
|
Math,
|
||||||
}
|
}
|
||||||
@ -122,18 +127,20 @@ pub enum RealizationKind<'a> {
|
|||||||
impl RealizationKind<'_> {
|
impl RealizationKind<'_> {
|
||||||
/// It this a realization for HTML export?
|
/// It this a realization for HTML export?
|
||||||
pub fn is_html(&self) -> bool {
|
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?
|
/// It this a realization for a container?
|
||||||
pub fn is_fragment(&self) -> bool {
|
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.
|
/// If this is a document-level realization, accesses the document info.
|
||||||
pub fn as_document_mut(&mut self) -> Option<&mut DocumentInfo> {
|
pub fn as_document_mut(&mut self) -> Option<&mut DocumentInfo> {
|
||||||
match self {
|
match self {
|
||||||
Self::LayoutDocument(info) | Self::HtmlDocument(info) => Some(*info),
|
Self::LayoutDocument { info } | Self::HtmlDocument { info, .. } => {
|
||||||
|
Some(*info)
|
||||||
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -141,7 +148,9 @@ impl RealizationKind<'_> {
|
|||||||
/// If this is a container-level realization, accesses the fragment kind.
|
/// If this is a container-level realization, accesses the fragment kind.
|
||||||
pub fn as_fragment_mut(&mut self) -> Option<&mut FragmentKind> {
|
pub fn as_fragment_mut(&mut self) -> Option<&mut FragmentKind> {
|
||||||
match self {
|
match self {
|
||||||
Self::LayoutFragment(kind) | Self::HtmlFragment(kind) => Some(*kind),
|
Self::LayoutFragment { kind } | Self::HtmlFragment { kind, .. } => {
|
||||||
|
Some(*kind)
|
||||||
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ pub use self::raster::{
|
|||||||
};
|
};
|
||||||
pub use self::svg::SvgImage;
|
pub use self::svg::SvgImage;
|
||||||
|
|
||||||
|
use std::ffi::OsStr;
|
||||||
use std::fmt::{self, Debug, Formatter};
|
use std::fmt::{self, Debug, Formatter};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@ -15,14 +16,16 @@ use ecow::EcoString;
|
|||||||
use typst_syntax::{Span, Spanned};
|
use typst_syntax::{Span, Spanned};
|
||||||
use typst_utils::LazyHash;
|
use typst_utils::LazyHash;
|
||||||
|
|
||||||
use crate::diag::StrResult;
|
use crate::diag::{warning, At, LoadedWithin, SourceResult, StrResult};
|
||||||
|
use crate::engine::Engine;
|
||||||
use crate::foundations::{
|
use crate::foundations::{
|
||||||
cast, elem, func, scope, Bytes, Cast, Content, Derived, NativeElement, Packed, Smart,
|
cast, elem, func, scope, Bytes, Cast, Content, Derived, NativeElement, Packed, Smart,
|
||||||
|
StyleChain,
|
||||||
};
|
};
|
||||||
use crate::layout::{Length, Rel, Sizing};
|
use crate::layout::{Length, Rel, Sizing};
|
||||||
use crate::loading::{DataSource, Load, LoadSource, Loaded, Readable};
|
use crate::loading::{DataSource, Load, LoadSource, Loaded, Readable};
|
||||||
use crate::model::Figurable;
|
use crate::model::Figurable;
|
||||||
use crate::text::LocalName;
|
use crate::text::{families, LocalName};
|
||||||
|
|
||||||
/// A raster or vector graphic.
|
/// A raster or vector graphic.
|
||||||
///
|
///
|
||||||
@ -217,6 +220,81 @@ impl ImageElem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Packed<ImageElem> {
|
||||||
|
/// Decodes the image.
|
||||||
|
pub fn decode(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Image> {
|
||||||
|
let span = self.span();
|
||||||
|
let loaded = &self.source.derived;
|
||||||
|
let format = self.determine_format(styles).at(span)?;
|
||||||
|
|
||||||
|
// Warn the user if the image contains a foreign object. Not perfect
|
||||||
|
// because the svg could also be encoded, but that's an edge case.
|
||||||
|
if format == ImageFormat::Vector(VectorFormat::Svg) {
|
||||||
|
let has_foreign_object =
|
||||||
|
memchr::memmem::find(&loaded.data, b"<foreignObject").is_some();
|
||||||
|
|
||||||
|
if has_foreign_object {
|
||||||
|
engine.sink.warn(warning!(
|
||||||
|
span,
|
||||||
|
"image contains foreign object";
|
||||||
|
hint: "SVG images with foreign objects might render incorrectly in typst";
|
||||||
|
hint: "see https://github.com/typst/typst/issues/1421 for more information"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct the image itself.
|
||||||
|
let kind = match format {
|
||||||
|
ImageFormat::Raster(format) => ImageKind::Raster(
|
||||||
|
RasterImage::new(
|
||||||
|
loaded.data.clone(),
|
||||||
|
format,
|
||||||
|
self.icc.get_ref(styles).as_ref().map(|icc| icc.derived.clone()),
|
||||||
|
)
|
||||||
|
.at(span)?,
|
||||||
|
),
|
||||||
|
ImageFormat::Vector(VectorFormat::Svg) => ImageKind::Svg(
|
||||||
|
SvgImage::with_fonts(
|
||||||
|
loaded.data.clone(),
|
||||||
|
engine.world,
|
||||||
|
&families(styles).map(|f| f.as_str()).collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
.within(loaded)?,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Image::new(kind, self.alt.get_cloned(styles), self.scaling.get(styles)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tries to determine the image format based on the format that was
|
||||||
|
/// explicitly defined, or else the extension, or else the data.
|
||||||
|
fn determine_format(&self, styles: StyleChain) -> StrResult<ImageFormat> {
|
||||||
|
if let Smart::Custom(v) = self.format.get(styles) {
|
||||||
|
return Ok(v);
|
||||||
|
};
|
||||||
|
|
||||||
|
let Derived { source, derived: loaded } = &self.source;
|
||||||
|
if let DataSource::Path(path) = source {
|
||||||
|
let ext = std::path::Path::new(path.as_str())
|
||||||
|
.extension()
|
||||||
|
.and_then(OsStr::to_str)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_lowercase();
|
||||||
|
|
||||||
|
match ext.as_str() {
|
||||||
|
"png" => return Ok(ExchangeFormat::Png.into()),
|
||||||
|
"jpg" | "jpeg" => return Ok(ExchangeFormat::Jpg.into()),
|
||||||
|
"gif" => return Ok(ExchangeFormat::Gif.into()),
|
||||||
|
"svg" | "svgz" => return Ok(VectorFormat::Svg.into()),
|
||||||
|
"webp" => return Ok(ExchangeFormat::Webp.into()),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ImageFormat::detect(&loaded.data).ok_or("unknown image format")?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl LocalName for Packed<ImageElem> {
|
impl LocalName for Packed<ImageElem> {
|
||||||
const KEY: &'static str = "figure";
|
const KEY: &'static str = "figure";
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,6 @@ use typst_library::foundations::{
|
|||||||
RecipeIndex, Selector, SequenceElem, ShowSet, Style, StyleChain, StyledElem, Styles,
|
RecipeIndex, Selector, SequenceElem, ShowSet, Style, StyleChain, StyledElem, Styles,
|
||||||
SymbolElem, Synthesize, TargetElem, Transformation,
|
SymbolElem, Synthesize, TargetElem, Transformation,
|
||||||
};
|
};
|
||||||
use typst_library::html::{tag, FrameElem, HtmlElem};
|
|
||||||
use typst_library::introspection::{Locatable, SplitLocator, Tag, TagElem};
|
use typst_library::introspection::{Locatable, SplitLocator, Tag, TagElem};
|
||||||
use typst_library::layout::{
|
use typst_library::layout::{
|
||||||
AlignElem, BoxElem, HElem, InlineElem, PageElem, PagebreakElem, VElem,
|
AlignElem, BoxElem, HElem, InlineElem, PageElem, PagebreakElem, VElem,
|
||||||
@ -48,16 +47,16 @@ pub fn realize<'a>(
|
|||||||
locator,
|
locator,
|
||||||
arenas,
|
arenas,
|
||||||
rules: match kind {
|
rules: match kind {
|
||||||
RealizationKind::LayoutDocument(_) => LAYOUT_RULES,
|
RealizationKind::LayoutDocument { .. } => LAYOUT_RULES,
|
||||||
RealizationKind::LayoutFragment(_) => LAYOUT_RULES,
|
RealizationKind::LayoutFragment { .. } => LAYOUT_RULES,
|
||||||
RealizationKind::LayoutPar => LAYOUT_PAR_RULES,
|
RealizationKind::LayoutPar => LAYOUT_PAR_RULES,
|
||||||
RealizationKind::HtmlDocument(_) => HTML_DOCUMENT_RULES,
|
RealizationKind::HtmlDocument { .. } => HTML_DOCUMENT_RULES,
|
||||||
RealizationKind::HtmlFragment(_) => HTML_FRAGMENT_RULES,
|
RealizationKind::HtmlFragment { .. } => HTML_FRAGMENT_RULES,
|
||||||
RealizationKind::Math => MATH_RULES,
|
RealizationKind::Math => MATH_RULES,
|
||||||
},
|
},
|
||||||
sink: vec![],
|
sink: vec![],
|
||||||
groupings: ArrayVec::new(),
|
groupings: ArrayVec::new(),
|
||||||
outside: matches!(kind, RealizationKind::LayoutDocument(_)),
|
outside: matches!(kind, RealizationKind::LayoutDocument { .. }),
|
||||||
may_attach: false,
|
may_attach: false,
|
||||||
saw_parbreak: false,
|
saw_parbreak: false,
|
||||||
kind,
|
kind,
|
||||||
@ -113,7 +112,7 @@ struct GroupingRule {
|
|||||||
/// be visible to `finish`.
|
/// be visible to `finish`.
|
||||||
tags: bool,
|
tags: bool,
|
||||||
/// Defines which kinds of elements start and make up this kind of grouping.
|
/// 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
|
/// Defines elements that may appear in the interior of the grouping, but
|
||||||
/// not at the edges.
|
/// not at the edges.
|
||||||
inner: fn(&Content) -> bool,
|
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)
|
Ok(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -601,7 +593,7 @@ fn visit_styled<'a>(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if elem == PageElem::ELEM {
|
} else if elem == PageElem::ELEM {
|
||||||
if !matches!(s.kind, RealizationKind::LayoutDocument(_)) {
|
if !matches!(s.kind, RealizationKind::LayoutDocument { .. }) {
|
||||||
bail!(
|
bail!(
|
||||||
style.span(),
|
style.span(),
|
||||||
"page configuration is not allowed inside of containers"
|
"page configuration is not allowed inside of containers"
|
||||||
@ -659,7 +651,7 @@ fn visit_grouping_rules<'a>(
|
|||||||
content: &'a Content,
|
content: &'a Content,
|
||||||
styles: StyleChain<'a>,
|
styles: StyleChain<'a>,
|
||||||
) -> SourceResult<bool> {
|
) -> 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.
|
// Try to continue or finish an existing grouping.
|
||||||
let mut i = 0;
|
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 the element can be added to the active grouping, do it.
|
||||||
if !active.interrupted
|
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));
|
s.sink.push((content, styles));
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
@ -806,7 +798,7 @@ fn finish_innermost_grouping(s: &mut State) -> SourceResult<()> {
|
|||||||
let Grouping { start, rule, .. } = s.groupings.pop().unwrap();
|
let Grouping { start, rule, .. } = s.groupings.pop().unwrap();
|
||||||
|
|
||||||
// Trim trailing non-trigger elements.
|
// 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 end = start + trimmed.len();
|
||||||
let tail = s.store_slice(&s.sink[end..]);
|
let tail = s.store_slice(&s.sink[end..]);
|
||||||
s.sink.truncate(end);
|
s.sink.truncate(end);
|
||||||
@ -885,7 +877,7 @@ static TEXTUAL: GroupingRule = GroupingRule {
|
|||||||
static PAR: GroupingRule = GroupingRule {
|
static PAR: GroupingRule = GroupingRule {
|
||||||
priority: 1,
|
priority: 1,
|
||||||
tags: true,
|
tags: true,
|
||||||
trigger: |content, kind| {
|
trigger: |content, state| {
|
||||||
let elem = content.elem();
|
let elem = content.elem();
|
||||||
elem == TextElem::ELEM
|
elem == TextElem::ELEM
|
||||||
|| elem == HElem::ELEM
|
|| elem == HElem::ELEM
|
||||||
@ -893,10 +885,11 @@ static PAR: GroupingRule = GroupingRule {
|
|||||||
|| elem == SmartQuoteElem::ELEM
|
|| elem == SmartQuoteElem::ELEM
|
||||||
|| elem == InlineElem::ELEM
|
|| elem == InlineElem::ELEM
|
||||||
|| elem == BoxElem::ELEM
|
|| elem == BoxElem::ELEM
|
||||||
|| (kind.is_html()
|
|| match state.kind {
|
||||||
&& content
|
RealizationKind::HtmlDocument { is_inline, .. }
|
||||||
.to_packed::<HtmlElem>()
|
| RealizationKind::HtmlFragment { is_inline, .. } => is_inline(content),
|
||||||
.is_some_and(|elem| tag::is_inline_by_default(elem.tag)))
|
_ => false,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
inner: |content| content.elem() == SpaceElem::ELEM,
|
inner: |content| content.elem() == SpaceElem::ELEM,
|
||||||
interrupt: |elem| elem == ParElem::ELEM || elem == AlignElem::ELEM,
|
interrupt: |elem| elem == ParElem::ELEM || elem == AlignElem::ELEM,
|
||||||
|
@ -18,21 +18,27 @@ impl SVGRenderer {
|
|||||||
self.xml.write_attribute("width", &size.x.to_pt());
|
self.xml.write_attribute("width", &size.x.to_pt());
|
||||||
self.xml.write_attribute("height", &size.y.to_pt());
|
self.xml.write_attribute("height", &size.y.to_pt());
|
||||||
self.xml.write_attribute("preserveAspectRatio", "none");
|
self.xml.write_attribute("preserveAspectRatio", "none");
|
||||||
match image.scaling() {
|
if let Some(value) = convert_image_scaling(image.scaling()) {
|
||||||
Smart::Auto => {}
|
self.xml
|
||||||
Smart::Custom(ImageScaling::Smooth) => {
|
.write_attribute("style", &format_args!("image-rendering: {value}"))
|
||||||
// This is still experimental and not implemented in all major browsers.
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/CSS/image-rendering#browser_compatibility
|
|
||||||
self.xml.write_attribute("style", "image-rendering: smooth")
|
|
||||||
}
|
|
||||||
Smart::Custom(ImageScaling::Pixelated) => {
|
|
||||||
self.xml.write_attribute("style", "image-rendering: pixelated")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
self.xml.end_element();
|
self.xml.end_element();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Converts an image scaling to a CSS `image-rendering` propery value.
|
||||||
|
pub fn convert_image_scaling(scaling: Smart<ImageScaling>) -> Option<&'static str> {
|
||||||
|
match scaling {
|
||||||
|
Smart::Auto => None,
|
||||||
|
Smart::Custom(ImageScaling::Smooth) => {
|
||||||
|
// This is still experimental and not implemented in all major browsers.
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/CSS/image-rendering#browser_compatibility
|
||||||
|
Some("smooth")
|
||||||
|
}
|
||||||
|
Smart::Custom(ImageScaling::Pixelated) => Some("pixelated"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Encode an image into a data URL. The format of the URL is
|
/// Encode an image into a data URL. The format of the URL is
|
||||||
/// `data:image/{format};base64,`.
|
/// `data:image/{format};base64,`.
|
||||||
#[comemo::memoize]
|
#[comemo::memoize]
|
||||||
|
@ -5,6 +5,8 @@ mod paint;
|
|||||||
mod shape;
|
mod shape;
|
||||||
mod text;
|
mod text;
|
||||||
|
|
||||||
|
pub use image::{convert_image_scaling, convert_image_to_base64_url};
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt::{self, Display, Formatter, Write};
|
use std::fmt::{self, Display, Formatter, Write};
|
||||||
|
|
||||||
|
@ -43,12 +43,12 @@ use std::sync::LazyLock;
|
|||||||
|
|
||||||
use comemo::{Track, Tracked, Validate};
|
use comemo::{Track, Tracked, Validate};
|
||||||
use ecow::{eco_format, eco_vec, EcoString, EcoVec};
|
use ecow::{eco_format, eco_vec, EcoString, EcoVec};
|
||||||
|
use typst_html::HtmlDocument;
|
||||||
use typst_library::diag::{
|
use typst_library::diag::{
|
||||||
bail, warning, FileError, SourceDiagnostic, SourceResult, Warned,
|
bail, warning, FileError, SourceDiagnostic, SourceResult, Warned,
|
||||||
};
|
};
|
||||||
use typst_library::engine::{Engine, Route, Sink, Traced};
|
use typst_library::engine::{Engine, Route, Sink, Traced};
|
||||||
use typst_library::foundations::{NativeRuleMap, StyleChain, Styles, Value};
|
use typst_library::foundations::{NativeRuleMap, StyleChain, Styles, Value};
|
||||||
use typst_library::html::HtmlDocument;
|
|
||||||
use typst_library::introspection::Introspector;
|
use typst_library::introspection::Introspector;
|
||||||
use typst_library::layout::PagedDocument;
|
use typst_library::layout::PagedDocument;
|
||||||
use typst_library::routines::Routines;
|
use typst_library::routines::Routines;
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
title: Variants
|
title: Variants
|
||||||
category: math
|
category: math
|
||||||
path: ["math"]
|
path: ["math"]
|
||||||
filter: ["serif", "sans", "frak", "mono", "bb", "cal"]
|
filter: ["serif", "sans", "frak", "mono", "bb", "cal", "scr"]
|
||||||
details: |
|
details: |
|
||||||
Alternate typefaces within formulas.
|
Alternate typefaces within formulas.
|
||||||
|
|
||||||
|
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>
|
8
tests/ref/html/image-jpg-html-base64.html
Normal file
8
tests/ref/html/image-jpg-html-base64.html
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body><img src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAgAA3ADcAAD/4QBiRXhpZgAATU0AKgAAAAgABQESAAMAAAABAAEAAAEaAAUAAAABAAAASgEbAAUAAAABAAAAUgEoAAMAAAABAAEAAAITAAMAAAABAAEAAAAAAAAAAADcAAAAAQAAANwAAAAB/8AACwgAUAAwAQERAP/bAEMACAYGBwYFCAcHBwkJCAoMFA0MCwsMGRITDxQdGh8eHRocHCAkLicgIiwjHBwoNyksMDE0NDQfJzk9ODI8LjM0Mv/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/aAAgBAQAAPwD5/ooooooooooooor7+ooor4Bor7+ooor4Booor7+or4Booor7+or4Bor7+ooor4Bor7+ooor4Bor7+ooor4Bor7+ooor/2Q==" alt="The letter F"></body>
|
||||||
|
</html>
|
10
tests/ref/html/image-scaling-methods.html
Normal file
10
tests/ref/html/image-scaling-methods.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>
|
||||||
|
<div style="display: flex; flex-direction: row; gap: 4pt"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAAKUlEQVR4AQEeAOH/AP8AAAD/AAAA/wCAAAAAgAAAAIAAgIAAAICAgACAcFMHfiTGz0oAAAAASUVORK5CYII=" style="width: 28.346456692913385pt"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAAKUlEQVR4AQEeAOH/AP8AAAD/AAAA/wCAAAAAgAAAAIAAgIAAAICAgACAcFMHfiTGz0oAAAAASUVORK5CYII=" style="image-rendering: smooth; width: 28.346456692913385pt"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAAKUlEQVR4AQEeAOH/AP8AAAD/AAAA/wCAAAAAgAAAAIAAgIAAAICAgACAcFMHfiTGz0oAAAAASUVORK5CYII=" style="image-rendering: pixelated; width: 28.346456692913385pt"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
BIN
tests/ref/math-style-fallback.png
Normal file
BIN
tests/ref/math-style-fallback.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 935 B |
Binary file not shown.
Before Width: | Height: | Size: 296 B After Width: | Height: | Size: 489 B |
BIN
tests/ref/math-style-script.png
Normal file
BIN
tests/ref/math-style-script.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 585 B |
@ -5,10 +5,10 @@ use std::path::PathBuf;
|
|||||||
use ecow::eco_vec;
|
use ecow::eco_vec;
|
||||||
use tiny_skia as sk;
|
use tiny_skia as sk;
|
||||||
use typst::diag::{SourceDiagnostic, SourceResult, Warned};
|
use typst::diag::{SourceDiagnostic, SourceResult, Warned};
|
||||||
use typst::html::HtmlDocument;
|
|
||||||
use typst::layout::{Abs, Frame, FrameItem, PagedDocument, Transform};
|
use typst::layout::{Abs, Frame, FrameItem, PagedDocument, Transform};
|
||||||
use typst::visualize::Color;
|
use typst::visualize::Color;
|
||||||
use typst::{Document, WorldExt};
|
use typst::{Document, WorldExt};
|
||||||
|
use typst_html::HtmlDocument;
|
||||||
use typst_pdf::PdfOptions;
|
use typst_pdf::PdfOptions;
|
||||||
use typst_syntax::{FileId, Lines};
|
use typst_syntax::{FileId, Lines};
|
||||||
|
|
||||||
|
@ -12,6 +12,15 @@ $A, italic(A), upright(A), bold(A), bold(upright(A)), \
|
|||||||
bb("hello") + bold(cal("world")), \
|
bb("hello") + bold(cal("world")), \
|
||||||
mono("SQRT")(x) wreath mono(123 + 456)$
|
mono("SQRT")(x) wreath mono(123 + 456)$
|
||||||
|
|
||||||
|
--- math-style-fallback ---
|
||||||
|
// Test how math styles fallback.
|
||||||
|
$upright(frak(bold(alpha))) = upright(bold(alpha)) \
|
||||||
|
bold(mono(ϝ)) = bold(ϝ) \
|
||||||
|
sans(Theta) = bold(sans(Theta)) \
|
||||||
|
bold(upright(planck)) != planck \
|
||||||
|
bb(e) != italic(bb(e)) \
|
||||||
|
serif(sans(A)) != serif(A)$
|
||||||
|
|
||||||
--- math-style-dotless ---
|
--- math-style-dotless ---
|
||||||
// Test styling dotless i and j.
|
// Test styling dotless i and j.
|
||||||
$ dotless.i dotless.j,
|
$ dotless.i dotless.j,
|
||||||
@ -38,7 +47,15 @@ $bb(Gamma) , bb(gamma), bb(Pi), bb(pi), bb(sum)$
|
|||||||
|
|
||||||
--- math-style-hebrew-exceptions ---
|
--- math-style-hebrew-exceptions ---
|
||||||
// Test hebrew exceptions.
|
// Test hebrew exceptions.
|
||||||
$aleph, beth, gimel, daleth$
|
$aleph, beth, gimel, daleth$ \
|
||||||
|
$upright(aleph), upright(beth), upright(gimel), upright(daleth)$
|
||||||
|
|
||||||
|
--- math-style-script ---
|
||||||
|
// Test variation selectors for scr and cal.
|
||||||
|
$cal(A) scr(A) bold(cal(O)) scr(bold(O))$
|
||||||
|
|
||||||
|
#show math.equation: set text(font: "Noto Sans Math")
|
||||||
|
$scr(E) cal(E) bold(scr(Y)) cal(bold(Y))$
|
||||||
|
|
||||||
--- issue-3650-italic-equation ---
|
--- issue-3650-italic-equation ---
|
||||||
_abc $sin(x) "abc"$_ \
|
_abc $sin(x) "abc"$_ \
|
||||||
|
@ -14,6 +14,10 @@
|
|||||||
// Check that cases are applied to symbols nested in content
|
// Check that cases are applied to symbols nested in content
|
||||||
#lower($H I !$.body)
|
#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 ---
|
--- upper-bad-type ---
|
||||||
// Error: 8-9 expected string or content, found integer
|
// Error: 8-9 expected string or content, found integer
|
||||||
#upper(1)
|
#upper(1)
|
||||||
|
@ -9,6 +9,9 @@
|
|||||||
#set page(height: 60pt)
|
#set page(height: 60pt)
|
||||||
#image("/assets/images/tiger.jpg")
|
#image("/assets/images/tiger.jpg")
|
||||||
|
|
||||||
|
--- image-jpg-html-base64 html ---
|
||||||
|
#image("/assets/images/f2t.jpg", alt: "The letter F")
|
||||||
|
|
||||||
--- image-sizing ---
|
--- image-sizing ---
|
||||||
// Test configuring the size and fitting behaviour of images.
|
// Test configuring the size and fitting behaviour of images.
|
||||||
|
|
||||||
@ -128,7 +131,7 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B
|
|||||||
width: 1cm,
|
width: 1cm,
|
||||||
)
|
)
|
||||||
|
|
||||||
--- image-scaling-methods ---
|
--- image-scaling-methods render html ---
|
||||||
#let img(scaling) = image(
|
#let img(scaling) = image(
|
||||||
bytes((
|
bytes((
|
||||||
0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF,
|
0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF,
|
||||||
@ -144,14 +147,26 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B
|
|||||||
scaling: scaling,
|
scaling: scaling,
|
||||||
)
|
)
|
||||||
|
|
||||||
#stack(
|
#let images = (
|
||||||
dir: ltr,
|
|
||||||
spacing: 4pt,
|
|
||||||
img(auto),
|
img(auto),
|
||||||
img("smooth"),
|
img("smooth"),
|
||||||
img("pixelated"),
|
img("pixelated"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
#context if target() == "html" {
|
||||||
|
// TODO: Remove this once `stack` is supported in HTML export.
|
||||||
|
html.div(
|
||||||
|
style: "display: flex; flex-direction: row; gap: 4pt",
|
||||||
|
images.join(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
stack(
|
||||||
|
dir: ltr,
|
||||||
|
spacing: 4pt,
|
||||||
|
..images,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
--- image-natural-dpi-sizing ---
|
--- image-natural-dpi-sizing ---
|
||||||
// Test that images aren't upscaled.
|
// Test that images aren't upscaled.
|
||||||
// Image is just 48x80 at 220dpi. It should not be scaled to fit the page
|
// Image is just 48x80 at 220dpi. It should not be scaled to fit the page
|
||||||
|
Loading…
x
Reference in New Issue
Block a user