Skip to content

Commit

Permalink
feat: multimedia support with sanitization (#78)
Browse files Browse the repository at this point in the history
  • Loading branch information
Myriad-Dreamin authored Jun 15, 2024
1 parent 4dc4fe3 commit 63b1b03
Show file tree
Hide file tree
Showing 11 changed files with 245 additions and 17 deletions.
2 changes: 1 addition & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[submodule "assets/artifacts"]
path = assets/artifacts
url = https://github.com/Myriad-Dreamin/typst/
branch = assets-book-v0.1.4
branch = assets-book-v0.2.0
shallow = true
11 changes: 6 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,14 @@ pathdiff = "0.2.1"
[patch.crates-io]
typst = { git = "https://github.com/Myriad-Dreamin/typst.git", branch = "typst.ts-v0.11.1-content-hint" }
typst-syntax = { git = "https://github.com/Myriad-Dreamin/typst.git", branch = "typst.ts-v0.11.1-content-hint" }
typst-ts-svg-exporter = { git = "https://github.com/Myriad-Dreamin/typst.ts", rev = "6cc978011c16328b7aaa0dc660bb73818977475b", package = "typst-ts-svg-exporter" }
typst-ts-text-exporter = { git = "https://github.com/Myriad-Dreamin/typst.ts", rev = "6cc978011c16328b7aaa0dc660bb73818977475b", package = "typst-ts-text-exporter" }
typst-ts-core = { git = "https://github.com/Myriad-Dreamin/typst.ts", rev = "6cc978011c16328b7aaa0dc660bb73818977475b", package = "typst-ts-core" }
typst-ts-compiler = { git = "https://github.com/Myriad-Dreamin/typst.ts", rev = "6cc978011c16328b7aaa0dc660bb73818977475b", package = "typst-ts-compiler" }
typst-ts-svg-exporter = { git = "https://github.com/Myriad-Dreamin/typst.ts", rev = "b5371c5821b962617272b38b18cb2c1ee4110c99", package = "typst-ts-svg-exporter" }
typst-ts-text-exporter = { git = "https://github.com/Myriad-Dreamin/typst.ts", rev = "b5371c5821b962617272b38b18cb2c1ee4110c99", package = "typst-ts-text-exporter" }
typst-ts-core = { git = "https://github.com/Myriad-Dreamin/typst.ts", rev = "b5371c5821b962617272b38b18cb2c1ee4110c99", package = "typst-ts-core" }
typst-ts-compiler = { git = "https://github.com/Myriad-Dreamin/typst.ts", rev = "b5371c5821b962617272b38b18cb2c1ee4110c99", package = "typst-ts-compiler" }

# typst = { path = "../../../typst/crates/typst" }
# typst-syntax = { path = "../../../typst/crates/typst-syntax" }
# typst-ts-svg-exporter = { path = "../../exporter/svg" }
# typst-ts-text-exporter = { path = "../../exporter/text" }
# typst-ts-compiler = { path = "../../compiler" }
# typst-ts-core = { path = "../../core" }
2 changes: 1 addition & 1 deletion assets/artifacts
1 change: 1 addition & 0 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ comemo.workspace = true
# chrono.workspace = true
tokio.workspace = true
indexmap = "2"
url = "2"
include_dir.workspace = true

serde.workspace = true
Expand Down
4 changes: 4 additions & 0 deletions cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ pub struct CompileArgs {
action = ArgAction::Append,
)]
pub font_paths: Vec<PathBuf>,

/// Specify a filter to only load files with a specific extension.
#[clap(long, default_value = "^(player.bilibili.com)$")]
pub allowed_url_source: Option<String>,
}

#[derive(Default, Debug, Clone, Parser)]
Expand Down
145 changes: 140 additions & 5 deletions cli/src/render/typst.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use std::{
borrow::Cow,
cell::RefCell,
collections::HashMap,
collections::{HashMap, HashSet},
path::{Path, PathBuf},
rc::Rc,
sync::Arc,
sync::{Arc, OnceLock},
};

