use std::sync::OnceLock;
mod common;
pub mod folder_list;
use common::path_extension_lower;
pub use common::{file_stem, html_escape_attr};
pub const CLASS_EMBED: &str = "moss-embed";
pub const CLASS_EMBED_IFRAME: &str = "moss-embed-iframe";
pub const CLASS_EMBED_PDF: &str = "moss-embed-pdf";
pub const CLASS_EMBED_AUDIO: &str = "moss-embed-audio";
pub const CLASS_EMBED_VIDEO: &str = "moss-embed-video";
pub const CLASS_EMBED_NOTEBOOK: &str = "moss-embed-notebook";
pub const CLASS_EMBED_3D: &str = "moss-embed-3d";
pub const CLASS_EMBED_TABLE: &str = "moss-embed-table";
pub const MARKER_MARKDOWN: &str = "moss-embed";
pub const MARKER_IPYNB: &str = "moss-embed-ipynb";
pub const MARKER_TABLE: &str = "moss-embed-table";
pub use folder_list::{MARKER_END, MARKER_FOLDER_LIST};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ParsedEmbed<'a> {
pub resolved_path: &'a str,
pub from_path: &'a str,
pub query: Option<&'a str>,
pub section: Option<&'a str>,
pub alias: Option<&'a str>,
pub width: Option<&'static str>,
pub attrs: Option<crate::ast::attrs::AttrBlock>,
}
#[derive(Debug, PartialEq, Eq)]
pub enum RenderedEmbed {
Inline(String),
Html(String),
Deferred { marker: String },
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Dim {
Px(u32),
Percent(f32),
Vh(f32),
}
impl Dim {
pub fn to_css(self) -> String {
match self {
Dim::Px(n) => format!("{}px", n),
Dim::Percent(v) => {
if v.fract() == 0.0 {
format!("{}%", v as i64)
} else {
format!("{}%", v)
}
}
Dim::Vh(v) => {
if v.fract() == 0.0 {
format!("{}vh", v as i64)
} else {
format!("{}vh", v)
}
}
}
}
fn parse(s: &str) -> Option<Self> {
let s = s.trim();
if s.is_empty() {
return None;
}
if let Some(rest) = s.strip_suffix('%') {
return rest.trim().parse::<f32>().ok().map(Dim::Percent);
}
if let Some(rest) = s.strip_suffix("vh") {
return rest.trim().parse::<f32>().ok().map(Dim::Vh);
}
if let Some(rest) = s.strip_suffix("px") {
return rest.trim().parse::<u32>().ok().map(Dim::Px);
}
s.parse::<u32>().ok().map(Dim::Px)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Sizing {
Width(Dim),
Box(Dim, Dim),
}
impl Sizing {
pub fn parse(s: &str) -> Option<Self> {
let s = s.trim();
if s.is_empty() {
return None;
}
if let Some((w, h)) = s.split_once('x') {
let wd = Dim::parse(w)?;
let hd = Dim::parse(h)?;
return Some(Sizing::Box(wd, hd));
}
Dim::parse(s).map(Sizing::Width)
}
}
pub trait EmbedRenderer: std::fmt::Debug + Send + Sync {
fn extensions(&self) -> &[&'static str];
fn render(&self, embed: &ParsedEmbed<'_>) -> RenderedEmbed;
fn head_assets(&self) -> &[&'static str] {
&[]
}
}
fn registry() -> &'static [&'static dyn EmbedRenderer] {
static INIT: OnceLock<Vec<&'static dyn EmbedRenderer>> = OnceLock::new();
INIT.get_or_init(|| {
vec![
&MarkdownEmbedRenderer as &'static dyn EmbedRenderer,
&IframeRenderer as &'static dyn EmbedRenderer,
&PdfRenderer as &'static dyn EmbedRenderer,
&AudioRenderer as &'static dyn EmbedRenderer,
&VideoRenderer as &'static dyn EmbedRenderer,
&NotebookRenderer as &'static dyn EmbedRenderer,
&ModelViewerRenderer as &'static dyn EmbedRenderer,
&TableRenderer as &'static dyn EmbedRenderer,
]
})
}
pub fn lookup_renderer(ext: &str) -> Option<&'static dyn EmbedRenderer> {
if ext.is_empty() {
return None;
}
registry()
.iter()
.copied()
.find(|r| r.extensions().iter().any(|e| e.eq_ignore_ascii_case(ext)))
}
use crate::heading_anchor::obsidian_heading_anchor;
use crate::media::parse_media_attrs;
use super::fuzzy_path::relative_asset_path;
use super::title_params::TitleParams;
pub(crate) const IMAGE_EXTENSIONS: &[&str] = &["png", "jpg", "jpeg", "gif", "svg", "webp"];
fn align_keyword(side: crate::media::AlignSide) -> &'static str {
match side {
crate::media::AlignSide::Left => "left",
crate::media::AlignSide::Right => "right",
}
}
fn add_class_dedup(acc: &mut Vec<String>, class: &str) {
if !acc.iter().any(|c| c == class) {
acc.push(class.to_string());
}
}
pub(super) fn fold_attrs_into_params(
block: &crate::ast::attrs::AttrBlock,
params: &mut TitleParams,
) {
let mut classes: Vec<String> = Vec::new();
let mut consumed_class_kv = false;
for class in &block.classes {
if let Some(side) = crate::media::AlignSide::from_keyword(class) {
params.insert("align", align_keyword(side));
} else {
add_class_dedup(&mut classes, class);
}
}
if let Some(w) = block.width {
params.insert("data-width", w);
}
for (k, v) in &block.kvs {
if k == "class" {
consumed_class_kv = true;
for c in v.split_whitespace() {
if let Some(side) = crate::media::AlignSide::from_keyword(c) {
params.insert("align", align_keyword(side));
} else {
add_class_dedup(&mut classes, c);
}
}
}
}
if !classes.is_empty() {
params.insert("classes", classes.join(" "));
}
for (k, v) in &block.kvs {
if consumed_class_kv && k == "class" {
continue;
}
params.insert(k.clone(), v.clone());
}
}
fn markdown_escape_alt(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('[', "\\[")
.replace(']', "\\]")
}
fn render_link_markdown(
embed: &ParsedEmbed<'_>,
kind: &'static str,
extra: impl FnOnce(&ParsedEmbed<'_>, &mut TitleParams),
) -> String {
let url = relative_asset_path(embed.from_path, embed.resolved_path);
let mut params = TitleParams::default();
params.insert("kind", kind);
if let Some(w) = embed.width {
params.insert("data-width", w);
}
extra(embed, &mut params);
if let Some(block) = &embed.attrs {
fold_attrs_into_params(block, &mut params);
}
let _ = params;
let name = file_stem(embed.resolved_path);
format!("[{}]({})", markdown_escape_alt(&name), url)
}
#[derive(Debug)]
pub struct MarkdownEmbedRenderer;
impl EmbedRenderer for MarkdownEmbedRenderer {
fn extensions(&self) -> &[&'static str] {
&["md"]
}
fn render(&self, embed: &ParsedEmbed<'_>) -> RenderedEmbed {
let anchor = build_embed_anchor(embed.section);
RenderedEmbed::Deferred {
marker: format!(
"<!-- {}:{}{} -->",
MARKER_MARKDOWN, embed.resolved_path, anchor
),
}
}
}
fn build_embed_anchor(section: Option<&str>) -> String {
match section {
None => String::new(),
Some(s) if s.is_empty() => String::new(),
Some(s) => {
if s.starts_with('^') {
format!("#{}", s)
} else {
format!("#{}", obsidian_heading_anchor(s))
}
}
}
}
#[derive(Debug)]
pub struct IframeRenderer;
impl EmbedRenderer for IframeRenderer {
fn extensions(&self) -> &[&'static str] {
&["html", "htm"]
}
fn render(&self, embed: &ParsedEmbed<'_>) -> RenderedEmbed {
RenderedEmbed::Inline(render_link_markdown(embed, "iframe", iframe_extra_params))
}
}
fn iframe_extra_params(embed: &ParsedEmbed<'_>, params: &mut TitleParams) {
if let Some(q) = embed.query {
params.insert("query", q);
}
if let Some(f) = embed.section {
params.insert("fragment", f);
}
let Some(alias) = embed.alias else {
return;
};
match Sizing::parse(alias) {
Some(Sizing::Width(w)) => {
params.insert("width", w.to_css());
}
Some(Sizing::Box(w, h)) => {
params.insert("width", w.to_css());
params.insert("height", h.to_css());
}
None => {
params.insert("title", alias);
}
}
}
#[derive(Debug)]
pub struct PdfRenderer;
impl EmbedRenderer for PdfRenderer {
fn extensions(&self) -> &[&'static str] {
&["pdf"]
}
fn render(&self, embed: &ParsedEmbed<'_>) -> RenderedEmbed {
RenderedEmbed::Inline(render_link_markdown(embed, "pdf", pdf_extra_params))
}
}
fn pdf_extra_params(embed: &ParsedEmbed<'_>, params: &mut TitleParams) {
if let Some(q) = embed.query {
params.insert("query", q);
}
if let Some(f) = embed.section {
params.insert("fragment", f);
}
if let Some(alias) = embed.alias {
match Sizing::parse(alias) {
Some(Sizing::Width(w)) => {
params.insert("width", w.to_css());
}
Some(Sizing::Box(w, h)) => {
params.insert("width", w.to_css());
params.insert("height", h.to_css());
}
None => {}
}
}
}
const AUDIO_EXTENSIONS: &[&str] = &["mp3", "wav", "ogg", "flac", "m4a", "opus"];
#[derive(Debug)]
pub struct AudioRenderer;
impl EmbedRenderer for AudioRenderer {
fn extensions(&self) -> &[&'static str] {
AUDIO_EXTENSIONS
}
fn render(&self, embed: &ParsedEmbed<'_>) -> RenderedEmbed {
RenderedEmbed::Inline(render_link_markdown(embed, "audio", audio_extra_params))
}
}
fn audio_extra_params(embed: &ParsedEmbed<'_>, params: &mut TitleParams) {
let ext = path_extension_lower(embed.resolved_path);
if !ext.is_empty() {
params.insert("ext", ext);
}
}
const VIDEO_EXTENSIONS: &[&str] = &["mp4", "webm", "mov", "m4v"];
#[derive(Debug)]
pub struct VideoRenderer;
impl EmbedRenderer for VideoRenderer {
fn extensions(&self) -> &[&'static str] {
VIDEO_EXTENSIONS
}
fn render(&self, embed: &ParsedEmbed<'_>) -> RenderedEmbed {
RenderedEmbed::Inline(render_link_markdown(embed, "video", video_extra_params))
}
}
fn video_extra_params(embed: &ParsedEmbed<'_>, params: &mut TitleParams) {
if let Some(alias) = embed.alias {
match Sizing::parse(alias) {
Some(Sizing::Width(w)) => {
params.insert("width", w.to_css());
}
Some(Sizing::Box(w, h)) => {
params.insert("width", w.to_css());
params.insert("height", h.to_css());
}
None => {}
}
}
}
#[derive(Debug)]
pub struct NotebookRenderer;
impl EmbedRenderer for NotebookRenderer {
fn extensions(&self) -> &[&'static str] {
&["ipynb"]
}
fn render(&self, embed: &ParsedEmbed<'_>) -> RenderedEmbed {
let target = match embed.query {
Some(q) => format!("{}?{}", embed.resolved_path, q),
None => embed.resolved_path.to_string(),
};
RenderedEmbed::Deferred {
marker: format!("<!-- {}:{} -->", MARKER_IPYNB, target),
}
}
}
const MODEL_VIEWER_SCRIPT: &str = "<script type=\"module\" src=\"https://ajax.googleapis.com/ajax/libs/model-viewer/3.4.0/model-viewer.min.js\"></script>";
#[derive(Debug)]
pub struct ModelViewerRenderer;
impl EmbedRenderer for ModelViewerRenderer {
fn extensions(&self) -> &[&'static str] {
&["glb", "gltf"]
}
fn render(&self, embed: &ParsedEmbed<'_>) -> RenderedEmbed {
RenderedEmbed::Inline(render_link_markdown(embed, "3d", model_viewer_extra_params))
}
fn head_assets(&self) -> &[&'static str] {
&[MODEL_VIEWER_SCRIPT]
}
}
fn model_viewer_extra_params(embed: &ParsedEmbed<'_>, params: &mut TitleParams) {
if let Some(alias) = embed.alias {
match Sizing::parse(alias) {
Some(Sizing::Width(w)) => {
params.insert("width", w.to_css());
}
Some(Sizing::Box(w, h)) => {
params.insert("width", w.to_css());
params.insert("height", h.to_css());
}
None => {}
}
}
}
#[derive(Debug)]
pub struct TableRenderer;
impl EmbedRenderer for TableRenderer {
fn extensions(&self) -> &[&'static str] {
&["csv", "tsv"]
}
fn render(&self, embed: &ParsedEmbed<'_>) -> RenderedEmbed {
RenderedEmbed::Deferred {
marker: format!("<!-- {}:{} -->", MARKER_TABLE, embed.resolved_path),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug)]
struct DummyRenderer;
impl EmbedRenderer for DummyRenderer {
fn extensions(&self) -> &[&'static str] {
&["xyz"]
}
fn render(&self, embed: &ParsedEmbed<'_>) -> RenderedEmbed {
RenderedEmbed::Inline(format!("<dummy src={}>", embed.resolved_path))
}
}
#[test]
fn test_dummy_renderer_trait_surface() {
let r = DummyRenderer;
assert_eq!(r.extensions(), &["xyz"]);
let embed = ParsedEmbed {
resolved_path: "a.xyz",
from_path: "post.md",
query: None,
section: None,
alias: None,
width: None,
attrs: None,
};
assert_eq!(
r.render(&embed),
RenderedEmbed::Inline("<dummy src=a.xyz>".to_string())
);
}
#[test]
fn test_markdown_embed_renderer_no_section() {
let r = MarkdownEmbedRenderer;
let embed = ParsedEmbed {
resolved_path: "posts/intro.md",
from_path: "index.md",
query: None,
section: None,
alias: None,
width: None,
attrs: None,
};
assert_eq!(
r.render(&embed),
RenderedEmbed::Deferred {
marker: "<!-- moss-embed:posts/intro.md -->".to_string()
}
);
}
#[test]
fn test_markdown_embed_renderer_heading_section() {
let r = MarkdownEmbedRenderer;
let embed = ParsedEmbed {
resolved_path: "guide.md",
from_path: "index.md",
query: None,
section: Some("Getting Started"),
alias: None,
width: None,
attrs: None,
};
assert_eq!(
r.render(&embed),
RenderedEmbed::Deferred {
marker: "<!-- moss-embed:guide.md#getting-started -->".to_string()
}
);
}
#[test]
fn test_markdown_embed_renderer_block_ref_section() {
let r = MarkdownEmbedRenderer;
let embed = ParsedEmbed {
resolved_path: "guide.md",
from_path: "index.md",
query: None,
section: Some("^block-xyz"),
alias: None,
width: None,
attrs: None,
};
assert_eq!(
r.render(&embed),
RenderedEmbed::Deferred {
marker: "<!-- moss-embed:guide.md#^block-xyz -->".to_string()
}
);
}
#[test]
fn test_markdown_embed_renderer_extensions() {
assert_eq!(MarkdownEmbedRenderer.extensions(), &["md"]);
}
#[test]
fn markdown_escape_alt_brackets() {
assert_eq!(markdown_escape_alt("plain"), "plain");
assert_eq!(markdown_escape_alt("has [brackets]"), r"has \[brackets\]");
assert_eq!(
markdown_escape_alt(r"with \ backslash"),
r"with \\ backslash"
);
}
#[test]
fn test_dim_css_px() {
assert_eq!(Dim::Px(200).to_css(), "200px");
}
#[test]
fn test_dim_css_percent() {
assert_eq!(Dim::Percent(100.0).to_css(), "100%");
assert_eq!(Dim::Percent(50.5).to_css(), "50.5%");
}
#[test]
fn test_dim_css_vh() {
assert_eq!(Dim::Vh(100.0).to_css(), "100vh");
}
#[test]
fn test_sizing_parse_width_only_px() {
assert_eq!(Sizing::parse("200"), Some(Sizing::Width(Dim::Px(200))));
}
#[test]
fn test_sizing_parse_width_only_percent() {
assert_eq!(
Sizing::parse("100%"),
Some(Sizing::Width(Dim::Percent(100.0)))
);
}
#[test]
fn test_sizing_parse_box_px() {
assert_eq!(
Sizing::parse("200x150"),
Some(Sizing::Box(Dim::Px(200), Dim::Px(150)))
);
}
#[test]
fn test_sizing_parse_box_percent_by_px() {
assert_eq!(
Sizing::parse("100%x600"),
Some(Sizing::Box(Dim::Percent(100.0), Dim::Px(600)))
);
}
#[test]
fn test_sizing_parse_box_vh_height() {
assert_eq!(
Sizing::parse("100%x100vh"),
Some(Sizing::Box(Dim::Percent(100.0), Dim::Vh(100.0)))
);
}
#[test]
fn test_sizing_parse_rejects_display_keywords() {
assert_eq!(Sizing::parse("contain"), None);
assert_eq!(Sizing::parse("left top"), None);
}
#[test]
fn test_sizing_parse_empty_returns_none() {
assert_eq!(Sizing::parse(""), None);
assert_eq!(Sizing::parse(" "), None);
}
#[test]
fn test_embed_class_constants_stable() {
assert_eq!(CLASS_EMBED, "moss-embed");
assert_eq!(CLASS_EMBED_IFRAME, "moss-embed-iframe");
assert_eq!(CLASS_EMBED_PDF, "moss-embed-pdf");
assert_eq!(CLASS_EMBED_AUDIO, "moss-embed-audio");
assert_eq!(CLASS_EMBED_VIDEO, "moss-embed-video");
assert_eq!(CLASS_EMBED_NOTEBOOK, "moss-embed-notebook");
assert_eq!(CLASS_EMBED_3D, "moss-embed-3d");
assert_eq!(CLASS_EMBED_TABLE, "moss-embed-table");
}
#[test]
fn test_embed_marker_prefixes_stable() {
assert_eq!(MARKER_MARKDOWN, "moss-embed");
assert_eq!(MARKER_IPYNB, "moss-embed-ipynb");
assert_eq!(MARKER_TABLE, "moss-embed-table");
}
#[test]
fn test_rendered_embed_html_variant() {
let h = RenderedEmbed::Html("<iframe src=\"x\"></iframe>".to_string());
match h {
RenderedEmbed::Html(s) => assert!(s.contains("iframe")),
_ => panic!("expected Html variant"),
}
}
#[test]
fn test_rendered_embed_deferred_variant() {
let d = RenderedEmbed::Deferred {
marker: "<!-- moss-embed-ipynb:nb.ipynb -->".to_string(),
};
match d {
RenderedEmbed::Deferred { marker } => assert!(marker.contains("ipynb")),
_ => panic!("expected Deferred variant"),
}
}
#[test]
fn test_lookup_renderer_by_extension() {
assert!(lookup_renderer("jpg").is_none());
assert!(lookup_renderer("JPG").is_none()); assert!(lookup_renderer("md").is_some());
assert!(lookup_renderer("MD").is_some()); assert!(lookup_renderer("xyz").is_none());
assert!(lookup_renderer("").is_none());
}
#[test]
fn test_iframe_renderer_extensions() {
let r = IframeRenderer;
let exts: Vec<&&str> = r.extensions().iter().collect();
assert!(exts.iter().any(|&&x| x == "html"));
assert!(exts.iter().any(|&&x| x == "htm"));
}
fn iframe_md(e: &ParsedEmbed) -> String {
match IframeRenderer.render(e) {
RenderedEmbed::Inline(s) => s,
_ => panic!("expected Inline (Stage 1 markdown)"),
}
}
#[test]
fn stage1_iframe_basic_is_bare_link() {
let out = iframe_md(&ParsedEmbed {
resolved_path: "widget.html",
from_path: "post.md",
query: None,
section: None,
alias: None,
width: None,
attrs: None,
});
assert_eq!(out, "[widget](widget.html)");
}
#[test]
fn stage1_iframe_with_query_emits_bare_link() {
let out = iframe_md(&ParsedEmbed {
resolved_path: "scale.html",
from_path: "post.md",
query: Some("a=major,minor&r=D"),
section: None,
alias: None,
width: None,
attrs: None,
});
assert_eq!(out, "[scale](scale.html)");
}
#[test]
fn stage1_iframe_with_sizing_alias_emits_bare_link() {
let out = iframe_md(&ParsedEmbed {
resolved_path: "widget.html",
from_path: "post.md",
query: None,
section: None,
alias: Some("100%x600"),
width: None,
attrs: None,
});
assert_eq!(out, "[widget](widget.html)");
}
#[test]
fn stage1_iframe_text_alias_emits_bare_link() {
let out = iframe_md(&ParsedEmbed {
resolved_path: "widget.html",
from_path: "post.md",
query: None,
section: None,
alias: Some("My cool widget"),
width: None,
attrs: None,
});
assert_eq!(out, "[widget](widget.html)");
}
#[test]
fn stage1_iframe_with_fragment_emits_bare_link() {
let out = iframe_md(&ParsedEmbed {
resolved_path: "doc.html",
from_path: "post.md",
query: Some("x=1"),
section: Some("section2"),
alias: None,
width: None,
attrs: None,
});
assert_eq!(out, "[doc](doc.html)");
}
#[test]
fn stage1_iframe_with_canonical_width_emits_bare_link() {
let out = iframe_md(&ParsedEmbed {
resolved_path: "widget.html",
from_path: "post.md",
query: None,
section: None,
alias: None,
width: Some("wide"),
attrs: None,
});
assert_eq!(out, "[widget](widget.html)");
}
#[test]
fn test_sizing_parse_malformed_box_is_none() {
assert_eq!(Sizing::parse("100xbad"), None);
assert_eq!(Sizing::parse("100x"), None);
assert_eq!(Sizing::parse("-100"), None);
}
#[test]
fn stage1_iframe_malformed_sizing_emits_bare_link() {
let out = iframe_md(&ParsedEmbed {
resolved_path: "widget.html",
from_path: "post.md",
query: None,
section: None,
alias: Some("100xbad"),
width: None,
attrs: None,
});
assert_eq!(out, "[widget](widget.html)");
}
fn pdf_md(e: &ParsedEmbed) -> String {
match PdfRenderer.render(e) {
RenderedEmbed::Inline(s) => s,
_ => panic!("expected Inline (Stage 1 markdown)"),
}
}
#[test]
fn test_pdf_renderer_extensions() {
assert_eq!(PdfRenderer.extensions(), &["pdf"]);
}
#[test]
fn stage1_pdf_basic_is_bare_link() {
let out = pdf_md(&ParsedEmbed {
resolved_path: "report.pdf",
from_path: "post.md",
query: None,
section: None,
alias: None,
width: None,
attrs: None,
});
assert_eq!(out, "[report](report.pdf)");
}
#[test]
fn stage1_pdf_with_page_fragment_emits_bare_link() {
let out = pdf_md(&ParsedEmbed {
resolved_path: "doc.pdf",
from_path: "post.md",
query: None,
section: Some("page=5"),
alias: None,
width: None,
attrs: None,
});
assert_eq!(out, "[doc](doc.pdf)");
}
#[test]
fn stage1_pdf_with_sizing_emits_bare_link() {
let out = pdf_md(&ParsedEmbed {
resolved_path: "doc.pdf",
from_path: "post.md",
query: None,
section: None,
alias: Some("100%x800"),
width: None,
attrs: None,
});
assert_eq!(out, "[doc](doc.pdf)");
}
fn audio_md(e: &ParsedEmbed) -> String {
match AudioRenderer.render(e) {
RenderedEmbed::Inline(s) => s,
_ => panic!("expected Inline (Stage 1 markdown)"),
}
}
#[test]
fn test_audio_renderer_extensions() {
let r = AudioRenderer;
let exts: Vec<&&str> = r.extensions().iter().collect();
for e in &["mp3", "wav", "ogg", "flac", "m4a", "opus"] {
assert!(exts.iter().any(|&&x| x == *e), "missing: {}", e);
}
}
#[test]
fn stage1_audio_basic_is_bare_link() {
let out = audio_md(&ParsedEmbed {
resolved_path: "song.mp3",
from_path: "post.md",
query: None,
section: None,
alias: None,
width: None,
attrs: None,
});
assert_eq!(out, "[song](song.mp3)");
}
#[test]
fn stage1_audio_each_extension_emits_bare_link() {
for ext in ["mp3", "wav", "ogg", "flac", "m4a", "opus"] {
let path = format!("a.{}", ext);
let out = audio_md(&ParsedEmbed {
resolved_path: &path,
from_path: "post.md",
query: None,
section: None,
alias: None,
width: None,
attrs: None,
});
assert_eq!(out, format!("[a](a.{})", ext), "ext={}", ext);
}
}
fn video_md(e: &ParsedEmbed) -> String {
match VideoRenderer.render(e) {
RenderedEmbed::Inline(s) => s,
_ => panic!("expected Inline (Stage 1 markdown)"),
}
}
#[test]
fn test_video_renderer_extensions() {
let r = VideoRenderer;
let exts: Vec<&&str> = r.extensions().iter().collect();
for e in &["mp4", "webm", "mov", "m4v"] {
assert!(exts.iter().any(|&&x| x == *e), "missing: {}", e);
}
}
#[test]
fn stage1_video_basic_is_bare_link() {
let out = video_md(&ParsedEmbed {
resolved_path: "trailer.mp4",
from_path: "post.md",
query: None,
section: None,
alias: None,
width: None,
attrs: None,
});
assert_eq!(out, "[trailer](trailer.mp4)");
}
#[test]
fn stage1_video_emits_original_extension_in_url() {
for ext in ["mp4", "webm", "mov", "m4v"] {
let path = format!("clip.{}", ext);
let out = video_md(&ParsedEmbed {
resolved_path: &path,
from_path: "post.md",
query: None,
section: None,
alias: None,
width: None,
attrs: None,
});
assert_eq!(out, format!("[clip](clip.{})", ext), "ext={}", ext);
}
}
#[test]
fn stage1_video_with_sizing_emits_bare_link() {
let out = video_md(&ParsedEmbed {
resolved_path: "clip.mp4",
from_path: "post.md",
query: None,
section: None,
alias: Some("640x360"),
width: None,
attrs: None,
});
assert_eq!(out, "[clip](clip.mp4)");
}
#[test]
fn test_notebook_renderer_extensions() {
assert_eq!(NotebookRenderer.extensions(), &["ipynb"]);
}
#[test]
fn test_notebook_renderer_basic() {
let embed = ParsedEmbed {
resolved_path: "resources/habitable-zone.ipynb",
from_path: "posts/hello.md",
query: None,
section: None,
alias: None,
width: None,
attrs: None,
};
match NotebookRenderer.render(&embed) {
RenderedEmbed::Deferred { marker } => assert_eq!(
marker,
"<!-- moss-embed-ipynb:resources/habitable-zone.ipynb -->"
),
_ => panic!("expected Deferred"),
}
}
#[test]
fn test_notebook_renderer_with_query() {
let embed = ParsedEmbed {
resolved_path: "nb.ipynb",
from_path: "post.md",
query: Some("cells=1-5"),
section: None,
alias: None,
width: None,
attrs: None,
};
match NotebookRenderer.render(&embed) {
RenderedEmbed::Deferred { marker } => {
assert!(marker.contains("nb.ipynb?cells=1-5"), "got: {}", marker)
}
_ => panic!("expected Deferred"),
}
}
#[test]
fn test_notebook_renderer_no_head_assets() {
assert!(NotebookRenderer.head_assets().is_empty());
}
fn mv_md(e: &ParsedEmbed) -> String {
match ModelViewerRenderer.render(e) {
RenderedEmbed::Inline(s) => s,
_ => panic!("expected Inline (Stage 1 markdown)"),
}
}
#[test]
fn test_model_viewer_extensions() {
let exts = ModelViewerRenderer.extensions();
assert!(exts.iter().any(|&x| x == "glb"));
assert!(exts.iter().any(|&x| x == "gltf"));
}
#[test]
fn stage1_model_viewer_basic_is_bare_link() {
let out = mv_md(&ParsedEmbed {
resolved_path: "teapot.glb",
from_path: "post.md",
query: None,
section: None,
alias: None,
width: None,
attrs: None,
});
assert_eq!(out, "[teapot](teapot.glb)");
}
#[test]
fn stage1_model_viewer_with_sizing_emits_bare_link() {
let out = mv_md(&ParsedEmbed {
resolved_path: "m.glb",
from_path: "post.md",
query: None,
section: None,
alias: Some("400x400"),
width: None,
attrs: None,
});
assert_eq!(out, "[m](m.glb)");
}
#[test]
fn test_model_viewer_head_assets() {
let assets = ModelViewerRenderer.head_assets();
assert_eq!(assets.len(), 1);
assert!(assets[0].contains("model-viewer"), "got: {}", assets[0]);
assert!(assets[0].contains("<script"), "got: {}", assets[0]);
}
#[test]
fn test_table_renderer_extensions() {
let exts = TableRenderer.extensions();
assert!(exts.iter().any(|&x| x == "csv"));
assert!(exts.iter().any(|&x| x == "tsv"));
}
#[test]
fn test_table_renderer_emits_deferred() {
let embed = ParsedEmbed {
resolved_path: "data/stars.csv",
from_path: "post.md",
query: None,
section: None,
alias: None,
width: None,
attrs: None,
};
match TableRenderer.render(&embed) {
RenderedEmbed::Deferred { marker } => {
assert_eq!(marker, "<!-- moss-embed-table:data/stars.csv -->")
}
_ => panic!("expected Deferred"),
}
}
fn embed_with_width<'a>(resolved_path: &'a str, width: &'static str) -> ParsedEmbed<'a> {
ParsedEmbed {
resolved_path,
from_path: "post.md",
query: None,
section: None,
alias: None,
width: Some(width),
attrs: None,
}
}
#[test]
fn stage1_iframe_width_emits_bare_link() {
let out = iframe_md(&embed_with_width("widget.html", "screen"));
assert_eq!(out, "[widget](widget.html)");
}
#[test]
fn stage1_iframe_no_width_emits_bare_link() {
let out = iframe_md(&ParsedEmbed {
resolved_path: "widget.html",
from_path: "post.md",
query: None,
section: None,
alias: None,
width: None,
attrs: None,
});
assert_eq!(out, "[widget](widget.html)");
}
#[test]
fn stage1_pdf_width_emits_bare_link() {
let out = pdf_md(&embed_with_width("doc.pdf", "wide"));
assert_eq!(out, "[doc](doc.pdf)");
}
#[test]
fn stage1_audio_width_emits_bare_link() {
let out = audio_md(&embed_with_width("song.mp3", "body"));
assert_eq!(out, "[song](song.mp3)");
}
#[test]
fn stage1_video_width_emits_bare_link() {
let out = video_md(&embed_with_width("clip.mp4", "screen"));
assert_eq!(out, "[clip](clip.mp4)");
}
#[test]
fn stage1_video_no_width_emits_bare_link() {
let out = video_md(&ParsedEmbed {
resolved_path: "clip.mp4",
from_path: "post.md",
query: None,
section: None,
alias: None,
width: None,
attrs: None,
});
assert_eq!(out, "[clip](clip.mp4)");
}
#[test]
fn stage1_model_viewer_width_emits_bare_link() {
let out = mv_md(&embed_with_width("model.glb", "page"));
assert_eq!(out, "[model](model.glb)");
}
}