refactor changes

This commit is contained in:
pj committed 2026-08-30 16:58:02 +05:30
1 parent c1c47bc513
commit f7a536b2c3
58 files changed
+2524 -320

No files matched your search

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 491 B

After

Width:  |  Height:  |  Size: 716 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 685 B

After

Width:  |  Height:  |  Size: 988 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 829 B

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1022 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 14 KiB

+13 -11
View File
@@ -1,12 +1,14 @@
<svg width="350" height="350" viewBox="0 0 350 350" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Margin Docs mark: a page with a margin rule. Glyph only, matching margin's logo-dark.svg conventions.
App icon composition on a 512 canvas: squircle rect x=49 y=49 w=414 h=414 rx=92.5 fill #0d0c0a,
then this glyph under transform="translate(56,48) scale(1.142857142857)".
That scale puts every stroke centreline on a pixel centre at 32x32, and lifts the glyph 8px
above the geometric centre for optical balance. -->
<rect x="70" y="35" width="210" height="280" rx="14" stroke="#fcfbf7" stroke-width="10"/>
<rect x="93" y="72" width="10" height="206" rx="5" fill="#fcfbf7"/>
<rect x="135" y="100" width="122" height="10" rx="5" fill="#fcfbf7"/>
<rect x="135" y="170" width="122" height="10" rx="5" fill="#fcfbf7"/>
<rect x="135" y="240" width="80" height="10" rx="5" fill="#fcfbf7"/>
<svg width="1024" height="1024" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<!-- Margin Docs mark: stacked text bars, a paragraph seen as an alignment glyph. Source of truth
for every file in this directory; regenerate them with `pnpm tauri icon` after editing.
Squircle 828 wide on a 1024 canvas is the macOS icon inset, and every coordinate is a multiple
of 32 so bars and gaps land on whole pixels at 32x32. The block spans y 224..768 against a
centre of 512, which lifts it 16 above the geometric centre for optical balance. -->
<rect x="98" y="98" width="828" height="828" rx="185" fill="#0d0c0a"/>
<g fill="#fcfbf7">
<rect x="224" y="224" width="576" height="64" rx="32"/>
<rect x="224" y="384" width="416" height="64" rx="32"/>
<rect x="224" y="544" width="576" height="64" rx="32"/>
<rect x="224" y="704" width="288" height="64" rx="32"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 915 B

After

Width:  |  Height:  |  Size: 895 B

