Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
9061092b64
|
|||
|
8964e760ee
|
@@ -14,6 +14,7 @@
|
||||
<Folder Name="/src/">
|
||||
<Project Path="src/YaeBlog.Abstractions/YaeBlog.Abstractions.csproj" />
|
||||
<Project Path="src/YaeBlog.Tests/YaeBlog.Tests.csproj" />
|
||||
<Project Path="src/YaeBlog.Typst/YaeBlog.Typst.csproj" />
|
||||
<Project Path="src/YaeBlog/YaeBlog.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/third-party/" />
|
||||
|
||||
18
src/YaeBlog.Tests/CallRustTests.cs
Normal file
18
src/YaeBlog.Tests/CallRustTests.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using YaeBlog.Typst;
|
||||
|
||||
namespace YaeBlog.Tests;
|
||||
|
||||
public class CallRustTests
|
||||
{
|
||||
[Fact]
|
||||
public void ProcessStringTest()
|
||||
{
|
||||
RustString test1 = RustCaller.ProcessString("123");
|
||||
Assert.NotNull(test1.Value);
|
||||
Assert.Equal("Process 123", test1.Value);
|
||||
|
||||
RustString test2 = RustCaller.ProcessString("世界");
|
||||
Assert.NotNull(test2.Value);
|
||||
Assert.Equal("Process 世界", test2.Value);
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\YaeBlog.Typst\YaeBlog.Typst.csproj" />
|
||||
<ProjectReference Include="..\YaeBlog\YaeBlog.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
9
src/YaeBlog.Typst/RustCaller.cs
Normal file
9
src/YaeBlog.Typst/RustCaller.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace YaeBlog.Typst;
|
||||
|
||||
public static partial class RustCaller
|
||||
{
|
||||
[LibraryImport("yaeblog_typst", EntryPoint = "process_string", StringMarshalling = StringMarshalling.Utf8)]
|
||||
public static partial RustString ProcessString(string value);
|
||||
}
|
||||
27
src/YaeBlog.Typst/RustString.cs
Normal file
27
src/YaeBlog.Typst/RustString.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.InteropServices.Marshalling;
|
||||
|
||||
namespace YaeBlog.Typst;
|
||||
|
||||
[NativeMarshalling(typeof(RustStringMarshaller))]
|
||||
public struct RustString
|
||||
{
|
||||
public string? Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Customize string marshaller used to handle `CString` returned value in Rust side.
|
||||
/// </summary>
|
||||
[CustomMarshaller(typeof(RustString), MarshalMode.ManagedToUnmanagedOut, typeof(RustStringMarshaller))]
|
||||
internal static unsafe partial class RustStringMarshaller
|
||||
{
|
||||
public static RustString ConvertToManaged(byte* ptr)
|
||||
{
|
||||
return new RustString { Value = Utf8StringMarshaller.ConvertToManaged(ptr) };
|
||||
}
|
||||
|
||||
[LibraryImport("yaeblog_typst", EntryPoint = "free_rust_string")]
|
||||
private static partial void FreeRustString(byte* ptr);
|
||||
|
||||
public static void Free(byte* ptr) => FreeRustString(ptr);
|
||||
}
|
||||
23
src/YaeBlog.Typst/YaeBlog.Typst.csproj
Normal file
23
src/YaeBlog.Typst/YaeBlog.Typst.csproj
Normal file
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<RustProjectPath>$([System.IO.Path]::Combine($(MSBuildProjectDirectory), `..`, `yaeblog-typst`))</RustProjectPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="BuildRustProject" BeforeTargets="Build">
|
||||
<Exec Command="cargo build --release" WorkingDirectory="$(RustProjectPath)"/>
|
||||
</Target>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="../yaeblog-typst/target/release/libyaeblog_typst.so"
|
||||
Link="runtimes/linux-x64/native/libyaeblog_typst.so" CopyToOutputDirectory="Always"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
6
src/yaeblog-typst/.gitignore
vendored
Normal file
6
src/yaeblog-typst/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/target
|
||||
/tmp
|
||||
.idea/
|
||||
*.S
|
||||
*.wav
|
||||
.DS_Store
|
||||
3107
src/yaeblog-typst/Cargo.lock
generated
Normal file
3107
src/yaeblog-typst/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
14
src/yaeblog-typst/Cargo.toml
Normal file
14
src/yaeblog-typst/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "yaeblog-typst"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.104"
|
||||
libc = "0.2.187"
|
||||
typst = "0.15.1"
|
||||
typst-html = "0.15.1"
|
||||
typst-kit = { version = "0.15.1", features = ["datetime", "system-packages", "embedded-fonts"]}
|
||||
85
src/yaeblog-typst/src/ffi_utils.rs
Normal file
85
src/yaeblog-typst/src/ffi_utils.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use std::ffi::{CStr, CString, c_char, c_void};
|
||||
|
||||
#[repr(C)]
|
||||
pub struct ExternResult<T> {
|
||||
pub content: T,
|
||||
pub error_message: *mut c_char,
|
||||
pub succeeded: bool,
|
||||
}
|
||||
|
||||
impl<T> ExternResult<T> where T : Default {
|
||||
pub fn from_result(r: anyhow::Result<T>) -> Self {
|
||||
match r {
|
||||
Ok(c) => {
|
||||
ExternResult {
|
||||
content: c,
|
||||
error_message: std::ptr::null_mut(),
|
||||
succeeded: true
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
ExternResult {
|
||||
content: T::default(),
|
||||
error_message: return_str(e.to_string()),
|
||||
succeeded: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extract_str<'a>(str: *const c_char) -> anyhow::Result<&'a str> {
|
||||
let c_str = unsafe { CStr::from_ptr(str) };
|
||||
|
||||
let result = c_str.to_str()?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn return_str(str: String) -> *mut c_char {
|
||||
let returned_c_str = match CString::new(str) {
|
||||
Ok(s) => s,
|
||||
Err(_) => {
|
||||
return std::ptr::null_mut();
|
||||
}
|
||||
};
|
||||
|
||||
returned_c_str.into_raw()
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn free_rust_string(ptr: *mut c_char) {
|
||||
if ptr.is_null() {
|
||||
return;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
// Retake the ownership of string and drop it.
|
||||
let _ = CString::from_raw(ptr);
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn process_string(str: *const c_char) -> *mut c_char {
|
||||
let rust_str = match extract_str(str) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
return return_str(format!("Encounter error: {e}"))
|
||||
}
|
||||
};
|
||||
|
||||
let mut returned_str = String::new();
|
||||
returned_str.push_str("Process ");
|
||||
returned_str.push_str(rust_str);
|
||||
|
||||
return_str(returned_str)
|
||||
}
|
||||
|
||||
pub fn into_handler<T>(handler: Box<T>) -> *mut c_void {
|
||||
Box::into_raw(handler) as *mut c_void
|
||||
}
|
||||
|
||||
pub fn from_handler<'a, T>(handler: *mut c_void) -> &'a mut T {
|
||||
unsafe {
|
||||
&mut *(handler as *mut T)
|
||||
}
|
||||
}
|
||||
122
src/yaeblog-typst/src/lib.rs
Normal file
122
src/yaeblog-typst/src/lib.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
mod ffi_utils;
|
||||
mod world;
|
||||
|
||||
use crate::ffi_utils::{ExternResult, extract_str, from_handler, into_handler, return_str};
|
||||
use crate::world::SystemWorld;
|
||||
use anyhow::anyhow;
|
||||
use std::ffi::{c_char, c_void};
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use typst::diag::{EcoVec, SourceDiagnostic, Warned};
|
||||
use typst_html::{HtmlDocument, HtmlOptions};
|
||||
|
||||
struct TypstCompiler {
|
||||
world: SystemWorld,
|
||||
}
|
||||
|
||||
impl TypstCompiler {
|
||||
fn new(work_dir: &str) -> anyhow::Result<Self> {
|
||||
let path = PathBuf::from_str(work_dir)?;
|
||||
|
||||
Ok(Self {
|
||||
world: SystemWorld::new(path),
|
||||
})
|
||||
}
|
||||
|
||||
fn compile(&mut self, input: &str) -> anyhow::Result<String> {
|
||||
self.world.set_current_file(input);
|
||||
|
||||
let Warned { output, warnings } = typst::compile::<HtmlDocument>(&mut self.world);
|
||||
let document = match output {
|
||||
Ok(d) => Ok(d),
|
||||
Err(e) => Err(anyhow!(typst_to_err(e))),
|
||||
}?;
|
||||
|
||||
let content = export_html(&document)?;
|
||||
Ok(content)
|
||||
}
|
||||
}
|
||||
|
||||
fn typst_to_err(e: EcoVec<SourceDiagnostic>) -> String {
|
||||
let mut message = String::new();
|
||||
message.push_str("Failed to compile: \n");
|
||||
|
||||
for err in e {
|
||||
message.push_str(format!(" - {} \n", err.message.as_str()).as_str());
|
||||
}
|
||||
|
||||
message
|
||||
}
|
||||
|
||||
fn export_html(document: &HtmlDocument) -> anyhow::Result<String> {
|
||||
let options = HtmlOptions { pretty: false };
|
||||
|
||||
match typst_html::html(document, &options) {
|
||||
Ok(o) => Ok(o),
|
||||
Err(e) => Err(anyhow!(typst_to_err(e))),
|
||||
}
|
||||
}
|
||||
|
||||
fn create_compiler(work_dir: *const c_char) -> anyhow::Result<Box<TypstCompiler>> {
|
||||
let work_dir = extract_str(work_dir)?;
|
||||
let compiler = TypstCompiler::new(work_dir)?;
|
||||
|
||||
Ok(Box::new(compiler))
|
||||
}
|
||||
|
||||
pub extern "C" fn init_compiler(work_dir: *const c_char) -> ExternResult<*mut c_void> {
|
||||
let compiler = create_compiler(work_dir);
|
||||
let handler = compiler.map(into_handler);
|
||||
|
||||
ExternResult::from_result(handler)
|
||||
}
|
||||
|
||||
fn typst_compile_inner(
|
||||
compiler: &mut TypstCompiler,
|
||||
str: *const c_char,
|
||||
) -> anyhow::Result<*mut c_char> {
|
||||
let input = extract_str(str)?;
|
||||
compiler.compile(input).map(return_str)
|
||||
}
|
||||
|
||||
pub extern "C" fn typst_compile(
|
||||
handler: *mut c_void,
|
||||
str: *const c_char,
|
||||
) -> ExternResult<*mut c_char> {
|
||||
let compiler = from_handler::<TypstCompiler>(handler);
|
||||
|
||||
ExternResult::from_result(typst_compile_inner(compiler, str))
|
||||
}
|
||||
|
||||
pub extern "C" fn free_compiler(handler: *mut c_void) {
|
||||
if handler.is_null() {
|
||||
return;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
// Drop it!
|
||||
let _ = Box::from_raw(handler as *mut TypstCompiler);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn smoke_test() {
|
||||
let mut compiler = TypstCompiler::new(".").unwrap();
|
||||
let result = compiler.compile("= 123").unwrap();
|
||||
|
||||
assert!(result.contains("<h2>123</h2>"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn math_test() {
|
||||
let mut compiler = TypstCompiler::new(".").unwrap();
|
||||
let result = compiler.compile(
|
||||
"$1 + 1 = 2$").unwrap();
|
||||
|
||||
assert!(result.contains("math"));
|
||||
}
|
||||
}
|
||||
127
src/yaeblog-typst/src/world.rs
Normal file
127
src/yaeblog-typst/src/world.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::LazyLock;
|
||||
use typst::diag::{FileError, FileResult, PackageError};
|
||||
use typst::foundations::{Bytes, Datetime, Duration};
|
||||
use typst::syntax::{FileId, RootedPath, Source, VirtualPath, VirtualRoot};
|
||||
use typst::text::{Font, FontBook};
|
||||
use typst::utils::LazyHash;
|
||||
use typst::{Feature, Features, Library, LibraryExt, World};
|
||||
use typst_kit::datetime::Time;
|
||||
use typst_kit::files::{FileLoader, FileStore, FsRoot};
|
||||
use typst_kit::fonts::{FontStore, embedded};
|
||||
|
||||
pub struct SystemWorld {
|
||||
work_dir: PathBuf,
|
||||
library: LazyHash<Library>,
|
||||
files: FileStore<SystemFiles>,
|
||||
fonts: LazyLock<FontStore, Box<dyn Fn() -> FontStore + Send + Sync>>,
|
||||
now: Time,
|
||||
}
|
||||
|
||||
const DEFAULT_FEATURES: [Feature; 1] = [Feature::Html];
|
||||
|
||||
impl SystemWorld {
|
||||
pub fn new(work_dir: PathBuf) -> Self {
|
||||
let features = DEFAULT_FEATURES.iter().copied().map(Into::into).collect();
|
||||
let library = Library::builder().with_features(features).build();
|
||||
|
||||
Self {
|
||||
work_dir: work_dir.clone(),
|
||||
library: LazyHash::new(library),
|
||||
files: FileStore::new(SystemFiles::new(work_dir)),
|
||||
fonts: LazyLock::new(Box::new(|| {
|
||||
let mut fonts = FontStore::new();
|
||||
|
||||
fonts.extend(embedded());
|
||||
|
||||
fonts
|
||||
})),
|
||||
now: Time::system(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_current_file(&mut self, content: &str) {
|
||||
let current_file = &mut self.files.loader_mut().current_file;
|
||||
let bytes = content.as_bytes();
|
||||
|
||||
current_file.clear();
|
||||
current_file.resize(bytes.len(), 0);
|
||||
current_file.copy_from_slice(content.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
impl World for SystemWorld {
|
||||
fn library(&self) -> &LazyHash<Library> {
|
||||
&self.library
|
||||
}
|
||||
|
||||
fn book(&self) -> &LazyHash<FontBook> {
|
||||
self.fonts.book()
|
||||
}
|
||||
|
||||
fn main(&self) -> FileId {
|
||||
*MAIN_ID
|
||||
}
|
||||
|
||||
fn source(&self, id: FileId) -> FileResult<Source> {
|
||||
self.files.source(id)
|
||||
}
|
||||
|
||||
fn file(&self, id: FileId) -> FileResult<Bytes> {
|
||||
self.files.file(id)
|
||||
}
|
||||
|
||||
fn font(&self, index: usize) -> Option<Font> {
|
||||
self.fonts.font(index)
|
||||
}
|
||||
|
||||
fn today(&self, offset: Option<Duration>) -> Option<Datetime> {
|
||||
self.now.today(offset)
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides project files from a configured directory and package files from
|
||||
/// standard locations.
|
||||
struct SystemFiles {
|
||||
pub main: FileId,
|
||||
project: FsRoot,
|
||||
current_file: Vec<u8>,
|
||||
}
|
||||
|
||||
static MAIN_ID: LazyLock<FileId> = LazyLock::new(|| {
|
||||
FileId::unique(RootedPath::new(
|
||||
VirtualRoot::Project,
|
||||
VirtualPath::new("<main>").unwrap(),
|
||||
))
|
||||
});
|
||||
|
||||
impl SystemFiles {
|
||||
pub fn new(work_dir: PathBuf) -> Self {
|
||||
Self {
|
||||
main: *MAIN_ID,
|
||||
project: FsRoot::new(work_dir),
|
||||
current_file: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve(&self, id: FileId) -> FileResult<PathBuf> {
|
||||
Ok(self.root(id)?.resolve(id.vpath())?)
|
||||
}
|
||||
|
||||
fn root(&self, id: FileId) -> FileResult<FsRoot> {
|
||||
Ok(match id.root() {
|
||||
VirtualRoot::Project => Ok(self.project.clone()),
|
||||
VirtualRoot::Package(_) => Err(FileError::Package(PackageError::Other(None))),
|
||||
}?)
|
||||
}
|
||||
}
|
||||
|
||||
impl FileLoader for SystemFiles {
|
||||
fn load(&self, id: FileId) -> FileResult<Bytes> {
|
||||
if id == *MAIN_ID {
|
||||
Ok(Bytes::new(self.current_file.clone()))
|
||||
} else {
|
||||
self.root(id)?.load(id.vpath())
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user