use crate::{
Expand All @@ -14,7 +14,8 @@ use crate::{
utils::{make_absolute, make_absolute_from, UnwrapOrExit},
CompileArgs,
};
use typst::diag::SourceResult;
use serde::Deserialize;
use typst::{diag::SourceResult, foundations::Regex};
use typst_ts_compiler::{
service::{
features::WITH_COMPILING_STATUS_FEATURE, CompileDriver, CompileEnv, CompileReport,
Expand All @@ -24,14 +25,18 @@ use typst_ts_compiler::{
};
use typst_ts_core::{
config::{compiler::EntryOpts, CompileOpts},
escape::{escape_str, AttributeEscapes},
path::PathClean,
vector::{
ir::{LayoutRegionNode, Module, Page, PageMetadata},
pass::Typst2VecPass,
},
TakeAs, Transformer, TypstAbs, TypstDocument,
IntoTypst, TakeAs, Transformer, TypstAbs, TypstDocument,
};
use typst_ts_svg_exporter::{
ir::{HtmlItem, ToItemMap, VecItem},
MultiVecDocument,
};
use typst_ts_svg_exporter::{ir::ToItemMap, MultiVecDocument};
// serialize_doc, LayoutRegionNode,

const THEME_LIST: [&str; 5] = ["light", "rust", "coal", "navy", "ayu"];
Expand Down Expand Up @@ -65,6 +70,10 @@ impl TypstRenderer {
let driver = CompileDriver::new(world);

let mut driver = DynamicLayoutCompiler::new(driver, Default::default()).with_enable(true);
driver.set_command_executor(Box::new(ShiroaCommands(
args.allowed_url_source
.map(|s| Arc::new(Regex::new(&s).context("invalid regex").unwrap_or_exit())),
)));
driver.set_extension("multi.sir.in".to_owned());
driver.set_layout_widths([750., 650., 550., 450., 350.].map(TypstAbs::raw).to_vec());
let driver =
Expand Down Expand Up @@ -650,3 +659,129 @@ impl TypstRenderer {
String::from_utf8(w).map_err(|e| error_once!("export text", error: format!("{e:?}")))
}
}

struct ShiroaCommands(Option<Arc<Regex>>);

impl typst_ts_core::vector::pass::CommandExecutor for ShiroaCommands {
fn execute(
&self,
cmd: typst::foundations::Bytes,
size: Option<typst::layout::Size>,
) -> Option<VecItem> {
let text = std::str::from_utf8(cmd.as_slice()).ok()?;
// log::info!("executing svg: {}", text);

let content = text
.find("<!-- embedded-content")
.and_then(|start| {
let text = &text[start + "<!-- embedded-content".len()..];
text.find("embedded-content -->").map(|end| &text[0..end])
})?
.trim();
let (cmd, payload) = content.split_once(',')?;

match cmd {
"html" => {
let args = serde_json::from_str::<HtmlCommandArgs>(payload).ok()?;

// todo: disallow iframe?
let allowed_tags = TAGS_META.get_or_init(|| {
HashMap::from_iter([
(
"iframe",
(
"",
HashSet::from_iter([
"id",
"class",
"src",
"allowfullscreen",
"scrolling",
"framespacing",
"frameborder",
"border",
"width",
"height",
]),
),
),
("div", ("", HashSet::from_iter(["id", "class"]))),
(
"audio",
(
"audio.",
HashSet::from_iter(["id", "class", "src", "controls"]),
),
),
(
"video",
(
"video.",
HashSet::from_iter(["id", "class", "src", "controls"]),
),
),
])
});

let tag = args.tag;
let Some((hint, allowed_attrs)) = allowed_tags.get(tag.as_str()) else {
log::warn!("disallowed tag: {tag}");
return None;
};
let allow_attr = |k: &str| k.starts_with("data-") || allowed_attrs.contains(k);

let attributes = args.attributes;

let mut attrs = String::new();
for (k, v) in attributes {
if k.contains(|c: char| !c.is_ascii_alphanumeric()) || !allow_attr(&k) {
log::warn!("disallowed attribute: {k} on tag {tag}");
return None;
}

if k == "src" {
let Some(v) = url::Url::parse(&v).ok() else {
log::warn!("invalid source url: {v} on tag {tag}");
return None;
};

if v.scheme() != "http" && v.scheme() != "https" {
log::warn!("invalid source url scheme: {v} on tag {tag}");
return None;
}

let allowed = self
.0
.as_ref()
.map(|re| v.host_str().is_some_and(|host| re.is_match(host)))
.unwrap_or(false);
if !allowed {
log::warn!("disallowed source url: {v} on tag {tag}");
return None;
}
}

attrs.push_str(&format!(" {k}=\"{}\"", escape_str::<AttributeEscapes>(&v)));
}

let html = format!("<{tag}{attrs}>{hint}</{tag}>");
return Some(VecItem::Html(HtmlItem {
html: html.into(),
size: size.unwrap_or_default().into_typst(),
}));
}
"ping" => {}
_ => {}
}

None
}
}

#[derive(Debug, Clone, Default, Deserialize)]
struct HtmlCommandArgs {
tag: String,
attributes: HashMap<String, String>,
}

static TAGS_META: OnceLock<HashMap<&str, (&str, HashSet<&str>)>> = OnceLock::new();
1 change: 1 addition & 0 deletions contrib/typst/book/lib.typ
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@

#import "sys.typ": target, page-width
#import "media.typ"

// export typst.ts variables again, don't use sys arguments directly
#let get-page-width() = page-width
Expand Down
18 changes: 18 additions & 0 deletions contrib/typst/book/media.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@

#import "xcommand.typ": xcommand

#let xhtml(..args, tag: none, attributes: (:)) = xcommand(
..args,
{
"html,"
json.encode((
tag: tag,
attributes: attributes,
))
},
)

#let iframe = xhtml.with(tag: "iframe")
#let video = xhtml.with(tag: "video")
#let audio = xhtml.with(tag: "audio")
#let div = xhtml.with(tag: "div")
46 changes: 46 additions & 0 deletions contrib/typst/book/xcommand.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@

/// HTML extension
#let xcommand(outer-width: 1024pt, outer-height: 768pt, inner-width: none, inner-height: none, content) = {
let content = if type(content) == str {
content
} else if content.func() == raw {
content.text
} else {
content
}

let inner-width = if inner-width == none {
outer-width
} else {
inner-width
}

let inner-height = if inner-height == none {
outer-height
} else {
inner-height
}

let html-embed = {
"<svg viewBox=\"0 0 "
str(inner-width.pt())
" "
str(inner-height.pt())
"\""
" width=\""
str(outer-width.pt())
"\" height=\""
str(outer-height.pt())
"\" xmlns=\"http://www.w3.org/2000/svg\">"
"<foreignObject width=\""
str(inner-width.pt())
"\" height=\""
str(inner-height.pt())
"\"><!-- embedded-content "
content
" embedded-content --></foreignObject>"
"</svg>"
}

image.decode(html-embed, alt: "!typst-embed-command")
}
23 changes: 22 additions & 1 deletion github-pages/docs/format/supports/multimedia.typ
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
#import "/github-pages/docs/book.typ": book-page
#import "/github-pages/docs/book.typ": book-page, media

#show: book-page.with(title: "Typst Supports - Multimedia components")

= Multi-media in Typst

This is a embed video.

#media.iframe(
outer-width: 640pt,
outer-height: 360pt,
attributes: (
src: "https://player.bilibili.com/player.html?aid=80433022&bvid=BV1GJ411x7h7&cid=137649199&page=1&danmaku=0&autoplay=0",
scrolling: "no",
border: "0",
width: "100%",
height: "100%",
frameborder: "no",
framespacing: "0",
allowfullscreen: "true",
),
)

That is a embed video.

0 comments on commit 63b1b03

Please sign in to comment.