mirror of
https://github.com/typst/typst
synced 2025-08-19 01:18:32 +08:00
Compare commits
9 Commits
bc08aacdba
...
fafb282885
Author | SHA1 | Date | |
---|---|---|---|
|
fafb282885 | ||
|
ac77fdbb6e | ||
|
3aa7e861e7 | ||
|
a45c3388a6 | ||
|
eaf63ca80c | ||
|
e4c316f2cc | ||
|
1130d6b746 | ||
|
1b44fea9d8 | ||
|
8311997274 |
217
Cargo.lock
generated
217
Cargo.lock
generated
@ -545,6 +545,29 @@ version = "0.2.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
|
checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cssparser"
|
||||||
|
version = "0.34.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b7c66d1cd8ed61bf80b38432613a7a2f09401ab8d0501110655f8b341484a3e3"
|
||||||
|
dependencies = [
|
||||||
|
"cssparser-macros",
|
||||||
|
"dtoa-short",
|
||||||
|
"itoa",
|
||||||
|
"phf",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cssparser-macros"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "csv"
|
name = "csv"
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
@ -592,6 +615,17 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_more"
|
||||||
|
version = "0.99.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dirs"
|
name = "dirs"
|
||||||
version = "6.0.0"
|
version = "6.0.0"
|
||||||
@ -630,6 +664,21 @@ version = "1.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dtoa"
|
||||||
|
version = "1.0.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dtoa-short"
|
||||||
|
version = "0.3.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87"
|
||||||
|
dependencies = [
|
||||||
|
"dtoa",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ecow"
|
name = "ecow"
|
||||||
version = "0.2.3"
|
version = "0.2.3"
|
||||||
@ -639,6 +688,12 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ego-tree"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b2972feb8dffe7bc8c5463b1dacda1b0dfbed3710e50f977d965429692d74cd8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.13.0"
|
version = "1.13.0"
|
||||||
@ -861,6 +916,16 @@ version = "2.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
|
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futf"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843"
|
||||||
|
dependencies = [
|
||||||
|
"mac",
|
||||||
|
"new_debug_unreachable",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fxhash"
|
name = "fxhash"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@ -970,6 +1035,18 @@ version = "0.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "html5ever"
|
||||||
|
version = "0.29.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"mac",
|
||||||
|
"markup5ever",
|
||||||
|
"match_token",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httpdate"
|
name = "httpdate"
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
@ -1540,6 +1617,37 @@ dependencies = [
|
|||||||
"pkg-config",
|
"pkg-config",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mac"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "markup5ever"
|
||||||
|
version = "0.14.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"phf",
|
||||||
|
"phf_codegen",
|
||||||
|
"string_cache",
|
||||||
|
"string_cache_codegen",
|
||||||
|
"tendril",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "match_token"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.7.4"
|
version = "2.7.4"
|
||||||
@ -1612,6 +1720,12 @@ dependencies = [
|
|||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "new_debug_unreachable"
|
||||||
|
version = "1.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nom"
|
name = "nom"
|
||||||
version = "7.1.3"
|
version = "7.1.3"
|
||||||
@ -1869,6 +1983,16 @@ dependencies = [
|
|||||||
"phf_shared",
|
"phf_shared",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_codegen"
|
||||||
|
version = "0.11.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
|
||||||
|
dependencies = [
|
||||||
|
"phf_generator",
|
||||||
|
"phf_shared",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "phf_generator"
|
name = "phf_generator"
|
||||||
version = "0.11.3"
|
version = "0.11.3"
|
||||||
@ -1981,6 +2105,12 @@ dependencies = [
|
|||||||
"zerocopy",
|
"zerocopy",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "precomputed-hash"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.93"
|
version = "1.0.93"
|
||||||
@ -2282,6 +2412,21 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scraper"
|
||||||
|
version = "0.23.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "527e65d9d888567588db4c12da1087598d0f6f8b346cc2c5abc91f05fc2dffe2"
|
||||||
|
dependencies = [
|
||||||
|
"cssparser",
|
||||||
|
"ego-tree",
|
||||||
|
"getopts",
|
||||||
|
"html5ever",
|
||||||
|
"precomputed-hash",
|
||||||
|
"selectors",
|
||||||
|
"tendril",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "security-framework"
|
name = "security-framework"
|
||||||
version = "2.11.1"
|
version = "2.11.1"
|
||||||
@ -2305,6 +2450,25 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "selectors"
|
||||||
|
version = "0.26.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fd568a4c9bb598e291a08244a5c1f5a8a6650bee243b5b0f8dbb3d9cc1d87fe8"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.8.0",
|
||||||
|
"cssparser",
|
||||||
|
"derive_more",
|
||||||
|
"fxhash",
|
||||||
|
"log",
|
||||||
|
"new_debug_unreachable",
|
||||||
|
"phf",
|
||||||
|
"phf_codegen",
|
||||||
|
"precomputed-hash",
|
||||||
|
"servo_arc",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "self-replace"
|
name = "self-replace"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@ -2388,6 +2552,15 @@ dependencies = [
|
|||||||
"unsafe-libyaml",
|
"unsafe-libyaml",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "servo_arc"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae65c4249478a2647db249fb43e23cec56a2c8974a427e7bd8cb5a1d0964921a"
|
||||||
|
dependencies = [
|
||||||
|
"stable_deref_trait",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shell-escape"
|
name = "shell-escape"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@ -2499,6 +2672,31 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "string_cache"
|
||||||
|
version = "0.8.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f"
|
||||||
|
dependencies = [
|
||||||
|
"new_debug_unreachable",
|
||||||
|
"parking_lot",
|
||||||
|
"phf_shared",
|
||||||
|
"precomputed-hash",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "string_cache_codegen"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0"
|
||||||
|
dependencies = [
|
||||||
|
"phf_generator",
|
||||||
|
"phf_shared",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
@ -2621,6 +2819,17 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tendril"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0"
|
||||||
|
dependencies = [
|
||||||
|
"futf",
|
||||||
|
"mac",
|
||||||
|
"utf-8",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "termcolor"
|
name = "termcolor"
|
||||||
version = "1.4.1"
|
version = "1.4.1"
|
||||||
@ -3071,6 +3280,7 @@ dependencies = [
|
|||||||
"comemo",
|
"comemo",
|
||||||
"csv",
|
"csv",
|
||||||
"ecow",
|
"ecow",
|
||||||
|
"ego-tree",
|
||||||
"flate2",
|
"flate2",
|
||||||
"fontdb",
|
"fontdb",
|
||||||
"glidesort",
|
"glidesort",
|
||||||
@ -3094,6 +3304,7 @@ dependencies = [
|
|||||||
"roxmltree",
|
"roxmltree",
|
||||||
"rust_decimal",
|
"rust_decimal",
|
||||||
"rustybuzz",
|
"rustybuzz",
|
||||||
|
"scraper",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_yaml 0.9.34+deprecated",
|
"serde_yaml 0.9.34+deprecated",
|
||||||
@ -3426,6 +3637,12 @@ dependencies = [
|
|||||||
"xmlwriter",
|
"xmlwriter",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf-8"
|
||||||
|
version = "0.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf16_iter"
|
name = "utf16_iter"
|
||||||
version = "1.0.5"
|
version = "1.0.5"
|
||||||
|
@ -54,6 +54,7 @@ csv = "1"
|
|||||||
ctrlc = "3.4.1"
|
ctrlc = "3.4.1"
|
||||||
dirs = "6"
|
dirs = "6"
|
||||||
ecow = { version = "0.2", features = ["serde"] }
|
ecow = { version = "0.2", features = ["serde"] }
|
||||||
|
ego-tree = "0.10"
|
||||||
env_proxy = "0.4"
|
env_proxy = "0.4"
|
||||||
fastrand = "2.3"
|
fastrand = "2.3"
|
||||||
flate2 = "1"
|
flate2 = "1"
|
||||||
@ -104,6 +105,7 @@ roxmltree = "0.20"
|
|||||||
rust_decimal = { version = "1.36.0", default-features = false, features = ["maths"] }
|
rust_decimal = { version = "1.36.0", default-features = false, features = ["maths"] }
|
||||||
rustybuzz = "0.20"
|
rustybuzz = "0.20"
|
||||||
same-file = "1"
|
same-file = "1"
|
||||||
|
scraper = "0.23.1"
|
||||||
self-replace = "1.3.7"
|
self-replace = "1.3.7"
|
||||||
semver = "1"
|
semver = "1"
|
||||||
serde = { version = "1.0.184", features = ["derive"] }
|
serde = { version = "1.0.184", features = ["derive"] }
|
||||||
|
@ -1,11 +1,72 @@
|
|||||||
//! 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::html::{attr, HtmlElem};
|
||||||
|
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;
|
||||||
|
|
||||||
|
/// Additional methods for [`HtmlElem`].
|
||||||
|
pub trait HtmlElemExt {
|
||||||
|
/// Adds the styles to an element if the property list is non-empty.
|
||||||
|
fn with_styles(self, properties: Properties) -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HtmlElemExt for HtmlElem {
|
||||||
|
/// Adds CSS styles to an element.
|
||||||
|
fn with_styles(self, properties: Properties) -> Self {
|
||||||
|
if let Some(value) = properties.into_inline_styles() {
|
||||||
|
self.with_attr(attr::style, value)
|
||||||
|
} else {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A list of CSS properties with values.
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct Properties(EcoString);
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
@ -3,12 +3,12 @@ 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::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,6 +18,9 @@ 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;
|
||||||
|
|
||||||
|
use crate::css::{self, HtmlElemExt};
|
||||||
|
|
||||||
/// Register show rules for the [HTML target](Target::Html).
|
/// Register show rules for the [HTML target](Target::Html).
|
||||||
pub fn register(rules: &mut NativeRuleMap) {
|
pub fn register(rules: &mut NativeRuleMap) {
|
||||||
@ -47,6 +50,9 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
const STRONG_RULE: ShowFn<StrongElem> = |elem, _, _| {
|
const STRONG_RULE: ShowFn<StrongElem> = |elem, _, _| {
|
||||||
@ -338,7 +344,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 +415,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())
|
||||||
|
};
|
||||||
|
@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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")?)
|
|
||||||
}
|
|
||||||
|
@ -27,6 +27,7 @@ codex = { workspace = true }
|
|||||||
comemo = { workspace = true }
|
comemo = { workspace = true }
|
||||||
csv = { workspace = true }
|
csv = { workspace = true }
|
||||||
ecow = { workspace = true }
|
ecow = { workspace = true }
|
||||||
|
ego-tree = { workspace = true }
|
||||||
flate2 = { workspace = true }
|
flate2 = { workspace = true }
|
||||||
fontdb = { workspace = true }
|
fontdb = { workspace = true }
|
||||||
glidesort = { workspace = true }
|
glidesort = { workspace = true }
|
||||||
@ -50,6 +51,7 @@ regex-syntax = { workspace = true }
|
|||||||
roxmltree = { workspace = true }
|
roxmltree = { workspace = true }
|
||||||
rust_decimal = { workspace = true }
|
rust_decimal = { workspace = true }
|
||||||
rustybuzz = { workspace = true }
|
rustybuzz = { workspace = true }
|
||||||
|
scraper = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
serde_yaml = { workspace = true }
|
serde_yaml = { workspace = true }
|
||||||
|
@ -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(),
|
||||||
|
@ -165,6 +165,11 @@ cast! {
|
|||||||
pub struct HtmlAttrs(pub EcoVec<(HtmlAttr, EcoString)>);
|
pub struct HtmlAttrs(pub EcoVec<(HtmlAttr, EcoString)>);
|
||||||
|
|
||||||
impl HtmlAttrs {
|
impl HtmlAttrs {
|
||||||
|
/// Creates an empty attribute list.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
/// Add an attribute.
|
/// Add an attribute.
|
||||||
pub fn push(&mut self, attr: HtmlAttr, value: impl Into<EcoString>) {
|
pub fn push(&mut self, attr: HtmlAttr, value: impl Into<EcoString>) {
|
||||||
self.0.push((attr, value.into()));
|
self.0.push((attr, value.into()));
|
||||||
|
98
crates/typst-library/src/loading/html.rs
Normal file
98
crates/typst-library/src/loading/html.rs
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
use ecow::eco_format;
|
||||||
|
use ego_tree::NodeRef;
|
||||||
|
use scraper::Node;
|
||||||
|
use typst_syntax::Spanned;
|
||||||
|
|
||||||
|
use crate::diag::{At, FileError, SourceDiagnostic, SourceResult};
|
||||||
|
use crate::engine::Engine;
|
||||||
|
use crate::foundations::{dict, func, Array, Dict, IntoValue, Value};
|
||||||
|
use crate::loading::{DataSource, Load};
|
||||||
|
|
||||||
|
/// Reads structured data from an HTML file.
|
||||||
|
///
|
||||||
|
/// The HTML file is parsed into an array of dictionaries and strings. It is compatible with
|
||||||
|
/// the XML format, parsed by the [`xml`]($xml) function.
|
||||||
|
#[func(title = "HTML")]
|
||||||
|
pub fn html_decode(
|
||||||
|
engine: &mut Engine,
|
||||||
|
/// A [path]($syntax/#paths) to an HTML file or raw HTML bytes.
|
||||||
|
source: Spanned<DataSource>,
|
||||||
|
) -> SourceResult<Value> {
|
||||||
|
let data = source.load(engine.world)?;
|
||||||
|
let text = data.as_str().map_err(FileError::from).at(source.span)?;
|
||||||
|
let document = scraper::Html::parse_document(text);
|
||||||
|
|
||||||
|
if !document.errors.is_empty() {
|
||||||
|
let errors = document.errors.iter();
|
||||||
|
return Err(errors
|
||||||
|
.map(|msg| {
|
||||||
|
SourceDiagnostic::error(
|
||||||
|
source.span,
|
||||||
|
eco_format!("failed to parse HTML ({msg})"),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(convert_html(document.tree.root()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert an HTML node to a Typst value.
|
||||||
|
fn convert_html(node_ref: NodeRef<Node>) -> Value {
|
||||||
|
// `prefix` and `name` are part of the tag name. For example,
|
||||||
|
// in the following HTML, `html5` is the prefix and `div` is the name:
|
||||||
|
// ```
|
||||||
|
// <html5:div class="example" />
|
||||||
|
// ```
|
||||||
|
let (prefix, name, attrs) = match node_ref.value() {
|
||||||
|
Node::Text(text) => return (*text).into_value(),
|
||||||
|
Node::Document => return Value::Array(convert_html_children(node_ref)),
|
||||||
|
// todo: the namespace is ignored
|
||||||
|
Node::Element(element) => {
|
||||||
|
(element.name.prefix.as_ref(), &*element.name.local, Some(element.attrs()))
|
||||||
|
}
|
||||||
|
Node::Fragment => (None, "fragment", None),
|
||||||
|
// todo: doc type and processing instruction are ignored
|
||||||
|
// https://en.wikipedia.org/wiki/Processing_Instruction
|
||||||
|
Node::Doctype(..) | Node::ProcessingInstruction(..) => return Value::None,
|
||||||
|
Node::Comment(comment) => {
|
||||||
|
return Value::Dict(dict! {
|
||||||
|
"tag" => "",
|
||||||
|
"attrs" => dict! {},
|
||||||
|
"children" => [(*comment).into_value()].into_iter().collect::<Array>(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let children = convert_html_children(node_ref);
|
||||||
|
|
||||||
|
let attrs: Dict = attrs
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.map(|(name, value)| (name.into(), value.into_value()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut converted = dict! {
|
||||||
|
"tag" => name.into_value(),
|
||||||
|
"attrs" => attrs,
|
||||||
|
"children" => children,
|
||||||
|
};
|
||||||
|
|
||||||
|
// In most cases, the prefix is not set, so we only add it if it exists.
|
||||||
|
if let Some(prefix) = prefix {
|
||||||
|
converted.insert("prefix".into(), (*prefix).into_value());
|
||||||
|
}
|
||||||
|
|
||||||
|
Value::Dict(converted)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert children an HTML node to a Typst value.
|
||||||
|
fn convert_html_children(node_ref: NodeRef<Node>) -> Array {
|
||||||
|
node_ref
|
||||||
|
.children()
|
||||||
|
.filter(|v| {
|
||||||
|
!matches!(v.value(), Node::Doctype(..) | Node::ProcessingInstruction(..))
|
||||||
|
})
|
||||||
|
.map(convert_html)
|
||||||
|
.collect()
|
||||||
|
}
|
@ -4,6 +4,8 @@
|
|||||||
mod cbor_;
|
mod cbor_;
|
||||||
#[path = "csv.rs"]
|
#[path = "csv.rs"]
|
||||||
mod csv_;
|
mod csv_;
|
||||||
|
#[path = "html.rs"]
|
||||||
|
mod html_;
|
||||||
#[path = "json.rs"]
|
#[path = "json.rs"]
|
||||||
mod json_;
|
mod json_;
|
||||||
#[path = "read.rs"]
|
#[path = "read.rs"]
|
||||||
@ -21,6 +23,7 @@ use typst_syntax::{FileId, Spanned};
|
|||||||
|
|
||||||
pub use self::cbor_::*;
|
pub use self::cbor_::*;
|
||||||
pub use self::csv_::*;
|
pub use self::csv_::*;
|
||||||
|
pub use self::html_::*;
|
||||||
pub use self::json_::*;
|
pub use self::json_::*;
|
||||||
pub use self::read_::*;
|
pub use self::read_::*;
|
||||||
pub use self::toml_::*;
|
pub use self::toml_::*;
|
||||||
@ -37,6 +40,7 @@ pub(super) fn define(global: &mut Scope) {
|
|||||||
global.start_category(crate::Category::DataLoading);
|
global.start_category(crate::Category::DataLoading);
|
||||||
global.define_func::<read>();
|
global.define_func::<read>();
|
||||||
global.define_func::<csv>();
|
global.define_func::<csv>();
|
||||||
|
global.define_func::<html_decode>();
|
||||||
global.define_func::<json>();
|
global.define_func::<json>();
|
||||||
global.define_func::<toml>();
|
global.define_func::<toml>();
|
||||||
global.define_func::<yaml>();
|
global.define_func::<yaml>();
|
||||||
|
@ -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,18 +18,24 @@ 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
|
||||||
|
.write_attribute("style", &format_args!("image-rendering: {value}"))
|
||||||
|
}
|
||||||
|
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) => {
|
Smart::Custom(ImageScaling::Smooth) => {
|
||||||
// This is still experimental and not implemented in all major browsers.
|
// This is still experimental and not implemented in all major browsers.
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/CSS/image-rendering#browser_compatibility
|
// https://developer.mozilla.org/en-US/docs/Web/CSS/image-rendering#browser_compatibility
|
||||||
self.xml.write_attribute("style", "image-rendering: smooth")
|
Some("smooth")
|
||||||
}
|
}
|
||||||
Smart::Custom(ImageScaling::Pixelated) => {
|
Smart::Custom(ImageScaling::Pixelated) => Some("pixelated"),
|
||||||
self.xml.write_attribute("style", "image-rendering: pixelated")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.xml.end_element();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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};
|
||||||
|
|
||||||
|
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>
|
46
tests/suite/loading/html.typ
Normal file
46
tests/suite/loading/html.typ
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
--- html ---
|
||||||
|
// Test reading XML data.
|
||||||
|
#let data = html-decode("/assets/text/example.html")
|
||||||
|
#test(data, ((
|
||||||
|
tag: "html",
|
||||||
|
attrs: (:),
|
||||||
|
children: (
|
||||||
|
(
|
||||||
|
tag: "head",
|
||||||
|
attrs: (:),
|
||||||
|
children: (
|
||||||
|
"\n ",
|
||||||
|
(
|
||||||
|
tag: "meta",
|
||||||
|
attrs: (charset: "UTF-8"),
|
||||||
|
children: (),
|
||||||
|
),
|
||||||
|
"\n ",
|
||||||
|
(
|
||||||
|
tag: "title",
|
||||||
|
attrs: (:),
|
||||||
|
children: ("Example document",),
|
||||||
|
),
|
||||||
|
"\n ",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
"\n ",
|
||||||
|
(
|
||||||
|
tag: "body",
|
||||||
|
attrs: (:),
|
||||||
|
children: (
|
||||||
|
"\n ",
|
||||||
|
(
|
||||||
|
tag: "h1",
|
||||||
|
attrs: (:),
|
||||||
|
children: ("Hello, world!",),
|
||||||
|
),
|
||||||
|
"\n \n\n",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),))
|
||||||
|
|
||||||
|
--- html-invalid ---
|
||||||
|
// Error: 14-38 failed to parse HTML (Unexpected token)
|
||||||
|
#html-decode("/assets/text/hello.txt")
|
@ -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