From 6f6e1453f7992598b40e78248f2e638ec8846664 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Tue, 11 Feb 2025 14:10:13 -0500 Subject: [PATCH] Use pyftsubset for font subsetting --- .gitmodules | 3 + Cargo.lock | 216 +++++++++++++++++++- Cargo.toml | 3 + crates/pyftsubset/Cargo.toml | 9 + crates/pyftsubset/fonttools | 1 + crates/pyftsubset/src/lib.rs | 65 ++++++ site_test/css/fonts.scss | 8 + site_test/css/fonts/BerkeleyMono-Bold.woff2 | Bin 0 -> 29544 bytes src/generator/css.rs | 12 +- src/generator/css/character_sets.rs | 104 +++++++--- src/generator/css/font_subset.rs | 8 +- 11 files changed, 392 insertions(+), 37 deletions(-) create mode 100644 .gitmodules create mode 100644 crates/pyftsubset/Cargo.toml create mode 160000 crates/pyftsubset/fonttools create mode 100644 crates/pyftsubset/src/lib.rs create mode 100644 site_test/css/fonts/BerkeleyMono-Bold.woff2 diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..d888c1c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "crates/pyftsubset/fonttools"] + path = crates/pyftsubset/fonttools + url = https://git.shadowfacts.net/shadowfacts/fonttools.git diff --git a/Cargo.lock b/Cargo.lock index 96feda4..4ae8722 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,7 +24,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom", + "getrandom 0.2.15", "once_cell", "version_check", "zerocopy", @@ -155,6 +155,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "1.3.2" @@ -517,6 +523,22 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "filetime" version = "0.2.25" @@ -683,10 +705,22 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets", +] + [[package]] name = "gimli" version = "0.31.1" @@ -722,7 +756,7 @@ name = "grass" version = "0.13.4" source = "git+https://git.shadowfacts.net/shadowfacts/grass.git?branch=custom-global-variables#e64e648b6174d23b6d4a8ad674eb443dc6fcbdff" dependencies = [ - "getrandom", + "getrandom 0.2.15", "grass_compiler", ] @@ -749,6 +783,12 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "html5ever" version = "0.27.0" @@ -935,6 +975,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + [[package]] name = "inotify" version = "0.10.2" @@ -1044,6 +1090,12 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "lock_api" version = "0.4.12" @@ -1056,9 +1108,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" [[package]] name = "mac" @@ -1098,6 +1150,15 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -1131,7 +1192,7 @@ checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] @@ -1195,9 +1256,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.2" +version = "1.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" [[package]] name = "parking_lot" @@ -1375,6 +1436,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "portable-atomic" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" + [[package]] name = "ppv-lite86" version = "0.2.20" @@ -1418,6 +1485,78 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" +[[package]] +name = "pyftsubset" +version = "0.1.0" +dependencies = [ + "log", + "pyo3", + "tempfile", +] + +[[package]] +name = "pyo3" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57fe09249128b3173d092de9523eaa75136bf7ba85e0d69eca241c7939c933cc" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "memoffset", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd3927b5a78757a0d71aa9dff669f903b1eb64b54142a9bd9f757f8fde65fd7" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab6bb2102bd8f991e7749f130a70d05dd557613e39ed2deeee8e9ca0c4d548d" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91871864b353fd5ffcb3f91f2f703a22a9797c91b9ab497b1acac7b07ae509c7" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43abc3b80bc20f3facd86cd3c60beed58c3e2aa26213f3cda368de39c60a27e4" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn", +] + [[package]] name = "quick-xml" version = "0.37.2" @@ -1464,7 +1603,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", ] [[package]] @@ -1523,6 +1662,19 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.7.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + [[package]] name = "ryu" version = "1.0.18" @@ -1727,6 +1879,26 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tempfile" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" +dependencies = [ + "cfg-if", + "fastrand", + "getrandom 0.3.1", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "tendril" version = "0.4.3" @@ -2220,6 +2392,12 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "unindent" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" + [[package]] name = "utf-8" version = "0.7.6" @@ -2239,6 +2417,7 @@ dependencies = [ "ahash", "anyhow", "base64", + "bit-vec", "bitflags 2.7.0", "chrono", "clap", @@ -2261,6 +2440,7 @@ dependencies = [ "once_cell", "pulldown-cmark", "pulldown-cmark-escape", + "pyftsubset", "regex", "rss", "serde", @@ -2308,6 +2488,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasm-bindgen" version = "0.2.99" @@ -2471,6 +2660,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags 2.7.0", +] + [[package]] name = "xml5ever" version = "0.18.1" diff --git a/Cargo.toml b/Cargo.toml index c48a7bd..c702407 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ workspace = { members = [ "crates/compute_graph", "crates/compute_graph_macros", "crates/derive_test", + "crates/pyftsubset", ] } [package] @@ -19,6 +20,7 @@ serde_json = "1.0" ahash = "0.8.11" anyhow = "1.0.95" base64 = "0.22.1" +bit-vec = "0.8.0" bitflags = "2.7.0" chrono = { version = "0.4.39", features = ["serde"] } clap = { version = "4.5.23", features = ["cargo"] } @@ -43,6 +45,7 @@ notify = "7.0.0" once_cell = "1.20.2" pulldown-cmark = "0.12.2" pulldown-cmark-escape = "0.11.0" +pyftsubset = { path = "crates/pyftsubset" } regex = "1.11.1" rss = { version = "2.0.11", features = ["atom"] } serde = { version = "1.0", features = ["derive"] } diff --git a/crates/pyftsubset/Cargo.toml b/crates/pyftsubset/Cargo.toml new file mode 100644 index 0000000..7ab2e67 --- /dev/null +++ b/crates/pyftsubset/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "pyftsubset" +version = "0.1.0" +edition = "2024" + +[dependencies] +log = "0.4.25" +pyo3 = "0.23.4" +tempfile = "3.16.0" diff --git a/crates/pyftsubset/fonttools b/crates/pyftsubset/fonttools new file mode 160000 index 0000000..d6f40c2 --- /dev/null +++ b/crates/pyftsubset/fonttools @@ -0,0 +1 @@ +Subproject commit d6f40c2e453f3c07807fef7926a31717f180b660 diff --git a/crates/pyftsubset/src/lib.rs b/crates/pyftsubset/src/lib.rs new file mode 100644 index 0000000..010f437 --- /dev/null +++ b/crates/pyftsubset/src/lib.rs @@ -0,0 +1,65 @@ +use std::{ + ffi::CString, + fs::read_to_string, + io::{Read, Write}, + path::Path, +}; + +use log::debug; +use pyo3::{ + ffi::c_str, + prelude::*, + types::{PyList, PyTuple}, +}; +use tempfile::NamedTempFile; + +pub fn subset(data: &[u8], unicodes: &[u32]) -> Vec { + pyo3::prepare_freethreaded_python(); + + let mut input = NamedTempFile::new().expect("input file"); + input.write_all(data).expect("writing input"); + let mut output = NamedTempFile::new().expect("output file"); + + let path = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/fonttools/Lib")); + let init_path = path.join("fontTools/subset/__init__.py"); + let py_init = CString::new(read_to_string(init_path).unwrap()).unwrap(); + let result = Python::with_gil(|py| -> PyResult> { + let syspath = py + .import("sys")? + .getattr("path")? + .downcast_into::()?; + syspath.insert(0, path)?; + let app: Py = PyModule::from_code(py, py_init.as_c_str(), c_str!(""), c_str!(""))? + .getattr("main")? + .into(); + let args = PyList::new(py, vec![ + input.path().to_str().unwrap(), + &format_unicodes(unicodes), + &format!("--output-file={}", output.path().to_str().unwrap()), + ])?; + debug!("Running pyftsubset with {:?}", args); + let args_tuple = PyTuple::new(py, &[args])?; + app.call1(py, args_tuple) + }); + result.expect("subsetting"); + + let mut buf = vec![]; + output.read_to_end(&mut buf).expect("reading output"); + buf +} + +fn format_unicodes(unicodes: &[u32]) -> String { + use std::fmt::Write; + + let mut s = "--unicodes=".to_owned(); + let mut first = true; + for u in unicodes { + if first { + write!(s, "{:x}", u).expect("append unicode"); + first = false; + } else { + write!(s, ",{:x}", u).expect("append unicode"); + } + } + s +} diff --git a/site_test/css/fonts.scss b/site_test/css/fonts.scss index 1cdc16a..159bbbb 100644 --- a/site_test/css/fonts.scss +++ b/site_test/css/fonts.scss @@ -41,9 +41,17 @@ font-weight: normal; font-style: normal; } + @font-face { font-family: "Berkeley Mono"; src: url("data:font/woff2;base64," + $berkeley-mono-italic) format("woff2"); font-weight: normal; font-style: italic; } + +@font-face { + font-family: "Berkeley Mono"; + src: url("data:font/woff2;base64," + $berkeley-mono-bold) format("woff2"); + font-weight: bold; + font-style: normal; +} diff --git a/site_test/css/fonts/BerkeleyMono-Bold.woff2 b/site_test/css/fonts/BerkeleyMono-Bold.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..268002022e0f7570684df1669de58034502e7e3d GIT binary patch literal 29544 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 Xryj|sx)bmm3ZUYT!IWCXmqp_TGc=ZB literal 0 HcmV?d00001 diff --git a/src/generator/css.rs b/src/generator/css.rs index aa4a108..4465330 100644 --- a/src/generator/css.rs +++ b/src/generator/css.rs @@ -46,7 +46,7 @@ pub fn make_graph( ); let character_sets = character_sets::make_graph(builder, render_inputs, render_dynamic_inputs); - let assertion = builder.add_rule(AssertNoBoldMonospace(character_sets.clone())); + let assertion = builder.add_rule(AssertNoBoldItalicMonospace(character_sets.clone())); let fonts = [ ( @@ -79,6 +79,11 @@ pub fn make_graph( content_path("css/fonts/BerkeleyMono-Oblique.woff2"), FontKey::MONOSPACE.union(FontKey::ITALIC), ), + ( + "berkeley-mono-bold", + content_path("css/fonts/BerkeleyMono-Bold.woff2"), + FontKey::MONOSPACE.union(FontKey::BOLD), + ), ]; for (name, path, font_key) in fonts.into_iter() { let file = read_file(path, builder, &mut *watcher_); @@ -223,12 +228,11 @@ impl Rule for ConvertToBase64 { } #[derive(InputVisitable)] -struct AssertNoBoldMonospace(Input); -impl Rule for AssertNoBoldMonospace { +struct AssertNoBoldItalicMonospace(Input); +impl Rule for AssertNoBoldItalicMonospace { type Output = (); fn evaluate(&mut self) -> Self::Output { let sets = self.input_0(); - assert!(sets.bold_monospace().is_empty()); assert!(sets.bold_italic_monospace().is_empty()); } } diff --git a/src/generator/css/character_sets.rs b/src/generator/css/character_sets.rs index 0befa6f..17dcede 100644 --- a/src/generator/css/character_sets.rs +++ b/src/generator/css/character_sets.rs @@ -1,3 +1,4 @@ +use bit_vec::BitVec; use bitflags::bitflags; use compute_graph::{ NodeId, @@ -9,7 +10,7 @@ use compute_graph::{ use html5ever::{ Attribute, local_name, tokenizer::{ - BufferQueue, TagKind, Token, TokenSink, TokenSinkResult, Tokenizer, TokenizerOpts, + BufferQueue, Tag, TagKind, Token, TokenSink, TokenSinkResult, Tokenizer, TokenizerOpts, TokenizerResult, }, }; @@ -105,7 +106,7 @@ impl Rule for UnionDynamicCharacterSets { } fn get_character_sets(html: &str) -> CharacterSets { - let accumulator = CharacterSetAccumulator::default(); + let accumulator = CharacterSetAccumulator::new(); let mut tokenizer = Tokenizer::new(accumulator, TokenizerOpts::default()); let mut queue = BufferQueue::default(); queue.push_back(html.into()); @@ -205,7 +206,7 @@ impl std::fmt::Debug for CharacterSets { } bitflags! { - #[derive(Clone, Copy)] + #[derive(Clone, Copy, PartialEq)] pub struct FontKey: u8 { const BOLD = 1; const ITALIC = 1 << 1; @@ -215,41 +216,72 @@ bitflags! { const FONT_VARIATIONS: usize = FontKey::all().bits() as usize + 1; +#[derive(Default)] +struct FontKeyStack { + bold: BitVec, + italic: BitVec, + monospace: BitVec, +} + +impl FontKeyStack { + fn push(&mut self, key: FontKey) { + self.bold.push(key.contains(FontKey::BOLD)); + self.italic.push(key.contains(FontKey::ITALIC)); + self.monospace.push(key.contains(FontKey::MONOSPACE)); + } + + fn pop(&mut self) { + self.bold.pop(); + self.italic.pop(); + self.monospace.pop(); + } + + fn all_ored(&self) -> FontKey { + let mut all = FontKey::empty(); + if self.bold.any() { + all.insert(FontKey::BOLD); + } + if self.italic.any() { + all.insert(FontKey::ITALIC); + } + if self.monospace.any() { + all.insert(FontKey::MONOSPACE); + } + all + } +} + // N.B.: html5ever 0.28.0 changed TokenSink::process_token to take &self. // At which point we upgrade, the state on this type will need to use something // else to provide interior mutability. -#[derive(Default)] struct CharacterSetAccumulator { characters: CharacterSets, - depths: [usize; 3], + keys: FontKeyStack, } impl CharacterSetAccumulator { + fn new() -> Self { + assert_eq!(FontKey::BOLD.bits().trailing_zeros(), 0); + assert_eq!(FontKey::ITALIC.bits().trailing_zeros(), 1); + assert_eq!(FontKey::MONOSPACE.bits().trailing_zeros(), 2); + + Self { + characters: CharacterSets::default(), + keys: FontKeyStack::default(), + } + } + fn handle_token(&mut self, token: Token) { match token { Token::TagToken(tag) => { - let depth = if tag.name == local_name!("strong") || tag.name == local_name!("b") { - &mut self.depths[0] - } else if tag.name == local_name!("em") - || tag.name == local_name!("i") - || tag.attrs.iter().any(Self::is_hl_cmt) - { - &mut self.depths[1] - } else if tag.name == local_name!("code") { - &mut self.depths[2] - } else { - return; - }; if tag.kind == TagKind::StartTag { - *depth += 1; + let key = Self::font_key_for(&tag); + self.keys.push(key); } else { - *depth -= 1; + self.keys.pop(); } } Token::CharacterTokens(s) => { - let mut key = FontKey::empty(); - key.set(FontKey::BOLD, self.depths[0] > 0); - key.set(FontKey::ITALIC, self.depths[1] > 0); - key.set(FontKey::MONOSPACE, self.depths[2] > 0); + let key = self.keys.all_ored(); let set = self.characters.get_mut(key); set.extend(s.chars()); } @@ -257,6 +289,32 @@ impl CharacterSetAccumulator { } } + fn font_key_for(tag: &Tag) -> FontKey { + if tag.name == local_name!("strong") || tag.name == local_name!("b") || Self::is_header(tag) + { + FontKey::BOLD + } else if tag.name == local_name!("em") + || tag.name == local_name!("i") + || tag.name == local_name!("header") + || tag.attrs.iter().any(Self::is_hl_cmt) + { + FontKey::ITALIC + } else if tag.name == local_name!("code") { + FontKey::MONOSPACE + } else { + FontKey::empty() + } + } + + fn is_header(tag: &Tag) -> bool { + tag.name == local_name!("h1") + || tag.name == local_name!("h2") + || tag.name == local_name!("h3") + || tag.name == local_name!("h4") + || tag.name == local_name!("h5") + || tag.name == local_name!("h6") + } + fn is_hl_cmt(attr: &Attribute) -> bool { attr.name.prefix == None && attr.name.local == local_name!("class") diff --git a/src/generator/css/font_subset.rs b/src/generator/css/font_subset.rs index 37ac8ee..f6f1ccd 100644 --- a/src/generator/css/font_subset.rs +++ b/src/generator/css/font_subset.rs @@ -4,6 +4,7 @@ use compute_graph::{ rule::Rule, synchronicity::Asynchronous, }; +use pyftsubset::subset; use super::character_sets::{CharacterSets, FontKey}; @@ -41,6 +42,11 @@ struct SubsetFont { impl Rule for SubsetFont { type Output = Vec; fn evaluate(&mut self) -> Self::Output { - self.font().clone() + let unicodes = self + .characters() + .iter() + .map(|&c| c as u32) + .collect::>(); + subset(self.font().as_ref(), &unicodes) } }