diff options
| author | Anhgelus Morhtuuzh <william@herges.fr> | 2026-04-29 19:55:57 +0200 |
|---|---|---|
| committer | Anhgelus Morhtuuzh <william@herges.fr> | 2026-04-29 19:55:57 +0200 |
| commit | afd538d6af0baf8a1861ff2cdef881edbfb57000 (patch) | |
| tree | db41d566c318163cb41916defd7cd274b92a9a43 | |
| parent | 0535fa152ae990a28d0b7b9a59e96911074118b8 (diff) | |
feat(): support math content
| -rw-r--r-- | build.zig | 31 | ||||
| -rw-r--r-- | src/content.zig | 15 | ||||
| -rw-r--r-- | src/data/.gitignore | 1 | ||||
| -rw-r--r-- | src/data/test_content_1.typ (renamed from template.typ) | 2 | ||||
| -rw-r--r-- | src/data/test_content_2.typ | 12 | ||||
| -rw-r--r-- | src/eval/Element.zig | 5 | ||||
| -rw-r--r-- | src/eval/html/Literal.zig | 7 | ||||
| -rw-r--r-- | src/eval/math.zig | 108 | ||||
| -rw-r--r-- | src/eval/template_math_block.typ | 12 | ||||
| -rw-r--r-- | src/eval/template_math_content.typ | 12 | ||||
| -rw-r--r-- | src/paragraph.zig | 3 | ||||
| -rw-r--r-- | src/quote.zig | 4 | ||||
| -rw-r--r-- | src/root.zig | 1 | ||||
| -rw-r--r-- | src/typst.zig | 11 | ||||
| -rw-r--r-- | typst/src/lib.rs | 4 |
15 files changed, 203 insertions, 25 deletions
@@ -10,7 +10,6 @@ pub fn build(b: *std.Build) void { }); build_typst.setCwd(b.path("typst/")); if (optimize != .Debug) build_typst.addArg("--release"); - b.getInstallStep().dependOn(&build_typst.step); const mod = b.addModule("typdown", .{ .root_source_file = b.path("src/root.zig"), @@ -20,10 +19,7 @@ pub fn build(b: *std.Build) void { if (!target.result.isWasiLibC()) mod.link_libc = true; // link typst module during build mod.addIncludePath(b.path("typst")); - mod.addLibraryPath( - if (optimize == .Debug) b.path("typst/target/debug/") - else b.path("typst/target/release/") - ); + mod.addLibraryPath(if (optimize == .Debug) b.path("typst/target/debug/") else b.path("typst/target/release/")); if (optimize != .Debug) mod.strip = true; const lib = b.addLibrary(.{ @@ -31,10 +27,11 @@ pub fn build(b: *std.Build) void { .linkage = .static, .root_module = mod, }); - // link typst module to build + // link typst module to build lib.linkSystemLibrary("typdown_typst"); const installed_lib = b.addInstallArtifact(lib, .{}); + installed_lib.step.dependOn(&build_typst.step); // when emitting headers will be fixed //installed_lib.emitted_h = lib.getEmittedH(); @@ -59,16 +56,34 @@ pub fn build(b: *std.Build) void { .use_llvm = true, // zig internal backend crashes during linking (for 0.15.2) }); const run_mod_tests = b.addRunArtifact(mod_tests); + generateSVG(b, &run_mod_tests.step) catch |err| run_mod_tests.step.addError("{}\n", .{err}) catch unreachable; + run_mod_tests.step.dependOn(b.getInstallStep()); const test_step = b.step("test", "Run tests"); - test_step.dependOn(b.getInstallStep()); test_step.dependOn(&run_mod_tests.step); const examples_step = b.step("examples", "Run examples"); - examples_step.dependOn(b.getInstallStep()); const example_run = b.addRunArtifact(example); + example_run.step.dependOn(b.getInstallStep()); examples_step.dependOn(&example_run.step); const check = b.step("check", "Check if foo compiles"); check.dependOn(&lib.step); } + +fn generateSVG(b: *std.Build, step: *std.Build.Step) !void { + var dir = try std.fs.cwd().openDir("src/data/", .{ .iterate = true }); + defer dir.close(); + var iter = dir.iterate(); + while (try iter.next()) |it| { + if (it.kind == .file and std.mem.endsWith(u8, it.name, ".typ")) { + const cmd = b.addSystemCommand(&[_][]const u8{ + "typst", "c", + "-f", "svg", + it.name, + }); + cmd.setCwd(b.path("src/data/")); + step.dependOn(&cmd.step); + } + } +} diff --git a/src/content.zig b/src/content.zig index fe1b844..067c44f 100644 --- a/src/content.zig +++ b/src/content.zig @@ -22,6 +22,7 @@ pub fn parse(alloc: Allocator, l: *Lexer) Error!Element { .bold => content.append(try parseModifier(alloc, l, .bold, "b")), .italic => content.append(try parseModifier(alloc, l, .italic, "em")), .code => content.append(try parseModifier(alloc, l, .code, "code")), + .math => content.append(try parseMath(alloc, l)), else => return Error.IllegalPlacement, } return content.element(); @@ -42,3 +43,17 @@ fn parseModifier(alloc: Allocator, l: *Lexer, knd: Token.Kind, comptime tag: []c } return Error.ModifierNotClosed; } + +fn parseMath(alloc: Allocator, l: *Lexer) Error!Element { + const el = try Element.Math.Content.init(alloc); + var acc = try std.ArrayList(u8).initCapacity(alloc, 2); + while (l.next()) |it| { + if (it.kind == .math) { + el.content = try acc.toOwnedSlice(alloc); + return el.element(); + } + if (it.kind.isDelimiter()) return Error.ModifierNotClosed; + try acc.appendSlice(alloc, it.content); + } + return Error.ModifierNotClosed; +} diff --git a/src/data/.gitignore b/src/data/.gitignore new file mode 100644 index 0000000..756b22f --- /dev/null +++ b/src/data/.gitignore @@ -0,0 +1 @@ +*.svg diff --git a/template.typ b/src/data/test_content_1.typ index 2eca2c0..3adf59a 100644 --- a/template.typ +++ b/src/data/test_content_1.typ @@ -9,4 +9,4 @@ body } -#display()[$pi^2$] +#display()[$x$] diff --git a/src/data/test_content_2.typ b/src/data/test_content_2.typ new file mode 100644 index 0000000..db02163 --- /dev/null +++ b/src/data/test_content_2.typ @@ -0,0 +1,12 @@ +#set page( + fill: none, + margin: 2pt, +); + +#let display(body) = context { + let m = measure(body) + set page(width: m.width + page.margin.length*2, height: m.height + page.margin.length*2) + body +} + +#display()[$x^2$] diff --git a/src/eval/Element.zig b/src/eval/Element.zig index 73dfb94..16905b4 100644 --- a/src/eval/Element.zig +++ b/src/eval/Element.zig @@ -11,6 +11,11 @@ pub const Code = blocks.Code; pub const Figure = blocks.Figure; pub const Callout = blocks.Callout; pub const Quote = blocks.Quote; +pub const Math = @import("math.zig"); + +comptime { + _ = Math; +} pub const Node = struct { ptr: *anyopaque, diff --git a/src/eval/html/Literal.zig b/src/eval/html/Literal.zig index ccad004..a75b16c 100644 --- a/src/eval/html/Literal.zig +++ b/src/eval/html/Literal.zig @@ -20,6 +20,13 @@ pub fn init(alloc: Allocator, literal: []const u8) Error!*Element.Literal { return v; } +pub fn initNoEscape(alloc: Allocator, literal: []const u8) Error!*Element.Literal { + const v = try alloc.create(Self); + v.* = .{ .literal = try alloc.dupe(u8, literal) }; + v.node.ptr = v; + return v; +} + pub fn element(self: *Self) Element { return .{ .vtable = .{ .render = render, .node = getNode }, .ptr = self }; } diff --git a/src/eval/math.zig b/src/eval/math.zig new file mode 100644 index 0000000..20d3fec --- /dev/null +++ b/src/eval/math.zig @@ -0,0 +1,108 @@ +const std = @import("std"); +const typst = @cImport(@cInclude("typdown_typst.h")); +const Allocator = std.mem.Allocator; +const HTML = Element.HTML; +const Element = @import("Element.zig"); +const Node = Element.Node; + +const content_template = @embedFile("template_math_content.typ"); +const block_template = @embedFile("template_math_block.typ"); + +pub const Error = error{InvalidTypstTemplate} || Allocator.Error; + +fn typstInterop(alloc: Allocator, comptime f: fn ([*c]const u8) callconv(.c) [*c]const u8, content: []const u8) ![]const u8 { + const source = try alloc.dupeZ(u8, content); + defer alloc.free(source); + const raw_res = f(source); + const res = try alloc.dupe(u8, std.mem.span(raw_res)); + defer typst.typst_freeString(raw_res); + return res; +} + +fn generateSVG(alloc: Allocator, content: []const u8) ![]const u8 { + return try typstInterop(alloc, typst.typst_generateSVG, content); +} + +fn escape(alloc: Allocator, content: []const u8) ![]const u8 { + return try typstInterop(alloc, typst.typst_escapeMath, content); +} + +fn generateFile(alloc: Allocator, template: []const u8, content: []const u8) Error![]const u8 { + var iter = std.mem.splitSequence(u8, template, "!!"); + const beg = iter.next() orelse return Error.InvalidTypstTemplate; + const end = iter.next() orelse return Error.InvalidTypstTemplate; + if (iter.next() != null) return Error.InvalidTypstTemplate; + + var acc = try std.ArrayList(u8).initCapacity(alloc, beg.len + end.len + content.len); + try acc.appendSlice(alloc, beg); + try acc.appendSlice(alloc, content); + try acc.appendSlice(alloc, end); + return try acc.toOwnedSlice(alloc); +} + +fn Math(comptime template: []const u8) type { + return struct { + content: ?[]const u8 = null, + node: Node, + + const Self = @This(); + + pub fn init(alloc: Allocator) !*Self { + const v = try alloc.create(Self); + v.node = .{ .ptr = v, .vtable = .{ .element = fromNode } }; + return v; + } + + pub fn element(self: *Self) Element { + return .{ .ptr = self, .vtable = .{ .html = html, .node = getNode } }; + } + + fn getNode(context: *anyopaque) *Node { + const self: *Self = @ptrCast(@alignCast(context)); + return &self.node; + } + + fn fromNode(context: *anyopaque) Element { + const self: *Self = @ptrCast(@alignCast(context)); + return self.element(); + } + + fn html(context: *anyopaque, alloc: Allocator) HTML.Error!HTML { + const self: *Self = @ptrCast(@alignCast(context)); + const content = self.content orelse return (try HTML.Literal.init(alloc, "")).element(); + + var arena = std.heap.ArenaAllocator.init(alloc); + defer arena.deinit(); + const escaped = try escape(arena.allocator(), content); + const file = generateFile(arena.allocator(), template, escaped) catch |err| switch (err) { + Error.InvalidTypstTemplate => @panic("invalid template"), + Error.OutOfMemory => return Error.OutOfMemory, + }; + const svg = try generateSVG(arena.allocator(), file); + return (try HTML.Literal.initNoEscape(alloc, svg)).element(); + } + }; +} + +pub const Content = Math(content_template); +pub const Block = Math(block_template); + +fn doTest(alloc: Allocator, v: []const u8, r: []const u8) !void { + const escaped = try escape(alloc, v); + defer alloc.free(escaped); + std.testing.expect(std.mem.eql(u8, escaped, r)) catch |err| { + std.debug.print("{s}\n", .{escaped}); + return err; + }; +} + +test "escape math" { + const alloc = std.testing.allocator; + + try doTest(alloc, "hello", "hello"); + try doTest(alloc, "hello $ world", "hello \\$ world"); + try doTest(alloc, + \\hello + \\world + , "hello\\ world"); +} diff --git a/src/eval/template_math_block.typ b/src/eval/template_math_block.typ new file mode 100644 index 0000000..3d5891a --- /dev/null +++ b/src/eval/template_math_block.typ @@ -0,0 +1,12 @@ +#set page( + fill: none, + margin: 2pt, +); + +#let display(body) = context { + let m = measure(body) + set page(width: m.width + page.margin.length*2, height: m.height + page.margin.length*2) + body +} + +#display()[$ !! $] diff --git a/src/eval/template_math_content.typ b/src/eval/template_math_content.typ new file mode 100644 index 0000000..03732af --- /dev/null +++ b/src/eval/template_math_content.typ @@ -0,0 +1,12 @@ +#set page( + fill: none, + margin: 2pt, +); + +#let display(body) = context { + let m = measure(body) + set page(width: m.width + page.margin.length*2, height: m.height + page.margin.length*2) + body +} + +#display()[$!!$] diff --git a/src/paragraph.zig b/src/paragraph.zig index 2e5383a..45af712 100644 --- a/src/paragraph.zig +++ b/src/paragraph.zig @@ -50,9 +50,12 @@ test "parse paragraphs" { try doTest(parse, alloc, "[](bar)", "<p><a href=\"bar\">bar</a></p>"); try doTest(parse, alloc, "[foo](bar)", "<p><a href=\"bar\">foo</a></p>"); try doTest(parse, alloc, "hello [foo](bar) world", "<p>hello <a href=\"bar\">foo</a> world</p>"); + try doTest(parse, alloc, "$x$", "<p>" ++ @embedFile("data/test_content_1.svg") ++ "</p>"); + try doTest(parse, alloc, "$x^2$", "<p>" ++ @embedFile("data/test_content_2.svg") ++ "</p>"); try doTestError(parse, alloc, "hello *world", Error.ModifierNotClosed); try doTestError(parse, alloc, "hello *wo_rld*", Error.ModifierNotClosed); try doTestError(parse, alloc, "*hell*o *wo_rld*", Error.ModifierNotClosed); + try doTestError(parse, alloc, "hello wo$rld", Error.ModifierNotClosed); try doTestError(parse, alloc, "hello ::: world", Error.IllegalPlacement); } diff --git a/src/quote.zig b/src/quote.zig index 5456d5b..ad5a156 100644 --- a/src/quote.zig +++ b/src/quote.zig @@ -46,11 +46,11 @@ test { try doTest(parse, alloc, ">hello world", "<figure><blockquote>hello world</blockquote></figure>"); try doTest(parse, alloc, "> hello world", "<figure><blockquote>hello world</blockquote></figure>"); - try doTest(parse, alloc, + try doTest(parse, alloc, \\> hello \\>world , "<figure><blockquote>hello world</blockquote></figure>"); - try doTest(parse, alloc, + try doTest(parse, alloc, \\> hello \\>world \\attribution sur diff --git a/src/root.zig b/src/root.zig index 648fb77..6fd15f5 100644 --- a/src/root.zig +++ b/src/root.zig @@ -2,7 +2,6 @@ const std = @import("std"); const builtin = @import("builtin"); const Allocator = std.mem.Allocator; const parser = @import("parser.zig"); -const typst = @import("typst.zig"); pub const Document = parser.Document; pub const Error = parser.Error; /// Parse the content. diff --git a/src/typst.zig b/src/typst.zig deleted file mode 100644 index 9608af5..0000000 --- a/src/typst.zig +++ /dev/null @@ -1,11 +0,0 @@ -const std = @import("std"); -const typst = @cImport(@cInclude("typdown_typst.h")); - -pub fn generateSVG(alloc: std.mem.Allocator, content: []const u8) ![]const u8 { - const source = try alloc.dupeZ(u8, content); - defer alloc.free(source); - const raw_res = typst.typst_generateSVG(source); - const res = try alloc.dupe(u8, std.mem.span(raw_res)); - defer typst.typst_freeString(raw_res); - return res; -} diff --git a/typst/src/lib.rs b/typst/src/lib.rs index 83a0afb..5c2ee0d 100644 --- a/typst/src/lib.rs +++ b/typst/src/lib.rs @@ -24,7 +24,7 @@ pub fn compile(content: &str) -> String { } pub fn escape_math(content: &str) -> String { - content.replace("$", r"\$") + content.replace("$", r"\$").replace("\n", r"\ ") } unsafe fn convert_call(source: *const c_char, f: fn(&str) -> String) -> *const c_char { @@ -47,6 +47,6 @@ pub unsafe extern "C" fn typst_freeString(res: *mut c_char) { } #[unsafe(no_mangle)] -pub unsafe extern "C" fn typst_espaceMath(source: *const c_char) -> *const c_char { +pub unsafe extern "C" fn typst_escapeMath(source: *const c_char) -> *const c_char { unsafe { convert_call(source, escape_math) } } |