+39
View File
@@ -141,3 +141,42 @@ fn collect_installed(
.map(|family| (*family).to_string())
.collect()
}
// ------------------------------------------------------------------------------------------------
// The document's own face
//
// Everything above is about the two families the bundle is missing. What follows is about the one
// the author chose: src/model/fonts.ts offers six bundled families and whatever this machine has,
// and an export has to be handed the bytes of whichever it names.
// ------------------------------------------------------------------------------------------------
/// Every family this machine has, for the "System" group of the font picker.
///
/// Sorted and deduplicated here rather than in the UI, because a font book has one entry per face
/// and the picker wants one per family: a machine with the full Helvetica Neue family would
/// otherwise show it nine times.
#[tauri::command(async)]
pub fn fonts_list_system() -> Vec<String> {
let db = system_db();
let mut names: Vec<String> = db
.faces()
.filter_map(|face| face.families.first().map(|(name, _)| name.clone()))
.collect();
names.sort();
names.dedup();
names
}
/// The upright and italic cuts of one installed family, for a document set in a face off the
/// machine.
///
/// Four queries and not two, because a family whose bold is a separate file gives fontdb a
/// different id for it and the collection has to be loaded either way. `seen` is what stops a `.ttc`
/// holding all four from being read four times.
pub fn system_faces(family: &str) -> Vec<Vec<u8>> {
let db = system_db();
let mut fonts = Vec::new();
let mut seen = HashSet::new();
faces_for(&db, family, &mut fonts, &mut seen);
fonts
}
+1
View File
@@ -297,6 +297,7 @@ pub fn run() {
pdf::pdf_write,
grammar::grammar_available,
grammar::grammar_check,
fonts::fonts_list_system,
])
.run(context)
.expect("error while running Margin Docs");
+70 -2
View File
@@ -41,6 +41,46 @@ static FACES: [&[u8]; 9] = [
include_bytes!("../fonts/HankenGrotesk-BoldItalic.ttf"),
];
// The other four families src/model/fonts.ts offers, in the variable files public/fonts already
// ships for the editor, and loaded only when a document is actually set in one.
//
// These carry the caveat the nine above exist to avoid, and it is not worth cutting eight more
// static instances to fix. Typst lays a variable file out at its default instance, so a document
// set in EB Garamond exports with its headings at the same weight as its body: the face is right,
// the hierarchy is flatter than the screen. The two the app defaults to are the two that export
// perfectly, and choosing one of these is choosing a face rather than a fallback.
static EB_GARAMOND: [&[u8]; 2] = [
include_bytes!("../../public/fonts/EBGaramond-VF.ttf"),
include_bytes!("../../public/fonts/EBGaramond-Italic-VF.ttf"),
];
static LORA: [&[u8]; 2] = [
include_bytes!("../../public/fonts/Lora-VF.ttf"),
include_bytes!("../../public/fonts/Lora-Italic-VF.ttf"),
];
static SOURCE_SERIF: [&[u8]; 2] = [
include_bytes!("../../public/fonts/SourceSerif4-VF.ttf"),
include_bytes!("../../public/fonts/SourceSerif4-Italic-VF.ttf"),
];
static FRAUNCES: [&[u8]; 2] = [
include_bytes!("../../public/fonts/Fraunces-VF.ttf"),
include_bytes!("../../public/fonts/Fraunces-Italic-VF.ttf"),
];
/// The bytes behind one `BundledFont` id from src/model/fonts.ts.
///
/// Literata and Hanken Grotesk are absent on purpose rather than missing: `FACES` already holds
/// their nine static instances and they are loaded for every export, so naming either here would
/// hand Typst the same family twice and let the variable copy win.
fn bundled_faces(id: &str) -> &'static [&'static [u8]] {
match id {
"eb-garamond" => &EB_GARAMOND,
"lora" => &LORA,
"source-serif" => &SOURCE_SERIF,
"fraunces" => &FRAUNCES,
_ => &[],
}
}
// mitex 0.2.5, vendored under src-tauri/vendor/mitex with its LICENSE, and served as ordinary
// paths under `/mitex/` rather than through Typst's package system. A package spec would mean a
// download on first export and a cache directory to keep, for a dependency that is 380K and never
@@ -242,7 +282,7 @@ fn compile_once(
Ok((bytes, warnings.into_iter().collect()))
}
/// Compiles Typst source to PDF bytes, with whatever the compiler had to work around.
/// Compiles Typst source to PDF bytes, in the app's default pair of faces.
///
/// Separate from the command so a test can reach it: a `#[tauri::command]` taking an `AppHandle`
/// needs a running app, and none of the work below wants one.
@@ -250,6 +290,22 @@ pub fn compile(
source: String,
images: &[ImageInput],
root_paths: &[String],
) -> Result<(Vec<u8>, Vec<PdfWarning>), String> {
compile_in(source, images, root_paths, &[], &[])
}
/// The same, for a document set in something other than the default.
///
/// `bundled` are ids from src/model/fonts.ts and `system` are family names off this machine, both
/// as the converter named them in the preamble it wrote. Naming a face Typst has no bytes for is a
/// warning and a fallback rather than a failure, so an id this build does not know and a family
/// that has since been uninstalled both end in a PDF.
pub fn compile_in(
source: String,
images: &[ImageInput],
root_paths: &[String],
bundled: &[String],
system: &[String],
) -> Result<(Vec<u8>, Vec<PdfWarning>), String> {
let mut warnings: Vec<PdfWarning> = Vec::new();
@@ -273,6 +329,16 @@ pub fn compile(
let mut fonts: Vec<Vec<u8>> = FACES.iter().map(|face| face.to_vec()).collect();
fonts.append(&mut fallbacks.fonts);
// The face the author chose, if it is not one of the two already in `FACES`. A system family is
// read off this machine, which is the one place an export can differ between two computers, and
// is the caveat the picker states next to the "System" group.
for id in bundled {
fonts.extend(bundled_faces(id).iter().map(|face| face.to_vec()));
}
for family in system {
fonts.extend(crate::fonts::system_faces(family));
}
let source = format!("{}{source}", font_preamble(&fallbacks));
let failure = match compile_once(&source, MITEX_LIB, &binaries, &fonts) {
@@ -322,6 +388,8 @@ pub fn pdf_compile(
app: tauri::AppHandle,
source: String,
images: Vec<ImageInput>,
bundled_fonts: Vec<String>,
system_fonts: Vec<String>,
) -> Result<tauri::ipc::Response, String> {
// The same gate `fs::checked` puts in front of every other read, reached the same way it is:
// the open roots out of shared state, then `resolve_in_roots`. The lock is dropped before the
@@ -332,7 +400,7 @@ pub fn pdf_compile(
open.iter().map(|root| root.path.clone()).collect()
};
let (bytes, warnings) = compile(source, &images, &root_paths)?;
let (bytes, warnings) = compile_in(source, &images, &root_paths, &bundled_fonts, &system_fonts)?;
if !warnings.is_empty() {
app.emit("pdf-warnings", warnings).ok();
}
+47 -1
View File
@@ -6,7 +6,7 @@
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use margin_docs_lib::dto::ImageInput;
use margin_docs_lib::pdf::compile;
use margin_docs_lib::pdf::{compile, compile_in};
use tempfile::TempDir;
/// A real 4x4 PNG, so an image that is meant to be read is one Typst can actually decode, and one
@@ -314,3 +314,49 @@ fn every_weight_the_converter_asks_for_has_a_face_of_its_own() {
}
}
}
/// A document set in one of the four extra bundled families really is typeset in it.
///
/// The bytes are compiled into the binary but only loaded when the front end names the id, so the
/// thing worth proving is that naming it changes the page: the same word in EB Garamond and in the
/// default face has to be two different documents. A family Typst cannot see is not an error, it is
/// a warning and a fallback, so the negative case would pass without the loading working at all.
#[test]
fn a_document_set_in_a_bundled_face_is_typeset_in_it() {
let (_dir, roots) = root();
let word = |family: &str, bundled: &[String]| {
let source = format!("{PREAMBLE}#text(font: \"{family}\")[Margin]\n");
compile_in(source, &[], &roots, bundled, &[]).expect("the face typesets").0
};
let default = word("Literata", &[]);
for (id, family) in [
("eb-garamond", "EB Garamond"),
("lora", "Lora"),
("source-serif", "Source Serif 4"),
("fraunces", "Fraunces"),
] {
let chosen = word(family, &[id.to_string()]);
assert_ne!(default, chosen, "{family} came out as the default face");
}
}
/// A face the compiler cannot find costs a warning and never the export.
///
/// Both halves reach here in the ordinary run of things. A bundled id is a preference written by
/// some other version of the app, and a system family is one the user has uninstalled since they
/// chose it; neither is a reason to refuse somebody their PDF.
#[test]
fn a_face_that_is_not_there_still_exports() {
let (_dir, roots) = root();
let source = format!("{PREAMBLE}#text(font: \"Nothing Installed\")[Margin]\n");
let (bytes, _) = compile_in(
source,
&[],
&roots,
&["a-face-no-build-has".to_string()],
&["Nothing Installed".to_string()],
)
.expect("an unknown family falls back rather than failing");
assert!(bytes.starts_with(b"%PDF-"), "the export still happened");
}