Skip to content

Commit

Permalink
feat(pkg::compiler): fetch package support (#373)
Browse files Browse the repository at this point in the history
* dev(compiler): add proxy package registry

* feat(pkg::compiler): fetch package support

* dev(pkg::core): add a memory access model
  • Loading branch information
Myriad-Dreamin authored Sep 27, 2023
1 parent 99763fc commit cc30f64
Show file tree
Hide file tree
Showing 15 changed files with 373 additions and 15 deletions.
9 changes: 5 additions & 4 deletions compiler/src/browser.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use std::path::PathBuf;

use typst_ts_core::{font::FontResolverImpl, package::dummy::DummyRegistry};
use typst_ts_core::font::FontResolverImpl;

use crate::vfs::browser::ProxyAccessModel;
use crate::{package::browser::ProxyRegistry, vfs::browser::ProxyAccessModel};

/// A world that provides access to the browser.
/// It is under development.
Expand All @@ -12,7 +12,7 @@ pub struct BrowserCompilerFeat;

impl crate::world::CompilerFeat for BrowserCompilerFeat {
type AccessModel = ProxyAccessModel;
type Registry = DummyRegistry;
type Registry = ProxyRegistry;

// manual construction 13MB
// let dummy_library = typst::eval::LangItems {
Expand All @@ -25,10 +25,11 @@ impl TypstBrowserWorld {
pub fn new(
root_dir: PathBuf,
access_model: ProxyAccessModel,
registry: ProxyRegistry,
font_resolver: FontResolverImpl,
) -> Self {
let vfs = crate::vfs::Vfs::new(access_model);

Self::new_raw(root_dir, vfs, DummyRegistry, font_resolver)
Self::new_raw(root_dir, vfs, registry, font_resolver)
}
}
107 changes: 107 additions & 0 deletions compiler/src/package/browser.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
use std::{io::Read, path::Path};

use js_sys::Uint8Array;
use typst_library::prelude::*;
use wasm_bindgen::{prelude::*, JsValue};

use super::{PackageError, PackageSpec, Registry};

#[wasm_bindgen]
#[derive(Clone)]
pub struct ProxyContext {
context: JsValue,
}

#[wasm_bindgen]
impl ProxyContext {
#[wasm_bindgen(constructor)]
pub fn new(context: JsValue) -> Self {
Self { context }
}

#[wasm_bindgen(getter)]
pub fn context(&self) -> JsValue {
self.context.clone()
}

pub fn untar(&self, data: &[u8], cb: js_sys::Function) -> Result<(), JsValue> {
let cb = move |key: String, value: &[u8], mtime: u64| -> Result<(), JsValue> {
let key = JsValue::from_str(&key);
let value = Uint8Array::from(value);
let mtime = JsValue::from_f64(mtime as f64);
cb.call3(&self.context, &key, &value, &mtime).map(|_| ())
};

let decompressed = flate2::read::GzDecoder::new(data);
let mut reader = tar::Archive::new(decompressed);
let entries = reader.entries();
let entries = entries.map_err(|err| {
let t = PackageError::MalformedArchive(Some(eco_format!("{err}")));
JsValue::from_str(&format!("{:?}", t))
})?;

let mut buf = Vec::with_capacity(1024);
for entry in entries {
// Read single entry
let mut entry = entry.map_err(|e| format!("{:?}", e))?;
let header = entry.header();

let is_file = header.entry_type().is_file();
if !is_file {
continue;
}

let mtime = header.mtime().unwrap_or(0);

let path = header.path().map_err(|e| format!("{:?}", e))?;
let path = path.to_string_lossy().as_ref().to_owned();

let size = header.size().map_err(|e| format!("{:?}", e))?;
buf.clear();
buf.reserve(size as usize);
entry
.read_to_end(&mut buf)
.map_err(|e| format!("{:?}", e))?;

cb(path, &buf, mtime)?
}

Ok(())
}
}

pub struct ProxyRegistry {
pub context: ProxyContext,
pub real_resolve_fn: js_sys::Function,
}

impl Registry for ProxyRegistry {
fn resolve(&self, spec: &PackageSpec) -> Result<std::sync::Arc<Path>, PackageError> {
// prepare js_spec
let js_spec = js_sys::Object::new();
js_sys::Reflect::set(&js_spec, &"name".into(), &spec.name.to_string().into()).unwrap();
js_sys::Reflect::set(
&js_spec,
&"namespace".into(),
&spec.namespace.to_string().into(),
)
.unwrap();
js_sys::Reflect::set(
&js_spec,
&"version".into(),
&spec.version.to_string().into(),
)
.unwrap();

self.real_resolve_fn
.call1(&self.context.clone().into(), &js_spec)
.map_err(|e| PackageError::Other(Some(eco_format!("{:?}", e))))
.and_then(|v| {
if v.is_undefined() {
Err(PackageError::NotFound(spec.clone()))
} else {
Ok(Path::new(&v.as_string().unwrap()).into())
}
})
}
}
3 changes: 3 additions & 0 deletions compiler/src/package/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
pub use typst_ts_core::package::{PackageError, PackageSpec, Registry};

#[cfg(feature = "browser-compile")]
pub mod browser;

#[cfg(feature = "system-compile")]
pub mod http;

Expand Down
17 changes: 16 additions & 1 deletion fuzzers/corpora/package/example.typ
Original file line number Diff line number Diff line change
@@ -1,2 +1,17 @@

#import "@preview/example:0.1.0": *;
#import "@preview/example:0.1.0": add

#show raw: rect.with(width: 100%, fill: luma(120).lighten(90%))

Input:

```typ
#import "@preview/example:0.1.0": add
Example package: add(1, 2) = #add(1, 2)
```

Output:

#h(2em) Example package: add(1, 2) = #add(1, 2)
36 changes: 33 additions & 3 deletions packages/compiler/src/builder.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
use js_sys::Uint8Array;
use wasm_bindgen::prelude::*;

use typst_ts_compiler::{font::web::BrowserFontSearcher, vfs::browser::ProxyAccessModel};
use typst_ts_compiler::{
font::web::BrowserFontSearcher,
package::browser::{ProxyContext, ProxyRegistry},
vfs::browser::ProxyAccessModel,
};
use typst_ts_core::{error::prelude::*, Bytes};

use crate::TypstCompiler;

#[wasm_bindgen]
pub struct TypstCompilerBuilder {
access_model: Option<ProxyAccessModel>,
package_registry: Option<ProxyRegistry>,
searcher: BrowserFontSearcher,
}

Expand All @@ -19,6 +24,7 @@ impl TypstCompilerBuilder {
console_error_panic_hook::set_once();
let mut res = Self {
access_model: None,
package_registry: None,
searcher: BrowserFontSearcher::new(),
};
res.set_dummy_access_model()?;
Expand All @@ -31,7 +37,15 @@ impl TypstCompilerBuilder {
mtime_fn: js_sys::Function::new_no_args("return new Date(0)"),
is_file_fn: js_sys::Function::new_no_args("return true"),
real_path_fn: js_sys::Function::new_with_args("path", "return path"),
read_all_fn: js_sys::Function::new_no_args("throw new Error('Dummy AccessModel')"),
read_all_fn: js_sys::Function::new_no_args(
"throw new Error('Dummy AccessModel, please initialize compiler with withAccessModel()')",
),
});
self.package_registry = Some(ProxyRegistry {
context: ProxyContext::new(wasm_bindgen::JsValue::UNDEFINED),
real_resolve_fn: js_sys::Function::new_no_args(
"throw new Error('Dummy Registry, please initialize compiler with withPackageRegistry()')",
),
});
Ok(())
}
Expand All @@ -55,6 +69,19 @@ impl TypstCompilerBuilder {
Ok(())
}

pub async fn set_package_registry(
&mut self,
context: JsValue,
real_resolve_fn: js_sys::Function,
) -> ZResult<()> {
self.package_registry = Some(ProxyRegistry {
context: ProxyContext::new(context),
real_resolve_fn,
});

Ok(())
}

// 400 KB
pub async fn add_raw_font(&mut self, font_buffer: Uint8Array) -> ZResult<()> {
self.add_raw_font_internal(font_buffer.to_vec().into());
Expand All @@ -74,7 +101,10 @@ impl TypstCompilerBuilder {
let access_model = self
.access_model
.ok_or_else(|| "TypstCompilerBuilder::build: access_model is not set".to_string())?;
TypstCompiler::new(access_model, self.searcher).await
let registry = self.package_registry.ok_or_else(|| {
"TypstCompilerBuilder::build: package_registry is not set".to_string()
})?;
TypstCompiler::new(access_model, registry, self.searcher).await
}
}

Expand Down
3 changes: 3 additions & 0 deletions packages/compiler/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use typst::font::Font;
pub use typst_ts_compiler::*;
use typst_ts_compiler::{
font::web::BrowserFontSearcher,
package::browser::ProxyRegistry,
service::{CompileDriverImpl, Compiler},
vfs::browser::ProxyAccessModel,
world::WorldSnapshot,
Expand All @@ -30,12 +31,14 @@ pub struct TypstCompiler {
impl TypstCompiler {
pub async fn new(
access_model: ProxyAccessModel,
registry: ProxyRegistry,
searcher: BrowserFontSearcher,
) -> Result<Self, JsValue> {
Ok(Self {
compiler: CompileDriverImpl::new(TypstBrowserWorld::new(
std::path::Path::new("/").to_owned(),
access_model,
registry,
searcher.into(),
)),
})
Expand Down
18 changes: 14 additions & 4 deletions packages/typst.ts/examples/compiler.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,12 @@
const begin = performance.now();
compilerPlugin.reset();

const mainFilePath = '/corpus/skyzh-cv/main.typ';
// const mainFilePath = '/corpus/package/example.typ';
// compilerPlugin.addSource(mainFilePath, `#import "@preview/example:0.1.0": add`);

if (fmt === 'ast') {
const ast = await compilerPlugin.getAst('corpus/skyzh-cv/main.typ');
const ast = await compilerPlugin.getAst(mainFilePath);
const end = performance.now();
const rounded = Math.round((end - begin) * 1000) / 1000;

Expand All @@ -41,7 +45,7 @@
terminalContent.innerHTML = [compileInfo, ast].join('\n');
} else if (fmt === 'pdf') {
const pdfData = await compilerPlugin.compile({
mainFilePath: 'corpus/skyzh-cv/main.typ',
mainFilePath,
format: 'pdf',
});
const end = performance.now();
Expand Down Expand Up @@ -69,11 +73,17 @@
};

let compilerPlugin = window.TypstCompileModule.createTypstCompiler();

// const fetchBackend = new window.TypstCompileModule.MemoryAccessModel();
const fetchBackend = new window.TypstCompileModule.FetchAccessModel(
'http://localhost:20810',
);
compilerPlugin
.init({
beforeBuild: [
window.TypstCompileModule.withAccessModel(
new window.TypstCompileModule.FetchAccessModel('http://localhost:20810'),
window.TypstCompileModule.withAccessModel(fetchBackend),
window.TypstCompileModule.withPackageRegistry(
new window.TypstCompileModule.FetchPackageRegistry(fetchBackend),
),
],
getModule: () =>
Expand Down
30 changes: 29 additions & 1 deletion packages/typst.ts/src/fs/fetch.mts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { FsAccessModel } from '../internal.types.mjs';
import { WritableAccessModel } from './index.mjs';

export interface FetchAccessOptions {
polyfillHeadRequest?: boolean;
Expand Down Expand Up @@ -30,7 +31,7 @@ const bufferToBase64 = async (data: Uint8Array) => {
return base64url || '';
};

export class FetchAccessModel implements FsAccessModel {
export class FetchAccessModel implements FsAccessModel, WritableAccessModel {
fullyCached: boolean;
mTimes: Map<string, Date | undefined> = new Map();
mRealPaths: Map<string, string | undefined> = new Map();
Expand Down Expand Up @@ -58,6 +59,16 @@ export class FetchAccessModel implements FsAccessModel {
return this.root + path;
}

insertFile(path: string, data: Uint8Array, mtime: Date) {
this.mTimes.set(path, mtime);
this.mData.set(path, data);
}

removeFile(path: string) {
this.mTimes.delete(path);
this.mData.delete(path);
}

async loadSnapshot(snapshot: FetchSnapshot): Promise<void> {
async function base64UrlToBuffer(base64Url: string) {
const res = await fetch(base64Url);
Expand Down Expand Up @@ -172,6 +183,15 @@ export class FetchAccessModel implements FsAccessModel {
}

getMTime(path: string): Date | undefined {
// todo: no hack
if (path.startsWith('/@memory/')) {
if (this.mTimes.has(path)) {
return this.mTimes.get(path);
}

return undefined;
}

if (!this.fullyCached) {
return this.getMTimeInternal(path);
}
Expand Down Expand Up @@ -215,6 +235,14 @@ export class FetchAccessModel implements FsAccessModel {
}

readAll(path: string): Uint8Array | undefined {
if (path.startsWith('/@memory/')) {
if (this.mData.has(path)) {
return this.mData.get(path);
}

return undefined;
}

if (!this.fullyCached) {
return this.readAllInternal(path);
}
Expand Down
9 changes: 9 additions & 0 deletions packages/typst.ts/src/fs/index.mts
Original file line number Diff line number Diff line change
@@ -1,2 +1,11 @@
import { FsAccessModel } from '../internal.types.mjs';

export { FetchAccessModel } from './fetch.mjs';
export type { FetchAccessOptions } from './fetch.mjs';

export { MemoryAccessModel } from './memory.mjs';

export interface WritableAccessModel extends FsAccessModel {
insertFile(path: string, data: Uint8Array, mtime: Date): void;
removeFile(path: string): void;
}
Loading

0 comments on commit cc30f64

Please sign in to comment.