Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions crates/core/src/assets/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,42 @@ impl<'a> AssetResolver<'a> {
paths.join(", ")
)));
}
// No on-disk source: fall back to the plugin's embedded default
// (if any), written as a real file so it is copied + linked like
// any other asset instead of being inlined.
if let Some(embedded) = asset_config.default_content {
let rel = embedded.name.to_string();
if let Some(prev) = seen_relative_paths.get(&rel) {
return Err(RheoError::project_config(format!(
"asset path collision: output '{}' would be written by both '{}' and the embedded default for '{}'",
rel,
prev.display(),
asset_config.name
)));
}
let dest = self.plugin_output_dir.join(&rel);
if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent).map_err(|e| {
RheoError::io(
e,
format!("creating directory for embedded default '{}'", rel),
)
})?;
}
std::fs::write(&dest, embedded.content).map_err(|e| {
RheoError::io(e, format!("writing embedded default asset to {:?}", dest))
})?;
seen_relative_paths.insert(rel.clone(), dest.clone());
resolved.insert(
asset_config.name,
vec![Asset {
config: asset_config.clone(),
source_path: dest.clone(),
resolved_path: dest.clone(),
built_relative_path: rel,
}],
);
}
continue;
}

Expand Down Expand Up @@ -360,6 +396,7 @@ mod tests {
name: "css_stylesheet",
default_path: "style.css",
required: false,
default_content: None,
}],
};
let section = PluginSection::default();
Expand Down Expand Up @@ -387,6 +424,7 @@ mod tests {
name: "css_stylesheet",
default_path: "style.css",
required: false,
default_content: None,
}],
};
let mut asset_extra = toml::map::Map::new();
Expand Down Expand Up @@ -423,6 +461,7 @@ mod tests {
name: "missing_asset",
default_path: "nonexistent.css",
required: true,
default_content: None,
}],
};
let section = PluginSection::default();
Expand Down Expand Up @@ -450,6 +489,7 @@ mod tests {
name: "optional_asset",
default_path: "nonexistent.css",
required: false,
default_content: None,
}],
};
let section = PluginSection::default();
Expand Down Expand Up @@ -479,6 +519,7 @@ mod tests {
name: "css_stylesheet",
default_path: "style.css",
required: false,
default_content: None,
}],
};
let mut asset_extra = toml::map::Map::new();
Expand Down Expand Up @@ -522,6 +563,7 @@ mod tests {
name: "css_stylesheet",
default_path: "style.css",
required: false,
default_content: None,
}],
};

Expand Down Expand Up @@ -570,6 +612,7 @@ mod tests {
name: "missing_asset",
default_path: "nonexistent.css",
required: true,
default_content: None,
}],
};
let section = PluginSection::default();
Expand Down Expand Up @@ -599,6 +642,7 @@ mod tests {
name: "css_stylesheet",
default_path: "style.css",
required: true,
default_content: None,
}],
};

Expand Down Expand Up @@ -652,6 +696,7 @@ mod tests {
name: "css_stylesheet",
default_path: "style.css",
required: false,
default_content: None,
}],
};

Expand Down Expand Up @@ -714,6 +759,7 @@ mod tests {
name: "css_stylesheet",
default_path: "style.css",
required: false,
default_content: None,
}],
};

Expand Down Expand Up @@ -758,6 +804,7 @@ mod tests {
name: "css_stylesheet",
default_path: "style.css",
required: false,
default_content: None,
}],
};

Expand Down Expand Up @@ -799,6 +846,7 @@ mod tests {
name: "css_stylesheet",
default_path: "style.css",
required: false,
default_content: None,
}],
};

Expand Down Expand Up @@ -851,6 +899,7 @@ mod tests {
name: "js_scripts",
default_path: "script.js",
required: false,
default_content: None,
}],
};

