use std::fmt::Write as _;
use asciidoc_parser::{
attributes::Attrlist,
blocks::{
Block,
IsBlock,
MediaBlock,
MediaType,
},
};
use crate::{
Renderer,
block,
html::escape_attr,
};
impl<'src> Renderer<'src> {
pub(super) fn media_block(&mut self, block: &'src Block<'src>, media: &'src MediaBlock<'src>) {
match media.type_() {
MediaType::Image => self.image_block(block, media),
MediaType::Video => self.video_block(block, media),
MediaType::Audio => self.audio_block(block, media),
}
}
fn in_images_dir(&self, target: &str) -> String {
let Some(dir) = self.attribute("imagesdir").filter(|dir| !dir.is_empty()) else {
return target.to_string();
};
if target.starts_with('/') || target.starts_with("data:") || target.contains("://") {
return target.to_string();
}
format!("{}/{target}", dir.trim_end_matches('/'))
}
fn open_media_wrapper(
&mut self,
block: &'src Block<'src>,
context: &str,
attrlist: &'src Attrlist<'src>,
aligned: bool,
) {
let align = if aligned {
named(attrlist, "align").map(|align| format!("text-{align}"))
} else {
None
};
let shape = [
context,
named(attrlist, "float").unwrap_or_default(),
align.as_deref().unwrap_or_default(),
];
let mut classes = block::wrapper_classes(block, &shape);
classes.extend(
named(attrlist, "role")
.unwrap_or_default()
.split_whitespace()
.map(str::to_string),
);
let classes: Vec<&str> = classes.iter().map(String::as_str).collect();
self.out.open("div", block.id(), &classes);
}
fn image_block(&mut self, block: &'src Block<'src>, media: &'src MediaBlock<'src>) {
let attrlist = media.macro_attrlist();
let target = self.in_images_dir(media.resolved_target());
let target = target.as_str();
let alt =
positional(attrlist, "alt", 1).map_or_else(|| alt_from_target(target), str::to_string);
let mut img = format!(
"<img src=\"{}\" alt=\"{}\"",
escape_attr(target),
escape_attr(&alt)
);
attribute(&mut img, "width", positional(attrlist, "width", 2));
attribute(&mut img, "height", positional(attrlist, "height", 3));
img.push('>');
if let Some(link) = named(attrlist, "link") {
img = format!(
"<a class=\"image\" href=\"{}\">{img}</a>",
escape_attr(link)
);
}
self.open_media_wrapper(block, "imageblock", attrlist, true);
self.out.open("div", None, &["content"]);
self.out.line(&img);
self.out.close("div");
let caption = self.figure_caption(block);
self.titled(block, &caption);
self.out.close("div");
}
fn video_block(&mut self, block: &'src Block<'src>, media: &'src MediaBlock<'src>) {
let attrlist = media.macro_attrlist();
let mut tag = format!(
"<video src=\"{}{}\"",
escape_attr(&self.in_images_dir(media.resolved_target())),
escape_attr(&time_fragment(attrlist))
);
let poster = named(attrlist, "poster").map(|poster| self.in_images_dir(poster));
attribute(&mut tag, "width", named(attrlist, "width"));
attribute(&mut tag, "height", named(attrlist, "height"));
attribute(&mut tag, "poster", poster.as_deref());
if !attrlist.has_option("nocontrols") {
tag.push_str(" controls");
}
if attrlist.has_option("autoplay") {
tag.push_str(" autoplay");
}
if attrlist.has_option("loop") {
tag.push_str(" loop");
}
tag.push('>');
self.open_media_wrapper(block, "videoblock", attrlist, false);
self.block_title(block);
self.out.open("div", None, &["content"]);
self.out.line(&tag);
self.out
.line("Your browser does not support the video tag.");
self.out.line("</video>");
self.out.close("div");
self.out.close("div");
}
fn audio_block(&mut self, block: &'src Block<'src>, media: &'src MediaBlock<'src>) {
let attrlist = media.macro_attrlist();
let mut tag = format!(
"<audio src=\"{}\"",
escape_attr(&self.in_images_dir(media.resolved_target()))
);
if !attrlist.has_option("nocontrols") {
tag.push_str(" controls");
}
if attrlist.has_option("autoplay") {
tag.push_str(" autoplay");
}
if attrlist.has_option("loop") {
tag.push_str(" loop");
}
tag.push('>');
self.open_media_wrapper(block, "audioblock", attrlist, false);
self.block_title(block);
self.out.open("div", None, &["content"]);
self.out.line(&tag);
self.out
.line("Your browser does not support the audio tag.");
self.out.line("</audio>");
self.out.close("div");
self.out.close("div");
}
}
fn time_fragment(attrlist: &Attrlist<'_>) -> String {
let start = named(attrlist, "start").unwrap_or_default();
let end = named(attrlist, "end").unwrap_or_default();
match (start.is_empty(), end.is_empty()) {
(true, true) => String::new(),
(_, true) => format!("#t={start}"),
_ => format!("#t={start},{end}"),
}
}
fn attribute(tag: &mut String, name: &str, value: Option<&str>) {
if let Some(value) = value {
let _ = write!(tag, " {name}=\"{}\"", escape_attr(value));
}
}
fn positional<'src>(attrlist: &'src Attrlist<'src>, name: &str, index: usize) -> Option<&'src str> {
named(attrlist, name).or_else(|| {
attrlist
.nth_attribute(index)
.filter(|attribute| attribute.name().is_none())
.map(asciidoc_parser::attributes::ElementAttribute::value)
.filter(|value| !value.is_empty())
})
}
fn named<'src>(attrlist: &'src Attrlist<'src>, name: &str) -> Option<&'src str> {
attrlist
.named_attribute(name)
.map(asciidoc_parser::attributes::ElementAttribute::value)
.filter(|value| !value.is_empty())
}
fn alt_from_target(target: &str) -> String {
let file_name = target.rsplit(['/', '\\']).next().unwrap_or(target);
let stem = file_name
.rsplit_once('.')
.map_or(file_name, |(stem, _)| stem);
stem.replace(['_', '-'], " ")
}