Expand Down Expand Up @@ -897,6 +946,7 @@ mod tests {
name: "css_stylesheet",
default_path: "style.css",
required: false,
default_content: None,
}],
};
let section = PluginSection::default();
Expand Down Expand Up @@ -939,6 +989,7 @@ mod tests {
name: "css_stylesheet",
default_path: "style.css",
required: false,
default_content: None,
}],
};
let section = PluginSection::default();
Expand Down Expand Up @@ -984,6 +1035,7 @@ mod tests {
name: "css_stylesheet",
default_path: "style.css",
required: false,
default_content: None,
}],
};
let mut user_extra = toml::map::Map::new();
Expand Down Expand Up @@ -1035,6 +1087,7 @@ mod tests {
name: "css_stylesheet",
default_path: "style.css",
required: false,
default_content: None,
}],
};
// User declares custom.css for dest "pkg"
Expand Down
12 changes: 9 additions & 3 deletions crates/core/src/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,16 +320,22 @@ impl Build {
// Inject CSS/JS link tags into each HTML entry in memory.
let needs_injection = !css_paths.is_empty() || !js_paths.is_empty();
if needs_injection {
let css_refs: Vec<&str> = css_paths.iter().map(|s| s.as_str()).collect();
let js_refs: Vec<&str> = js_paths.iter().map(|s| s.as_str()).collect();
let injected: Result<typst_bundle::VirtualFs> = virtual_fs
.into_iter()
.map(|(vpath, bytes)| {
let path_str = vpath.get_with_slash().trim_start_matches('/').to_string();
if path_str.ends_with(".html") {
let html = String::from_utf8_lossy(&bytes);
let mut dom = crate::util::html::HtmlDom::parse(&html)?;
dom.inject_head_links(&[], &css_refs, &js_refs)?;
// Depth-relative asset refs so nested pages resolve them.
let prefix = crate::util::html::depth_prefix(&path_str);
let css_refs: Vec<String> =
css_paths.iter().map(|s| format!("{prefix}{s}")).collect();
let js_refs: Vec<String> =
js_paths.iter().map(|s| format!("{prefix}{s}")).collect();
let css: Vec<&str> = css_refs.iter().map(|s| s.as_str()).collect();
let js: Vec<&str> = js_refs.iter().map(|s| s.as_str()).collect();
dom.inject_head_links(&[], &css, &js)?;
let modified = dom.serialize()?;
Ok((vpath, typst::foundations::Bytes::new(modified.into_bytes())))
} else {
Expand Down
4 changes: 2 additions & 2 deletions crates/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ pub use config::{AssetsField, PluginAssets, PluginSection, Spine};
// Plugin trait and context
pub use parser::RheoValue;
pub use plugins::{
AssetConfig, CastVertebra, FormatInitTemplate, FormatPlugin, OpenHandle, PackageAssets,
PluginContext, ResolvedPackage, ServerHandle, SpineLayoutKind, TypstFormat,
AssetConfig, CastVertebra, EmbeddedDefault, FormatInitTemplate, FormatPlugin, OpenHandle,
PackageAssets, PluginContext, ResolvedPackage, ServerHandle, SpineLayoutKind, TypstFormat,
};

// HTML/PDF export utilities
Expand Down
15 changes: 15 additions & 0 deletions crates/core/src/plugins/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ pub enum OpenHandle {

use crate::config::PluginAssets;

/// An embedded fallback for an asset: content the plugin ships built in, written
/// into the output directory (under `name`) when no on-disk source resolves, then
/// linked like any copied asset. Lets a plugin ship a default (e.g. the HTML
/// default stylesheet) as a real linked file rather than inlining it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EmbeddedDefault {
/// Output filename (relative to the plugin output dir) to write the content as.
pub name: &'static str,
/// The verbatim bytes to write.
pub content: &'static str,
}

/// Declares an additional non-Typst input file needed from the project directory.
#[derive(Debug, Clone)]
pub struct AssetConfig {
Expand All @@ -43,6 +55,9 @@ pub struct AssetConfig {
pub default_path: &'static str,
/// If true, a missing file is a compile error; if false, it is absent from ctx.inputs
pub required: bool,
/// Embedded fallback written to the output dir and linked when no on-disk
/// source (default path or override) resolves. `None` = no fallback.
pub default_content: Option<EmbeddedDefault>,
}

#[derive(Debug, Clone)]
Expand Down
Loading
Loading