├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── TODO.md ├── build.zig ├── src ├── main.zig ├── md │ ├── lexer.zig │ ├── log.zig │ ├── markdown.zig │ ├── parse.zig │ ├── parse_atx_heading.zig │ ├── parse_codeblock.zig │ ├── token.zig │ ├── token_atx_heading.zig │ ├── token_inline.zig │ └── translate.zig ├── unicode │ └── unicode.zig └── webview │ ├── webview.cc │ ├── webview.h │ └── webview.zig ├── test.zig └── test ├── docs ├── test_basics.md └── test_headings.md ├── expect ├── 01-section-tabs │ ├── testl_001.json │ ├── testl_002.json │ ├── testl_003.json │ ├── testp_001.json │ ├── testp_002.json │ └── testp_003.json └── 03-section-atx-headings │ ├── testl_032.json │ └── testp_032.json ├── section_atx_headings.zig ├── section_tabs.zig ├── spec └── commonmark_spec_0.29.json └── util.zig /.gitignore: -------------------------------------------------------------------------------- 1 | zig-cache 2 | ziglib 3 | src/webview/webview.o 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/log.zig"] 2 | path = lib/log.zig 3 | url = git@github.com:demizer/log.zig.git 4 | [submodule "lib/zig-time"] 5 | path = lib/zig-time 6 | url = git@github.com:demizer/time.git 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2020, Jesus Alvarez 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Markzig 2 | 3 | CommonMark compliant Markdown parsing for Zig! 4 | 5 | Markzig is a library and command line tool for converting markdown to html or showing rendered markdown in a webview. 6 | 7 | ## Usage 8 | 9 | Not yet applicable. 10 | 11 | ## Status 12 | 13 | 4/649 tests of the CommonMark 0.29 test suite pass! 14 | 15 | ### Milestone #1 16 | 17 | Parse and display a basic document [test.md](https://github.com/demizer/markzig/blob/master/test/test.md). 18 | 19 | - [ ] Tokenize 20 | - [ ] Parse 21 | - [ ] Render to HTML 22 | - [X] Display markdown document in [webview](https://github.com/zserge/webview). 23 | 24 | ### Milestone #2 25 | 26 | - [ ] 50% tests pass 27 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # Links 2 | 3 | * [golang markdown parser](https://gitlab.com/golang-commonmark/markdown/-/blob/master/markdown.go) 4 | The inspiration for this parser. 5 | * [cmark](https://github.com/commonmark/cmark) 6 | The commonmark reference implementation in C. 7 | * [md4t](https://github.com/mity/md4c) 8 | Another C markdown parser. 9 | * [webview library](https://github.com/zserge/webview) 10 | This will be used to show the rendered markdown document with live updating. 11 | * [Commonmark Spec 0.29](https://spec.commonmark.org/0.29/) 12 | * [Commonmark Reference Implementations](https://github.com/commonmark/commonmark-spec/wiki/list-of-commonmark-implementations) 13 | * [Zig Zasm](https://github.com/andrewrk/zasm/blob/master/src/main.zig) 14 | * [Zig Docs](https://ziglang.org/documentation/master) 15 | * [Zig Standard Library Docs](https://ziglang.org/documentation/master/std) 16 | * [Zig Awesome Examples](https://github.com/nrdmn/awesome-zig) 17 | * [Zig github search](https://github.com/search?q=json+getValue+language%3AZig+created%3A%3E2020-01-01&type=Code&ref=advsearch&l=&l=) 18 | * [zig-window (xorg window)](https://github.com/andrewrk/zig-window) 19 | * [zig-regex](https://github.com/tiehuis/zig-regex) 20 | * [zhp (Http)](https://github.com/frmdstryr/zhp) 21 | * [Let's build a simple interpreter](https://ruslanspivak.com/lsbasi-part1/) 22 | * [astexplorer.net](https://astexplorer.net/) 23 | Examine an AST of a javascript markdown parser 24 | 25 | # Things To Do 26 | 27 | ## Wed Nov 11 21:45 2020: INVESTIGATE: \n should be it's own token 28 | 29 | This might make it hard to detect newlines? I might be searching the string for \n to detect 30 | newlines. 31 | 32 | const input = "foo\nbar \t\nbaz"; 33 | 1605159814 [DEBUG]: lexer emit: { "ID": "Whitespace", "startOffset": 7, "endOffset": 9, "string": " \t\n", "lineNumber": 2, "column": 4 } 34 | 35 | ## Tue Nov 10 15:01 2020: add line number to log output 36 | 37 | ## Mon Nov 09 20:54 2020: combine lexer, parser, and html tests into one test function. 38 | 39 | Do each in order to reduce time to run tests. 40 | 41 | ## Mon Nov 09 18:13 2020: use html-diff in a container if the diff streamer fails 42 | 43 | https://github.com/bem/html-differ 44 | 45 | ## Mon Nov 09 21:57 2020: fix date/time logging in md/log.zig 46 | 47 | ## Mon Nov 09 21:26 2020: update outStream to writer in all files 48 | 49 | ## Mon Nov 09 21:56 2020: use testing.TmpDir instead of mktemp command 50 | 51 | ## Mon Nov 09 20:45 2020: fix linting errors is md/log.zig 52 | 53 | ### Sat Jun 06 13:47 2020: move test convert html 32 to parse test 32 test func 54 | 55 | ## Mon Jun 01 11:30 2020: Parse inline blocks 56 | 57 | ## Mon Jun 01 11:30 2020: Parse lists 58 | 59 | ## Mon Jun 01 15:54 2020: Add markzig to awesome-zig 60 | 61 | ## Mon Jun 01 11:31 2020: Announce on reddit 62 | 63 | # DONE THINGS 64 | 65 | ## Sat Jun 06 13:39 2020: Remove the json / html comparitor 66 | :DONE: Mon Nov 09 20:38 2020 67 | 68 | It sucks. Just dump both the expect and got and that's it. 69 | 70 | ## Sun Oct 25 22:11 2020: rename project to markzig 71 | :DONE: Mon Nov 09 20:38 2020 72 | 73 | ## Mon Nov 09 11:36 2020: only run docker json-diff if the json actually differs. 74 | :DONE: Mon Nov 09 20:37 2020 75 | 76 | Get the old system from git history and restore it. 77 | 78 | Check the json using zig, if it fails, then use json-diff. 79 | 80 | ## Mon Nov 09 12:27 2020: printing of Token and Node should escape the string better: 81 | :DONE: Mon Nov 09 20:37 2020 82 | 83 | It's hard to see what is going on here: 84 | 85 | 1604953496 [DEBUG]: expect: Token{ .ID = TokenId.Whitespace, .startOffset = 0, .endOffset = 0, .string = , .lineNumber = 1, .column = 1 } 86 | 1604953496 [DEBUG]: got: Token{ .ID = TokenId.Whitespace, .startOffset = 0, .endOffset = 0, .string = , .lineNumber = 1, .column = 1 } 87 | 88 | The .string section should be escaped and enclosed with ''. 89 | 90 | Maybe there is a pretty print module for zig. 91 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Builder = @import("std").build.Builder; 3 | 4 | fn addWebviewDeps(exe: *std.build.LibExeObjStep, webviewObjStep: *std.build.Step) void { 5 | exe.step.dependOn(webviewObjStep); 6 | exe.addIncludeDir("src/webview"); 7 | exe.addLibPath("/usr/lib"); 8 | exe.addObjectFile("src/webview/webview.o"); 9 | exe.linkSystemLibrary("c++"); 10 | exe.linkSystemLibrary("gtk+-3.0"); 11 | exe.linkSystemLibrary("webkit2gtk-4.0"); 12 | } 13 | 14 | pub fn build(b: *Builder) void { 15 | { 16 | // b.verbose_cc = true; 17 | const mode = b.standardReleaseOptions(); 18 | const mdView = b.addExecutable("mdv", "src/main.zig"); 19 | mdView.setBuildMode(mode); 20 | mdView.addPackagePath("zig-log", "lib/log.zig/src/index.zig"); 21 | mdView.addPackagePath("mylog", "src/log/log.zig"); 22 | mdView.addPackagePath("zig-time", "lib/zig-time/src/time.zig"); 23 | mdView.c_std = Builder.CStd.C11; 24 | const webviewObjStep = WebviewLibraryStep.create(b); 25 | addWebviewDeps(mdView, &webviewObjStep.step); 26 | mdView.install(); 27 | const run_cmd = mdView.run(); 28 | run_cmd.step.dependOn(b.getInstallStep()); 29 | 30 | const run_step = b.step("run", "Run the app"); 31 | run_step.dependOn(&run_cmd.step); 32 | } 33 | 34 | { 35 | const mdTest = b.addTest("test.zig"); 36 | mdTest.addPackagePath("zig-time", "lib/zig-time/src/time.zig"); 37 | mdTest.addPackagePath("zig-log", "lib/log.zig/src/index.zig"); 38 | b.step("test", "Run all tests").dependOn(&mdTest.step); 39 | } 40 | } 41 | 42 | const WebviewLibraryStep = struct { 43 | builder: *std.build.Builder, 44 | step: std.build.Step, 45 | 46 | fn create(builder: *std.build.Builder) *WebviewLibraryStep { 47 | const self = builder.allocator.create(WebviewLibraryStep) catch unreachable; 48 | self.* = init(builder); 49 | return self; 50 | } 51 | 52 | fn init(builder: *std.build.Builder) WebviewLibraryStep { 53 | return WebviewLibraryStep{ 54 | .builder = builder, 55 | .step = std.build.Step.init(std.build.Step.Id.LibExeObj, "Webview Library Compile", builder.allocator, make), 56 | }; 57 | } 58 | 59 | fn make(step: *std.build.Step) !void { 60 | const self = @fieldParentPtr(WebviewLibraryStep, "step", step); 61 | const libs = std.mem.trim(u8, try self.builder.exec( 62 | &[_][]const u8{ "pkg-config", "--cflags", "--libs", "gtk+-3.0", "webkit2gtk-4.0" }, 63 | ), &std.ascii.spaces); 64 | 65 | var cmd = std.ArrayList([]const u8).init(self.builder.allocator); 66 | defer cmd.deinit(); 67 | 68 | try cmd.append("zig"); 69 | try cmd.append("c++"); 70 | // try cmd.append("--version"); 71 | // try cmd.append("-print-search-dirs"); 72 | try cmd.append("-v"); 73 | try cmd.append("-c"); 74 | try cmd.append("src/webview/webview.cc"); 75 | try cmd.append("-DWEBVIEW_GTK"); 76 | try cmd.append("-std=c++11"); 77 | var line_it = std.mem.tokenize(libs, " "); 78 | while (line_it.next()) |item| { 79 | try cmd.append(item); 80 | } 81 | try cmd.append("-o"); 82 | try cmd.append("src/webview/webview.o"); 83 | 84 | _ = std.mem.trim(u8, try self.builder.exec(cmd.items), &std.ascii.spaces); 85 | } 86 | }; 87 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const mem = std.mem; 3 | const fs = std.fs; 4 | const math = std.math; 5 | const process = std.process; 6 | const md = @import("md/markdown.zig").Markdown; 7 | const log = @import("md/log.zig"); 8 | const webview = @import("webview/webview.zig"); 9 | 10 | var DEBUG = false; 11 | var LOG_LEVEL = log.logger.Level.Error; 12 | var LOG_DATESTAMP = true; 13 | 14 | const Cmd = enum { 15 | view, 16 | }; 17 | 18 | /// Translates markdown input_files into html, returns a slice. Caller ows the memory. 19 | fn translate(allocator: *mem.Allocator, input_files: *std.ArrayList([]const u8)) ![]const u8 { 20 | var str = std.ArrayList(u8).init(allocator); 21 | defer str.deinit(); 22 | const cwd = fs.cwd(); 23 | for (input_files.items) |input_file| { 24 | const source = try cwd.readFileAlloc(allocator, input_file, math.maxInt(usize)); 25 | // try stdout.print("File: {}\nSource:\n````\n{}````\n", .{ input_file, source }); 26 | try md.renderToHtml( 27 | allocator, 28 | source, 29 | str.outStream(), 30 | ); 31 | } 32 | return str.toOwnedSlice(); 33 | } 34 | 35 | pub fn main() anyerror!void { 36 | var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 37 | defer arena.deinit(); 38 | const allocator = &arena.allocator; 39 | 40 | const args = try process.argsAlloc(allocator); 41 | var arg_i: usize = 1; 42 | var maybe_cmd: ?Cmd = null; 43 | 44 | var input_files = std.ArrayList([]const u8).init(allocator); 45 | 46 | log.config(LOG_LEVEL, LOG_DATESTAMP); 47 | 48 | while (arg_i < args.len) : (arg_i += 1) { 49 | const full_arg = args[arg_i]; 50 | if (mem.startsWith(u8, full_arg, "--")) { 51 | const arg = full_arg[2..]; 52 | if (mem.eql(u8, arg, "help")) { 53 | try dumpUsage(std.io.getStdOut()); 54 | return; 55 | } else if (mem.eql(u8, arg, "debug")) { 56 | DEBUG = true; 57 | LOG_LEVEL = log.logger.Level.Debug; 58 | log.config(LOG_LEVEL, LOG_DATESTAMP); 59 | } else { 60 | log.Errorf("Invalid parameter: {}\n", .{full_arg}); 61 | dumpStdErrUsageAndExit(); 62 | } 63 | } else if (mem.startsWith(u8, full_arg, "-")) { 64 | const arg = full_arg[1..]; 65 | if (mem.eql(u8, arg, "h")) { 66 | try dumpUsage(std.io.getStdOut()); 67 | return; 68 | } 69 | } else { 70 | inline for (std.meta.fields(Cmd)) |field| { 71 | log.Debugf("full_arg: {} field: {}\n", .{ full_arg, field }); 72 | if (mem.eql(u8, full_arg, field.name)) { 73 | maybe_cmd = @field(Cmd, field.name); 74 | log.Infof("Have command: {}\n", .{field.name}); 75 | break; 76 | // } else { 77 | // std.debug.warn("Invalid command: {}\n", .{full_arg}); 78 | // dumpStdErrUsageAndExit(); 79 | // } 80 | } else { 81 | _ = try input_files.append(full_arg); 82 | } 83 | } 84 | } 85 | } 86 | 87 | if (args.len <= 1) { 88 | log.Error("No arguments given!\n"); 89 | dumpStdErrUsageAndExit(); 90 | } 91 | 92 | if (input_files.items.len == 0) { 93 | log.Error("No input files were given!\n"); 94 | dumpStdErrUsageAndExit(); 95 | } 96 | 97 | var html = try std.ArrayListSentineled(u8, 0).init(allocator, ""); 98 | defer html.deinit(); 99 | 100 | const translated: []const u8 = try translate(allocator, &input_files); 101 | defer allocator.free(translated); 102 | 103 | const yava_script = 104 | \\ window.onload = function() { 105 | \\ }; 106 | ; 107 | 108 | try std.fmt.format(html.outStream(), 109 | \\ data:text/html, 110 | \\ 111 | \\ 112 | \\ 113 | \\ {} 114 | \\ 115 | \\ 118 | \\ 119 | , .{ translated, yava_script }); 120 | 121 | const final_doc = mem.span(@ptrCast([*c]const u8, html.span())); 122 | log.Debugf("final_doc: {} type: {}\n", .{ final_doc, @typeInfo(@TypeOf(final_doc)) }); 123 | 124 | if (maybe_cmd) |cmd| { 125 | switch (cmd) { 126 | .view => { 127 | var handle = webview.webview_create(1, null); 128 | webview.webview_set_size(handle, 1240, 1400, webview.WEBVIEW_HINT_MIN); 129 | webview.webview_set_title(handle, "Zig Markdown Viewer"); 130 | webview.webview_navigate(handle, final_doc); 131 | webview.webview_run(handle); 132 | return; 133 | }, 134 | else => {}, 135 | } 136 | } 137 | } 138 | 139 | fn dumpStdErrUsageAndExit() noreturn { 140 | dumpUsage(std.io.getStdErr()) catch {}; 141 | process.exit(1); 142 | } 143 | 144 | fn dumpUsage(file: fs.File) !void { 145 | _ = try file.write( 146 | \\Usage: mdcf [command] [options] 147 | \\ 148 | \\If no commands are specified, the html translated markdown is dumped to stdout. 149 | \\ 150 | \\Commands: 151 | \\ view Show the translated markdown in webview. 152 | \\ 153 | \\Options: 154 | \\ -h, --help Dump this help text to stdout. 155 | \\ --debug Show debug output. 156 | \\ 157 | ); 158 | } 159 | -------------------------------------------------------------------------------- /src/md/lexer.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const assert = std.debug.assert; 3 | const mem = std.mem; 4 | const ArrayList = std.ArrayList; 5 | const json = std.json; 6 | const utf8 = @import("../unicode/unicode.zig"); 7 | 8 | const log = @import("log.zig"); 9 | const Token = @import("token.zig").Token; 10 | const TokenRule = @import("token.zig").TokenRule; 11 | const TokenId = @import("token.zig").TokenId; 12 | const atxRules = @import("token_atx_heading.zig"); 13 | const inlineRules = @import("token_inline.zig"); 14 | 15 | pub const Lexer = struct { 16 | view: utf8.Utf8View, 17 | index: u32, 18 | rules: ArrayList(TokenRule), 19 | tokens: ArrayList(Token), 20 | tokenIndex: u64, 21 | lineNumber: u32, 22 | allocator: *mem.Allocator, 23 | 24 | pub fn init(allocator: *mem.Allocator, input: []const u8) !Lexer { 25 | // Skip the UTF-8 BOM if present 26 | var t = Lexer{ 27 | .view = try utf8.Utf8View.init(input), 28 | .index = 0, 29 | .allocator = allocator, 30 | .rules = ArrayList(TokenRule).init(allocator), 31 | .tokens = ArrayList(Token).init(allocator), 32 | .tokenIndex = 0, 33 | .lineNumber = 1, 34 | }; 35 | try t.registerRule(ruleWhitespace); 36 | try t.registerRule(atxRules.ruleAtxHeader); 37 | try t.registerRule(inlineRules.ruleInline); 38 | try t.registerRule(ruleEOF); 39 | return t; 40 | } 41 | 42 | pub fn deinit(l: *Lexer) void { 43 | l.rules.deinit(); 44 | l.tokens.deinit(); 45 | } 46 | 47 | pub fn registerRule(l: *Lexer, rule: TokenRule) !void { 48 | try l.rules.append(rule); 49 | } 50 | 51 | /// Get the next token from the input. 52 | pub fn next(l: *Lexer) !?Token { 53 | for (l.rules.items) |rule| { 54 | if (try rule(l)) |v| { 55 | return v; 56 | } 57 | } 58 | return null; 59 | } 60 | 61 | /// Peek at the next token. 62 | pub fn peekNext(l: *Lexer) !?Token { 63 | var indexBefore = l.index; 64 | var tokenIndexBefore = l.tokenIndex; 65 | var pNext = try l.next(); 66 | l.index = indexBefore; 67 | l.tokenIndex = tokenIndexBefore; 68 | return pNext; 69 | } 70 | 71 | /// Gets a codepoint at index from the input. Returns null if index exceeds the length of the view. 72 | pub fn getRune(l: *Lexer, index: u32) ?[]const u8 { 73 | return l.view.index(index); 74 | } 75 | 76 | pub fn debugPrintToken(l: *Lexer, msg: []const u8, token: anytype) !void { 77 | // TODO: only stringify json if debug logging 78 | var buf = std.ArrayList(u8).init(l.allocator); 79 | defer buf.deinit(); 80 | try json.stringify(token, json.StringifyOptions{ 81 | // This works differently than normal StringifyOptions for Tokens, separator does not 82 | // add \n. 83 | .whitespace = .{ 84 | .indent = .{ .Space = 1 }, 85 | .separator = true, 86 | }, 87 | }, buf.outStream()); 88 | log.Debugf("{}: {}\n", .{ msg, buf.items }); 89 | } 90 | 91 | pub fn emit(l: *Lexer, tok: TokenId, startOffset: u32, endOffset: u32) !?Token { 92 | // log.Debugf("start: {} end: {}\n", .{ start, end }); 93 | var str = l.view.slice(startOffset, endOffset); 94 | // check for diacritic 95 | log.Debugf("str: '{Z}'\n", .{str.bytes}); 96 | var nEndOffset: u32 = endOffset - 1; 97 | if ((endOffset - startOffset) == 1 or nEndOffset < startOffset) { 98 | nEndOffset = startOffset; 99 | } 100 | // check if token already emitted 101 | if (l.tokens.items.len > l.tokenIndex) { 102 | // try l.debugPrintToken("lexer last token", l.tokens.items[l.tokens.items.len - 1]); 103 | var lastTok = l.tokens.items[l.tokens.items.len - 1]; 104 | if (lastTok.ID == tok and lastTok.startOffset == startOffset and lastTok.endOffset == nEndOffset) { 105 | log.Debug("Token already encountered"); 106 | l.tokenIndex = l.tokens.items.len - 1; 107 | l.index = endOffset; 108 | return lastTok; 109 | } 110 | } 111 | var column: u32 = l.offsetToColumn(startOffset); 112 | if (tok == TokenId.EOF) { 113 | column = l.tokens.items[l.tokens.items.len - 1].column; 114 | l.lineNumber -= 1; 115 | } 116 | var newTok = Token{ 117 | .ID = tok, 118 | .startOffset = startOffset, 119 | .endOffset = nEndOffset, 120 | .string = str.bytes, 121 | .lineNumber = l.lineNumber, 122 | .column = column, 123 | }; 124 | try l.debugPrintToken("lexer emit", &newTok); 125 | try l.tokens.append(newTok); 126 | l.index = endOffset; 127 | l.tokenIndex = l.tokens.items.len - 1; 128 | if (mem.eql(u8, str.bytes, "\n")) { 129 | l.lineNumber += 1; 130 | } 131 | return newTok; 132 | } 133 | 134 | /// Returns the column number of offset translated from the start of the line 135 | pub fn offsetToColumn(l: *Lexer, offset: u32) u32 { 136 | var i: u32 = offset; 137 | var start: u32 = 1; 138 | var char: []const u8 = ""; 139 | var foundLastNewline: bool = false; 140 | if (offset > 0) { 141 | i = offset - 1; 142 | } 143 | // Get the last newline starting from offset 144 | while (!mem.eql(u8, char, "\n")) : (i -= 1) { 145 | if (i == 0) { 146 | break; 147 | } 148 | char = l.view.index(i).?; 149 | start = i; 150 | } 151 | if (mem.eql(u8, char, "\n")) { 152 | foundLastNewline = true; 153 | start = i + 1; 154 | } 155 | char = ""; 156 | i = offset; 157 | // Get the next newline starting from offset 158 | while (!mem.eql(u8, char, "\n")) : (i += 1) { 159 | if (i == l.view.len) { 160 | break; 161 | } 162 | char = l.view.index(i).?; 163 | } 164 | // only one line of input or on the first line of input 165 | if (!foundLastNewline) { 166 | return offset + 1; 167 | } 168 | return offset - start; 169 | } 170 | 171 | /// Checks for a single whitespace character. Returns true if char is a space character. 172 | pub fn isSpace(l: *Lexer, char: u8) bool { 173 | if (char == '\u{0020}') { 174 | return true; 175 | } 176 | return false; 177 | } 178 | 179 | /// Checks for all the whitespace characters. Returns true if the rune is a whitespace. 180 | pub fn isWhitespace(l: *Lexer, rune: []const u8) bool { 181 | // A whitespace character is a space (U+0020), tab (U+0009), newline (U+000A), line tabulation (U+000B), form feed 182 | // (U+000C), or carriage return (U+000D). 183 | const runes = &[_][]const u8{ 184 | "\u{0020}", "\u{0009}", "\u{000A}", "\u{000B}", "\u{000C}", "\u{000D}", 185 | }; 186 | for (runes) |itrune| 187 | if (mem.eql(u8, itrune, rune)) 188 | return true; 189 | return false; 190 | } 191 | 192 | pub fn isPunctuation(l: *Lexer, rune: []const u8) bool { 193 | // Check for ASCII punctuation characters... 194 | // 195 | // FIXME: Check against the unicode punctuation tables... there isn't a Zig library that does this that I have found. 196 | // 197 | // A punctuation character is an ASCII punctuation character or anything in the general Unicode categories Pc, Pd, 198 | // Pe, Pf, Pi, Po, or Ps. 199 | const runes = &[_][]const u8{ 200 | "!", "\"", "#", "$", "%", "&", "\'", "(", ")", "*", "+", ",", "-", ".", "/", ":", ";", "<", "=", ">", "?", "@", "[", "\\", "]", "^", "_", "`", "{", "|", "}", "~", 201 | }; 202 | for (runes) |itrune| 203 | if (mem.eql(u8, itrune, rune)) 204 | return true; 205 | return false; 206 | } 207 | 208 | pub fn isLetter(l: *Lexer, rune: []const u8) bool { 209 | // TODO: make this more robust by using unicode character sets 210 | if (!l.isPunctuation(rune) and !l.isWhitespace(rune)) { 211 | return true; 212 | } 213 | return false; 214 | } 215 | 216 | /// Get the last token emitted, exclude peek tokens 217 | pub fn lastToken(l: *Lexer) Token { 218 | return l.tokens.items[l.tokenIndex]; 219 | } 220 | 221 | /// Skip the next token 222 | pub fn skipNext(l: *Lexer) !void { 223 | _ = try l.next(); 224 | } 225 | }; 226 | 227 | /// Get all the whitespace characters greedly. 228 | pub fn ruleWhitespace(t: *Lexer) !?Token { 229 | var index: u32 = t.index; 230 | log.Debug("in ruleWhitespace"); 231 | while (t.getRune(index)) |val| { 232 | if (t.isWhitespace(val)) { 233 | index += 1; 234 | if (mem.eql(u8, "\n", val)) { 235 | break; 236 | } 237 | } else { 238 | log.Debugf("index: {}\n", .{index}); 239 | break; 240 | } 241 | } 242 | log.Debugf("t.index: {} index: {}\n", .{ t.index, index }); 243 | if (index > t.index) { 244 | return t.emit(.Whitespace, t.index, index); 245 | } 246 | return null; 247 | } 248 | 249 | /// Return EOF at the end of the input 250 | pub fn ruleEOF(t: *Lexer) !?Token { 251 | if (t.index == t.view.len) { 252 | return t.emit(.EOF, t.index, t.index); 253 | } 254 | return null; 255 | } 256 | 257 | test "lexer: peekNext " { 258 | var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 259 | defer arena.deinit(); 260 | const allocator = &arena.allocator; 261 | 262 | const input = "# foo"; 263 | log.Debugf("input:\n{}-- END OF TEST --\n", .{input}); 264 | 265 | var t = try Lexer.init(allocator, input); 266 | 267 | if (try t.next()) |tok| { 268 | assert(tok.ID == TokenId.AtxHeader); 269 | } 270 | 271 | // two consecutive peeks should return the same token 272 | if (try t.peekNext()) |tok| { 273 | assert(tok.ID == TokenId.Whitespace); 274 | } 275 | if (try t.peekNext()) |tok| { 276 | assert(tok.ID == TokenId.Whitespace); 277 | } 278 | // The last token does not include peek'd tokens 279 | assert(t.lastToken().ID == TokenId.AtxHeader); 280 | 281 | if (try t.next()) |tok| { 282 | assert(tok.ID == TokenId.Whitespace); 283 | } 284 | } 285 | 286 | test "lexer: offsetToColumn" { 287 | var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 288 | defer arena.deinit(); 289 | const allocator = &arena.allocator; 290 | const input = "foo\nbar \t\nbaz"; 291 | var t = try Lexer.init(allocator, input); 292 | _ = try t.next(); 293 | _ = try t.next(); 294 | if (try t.next()) |tok| { 295 | assert(tok.column == 1); 296 | } 297 | _ = try t.next(); 298 | _ = try t.next(); 299 | _ = try t.next(); 300 | if (try t.next()) |tok| { 301 | assert(tok.column == 1); 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /src/md/log.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const time = @import("zig-time"); 3 | pub const logger = @import("zig-log"); 4 | 5 | var log = logger.Logger.new(std.io.getStdOut(), true); 6 | 7 | pub fn config(level: logger.Level, dates: bool) void { 8 | log.setLevel(level); 9 | if (dates) { 10 | log.set_date_handler(log_rfc3330_date_handler); 11 | } 12 | } 13 | 14 | pub fn log_rfc3330_date_handler( 15 | l: *logger.Logger, 16 | ) void { 17 | var local = time.Location.getLocal(); 18 | var now = time.now(&local); 19 | var buf = std.ArrayList(u8).init(std.testing.allocator); 20 | defer buf.deinit(); 21 | now.formatBuffer(&buf, time.RFC3339) catch unreachable; 22 | l.file_stream.print("{} ", .{buf.items}) catch unreachable; 23 | } 24 | 25 | pub fn Debug(comptime str: []const u8) void { 26 | log.Debug(str); 27 | } 28 | 29 | pub fn Debugf(comptime fmt: []const u8, args: anytype) void { 30 | log.Debugf(fmt, args); 31 | } 32 | 33 | pub fn Info(comptime str: []const u8) void { 34 | log.Info(str); 35 | } 36 | 37 | pub fn Infof(comptime fmt: []const u8, args: anytype) void { 38 | log.Infof(fmt, args); 39 | } 40 | 41 | pub fn Error(comptime str: []const u8) void { 42 | log.Error(str); 43 | } 44 | 45 | pub fn Errorf(comptime fmt: []const u8, args: anytype) void { 46 | log.Errorf(fmt, args); 47 | } 48 | 49 | pub fn Fatal(comptime str: []const u8) void { 50 | log.Fatal(str); 51 | std.os.exit(1); 52 | } 53 | 54 | pub fn Fatalf(comptime fmt: []const u8, args: anytype) void { 55 | log.Fatalf(fmt, args); 56 | std.os.exit(1); 57 | } 58 | -------------------------------------------------------------------------------- /src/md/markdown.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const mem = std.mem; 3 | const io = std.io; 4 | const parser = @import("parse.zig"); 5 | const translate = @import("translate.zig"); 6 | 7 | pub const Markdown = struct { 8 | pub fn renderToHtml(allocator: *mem.Allocator, input: []const u8, out: var) !void { 9 | var p = parser.Parser.init(allocator); 10 | defer p.deinit(); 11 | try p.parse(input); 12 | try translate.markdownToHtml( 13 | allocator, 14 | p, 15 | out, 16 | ); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /src/md/parse.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const mem = std.mem; 3 | const json = std.json; 4 | const Lexer = @import("lexer.zig").Lexer; 5 | const log = @import("log.zig"); 6 | 7 | usingnamespace @import("parse_atx_heading.zig"); 8 | usingnamespace @import("parse_codeblock.zig"); 9 | 10 | /// Function prototype for a State Transition in the Parser 11 | pub const StateTransition = fn (lexer: *Lexer) anyerror!?AstNode; 12 | 13 | pub const Node = struct { 14 | ID: ID, 15 | Value: ?[]const u8, 16 | 17 | PositionStart: Position, 18 | PositionEnd: Position, 19 | 20 | Children: std.ArrayList(Node), 21 | 22 | Level: u32, 23 | 24 | pub const Position = struct { 25 | Line: u32, 26 | Column: u32, 27 | Offset: u32, 28 | }; 29 | 30 | pub const ID = enum { 31 | AtxHeading, 32 | Text, 33 | CodeBlock, 34 | pub fn jsonStringify( 35 | value: ID, 36 | options: json.StringifyOptions, 37 | out_stream: anytype, 38 | ) !void { 39 | try json.stringify(@tagName(value), options, out_stream); 40 | } 41 | }; 42 | 43 | pub const StringifyOptions = struct { 44 | pub const Whitespace = struct { 45 | /// How many indentation levels deep are we? 46 | indent_level: usize = 0, 47 | 48 | /// What character(s) should be used for indentation? 49 | indent: union(enum) { 50 | Space: u8, 51 | Tab: void, 52 | } = .{ .Space = 4 }, 53 | 54 | /// Newline after each element 55 | separator: bool = true, 56 | 57 | pub fn outputIndent( 58 | whitespace: @This(), 59 | out_stream: anytype, 60 | ) @TypeOf(out_stream).Error!void { 61 | var char: u8 = undefined; 62 | var n_chars: usize = undefined; 63 | switch (whitespace.indent) { 64 | .Space => |n_spaces| { 65 | char = ' '; 66 | n_chars = n_spaces; 67 | }, 68 | .Tab => { 69 | char = '\t'; 70 | n_chars = 1; 71 | }, 72 | } 73 | n_chars *= whitespace.indent_level; 74 | try out_stream.writeByteNTimes(char, n_chars); 75 | } 76 | }; 77 | 78 | /// Controls the whitespace emitted 79 | whitespace: ?Whitespace = null, 80 | 81 | string: StringOptions = StringOptions{ .String = .{} }, 82 | 83 | /// Should []u8 be serialised as a string? or an array? 84 | pub const StringOptions = union(enum) { 85 | Array, 86 | String: StringOutputOptions, 87 | 88 | /// String output options 89 | const StringOutputOptions = struct { 90 | /// Should '/' be escaped in strings? 91 | escape_solidus: bool = false, 92 | 93 | /// Should unicode characters be escaped in strings? 94 | escape_unicode: bool = false, 95 | }; 96 | }; 97 | }; 98 | 99 | pub fn deinit(self: @This()) void { 100 | self.Children.deinit(); 101 | } 102 | 103 | pub fn jsonStringify( 104 | value: @This(), 105 | options: json.StringifyOptions, 106 | out_stream: anytype, 107 | ) !void { 108 | try out_stream.writeByte('{'); 109 | const T = @TypeOf(value); 110 | const S = @typeInfo(T).Struct; 111 | comptime var field_output = false; 112 | var child_options = options; 113 | if (child_options.whitespace) |*child_whitespace| { 114 | child_whitespace.indent_level += 1; 115 | } 116 | inline for (S.fields) |Field, field_i| { 117 | if (Field.field_type == void) continue; 118 | 119 | if (!field_output) { 120 | field_output = true; 121 | } else { 122 | try out_stream.writeByte(','); 123 | } 124 | if (child_options.whitespace) |child_whitespace| { 125 | try out_stream.writeByte('\n'); 126 | try child_whitespace.outputIndent(out_stream); 127 | } 128 | try json.stringify(Field.name, options, out_stream); 129 | try out_stream.writeByte(':'); 130 | if (child_options.whitespace) |child_whitespace| { 131 | if (child_whitespace.separator) { 132 | try out_stream.writeByte(' '); 133 | } 134 | } 135 | if (comptime !mem.eql(u8, Field.name, "Children")) { 136 | try json.stringify(@field(value, Field.name), child_options, out_stream); 137 | } else { 138 | var boop = @field(value, Field.name); 139 | if (boop.items.len == 0) { 140 | _ = try out_stream.writeAll("[]"); 141 | } else { 142 | _ = try out_stream.write("["); 143 | for (boop.items) |item| { 144 | try json.stringify(item, child_options, out_stream); 145 | } 146 | _ = try out_stream.write("]"); 147 | } 148 | } 149 | } 150 | if (field_output) { 151 | if (options.whitespace) |whitespace| { 152 | try out_stream.writeByte('\n'); 153 | try whitespace.outputIndent(out_stream); 154 | } 155 | } 156 | try out_stream.writeByte('}'); 157 | return; 158 | } 159 | 160 | pub fn htmlStringify( 161 | value: @This(), 162 | options: StringifyOptions, 163 | out_stream: anytype, 164 | ) !void { 165 | var child_options = options; 166 | switch (value.ID) { 167 | .AtxHeading => { 168 | var lvl = value.Level; 169 | var text = value.Children.items[0].Value; 170 | _ = try out_stream.print("{}", .{ lvl, text, lvl }); 171 | if (child_options.whitespace) |child_whitespace| { 172 | if (child_whitespace.separator) { 173 | try out_stream.writeByte('\n'); 174 | } 175 | } 176 | }, 177 | .CodeBlock => { 178 | var lvl = value.Level; 179 | var text = value.Children.items[0].Value; 180 | _ = try out_stream.print("
{}
", .{text}); 181 | if (child_options.whitespace) |child_whitespace| { 182 | if (child_whitespace.separator) { 183 | try out_stream.writeByte('\n'); 184 | } 185 | } 186 | }, 187 | .Text => {}, 188 | } 189 | } 190 | }; 191 | 192 | /// A non-stream Markdown parser which constructs a tree of Nodes 193 | pub const Parser = struct { 194 | allocator: *mem.Allocator, 195 | 196 | root: std.ArrayList(Node), 197 | state: State, 198 | lex: Lexer, 199 | 200 | pub const State = enum { 201 | Start, 202 | AtxHeader, 203 | CodeBlock, 204 | }; 205 | 206 | pub fn init( 207 | allocator: *mem.Allocator, 208 | ) Parser { 209 | return Parser{ 210 | .allocator = allocator, 211 | .state = .Start, 212 | .root = std.ArrayList(Node).init(allocator), 213 | .lex = undefined, 214 | }; 215 | } 216 | 217 | pub fn deinit(self: *Parser) void { 218 | for (self.root.items) |item| { 219 | for (item.Children.items) |subchild| { 220 | if (subchild.Value) |val2| { 221 | self.allocator.free(val2); 222 | } 223 | subchild.deinit(); 224 | } 225 | if (item.Value) |val| { 226 | self.allocator.free(val); 227 | } 228 | item.deinit(); 229 | } 230 | self.root.deinit(); 231 | self.lex.deinit(); 232 | } 233 | 234 | pub fn parse(self: *Parser, input: []const u8) !void { 235 | self.lex = try Lexer.init(self.allocator, input); 236 | while (true) { 237 | if (try self.lex.next()) |tok| { 238 | switch (tok.ID) { 239 | .Invalid => {}, 240 | .Text => {}, 241 | .Whitespace => { 242 | try stateCodeBlock(self); 243 | // if (mem.eql(u8, tok.string, "\n")) {} 244 | }, 245 | .AtxHeader => { 246 | try stateAtxHeader(self); 247 | }, 248 | .EOF => { 249 | log.Debug("Found EOF"); 250 | break; 251 | }, 252 | } 253 | } 254 | } 255 | } 256 | }; 257 | -------------------------------------------------------------------------------- /src/md/parse_atx_heading.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const mem = std.mem; 3 | const State = @import("ast.zig").State; 4 | const Parser = @import("parse.zig").Parser; 5 | const Node = @import("parse.zig").Node; 6 | const Lexer = @import("lexer.zig").Lexer; 7 | const TokenId = @import("token.zig").TokenId; 8 | const log = @import("log.zig"); 9 | 10 | pub fn stateAtxHeader(p: *Parser) !void { 11 | p.state = Parser.State.AtxHeader; 12 | if (try p.lex.peekNext()) |tok| { 13 | if (tok.ID == TokenId.Whitespace and mem.eql(u8, tok.string, " ")) { 14 | var openTok = p.lex.lastToken(); 15 | var i: u32 = 0; 16 | var level: u32 = 0; 17 | while (i < openTok.string.len) : ({ 18 | level += 1; 19 | i += 1; 20 | }) {} 21 | var newChild = Node{ 22 | .ID = Node.ID.AtxHeading, 23 | .Value = null, 24 | .PositionStart = Node.Position{ 25 | .Line = openTok.lineNumber, 26 | .Column = openTok.column, 27 | .Offset = openTok.startOffset, 28 | }, 29 | .PositionEnd = Node.Position{ 30 | .Line = openTok.lineNumber, 31 | .Column = openTok.column, 32 | .Offset = openTok.endOffset, 33 | }, 34 | .Children = std.ArrayList(Node).init(p.allocator), 35 | .Level = level, 36 | }; 37 | // skip the whitespace after the header opening 38 | try p.lex.skipNext(); 39 | while (try p.lex.next()) |ntok| { 40 | if (ntok.ID == TokenId.Whitespace and mem.eql(u8, ntok.string, "\n")) { 41 | log.Debug("Found a newline, exiting state"); 42 | break; 43 | } 44 | var subChild = Node{ 45 | .ID = Node.ID.Text, 46 | .Value = ntok.string, 47 | .PositionStart = Node.Position{ 48 | .Line = ntok.lineNumber, 49 | .Column = ntok.column, 50 | .Offset = ntok.startOffset, 51 | }, 52 | .PositionEnd = Node.Position{ 53 | .Line = ntok.lineNumber, 54 | .Column = ntok.column, 55 | .Offset = ntok.endOffset, 56 | }, 57 | .Children = std.ArrayList(Node).init(p.allocator), 58 | .Level = level, 59 | }; 60 | try newChild.Children.append(subChild); 61 | } 62 | newChild.PositionEnd = newChild.Children.items[newChild.Children.items.len - 1].PositionEnd; 63 | try p.root.append(newChild); 64 | p.state = Parser.State.Start; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/md/parse_codeblock.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const mem = std.mem; 3 | const log = @import("log.zig"); 4 | const State = @import("ast.zig").State; 5 | const Parser = @import("parse.zig").Parser; 6 | const Node = @import("parse.zig").Node; 7 | const Lexer = @import("lexer.zig").Lexer; 8 | const TokenId = @import("token.zig").TokenId; 9 | 10 | pub fn stateCodeBlock(p: *Parser) !void { 11 | if (try p.lex.peekNext()) |tok| { 12 | var openTok = p.lex.lastToken(); 13 | // log.Debugf("parse block code before openTok: '{}' id: {} len: {}, tok: '{}' id: {} len: {}\n", .{ 14 | // openTok.string, openTok.ID, openTok.string.len, 15 | // tok.string, tok.ID, tok.string.len, 16 | // }); 17 | var hazCodeBlockWhitespace: bool = false; 18 | // var hazCodeBlockWhitespaceNextToken: bool = false; 19 | if (openTok.ID == TokenId.Whitespace and openTok.string.len >= 1) { 20 | if (mem.indexOf(u8, openTok.string, "\t") != null or openTok.string.len >= 4) { 21 | hazCodeBlockWhitespace = true; 22 | // } else if (try p.lex.peekNext()) |peekTok| { 23 | // if (peekTok.ID == TokenId.Whitespace and peekTok.string.len >= 1) { 24 | // if (mem.indexOf(u8, peekTok.string, "\t") != null or peekTok.string.len >= 4) { 25 | // hazCodeBlockWhitespaceNextToken = true; 26 | // } 27 | // } 28 | } 29 | } 30 | if (hazCodeBlockWhitespace and tok.ID == TokenId.Text) { 31 | log.Debug("Found a code block!"); 32 | try p.lex.debugPrintToken("stateCodeBlock openTok", &openTok); 33 | try p.lex.debugPrintToken("stateCodeBlock tok", &tok); 34 | p.state = Parser.State.CodeBlock; 35 | var newChild = Node{ 36 | .ID = Node.ID.CodeBlock, 37 | .Value = openTok.string, 38 | .PositionStart = Node.Position{ 39 | .Line = openTok.lineNumber, 40 | .Column = openTok.column, 41 | .Offset = openTok.startOffset, 42 | }, 43 | .PositionEnd = undefined, 44 | .Children = std.ArrayList(Node).init(p.allocator), 45 | .Level = 0, 46 | }; 47 | 48 | var buf = try std.ArrayListSentineled(u8, 0).init(p.allocator, tok.string); 49 | defer buf.deinit(); 50 | 51 | // skip the whitespace after the codeblock opening 52 | try p.lex.skipNext(); 53 | var startPos = Node.Position{ 54 | .Line = tok.lineNumber, 55 | .Column = tok.column, 56 | .Offset = tok.startOffset, 57 | }; 58 | 59 | while (try p.lex.next()) |ntok| { 60 | // if (ntok.ID == TokenId.Whitespace and mem.eql(u8, ntok.string, "\n")) { 61 | if (ntok.ID == TokenId.Whitespace and ntok.column == 1) { 62 | continue; 63 | } 64 | if (ntok.ID == TokenId.EOF) { 65 | // FIXME: loop until de-indent 66 | // FIXME: blanklines or eof should cause the state to exit 67 | try p.lex.debugPrintToken("stateCodeBlock ntok", &ntok); 68 | log.Debug("Found a newline, exiting state"); 69 | try buf.appendSlice(ntok.string); 70 | try newChild.Children.append(Node{ 71 | .ID = Node.ID.Text, 72 | .Value = buf.toOwnedSlice(), 73 | .PositionStart = startPos, 74 | .PositionEnd = Node.Position{ 75 | .Line = ntok.lineNumber, 76 | .Column = ntok.column, 77 | .Offset = ntok.endOffset - 1, 78 | }, 79 | .Children = std.ArrayList(Node).init(p.allocator), 80 | .Level = 0, 81 | }); 82 | break; 83 | } 84 | try buf.appendSlice(ntok.string); 85 | } 86 | 87 | newChild.PositionEnd = newChild.Children.items[newChild.Children.items.len - 1].PositionEnd; 88 | // p.lex.index = newChild.PositionEnd.Offset; 89 | try p.root.append(newChild); 90 | p.state = Parser.State.Start; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/md/token.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const mem = std.mem; 3 | const json = std.json; 4 | 5 | const Lexer = @import("lexer.zig").Lexer; 6 | 7 | const TokenIds = [_][]const u8{ 8 | "Invalid", 9 | "Whitespace", 10 | "Text", 11 | "AtxHeader", 12 | "EOF", 13 | }; 14 | 15 | pub const TokenId = enum { 16 | Invalid, 17 | Whitespace, 18 | Text, 19 | AtxHeader, 20 | EOF, 21 | 22 | pub fn string(self: TokenId) []const u8 { 23 | const m = @enumToInt(self); 24 | if (@enumToInt(TokenId.Invalid) <= m and m <= @enumToInt(TokenId.EOF)) { 25 | return TokenIds[m]; 26 | } 27 | unreachable; 28 | } 29 | 30 | pub fn jsonStringify( 31 | self: @This(), 32 | options: json.StringifyOptions, 33 | out_stream: anytype, 34 | ) !void { 35 | try json.stringify(self.string(), options, out_stream); 36 | } 37 | }; 38 | 39 | pub const Token = struct { 40 | ID: TokenId, 41 | startOffset: u32, 42 | endOffset: u32, 43 | string: []const u8, 44 | lineNumber: u32, 45 | column: u32, 46 | 47 | pub fn jsonStringify( 48 | value: @This(), 49 | options: json.StringifyOptions, 50 | out_stream: anytype, 51 | ) !void { 52 | try out_stream.writeByte('{'); 53 | const T = @TypeOf(value); 54 | const S = @typeInfo(T).Struct; 55 | comptime var field_output = false; 56 | var child_options = options; 57 | if (child_options.whitespace) |*child_whitespace| { 58 | child_whitespace.indent_level += 1; 59 | } 60 | inline for (S.fields) |Field, field_i| { 61 | if (Field.field_type == void) continue; 62 | 63 | if (!field_output) { 64 | field_output = true; 65 | } else { 66 | try out_stream.writeByte(','); 67 | } 68 | if (child_options.whitespace) |child_whitespace| { 69 | // FIXME: all this to remove this line... 70 | // try out_stream.writeByte('\n'); 71 | try child_whitespace.outputIndent(out_stream); 72 | } 73 | try json.stringify(Field.name, options, out_stream); 74 | try out_stream.writeByte(':'); 75 | if (child_options.whitespace) |child_whitespace| { 76 | if (child_whitespace.separator) { 77 | try out_stream.writeByte(' '); 78 | } 79 | } 80 | if (comptime !mem.eql(u8, Field.name, "Children")) { 81 | try json.stringify(@field(value, Field.name), child_options, out_stream); 82 | } else { 83 | var boop = @field(value, Field.name); 84 | if (boop.items.len == 0) { 85 | _ = try out_stream.writeAll("[]"); 86 | } else { 87 | _ = try out_stream.write("["); 88 | for (boop.items) |item| { 89 | try json.stringify(item, child_options, out_stream); 90 | } 91 | _ = try out_stream.write("]"); 92 | } 93 | } 94 | } 95 | if (field_output) { 96 | if (options.whitespace) |whitespace| { 97 | // FIXME: all this to remove this line... 98 | // try out_stream.writeByte('\n'); 99 | try whitespace.outputIndent(out_stream); 100 | } 101 | } 102 | try out_stream.writeByte(' '); 103 | try out_stream.writeByte('}'); 104 | return; 105 | } 106 | }; 107 | 108 | pub const TokenRule = fn (lexer: *Lexer) anyerror!?Token; 109 | -------------------------------------------------------------------------------- /src/md/token_atx_heading.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const mem = std.mem; 3 | const Lexer = @import("lexer.zig").Lexer; 4 | const Token = @import("token.zig").Token; 5 | 6 | pub fn ruleAtxHeader(l: *Lexer) !?Token { 7 | var index: u32 = l.index; 8 | while (l.getRune(index)) |val| { 9 | if (mem.eql(u8, "#", val)) { 10 | index += 1; 11 | } else { 12 | break; 13 | } 14 | } 15 | if (index > l.index) { 16 | return l.emit(.AtxHeader, l.index, index); 17 | } 18 | return null; 19 | } 20 | -------------------------------------------------------------------------------- /src/md/token_inline.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const log = @import("log.zig"); 3 | const token = @import("token.zig"); 4 | const Lexer = @import("lexer.zig").Lexer; 5 | 6 | pub fn ruleInline(l: *Lexer) !?token.Token { 7 | var index: u32 = l.index; 8 | while (l.getRune(index)) |val| { 9 | if (l.isLetter(val)) { 10 | index += 1; 11 | } else { 12 | break; 13 | } 14 | } 15 | if (index > l.index) { 16 | // // log.Debug("in here yo"); 17 | // log.Debugf("foo: {}\n", .{l.index}); 18 | // if (true) { 19 | // @panic("boo!"); 20 | // } 21 | return l.emit(.Text, l.index, index); 22 | } 23 | return null; 24 | } 25 | -------------------------------------------------------------------------------- /src/md/translate.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const parser = @import("parse.zig"); 3 | const Node = @import("parse.zig").Node; 4 | 5 | pub fn markdownToHtml(nodeList: *std.ArrayList(Node), outStream: anytype) !void { 6 | for (nodeList.items) |item| { 7 | try parser.Node.htmlStringify( 8 | item, 9 | parser.Node.StringifyOptions{ 10 | .whitespace = .{ 11 | .indent = .{ .Space = 4 }, 12 | .separator = true, 13 | }, 14 | }, 15 | outStream, 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/unicode/unicode.zig: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2018 Jimmi Holst Christensen 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | // 23 | // From https://github.com/TM35-Metronome/metronome 24 | // 25 | const std = @import("std"); 26 | 27 | const mem = std.mem; 28 | const testing = std.testing; 29 | 30 | const log = @import("../md/log.zig"); 31 | 32 | test "Utf8View Index" { 33 | const s = try Utf8View.init("noël n"); 34 | var it = s.iterator(); 35 | 36 | testing.expect(std.mem.eql(u8, "n", it.nextCodepointSlice().?)); 37 | 38 | testing.expect(std.mem.eql(u8, "o", it.peek(1))); 39 | testing.expect(std.mem.eql(u8, "oë", it.peek(2))); 40 | testing.expect(std.mem.eql(u8, "oël", it.peek(3))); 41 | testing.expect(std.mem.eql(u8, "oël ", it.peek(4))); 42 | testing.expect(std.mem.eql(u8, "oël n", it.peek(10))); 43 | 44 | testing.expect(std.mem.eql(u8, "o", it.nextCodepointSlice().?)); 45 | testing.expect(std.mem.eql(u8, "ë", it.nextCodepointSlice().?)); 46 | testing.expect(std.mem.eql(u8, "l", it.nextCodepointSlice().?)); 47 | testing.expect(std.mem.eql(u8, " ", it.nextCodepointSlice().?)); 48 | testing.expect(std.mem.eql(u8, "n", it.nextCodepointSlice().?)); 49 | testing.expect(it.nextCodepointSlice() == null); 50 | 51 | testing.expect(std.mem.eql(u8, "n", s.index(0).?)); 52 | testing.expect(std.mem.eql(u8, "ë", s.index(2).?)); 53 | testing.expect(std.mem.eql(u8, "o", s.index(1).?)); 54 | testing.expect(std.mem.eql(u8, "l", s.index(3).?)); 55 | 56 | testing.expect(std.mem.eql(u8, &[_]u8{}, it.peek(1))); 57 | } 58 | 59 | /// Improved Utf8View which also keeps track of the length in codepoints 60 | pub const Utf8View = struct { 61 | bytes: []const u8, 62 | len: usize, 63 | 64 | pub fn init(str: []const u8) !Utf8View { 65 | return Utf8View{ 66 | .bytes = str, 67 | .len = try utf8Len(str), 68 | }; 69 | } 70 | 71 | /// Returns the codepoint at i. Returns null if i is greater than the length of the view. 72 | pub fn index(view: Utf8View, i: usize) ?[]const u8 { 73 | if (i >= view.len) { 74 | return null; 75 | } 76 | var y: usize = 0; 77 | var it = view.iterator(); 78 | var rune: ?[]const u8 = null; 79 | while (y < i + 1) : (y += 1) if (it.nextCodepointSlice()) |r| { 80 | rune = r; 81 | }; 82 | if (rune) |nrune| 83 | _ = std.unicode.utf8Decode(nrune) catch unreachable; 84 | return rune; 85 | } 86 | 87 | pub fn slice(view: Utf8View, start: usize, end: usize) Utf8View { 88 | var len: usize = 0; 89 | var i: usize = 0; 90 | var it = view.iterator(); 91 | while (i < start) : (i += 1) 92 | len += @boolToInt(it.nextCodepointSlice() != null); 93 | 94 | const start_i = it.i; 95 | while (i < end) : (i += 1) 96 | len += @boolToInt(it.nextCodepointSlice() != null); 97 | 98 | return .{ 99 | .bytes = view.bytes[start_i..it.i], 100 | .len = len, 101 | }; 102 | } 103 | 104 | pub fn iterator(view: Utf8View) std.unicode.Utf8Iterator { 105 | return std.unicode.Utf8View.initUnchecked(view.bytes).iterator(); 106 | } 107 | }; 108 | 109 | /// Given a string of words, this function will split the string into lines where 110 | /// a maximum of `max_line_len` characters can occure on each line. 111 | pub fn splitIntoLines(allocator: *mem.Allocator, max_line_len: usize, string: Utf8View) !Utf8View { 112 | var res = std.ArrayList(u8).init(allocator); 113 | errdefer res.deinit(); 114 | 115 | // A decent estimate that will most likely ensure that we only do one allocation. 116 | try res.ensureCapacity(string.len + (string.len / max_line_len) + 1); 117 | 118 | var curr_line_len: usize = 0; 119 | var it = mem.tokenize(string.bytes, " \n"); 120 | while (it.next()) |word_bytes| { 121 | const word = Utf8View.init(word_bytes) catch unreachable; 122 | const next_line_len = word.len + curr_line_len + (1 * @boolToInt(curr_line_len != 0)); 123 | if (next_line_len > max_line_len) { 124 | try res.appendSlice("\n"); 125 | try res.appendSlice(word_bytes); 126 | curr_line_len = word.len; 127 | } else { 128 | if (curr_line_len != 0) 129 | try res.appendSlice(" "); 130 | try res.appendSlice(word_bytes); 131 | curr_line_len = next_line_len; 132 | } 133 | } 134 | 135 | return Utf8View.init(res.toOwnedSlice()) catch unreachable; 136 | } 137 | 138 | fn utf8Len(s: []const u8) !usize { 139 | var res: usize = 0; 140 | var i: usize = 0; 141 | while (i < s.len) : (res += 1) { 142 | if (std.unicode.utf8ByteSequenceLength(s[i])) |cp_len| { 143 | if (i + cp_len > s.len) { 144 | return error.InvalidUtf8; 145 | } 146 | 147 | if (std.unicode.utf8Decode(s[i .. i + cp_len])) |_| {} else |_| { 148 | return error.InvalidUtf8; 149 | } 150 | i += cp_len; 151 | } else |err| { 152 | return error.InvalidUtf8; 153 | } 154 | } 155 | return res; 156 | } 157 | -------------------------------------------------------------------------------- /src/webview/webview.cc: -------------------------------------------------------------------------------- 1 | // This is an attempt to use the header only portion of 2 | #include "webview.h" 3 | -------------------------------------------------------------------------------- /src/webview/webview.h: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2017 Serge Zaitsev 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | #ifndef WEBVIEW_H 25 | #define WEBVIEW_H 26 | 27 | #ifndef WEBVIEW_API 28 | #define WEBVIEW_API extern 29 | #endif 30 | 31 | #ifdef __cplusplus 32 | extern "C" { 33 | #endif 34 | 35 | typedef void *webview_t; 36 | 37 | // Creates a new webview instance. If debug is non-zero - developer tools will 38 | // be enabled (if the platform supports them). Window parameter can be a 39 | // pointer to the native window handle. If it's non-null - then child WebView 40 | // is embedded into the given parent window. Otherwise a new window is created. 41 | // Depending on the platform, a GtkWindow, NSWindow or HWND pointer can be 42 | // passed here. 43 | WEBVIEW_API webview_t webview_create(int debug, void *window); 44 | 45 | // Destroys a webview and closes the native window. 46 | WEBVIEW_API void webview_destroy(webview_t w); 47 | 48 | // Runs the main loop until it's terminated. After this function exits - you 49 | // must destroy the webview. 50 | WEBVIEW_API void webview_run(webview_t w); 51 | 52 | // Stops the main loop. It is safe to call this function from another other 53 | // background thread. 54 | WEBVIEW_API void webview_terminate(webview_t w); 55 | 56 | // Posts a function to be executed on the main thread. You normally do not need 57 | // to call this function, unless you want to tweak the native window. 58 | WEBVIEW_API void 59 | webview_dispatch(webview_t w, void (*fn)(webview_t w, void *arg), void *arg); 60 | 61 | // Returns a native window handle pointer. When using GTK backend the pointer 62 | // is GtkWindow pointer, when using Cocoa backend the pointer is NSWindow 63 | // pointer, when using Win32 backend the pointer is HWND pointer. 64 | WEBVIEW_API void *webview_get_window(webview_t w); 65 | 66 | // Updates the title of the native window. Must be called from the UI thread. 67 | WEBVIEW_API void webview_set_title(webview_t w, const char *title); 68 | 69 | // Window size hints 70 | #define WEBVIEW_HINT_NONE 0 // Width and height are default size 71 | #define WEBVIEW_HINT_MIN 1 // Width and height are minimum bounds 72 | #define WEBVIEW_HINT_MAX 2 // Width and height are maximum bounds 73 | #define WEBVIEW_HINT_FIXED 3 // Window size can not be changed by a user 74 | // Updates native window size. See WEBVIEW_HINT constants. 75 | WEBVIEW_API void webview_set_size(webview_t w, int width, int height, 76 | int hints); 77 | 78 | // Navigates webview to the given URL. URL may be a data URI, i.e. 79 | // "data:text/text,...". It is often ok not to url-encode it 80 | // properly, webview will re-encode it for you. 81 | WEBVIEW_API void webview_navigate(webview_t w, const char *url); 82 | 83 | // Injects JavaScript code at the initialization of the new page. Every time 84 | // the webview will open a the new page - this initialization code will be 85 | // executed. It is guaranteed that code is executed before window.onload. 86 | WEBVIEW_API void webview_init(webview_t w, const char *js); 87 | 88 | // Evaluates arbitrary JavaScript code. Evaluation happens asynchronously, also 89 | // the result of the expression is ignored. Use RPC bindings if you want to 90 | // receive notifications about the results of the evaluation. 91 | WEBVIEW_API void webview_eval(webview_t w, const char *js); 92 | 93 | // Binds a native C callback so that it will appear under the given name as a 94 | // global JavaScript function. Internally it uses webview_init(). Callback 95 | // receives a request string and a user-provided argument pointer. Request 96 | // string is a JSON array of all the arguments passed to the JavaScript 97 | // function. 98 | WEBVIEW_API void webview_bind(webview_t w, const char *name, 99 | void (*fn)(const char *seq, const char *req, 100 | void *arg), 101 | void *arg); 102 | 103 | // Allows to return a value from the native binding. Original request pointer 104 | // must be provided to help internal RPC engine match requests with responses. 105 | // If status is zero - result is expected to be a valid JSON result value. 106 | // If status is not zero - result is an error JSON object. 107 | WEBVIEW_API void webview_return(webview_t w, const char *seq, int status, 108 | const char *result); 109 | 110 | #ifdef __cplusplus 111 | } 112 | #endif 113 | 114 | #ifndef WEBVIEW_HEADER 115 | 116 | #if !defined(WEBVIEW_GTK) && !defined(WEBVIEW_COCOA) && !defined(WEBVIEW_EDGE) 117 | #if defined(__linux__) 118 | #define WEBVIEW_GTK 119 | #elif defined(__APPLE__) 120 | #define WEBVIEW_COCOA 121 | #elif defined(_WIN32) 122 | #define WEBVIEW_EDGE 123 | #else 124 | #error "please, specify webview backend" 125 | #endif 126 | #endif 127 | 128 | #include 129 | #include 130 | #include 131 | #include 132 | #include 133 | #include 134 | #include 135 | 136 | #include 137 | 138 | namespace webview { 139 | using dispatch_fn_t = std::function; 140 | 141 | inline std::string url_encode(const std::string s) { 142 | std::string encoded; 143 | for (unsigned int i = 0; i < s.length(); i++) { 144 | auto c = s[i]; 145 | if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') { 146 | encoded = encoded + c; 147 | } else { 148 | char hex[4]; 149 | snprintf(hex, sizeof(hex), "%%%02x", c); 150 | encoded = encoded + hex; 151 | } 152 | } 153 | return encoded; 154 | } 155 | 156 | inline std::string url_decode(const std::string s) { 157 | std::string decoded; 158 | for (unsigned int i = 0; i < s.length(); i++) { 159 | if (s[i] == '%') { 160 | int n; 161 | n = std::stoul(s.substr(i + 1, 2), nullptr, 16); 162 | decoded = decoded + static_cast(n); 163 | i = i + 2; 164 | } else if (s[i] == '+') { 165 | decoded = decoded + ' '; 166 | } else { 167 | decoded = decoded + s[i]; 168 | } 169 | } 170 | return decoded; 171 | } 172 | 173 | inline std::string html_from_uri(const std::string s) { 174 | if (s.substr(0, 15) == "data:text/html,") { 175 | return url_decode(s.substr(15)); 176 | } 177 | return ""; 178 | } 179 | 180 | inline int json_parse_c(const char *s, size_t sz, const char *key, size_t keysz, 181 | const char **value, size_t *valuesz) { 182 | enum { 183 | JSON_STATE_VALUE, 184 | JSON_STATE_LITERAL, 185 | JSON_STATE_STRING, 186 | JSON_STATE_ESCAPE, 187 | JSON_STATE_UTF8 188 | } state = JSON_STATE_VALUE; 189 | const char *k = NULL; 190 | int index = 1; 191 | int depth = 0; 192 | int utf8_bytes = 0; 193 | 194 | if (key == NULL) { 195 | index = keysz; 196 | keysz = 0; 197 | } 198 | 199 | *value = NULL; 200 | *valuesz = 0; 201 | 202 | for (; sz > 0; s++, sz--) { 203 | enum { 204 | JSON_ACTION_NONE, 205 | JSON_ACTION_START, 206 | JSON_ACTION_END, 207 | JSON_ACTION_START_STRUCT, 208 | JSON_ACTION_END_STRUCT 209 | } action = JSON_ACTION_NONE; 210 | unsigned char c = *s; 211 | switch (state) { 212 | case JSON_STATE_VALUE: 213 | if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == ',' || 214 | c == ':') { 215 | continue; 216 | } else if (c == '"') { 217 | action = JSON_ACTION_START; 218 | state = JSON_STATE_STRING; 219 | } else if (c == '{' || c == '[') { 220 | action = JSON_ACTION_START_STRUCT; 221 | } else if (c == '}' || c == ']') { 222 | action = JSON_ACTION_END_STRUCT; 223 | } else if (c == 't' || c == 'f' || c == 'n' || c == '-' || 224 | (c >= '0' && c <= '9')) { 225 | action = JSON_ACTION_START; 226 | state = JSON_STATE_LITERAL; 227 | } else { 228 | return -1; 229 | } 230 | break; 231 | case JSON_STATE_LITERAL: 232 | if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == ',' || 233 | c == ']' || c == '}' || c == ':') { 234 | state = JSON_STATE_VALUE; 235 | s--; 236 | sz++; 237 | action = JSON_ACTION_END; 238 | } else if (c < 32 || c > 126) { 239 | return -1; 240 | } // fallthrough 241 | case JSON_STATE_STRING: 242 | if (c < 32 || (c > 126 && c < 192)) { 243 | return -1; 244 | } else if (c == '"') { 245 | action = JSON_ACTION_END; 246 | state = JSON_STATE_VALUE; 247 | } else if (c == '\\') { 248 | state = JSON_STATE_ESCAPE; 249 | } else if (c >= 192 && c < 224) { 250 | utf8_bytes = 1; 251 | state = JSON_STATE_UTF8; 252 | } else if (c >= 224 && c < 240) { 253 | utf8_bytes = 2; 254 | state = JSON_STATE_UTF8; 255 | } else if (c >= 240 && c < 247) { 256 | utf8_bytes = 3; 257 | state = JSON_STATE_UTF8; 258 | } else if (c >= 128 && c < 192) { 259 | return -1; 260 | } 261 | break; 262 | case JSON_STATE_ESCAPE: 263 | if (c == '"' || c == '\\' || c == '/' || c == 'b' || c == 'f' || 264 | c == 'n' || c == 'r' || c == 't' || c == 'u') { 265 | state = JSON_STATE_STRING; 266 | } else { 267 | return -1; 268 | } 269 | break; 270 | case JSON_STATE_UTF8: 271 | if (c < 128 || c > 191) { 272 | return -1; 273 | } 274 | utf8_bytes--; 275 | if (utf8_bytes == 0) { 276 | state = JSON_STATE_STRING; 277 | } 278 | break; 279 | default: 280 | return -1; 281 | } 282 | 283 | if (action == JSON_ACTION_END_STRUCT) { 284 | depth--; 285 | } 286 | 287 | if (depth == 1) { 288 | if (action == JSON_ACTION_START || action == JSON_ACTION_START_STRUCT) { 289 | if (index == 0) { 290 | *value = s; 291 | } else if (keysz > 0 && index == 1) { 292 | k = s; 293 | } else { 294 | index--; 295 | } 296 | } else if (action == JSON_ACTION_END || 297 | action == JSON_ACTION_END_STRUCT) { 298 | if (*value != NULL && index == 0) { 299 | *valuesz = (size_t)(s + 1 - *value); 300 | return 0; 301 | } else if (keysz > 0 && k != NULL) { 302 | if (keysz == (size_t)(s - k - 1) && memcmp(key, k + 1, keysz) == 0) { 303 | index = 0; 304 | } else { 305 | index = 2; 306 | } 307 | k = NULL; 308 | } 309 | } 310 | } 311 | 312 | if (action == JSON_ACTION_START_STRUCT) { 313 | depth++; 314 | } 315 | } 316 | return -1; 317 | } 318 | 319 | inline std::string json_escape(std::string s) { 320 | // TODO: implement 321 | return '"' + s + '"'; 322 | } 323 | 324 | inline int json_unescape(const char *s, size_t n, char *out) { 325 | int r = 0; 326 | if (*s++ != '"') { 327 | return -1; 328 | } 329 | while (n > 2) { 330 | char c = *s; 331 | if (c == '\\') { 332 | s++; 333 | n--; 334 | switch (*s) { 335 | case 'b': 336 | c = '\b'; 337 | break; 338 | case 'f': 339 | c = '\f'; 340 | break; 341 | case 'n': 342 | c = '\n'; 343 | break; 344 | case 'r': 345 | c = '\r'; 346 | break; 347 | case 't': 348 | c = '\t'; 349 | break; 350 | case '\\': 351 | c = '\\'; 352 | break; 353 | case '/': 354 | c = '/'; 355 | break; 356 | case '\"': 357 | c = '\"'; 358 | break; 359 | default: // TODO: support unicode decoding 360 | return -1; 361 | } 362 | } 363 | if (out != NULL) { 364 | *out++ = c; 365 | } 366 | s++; 367 | n--; 368 | r++; 369 | } 370 | if (*s != '"') { 371 | return -1; 372 | } 373 | if (out != NULL) { 374 | *out = '\0'; 375 | } 376 | return r; 377 | } 378 | 379 | inline std::string json_parse(const std::string s, const std::string key, 380 | const int index) { 381 | const char *value; 382 | size_t value_sz; 383 | if (key == "") { 384 | json_parse_c(s.c_str(), s.length(), nullptr, index, &value, &value_sz); 385 | } else { 386 | json_parse_c(s.c_str(), s.length(), key.c_str(), key.length(), &value, 387 | &value_sz); 388 | } 389 | if (value != nullptr) { 390 | if (value[0] != '"') { 391 | return std::string(value, value_sz); 392 | } 393 | int n = json_unescape(value, value_sz, nullptr); 394 | if (n > 0) { 395 | char *decoded = new char[n + 1]; 396 | json_unescape(value, value_sz, decoded); 397 | std::string result(decoded, n); 398 | delete[] decoded; 399 | return result; 400 | } 401 | } 402 | return ""; 403 | } 404 | 405 | } // namespace webview 406 | 407 | #if defined(WEBVIEW_GTK) 408 | // 409 | // ==================================================================== 410 | // 411 | // This implementation uses webkit2gtk backend. It requires gtk+3.0 and 412 | // webkit2gtk-4.0 libraries. Proper compiler flags can be retrieved via: 413 | // 414 | // pkg-config --cflags --libs gtk+-3.0 webkit2gtk-4.0 415 | // 416 | // ==================================================================== 417 | // 418 | #include 419 | #include 420 | #include 421 | 422 | namespace webview { 423 | 424 | class gtk_webkit_engine { 425 | public: 426 | gtk_webkit_engine(bool debug, void *window) 427 | : m_window(static_cast(window)) { 428 | gtk_init_check(0, NULL); 429 | m_window = static_cast(window); 430 | if (m_window == nullptr) { 431 | m_window = gtk_window_new(GTK_WINDOW_TOPLEVEL); 432 | } 433 | g_signal_connect(G_OBJECT(m_window), "destroy", 434 | G_CALLBACK(+[](GtkWidget *, gpointer arg) { 435 | static_cast(arg)->terminate(); 436 | }), 437 | this); 438 | // Initialize webview widget 439 | m_webview = webkit_web_view_new(); 440 | WebKitUserContentManager *manager = 441 | webkit_web_view_get_user_content_manager(WEBKIT_WEB_VIEW(m_webview)); 442 | g_signal_connect(manager, "script-message-received::external", 443 | G_CALLBACK(+[](WebKitUserContentManager *, 444 | WebKitJavascriptResult *r, gpointer arg) { 445 | auto *w = static_cast(arg); 446 | #if WEBKIT_MAJOR_VERSION >= 2 && WEBKIT_MINOR_VERSION >= 22 447 | JSCValue *value = 448 | webkit_javascript_result_get_js_value(r); 449 | char *s = jsc_value_to_string(value); 450 | #else 451 | JSGlobalContextRef ctx = 452 | webkit_javascript_result_get_global_context(r); 453 | JSValueRef value = webkit_javascript_result_get_value(r); 454 | JSStringRef js = JSValueToStringCopy(ctx, value, NULL); 455 | size_t n = JSStringGetMaximumUTF8CStringSize(js); 456 | char *s = g_new(char, n); 457 | JSStringGetUTF8CString(js, s, n); 458 | JSStringRelease(js); 459 | #endif 460 | w->on_message(s); 461 | g_free(s); 462 | }), 463 | this); 464 | webkit_user_content_manager_register_script_message_handler(manager, 465 | "external"); 466 | init("window.external={invoke:function(s){window.webkit.messageHandlers." 467 | "external.postMessage(s);}}"); 468 | 469 | gtk_container_add(GTK_CONTAINER(m_window), GTK_WIDGET(m_webview)); 470 | gtk_widget_grab_focus(GTK_WIDGET(m_webview)); 471 | 472 | if (debug) { 473 | WebKitSettings *settings = 474 | webkit_web_view_get_settings(WEBKIT_WEB_VIEW(m_webview)); 475 | webkit_settings_set_enable_write_console_messages_to_stdout(settings, 476 | true); 477 | webkit_settings_set_enable_developer_extras(settings, true); 478 | } 479 | 480 | gtk_widget_show_all(m_window); 481 | } 482 | void *window() { return (void *)m_window; } 483 | void run() { gtk_main(); } 484 | void terminate() { gtk_main_quit(); } 485 | void dispatch(std::function f) { 486 | g_idle_add_full(G_PRIORITY_HIGH_IDLE, (GSourceFunc)([](void *f) -> int { 487 | (*static_cast(f))(); 488 | return G_SOURCE_REMOVE; 489 | }), 490 | new std::function(f), 491 | [](void *f) { delete static_cast(f); }); 492 | } 493 | 494 | void set_title(const std::string title) { 495 | gtk_window_set_title(GTK_WINDOW(m_window), title.c_str()); 496 | } 497 | 498 | void set_size(int width, int height, int hints) { 499 | gtk_window_set_resizable(GTK_WINDOW(m_window), hints != WEBVIEW_HINT_FIXED); 500 | if (hints == WEBVIEW_HINT_NONE) { 501 | gtk_window_resize(GTK_WINDOW(m_window), width, height); 502 | } else if (hints == WEBVIEW_HINT_FIXED) { 503 | gtk_widget_set_size_request(m_window, width, height); 504 | } else { 505 | GdkGeometry g; 506 | g.min_width = g.max_width = width; 507 | g.min_height = g.max_height = height; 508 | GdkWindowHints h = 509 | (hints == WEBVIEW_HINT_MIN ? GDK_HINT_MIN_SIZE : GDK_HINT_MAX_SIZE); 510 | // This defines either MIN_SIZE, or MAX_SIZE, but not both: 511 | gtk_window_set_geometry_hints(GTK_WINDOW(m_window), nullptr, &g, h); 512 | } 513 | } 514 | 515 | void navigate(const std::string url) { 516 | webkit_web_view_load_uri(WEBKIT_WEB_VIEW(m_webview), url.c_str()); 517 | } 518 | 519 | void init(const std::string js) { 520 | WebKitUserContentManager *manager = 521 | webkit_web_view_get_user_content_manager(WEBKIT_WEB_VIEW(m_webview)); 522 | webkit_user_content_manager_add_script( 523 | manager, webkit_user_script_new( 524 | js.c_str(), WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, 525 | WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, NULL, NULL)); 526 | } 527 | 528 | void eval(const std::string js) { 529 | webkit_web_view_run_javascript(WEBKIT_WEB_VIEW(m_webview), js.c_str(), NULL, 530 | NULL, NULL); 531 | } 532 | 533 | private: 534 | virtual void on_message(const std::string msg) = 0; 535 | GtkWidget *m_window; 536 | GtkWidget *m_webview; 537 | }; 538 | 539 | using browser_engine = gtk_webkit_engine; 540 | 541 | } // namespace webview 542 | 543 | #elif defined(WEBVIEW_COCOA) 544 | 545 | // 546 | // ==================================================================== 547 | // 548 | // This implementation uses Cocoa WKWebView backend on macOS. It is 549 | // written using ObjC runtime and uses WKWebView class as a browser runtime. 550 | // You should pass "-framework Webkit" flag to the compiler. 551 | // 552 | // ==================================================================== 553 | // 554 | 555 | #define OBJC_OLD_DISPATCH_PROTOTYPES 1 556 | #include 557 | #include 558 | 559 | #define NSBackingStoreBuffered 2 560 | 561 | #define NSWindowStyleMaskResizable 8 562 | #define NSWindowStyleMaskMiniaturizable 4 563 | #define NSWindowStyleMaskTitled 1 564 | #define NSWindowStyleMaskClosable 2 565 | 566 | #define NSApplicationActivationPolicyRegular 0 567 | 568 | #define WKUserScriptInjectionTimeAtDocumentStart 0 569 | 570 | namespace webview { 571 | 572 | // Helpers to avoid too much typing 573 | id operator"" _cls(const char *s, std::size_t) { return (id)objc_getClass(s); } 574 | SEL operator"" _sel(const char *s, std::size_t) { return sel_registerName(s); } 575 | id operator"" _str(const char *s, std::size_t) { 576 | return objc_msgSend("NSString"_cls, "stringWithUTF8String:"_sel, s); 577 | } 578 | 579 | class cocoa_wkwebview_engine { 580 | public: 581 | cocoa_wkwebview_engine(bool debug, void *window) { 582 | // Application 583 | id app = objc_msgSend("NSApplication"_cls, "sharedApplication"_sel); 584 | objc_msgSend(app, "setActivationPolicy:"_sel, 585 | NSApplicationActivationPolicyRegular); 586 | 587 | // Delegate 588 | auto cls = objc_allocateClassPair((Class) "NSResponder"_cls, "AppDelegate", 0); 589 | class_addProtocol(cls, objc_getProtocol("NSTouchBarProvider")); 590 | class_addMethod(cls, "applicationShouldTerminateAfterLastWindowClosed:"_sel, 591 | (IMP)(+[](id, SEL, id) -> BOOL { return 1; }), "c@:@"); 592 | class_addMethod(cls, "userContentController:didReceiveScriptMessage:"_sel, 593 | (IMP)(+[](id self, SEL, id, id msg) { 594 | auto w = 595 | (cocoa_wkwebview_engine *)objc_getAssociatedObject( 596 | self, "webview"); 597 | w->on_message((const char *)objc_msgSend( 598 | objc_msgSend(msg, "body"_sel), "UTF8String"_sel)); 599 | }), 600 | "v@:@@"); 601 | objc_registerClassPair(cls); 602 | 603 | auto delegate = objc_msgSend((id)cls, "new"_sel); 604 | objc_setAssociatedObject(delegate, "webview", (id)this, 605 | OBJC_ASSOCIATION_ASSIGN); 606 | objc_msgSend(app, sel_registerName("setDelegate:"), delegate); 607 | 608 | // Main window 609 | if (window == nullptr) { 610 | m_window = objc_msgSend("NSWindow"_cls, "alloc"_sel); 611 | m_window = objc_msgSend( 612 | m_window, "initWithContentRect:styleMask:backing:defer:"_sel, 613 | CGRectMake(0, 0, 0, 0), 0, NSBackingStoreBuffered, 0); 614 | } else { 615 | m_window = (id)window; 616 | } 617 | 618 | // Webview 619 | auto config = objc_msgSend("WKWebViewConfiguration"_cls, "new"_sel); 620 | m_manager = objc_msgSend(config, "userContentController"_sel); 621 | m_webview = objc_msgSend("WKWebView"_cls, "alloc"_sel); 622 | if (debug) { 623 | objc_msgSend(objc_msgSend(config, "preferences"_sel), 624 | "setValue:forKey:"_sel, 625 | objc_msgSend("NSNumber"_cls, "numberWithBool:"_sel, 1), 626 | "developerExtrasEnabled"_str); 627 | } 628 | objc_msgSend(m_webview, "initWithFrame:configuration:"_sel, 629 | CGRectMake(0, 0, 0, 0), config); 630 | objc_msgSend(m_manager, "addScriptMessageHandler:name:"_sel, delegate, 631 | "external"_str); 632 | init(R"script( 633 | window.external = { 634 | invoke: function(s) { 635 | window.webkit.messageHandlers.external.postMessage(s); 636 | }, 637 | }; 638 | )script"); 639 | objc_msgSend(m_window, "setContentView:"_sel, m_webview); 640 | objc_msgSend(m_window, "makeKeyAndOrderFront:"_sel, nullptr); 641 | } 642 | ~cocoa_wkwebview_engine() { close(); } 643 | void *window() { return (void *)m_window; } 644 | void terminate() { 645 | close(); 646 | objc_msgSend("NSApp"_cls, "terminate:"_sel, nullptr); 647 | } 648 | void run() { 649 | id app = objc_msgSend("NSApplication"_cls, "sharedApplication"_sel); 650 | dispatch([&]() { objc_msgSend(app, "activateIgnoringOtherApps:"_sel, 1); }); 651 | objc_msgSend(app, "run"_sel); 652 | } 653 | void dispatch(std::function f) { 654 | dispatch_async_f(dispatch_get_main_queue(), new dispatch_fn_t(f), 655 | (dispatch_function_t)([](void *arg) { 656 | auto f = static_cast(arg); 657 | (*f)(); 658 | delete f; 659 | })); 660 | } 661 | void set_title(const std::string title) { 662 | objc_msgSend(m_window, "setTitle:"_sel, 663 | objc_msgSend("NSString"_cls, "stringWithUTF8String:"_sel, 664 | title.c_str())); 665 | } 666 | void set_size(int width, int height, int hints) { 667 | auto style = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | 668 | NSWindowStyleMaskMiniaturizable; 669 | if (hints != WEBVIEW_HINT_FIXED) { 670 | style = style | NSWindowStyleMaskResizable; 671 | } 672 | objc_msgSend(m_window, "setStyleMask:"_sel, style); 673 | 674 | struct { 675 | CGFloat width; 676 | CGFloat height; 677 | } size; 678 | if (hints == WEBVIEW_HINT_MIN) { 679 | size.width = width; 680 | size.height = height; 681 | objc_msgSend(m_window, "setContentMinSize:"_sel, size); 682 | } else if (hints == WEBVIEW_HINT_MAX) { 683 | size.width = width; 684 | size.height = height; 685 | objc_msgSend(m_window, "setContentMaxSize:"_sel, size); 686 | } else { 687 | objc_msgSend(m_window, "setFrame:display:animate:"_sel, 688 | CGRectMake(0, 0, width, height), 1, 0); 689 | } 690 | } 691 | void navigate(const std::string url) { 692 | auto nsurl = objc_msgSend( 693 | "NSURL"_cls, "URLWithString:"_sel, 694 | objc_msgSend("NSString"_cls, "stringWithUTF8String:"_sel, url.c_str())); 695 | objc_msgSend( 696 | m_webview, "loadRequest:"_sel, 697 | objc_msgSend("NSURLRequest"_cls, "requestWithURL:"_sel, nsurl)); 698 | } 699 | void init(const std::string js) { 700 | objc_msgSend( 701 | m_manager, "addUserScript:"_sel, 702 | objc_msgSend(objc_msgSend("WKUserScript"_cls, "alloc"_sel), 703 | "initWithSource:injectionTime:forMainFrameOnly:"_sel, 704 | objc_msgSend("NSString"_cls, "stringWithUTF8String:"_sel, 705 | js.c_str()), 706 | WKUserScriptInjectionTimeAtDocumentStart, 1)); 707 | } 708 | void eval(const std::string js) { 709 | objc_msgSend( 710 | m_webview, "evaluateJavaScript:completionHandler:"_sel, 711 | objc_msgSend("NSString"_cls, "stringWithUTF8String:"_sel, js.c_str()), 712 | nullptr); 713 | } 714 | 715 | private: 716 | virtual void on_message(const std::string msg) = 0; 717 | void close() { objc_msgSend(m_window, "close"_sel); } 718 | id m_window; 719 | id m_webview; 720 | id m_manager; 721 | }; 722 | 723 | using browser_engine = cocoa_wkwebview_engine; 724 | 725 | } // namespace webview 726 | 727 | #elif defined(WEBVIEW_EDGE) 728 | 729 | // 730 | // ==================================================================== 731 | // 732 | // This implementation uses Win32 API to create a native window. It can 733 | // use either EdgeHTML or Edge/Chromium backend as a browser engine. 734 | // 735 | // ==================================================================== 736 | // 737 | 738 | #define WIN32_LEAN_AND_MEAN 739 | #include 740 | 741 | #pragma comment(lib, "user32.lib") 742 | 743 | // EdgeHTML headers and libs 744 | #include 745 | #include 746 | #include 747 | #pragma comment(lib, "windowsapp") 748 | 749 | // Edge/Chromium headers and libs 750 | #include "webview2.h" 751 | #pragma comment(lib, "ole32.lib") 752 | #pragma comment(lib, "oleaut32.lib") 753 | 754 | namespace webview { 755 | 756 | using msg_cb_t = std::function; 757 | 758 | // Common interface for EdgeHTML and Edge/Chromium 759 | class browser { 760 | public: 761 | virtual ~browser() = default; 762 | virtual bool embed(HWND, bool, msg_cb_t) = 0; 763 | virtual void navigate(const std::string url) = 0; 764 | virtual void eval(const std::string js) = 0; 765 | virtual void init(const std::string js) = 0; 766 | virtual void resize(HWND) = 0; 767 | }; 768 | 769 | // 770 | // EdgeHTML browser engine 771 | // 772 | using namespace winrt; 773 | using namespace Windows::Foundation; 774 | using namespace Windows::Web::UI; 775 | using namespace Windows::Web::UI::Interop; 776 | 777 | class edge_html : public browser { 778 | public: 779 | bool embed(HWND wnd, bool debug, msg_cb_t cb) override { 780 | init_apartment(winrt::apartment_type::single_threaded); 781 | auto process = WebViewControlProcess(); 782 | auto op = process.CreateWebViewControlAsync(reinterpret_cast(wnd), 783 | Rect()); 784 | if (op.Status() != AsyncStatus::Completed) { 785 | handle h(CreateEvent(nullptr, false, false, nullptr)); 786 | op.Completed([h = h.get()](auto, auto) { SetEvent(h); }); 787 | HANDLE hs[] = {h.get()}; 788 | DWORD i; 789 | CoWaitForMultipleHandles(COWAIT_DISPATCH_WINDOW_MESSAGES | 790 | COWAIT_DISPATCH_CALLS | 791 | COWAIT_INPUTAVAILABLE, 792 | INFINITE, 1, hs, &i); 793 | } 794 | m_webview = op.GetResults(); 795 | m_webview.Settings().IsScriptNotifyAllowed(true); 796 | m_webview.IsVisible(true); 797 | m_webview.ScriptNotify([=](auto const &sender, auto const &args) { 798 | std::string s = winrt::to_string(args.Value()); 799 | cb(s.c_str()); 800 | }); 801 | m_webview.NavigationStarting([=](auto const &sender, auto const &args) { 802 | m_webview.AddInitializeScript(winrt::to_hstring(init_js)); 803 | }); 804 | init("window.external.invoke = s => window.external.notify(s)"); 805 | return true; 806 | } 807 | 808 | void navigate(const std::string url) override { 809 | std::string html = html_from_uri(url); 810 | if (html != "") { 811 | m_webview.NavigateToString(winrt::to_hstring(html)); 812 | } else { 813 | Uri uri(winrt::to_hstring(url)); 814 | m_webview.Navigate(uri); 815 | } 816 | } 817 | 818 | void init(const std::string js) override { 819 | init_js = init_js + "(function(){" + js + "})();"; 820 | } 821 | 822 | void eval(const std::string js) override { 823 | m_webview.InvokeScriptAsync( 824 | L"eval", single_threaded_vector({winrt::to_hstring(js)})); 825 | } 826 | 827 | void resize(HWND wnd) override { 828 | if (m_webview == nullptr) { 829 | return; 830 | } 831 | RECT r; 832 | GetClientRect(wnd, &r); 833 | Rect bounds(r.left, r.top, r.right - r.left, r.bottom - r.top); 834 | m_webview.Bounds(bounds); 835 | } 836 | 837 | private: 838 | WebViewControl m_webview = nullptr; 839 | std::string init_js = ""; 840 | }; 841 | 842 | // 843 | // Edge/Chromium browser engine 844 | // 845 | class edge_chromium : public browser { 846 | public: 847 | bool embed(HWND wnd, bool debug, msg_cb_t cb) override { 848 | CoInitializeEx(nullptr, 0); 849 | std::atomic_flag flag = ATOMIC_FLAG_INIT; 850 | flag.test_and_set(); 851 | HRESULT res = CreateWebView2EnvironmentWithDetails( 852 | nullptr, nullptr, nullptr, 853 | new webview2_com_handler(wnd, [&](IWebView2WebView *webview) { 854 | m_webview = webview; 855 | flag.clear(); 856 | })); 857 | if (res != S_OK) { 858 | CoUninitialize(); 859 | return false; 860 | } 861 | MSG msg = {}; 862 | while (flag.test_and_set() && GetMessage(&msg, NULL, 0, 0)) { 863 | TranslateMessage(&msg); 864 | DispatchMessage(&msg); 865 | } 866 | init("window.external={invoke:s=>window.chrome.webview.postMessage(s)}"); 867 | return true; 868 | } 869 | 870 | void resize(HWND wnd) override { 871 | if (m_webview == nullptr) { 872 | return; 873 | } 874 | RECT bounds; 875 | GetClientRect(wnd, &bounds); 876 | m_webview->put_Bounds(bounds); 877 | } 878 | 879 | void navigate(const std::string url) override { 880 | auto wurl = to_lpwstr(url); 881 | m_webview->Navigate(wurl); 882 | delete[] wurl; 883 | } 884 | 885 | void init(const std::string js) override { 886 | LPCWSTR wjs = to_lpwstr(js); 887 | m_webview->AddScriptToExecuteOnDocumentCreated(wjs, nullptr); 888 | delete[] wjs; 889 | } 890 | 891 | void eval(const std::string js) override { 892 | LPCWSTR wjs = to_lpwstr(js); 893 | m_webview->ExecuteScript(wjs, nullptr); 894 | delete[] wjs; 895 | } 896 | 897 | private: 898 | LPWSTR to_lpwstr(const std::string s) { 899 | int n = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), -1, NULL, 0); 900 | wchar_t *ws = new wchar_t[n]; 901 | MultiByteToWideChar(CP_UTF8, 0, s.c_str(), -1, ws, n); 902 | return ws; 903 | } 904 | 905 | IWebView2WebView *m_webview = nullptr; 906 | 907 | class webview2_com_handler 908 | : public IWebView2CreateWebView2EnvironmentCompletedHandler, 909 | public IWebView2CreateWebViewCompletedHandler { 910 | using webview2_com_handler_cb_t = std::function; 911 | 912 | public: 913 | webview2_com_handler(HWND hwnd, webview2_com_handler_cb_t cb) 914 | : m_window(hwnd), m_cb(cb) {} 915 | ULONG STDMETHODCALLTYPE AddRef() { return 1; } 916 | ULONG STDMETHODCALLTYPE Release() { return 1; } 917 | HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID *ppv) { 918 | return S_OK; 919 | } 920 | HRESULT STDMETHODCALLTYPE Invoke(HRESULT res, IWebView2Environment *env) { 921 | env->CreateWebView(m_window, this); 922 | return S_OK; 923 | } 924 | HRESULT STDMETHODCALLTYPE Invoke(HRESULT res, IWebView2WebView *webview) { 925 | webview->AddRef(); 926 | m_cb(webview); 927 | return S_OK; 928 | } 929 | 930 | private: 931 | HWND m_window; 932 | webview2_com_handler_cb_t m_cb; 933 | }; 934 | }; 935 | 936 | class win32_edge_engine { 937 | public: 938 | win32_edge_engine(bool debug, void *window) { 939 | if (window == nullptr) { 940 | WNDCLASSEX wc; 941 | ZeroMemory(&wc, sizeof(WNDCLASSEX)); 942 | wc.cbSize = sizeof(WNDCLASSEX); 943 | wc.hInstance = GetModuleHandle(nullptr); 944 | wc.lpszClassName = "webview"; 945 | wc.lpfnWndProc = 946 | (WNDPROC)(+[](HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) -> int { 947 | auto w = (win32_edge_engine *)GetWindowLongPtr(hwnd, GWLP_USERDATA); 948 | switch (msg) { 949 | case WM_SIZE: 950 | w->m_browser->resize(hwnd); 951 | break; 952 | case WM_CLOSE: 953 | DestroyWindow(hwnd); 954 | break; 955 | case WM_DESTROY: 956 | w->terminate(); 957 | break; 958 | case WM_GETMINMAXINFO: { 959 | auto lpmmi = (LPMINMAXINFO)lp; 960 | if (w == nullptr) { 961 | return 0; 962 | } 963 | if (w->m_maxsz.x > 0 && w->m_maxsz.y > 0) { 964 | lpmmi->ptMaxSize = w->m_maxsz; 965 | lpmmi->ptMaxTrackSize = w->m_maxsz; 966 | } 967 | if (w->m_minsz.x > 0 && w->m_minsz.y > 0) { 968 | lpmmi->ptMinTrackSize = w->m_minsz; 969 | } 970 | } break; 971 | default: 972 | return DefWindowProc(hwnd, msg, wp, lp); 973 | } 974 | return 0; 975 | }); 976 | RegisterClassEx(&wc); 977 | m_window = CreateWindow("webview", "", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 978 | CW_USEDEFAULT, 640, 480, nullptr, nullptr, 979 | GetModuleHandle(nullptr), nullptr); 980 | SetWindowLongPtr(m_window, GWLP_USERDATA, (LONG_PTR)this); 981 | } else { 982 | m_window = *(static_cast(window)); 983 | } 984 | 985 | ShowWindow(m_window, SW_SHOW); 986 | UpdateWindow(m_window); 987 | SetFocus(m_window); 988 | 989 | auto cb = 990 | std::bind(&win32_edge_engine::on_message, this, std::placeholders::_1); 991 | 992 | if (!m_browser->embed(m_window, debug, cb)) { 993 | m_browser = std::make_unique(); 994 | m_browser->embed(m_window, debug, cb); 995 | } 996 | 997 | m_browser->resize(m_window); 998 | } 999 | 1000 | void run() { 1001 | MSG msg; 1002 | BOOL res; 1003 | while ((res = GetMessage(&msg, nullptr, 0, 0)) != -1) { 1004 | if (msg.hwnd) { 1005 | TranslateMessage(&msg); 1006 | DispatchMessage(&msg); 1007 | continue; 1008 | } 1009 | if (msg.message == WM_APP) { 1010 | auto f = (dispatch_fn_t *)(msg.lParam); 1011 | (*f)(); 1012 | delete f; 1013 | } else if (msg.message == WM_QUIT) { 1014 | return; 1015 | } 1016 | } 1017 | } 1018 | void *window() { return (void *)m_window; } 1019 | void terminate() { PostQuitMessage(0); } 1020 | void dispatch(dispatch_fn_t f) { 1021 | PostThreadMessage(m_main_thread, WM_APP, 0, (LPARAM) new dispatch_fn_t(f)); 1022 | } 1023 | 1024 | void set_title(const std::string title) { 1025 | SetWindowText(m_window, title.c_str()); 1026 | } 1027 | 1028 | void set_size(int width, int height, int hints) { 1029 | auto style = GetWindowLong(m_window, GWL_STYLE); 1030 | if (hints == WEBVIEW_HINT_FIXED) { 1031 | style &= ~(WS_THICKFRAME | WS_MAXIMIZEBOX); 1032 | } else { 1033 | style |= (WS_THICKFRAME | WS_MAXIMIZEBOX); 1034 | } 1035 | SetWindowLong(m_window, GWL_STYLE, style); 1036 | 1037 | if (hints == WEBVIEW_HINT_MAX) { 1038 | m_maxsz.x = width; 1039 | m_maxsz.y = height; 1040 | } else if (hints == WEBVIEW_HINT_MIN) { 1041 | m_minsz.x = width; 1042 | m_minsz.y = height; 1043 | } else { 1044 | RECT r; 1045 | r.left = r.top = 0; 1046 | r.right = width; 1047 | r.bottom = height; 1048 | AdjustWindowRect(&r, WS_OVERLAPPEDWINDOW, 0); 1049 | SetWindowPos( 1050 | m_window, NULL, r.left, r.top, r.right - r.left, r.bottom - r.top, 1051 | SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOMOVE | SWP_FRAMECHANGED); 1052 | m_browser->resize(m_window); 1053 | } 1054 | } 1055 | 1056 | void navigate(const std::string url) { m_browser->navigate(url); } 1057 | void eval(const std::string js) { m_browser->eval(js); } 1058 | void init(const std::string js) { m_browser->init(js); } 1059 | 1060 | private: 1061 | virtual void on_message(const std::string msg) = 0; 1062 | 1063 | HWND m_window; 1064 | POINT m_minsz = POINT { 0, 0 }; 1065 | POINT m_maxsz = POINT { 0, 0 }; 1066 | DWORD m_main_thread = GetCurrentThreadId(); 1067 | std::unique_ptr m_browser = 1068 | std::make_unique(); 1069 | }; 1070 | 1071 | using browser_engine = win32_edge_engine; 1072 | } // namespace webview 1073 | 1074 | #endif /* WEBVIEW_GTK, WEBVIEW_COCOA, WEBVIEW_EDGE */ 1075 | 1076 | namespace webview { 1077 | 1078 | class webview : public browser_engine { 1079 | public: 1080 | webview(bool debug = false, void *wnd = nullptr) 1081 | : browser_engine(debug, wnd) {} 1082 | 1083 | void navigate(const std::string url) { 1084 | if (url == "") { 1085 | browser_engine::navigate("data:text/html," + 1086 | url_encode("Hello")); 1087 | return; 1088 | } 1089 | std::string html = html_from_uri(url); 1090 | if (html != "") { 1091 | browser_engine::navigate("data:text/html," + url_encode(html)); 1092 | } else { 1093 | browser_engine::navigate(url); 1094 | } 1095 | } 1096 | 1097 | using binding_t = std::function; 1098 | using binding_ctx_t = std::pair; 1099 | 1100 | using sync_binding_t = std::function; 1101 | using sync_binding_ctx_t = std::pair; 1102 | 1103 | void bind(const std::string name, sync_binding_t fn) { 1104 | bind(name, 1105 | [](std::string seq, std::string req, void *arg) { 1106 | auto pair = static_cast(arg); 1107 | pair->first->resolve(seq, 0, pair->second(req)); 1108 | }, 1109 | new sync_binding_ctx_t(this, fn)); 1110 | } 1111 | 1112 | void bind(const std::string name, binding_t f, void *arg) { 1113 | auto js = "(function() { var name = '" + name + "';" + R"( 1114 | var RPC = window._rpc = (window._rpc || {nextSeq: 1}); 1115 | window[name] = function() { 1116 | var seq = RPC.nextSeq++; 1117 | var promise = new Promise(function(resolve, reject) { 1118 | RPC[seq] = { 1119 | resolve: resolve, 1120 | reject: reject, 1121 | }; 1122 | }); 1123 | window.external.invoke(JSON.stringify({ 1124 | id: seq, 1125 | method: name, 1126 | params: Array.prototype.slice.call(arguments), 1127 | })); 1128 | return promise; 1129 | } 1130 | })())"; 1131 | init(js); 1132 | bindings[name] = new binding_ctx_t(new binding_t(f), arg); 1133 | } 1134 | 1135 | void resolve(const std::string seq, int status, const std::string result) { 1136 | dispatch([=]() { 1137 | if (status == 0) { 1138 | eval("window._rpc[" + seq + "].resolve(" + result + "); window._rpc[" + 1139 | seq + "] = undefined"); 1140 | } else { 1141 | eval("window._rpc[" + seq + "].reject(" + result + "); window._rpc[" + 1142 | seq + "] = undefined"); 1143 | } 1144 | }); 1145 | } 1146 | 1147 | private: 1148 | void on_message(const std::string msg) { 1149 | auto seq = json_parse(msg, "id", 0); 1150 | auto name = json_parse(msg, "method", 0); 1151 | auto args = json_parse(msg, "params", 0); 1152 | if (bindings.find(name) == bindings.end()) { 1153 | return; 1154 | } 1155 | auto fn = bindings[name]; 1156 | (*fn->first)(seq, args, fn->second); 1157 | } 1158 | std::map bindings; 1159 | }; 1160 | } // namespace webview 1161 | 1162 | WEBVIEW_API webview_t webview_create(int debug, void *wnd) { 1163 | return new webview::webview(debug, wnd); 1164 | } 1165 | 1166 | WEBVIEW_API void webview_destroy(webview_t w) { 1167 | delete static_cast(w); 1168 | } 1169 | 1170 | WEBVIEW_API void webview_run(webview_t w) { 1171 | static_cast(w)->run(); 1172 | } 1173 | 1174 | WEBVIEW_API void webview_terminate(webview_t w) { 1175 | static_cast(w)->terminate(); 1176 | } 1177 | 1178 | WEBVIEW_API void webview_dispatch(webview_t w, void (*fn)(webview_t, void *), 1179 | void *arg) { 1180 | static_cast(w)->dispatch([=]() { fn(w, arg); }); 1181 | } 1182 | 1183 | WEBVIEW_API void *webview_get_window(webview_t w) { 1184 | return static_cast(w)->window(); 1185 | } 1186 | 1187 | WEBVIEW_API void webview_set_title(webview_t w, const char *title) { 1188 | static_cast(w)->set_title(title); 1189 | } 1190 | 1191 | WEBVIEW_API void webview_set_size(webview_t w, int width, int height, 1192 | int hints) { 1193 | static_cast(w)->set_size(width, height, hints); 1194 | } 1195 | 1196 | WEBVIEW_API void webview_navigate(webview_t w, const char *url) { 1197 | static_cast(w)->navigate(url); 1198 | } 1199 | 1200 | WEBVIEW_API void webview_init(webview_t w, const char *js) { 1201 | static_cast(w)->init(js); 1202 | } 1203 | 1204 | WEBVIEW_API void webview_eval(webview_t w, const char *js) { 1205 | static_cast(w)->eval(js); 1206 | } 1207 | 1208 | WEBVIEW_API void webview_bind(webview_t w, const char *name, 1209 | void (*fn)(const char *seq, const char *req, 1210 | void *arg), 1211 | void *arg) { 1212 | static_cast(w)->bind( 1213 | name, 1214 | [=](std::string seq, std::string req, void *arg) { 1215 | fn(seq.c_str(), req.c_str(), arg); 1216 | }, 1217 | arg); 1218 | } 1219 | 1220 | WEBVIEW_API void webview_return(webview_t w, const char *seq, int status, 1221 | const char *result) { 1222 | static_cast(w)->resolve(seq, status, result); 1223 | } 1224 | 1225 | #endif /* WEBVIEW_HEADER */ 1226 | 1227 | #endif /* WEBVIEW_H */ 1228 | -------------------------------------------------------------------------------- /src/webview/webview.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub usingnamespace @cImport({ 4 | @cDefine("WEBVIEW_HEADER", ""); // tells webview.h to be header only 5 | @cInclude("webview.h"); 6 | }); 7 | -------------------------------------------------------------------------------- /test.zig: -------------------------------------------------------------------------------- 1 | comptime { 2 | _ = @import("src/unicode/unicode.zig"); 3 | _ = @import("src/md/lexer.zig"); 4 | _ = @import("test/section_tabs.zig"); 5 | } 6 | -------------------------------------------------------------------------------- /test/docs/test_basics.md: -------------------------------------------------------------------------------- 1 | # Hello World 2 | 3 | **Bold** text. 4 | 5 | ## Hello again 6 | 7 | *Itaclics* text. 8 | 9 | ``` 10 | A code block. 11 | ``` 12 | 13 | ## A list 14 | 15 | 1. One 16 | 17 | List item text. 18 | 19 | 1. Two 20 | 21 | 1. Three 22 | 23 | -------------------------------------------------------------------------------- /test/docs/test_headings.md: -------------------------------------------------------------------------------- 1 | # One 2 | ## Two 3 | ### Three 4 | #### Four 5 | ##### Five 6 | ###### Six 7 | -------------------------------------------------------------------------------- /test/expect/01-section-tabs/testl_001.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ID": "Whitespace", 4 | "startOffset": 0, 5 | "endOffset": 0, 6 | "string": "\t", 7 | "lineNumber": 1, 8 | "column": 1 9 | }, 10 | { 11 | "ID": "Text", 12 | "startOffset": 1, 13 | "endOffset": 3, 14 | "string": "foo", 15 | "lineNumber": 1, 16 | "column": 2 17 | }, 18 | { 19 | "ID": "Whitespace", 20 | "startOffset": 4, 21 | "endOffset": 4, 22 | "string": "\t", 23 | "lineNumber": 1, 24 | "column": 5 25 | }, 26 | { 27 | "ID": "Text", 28 | "startOffset": 5, 29 | "endOffset": 7, 30 | "string": "baz", 31 | "lineNumber": 1, 32 | "column": 6 33 | }, 34 | { 35 | "ID": "Whitespace", 36 | "startOffset": 8, 37 | "endOffset": 9, 38 | "string": "\t\t", 39 | "lineNumber": 1, 40 | "column": 9 41 | }, 42 | { 43 | "ID": "Text", 44 | "startOffset": 10, 45 | "endOffset": 12, 46 | "string": "bim", 47 | "lineNumber": 1, 48 | "column": 11 49 | }, 50 | { 51 | "ID": "Whitespace", 52 | "startOffset": 13, 53 | "endOffset": 13, 54 | "string": "\n", 55 | "lineNumber": 1, 56 | "column": 14 57 | }, 58 | { 59 | "ID": "EOF", 60 | "startOffset": 14, 61 | "endOffset": 14, 62 | "string": "", 63 | "lineNumber": 1, 64 | "column": 14 65 | } 66 | ] 67 | -------------------------------------------------------------------------------- /test/expect/01-section-tabs/testl_002.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ID": "Whitespace", 4 | "startOffset": 0, 5 | "endOffset": 2, 6 | "string": " \t", 7 | "lineNumber": 1, 8 | "column": 1 9 | }, 10 | { 11 | "ID": "Text", 12 | "startOffset": 3, 13 | "endOffset": 5, 14 | "string": "foo", 15 | "lineNumber": 1, 16 | "column": 4 17 | }, 18 | { 19 | "ID": "Whitespace", 20 | "startOffset": 6, 21 | "endOffset": 6, 22 | "string": "\t", 23 | "lineNumber": 1, 24 | "column": 7 25 | }, 26 | { 27 | "ID": "Text", 28 | "startOffset": 7, 29 | "endOffset": 9, 30 | "string": "baz", 31 | "lineNumber": 1, 32 | "column": 8 33 | }, 34 | { 35 | "ID": "Whitespace", 36 | "startOffset": 10, 37 | "endOffset": 11, 38 | "string": "\t\t", 39 | "lineNumber": 1, 40 | "column": 11 41 | }, 42 | { 43 | "ID": "Text", 44 | "startOffset": 12, 45 | "endOffset": 14, 46 | "string": "bim", 47 | "lineNumber": 1, 48 | "column": 13 49 | }, 50 | { 51 | "ID": "Whitespace", 52 | "startOffset": 15, 53 | "endOffset": 15, 54 | "string": "\n", 55 | "lineNumber": 1, 56 | "column": 16 57 | }, 58 | { 59 | "ID": "EOF", 60 | "startOffset": 16, 61 | "endOffset": 16, 62 | "string": "", 63 | "lineNumber": 1, 64 | "column": 16 65 | } 66 | ] 67 | -------------------------------------------------------------------------------- /test/expect/01-section-tabs/testl_003.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ID": "Whitespace", 4 | "startOffset": 0, 5 | "endOffset": 3, 6 | "string": " ", 7 | "lineNumber": 1, 8 | "column": 1 9 | }, 10 | { 11 | "ID": "Text", 12 | "startOffset": 4, 13 | "endOffset": 4, 14 | "string": "a", 15 | "lineNumber": 1, 16 | "column": 5 17 | }, 18 | { 19 | "ID": "Whitespace", 20 | "startOffset": 5, 21 | "endOffset": 5, 22 | "string": "\t", 23 | "lineNumber": 1, 24 | "column": 6 25 | }, 26 | { 27 | "ID": "Text", 28 | "startOffset": 6, 29 | "endOffset": 6, 30 | "string": "a", 31 | "lineNumber": 1, 32 | "column": 7 33 | }, 34 | { 35 | "ID": "Whitespace", 36 | "startOffset": 7, 37 | "endOffset": 7, 38 | "string": "\n", 39 | "lineNumber": 1, 40 | "column": 8 41 | }, 42 | { 43 | "ID": "Whitespace", 44 | "startOffset": 8, 45 | "endOffset": 11, 46 | "string": " ", 47 | "lineNumber": 2, 48 | "column": 1 49 | }, 50 | { 51 | "ID": "Text", 52 | "startOffset": 12, 53 | "endOffset": 12, 54 | "string": "ὐ", 55 | "lineNumber": 2, 56 | "column": 5 57 | }, 58 | { 59 | "ID": "Whitespace", 60 | "startOffset": 13, 61 | "endOffset": 13, 62 | "string": "\t", 63 | "lineNumber": 2, 64 | "column": 6 65 | }, 66 | { 67 | "ID": "Text", 68 | "startOffset": 14, 69 | "endOffset": 14, 70 | "string": "a", 71 | "lineNumber": 2, 72 | "column": 7 73 | }, 74 | { 75 | "ID": "Whitespace", 76 | "startOffset": 15, 77 | "endOffset": 15, 78 | "string": "\n", 79 | "lineNumber": 2, 80 | "column": 8 81 | }, 82 | { 83 | "ID": "EOF", 84 | "startOffset": 16, 85 | "endOffset": 16, 86 | "string": "", 87 | "lineNumber": 2, 88 | "column": 8 89 | } 90 | ] 91 | -------------------------------------------------------------------------------- /test/expect/01-section-tabs/testp_001.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ID": "CodeBlock", 4 | "Value": "\t", 5 | "PositionStart": { 6 | "Line": 1, 7 | "Column": 1, 8 | "Offset": 0 9 | }, 10 | "PositionEnd": { 11 | "Line": 1, 12 | "Column": 14, 13 | "Offset": 13 14 | }, 15 | "Children": [{ 16 | "ID": "Text", 17 | "Value": "foo\tbaz\t\tbim\n", 18 | "PositionStart": { 19 | "Line": 1, 20 | "Column": 2, 21 | "Offset": 1 22 | }, 23 | "PositionEnd": { 24 | "Line": 1, 25 | "Column": 14, 26 | "Offset": 13 27 | }, 28 | "Children": [], 29 | "Level": 0 30 | }], 31 | "Level": 0 32 | } 33 | ] 34 | -------------------------------------------------------------------------------- /test/expect/01-section-tabs/testp_002.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ID": "CodeBlock", 4 | "Value": " \t", 5 | "PositionStart": { 6 | "Line": 1, 7 | "Column": 1, 8 | "Offset": 0 9 | }, 10 | "PositionEnd": { 11 | "Line": 1, 12 | "Column": 16, 13 | "Offset": 15 14 | }, 15 | "Children": [{ 16 | "ID": "Text", 17 | "Value": "foo\tbaz\t\tbim\n", 18 | "PositionStart": { 19 | "Line": 1, 20 | "Column": 4, 21 | "Offset": 3 22 | }, 23 | "PositionEnd": { 24 | "Line": 1, 25 | "Column": 16, 26 | "Offset": 15 27 | }, 28 | "Children": [], 29 | "Level": 0 30 | }], 31 | "Level": 0 32 | } 33 | ] 34 | -------------------------------------------------------------------------------- /test/expect/01-section-tabs/testp_003.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ID": "CodeBlock", 4 | "Value": " ", 5 | "PositionStart": { 6 | "Line": 1, 7 | "Column": 1, 8 | "Offset": 0 9 | }, 10 | "PositionEnd": { 11 | "Line": 2, 12 | "Column": 8, 13 | "Offset": 15 14 | }, 15 | "Children": [{ 16 | "ID": "Text", 17 | "Value": "a\ta\nὐ\ta\n", 18 | "PositionStart": { 19 | "Line": 1, 20 | "Column": 5, 21 | "Offset": 4 22 | }, 23 | "PositionEnd": { 24 | "Line": 2, 25 | "Column": 8, 26 | "Offset": 15 27 | }, 28 | "Children": [], 29 | "Level": 0 30 | }], 31 | "Level": 0 32 | } 33 | ] 34 | -------------------------------------------------------------------------------- /test/expect/03-section-atx-headings/testl_032.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ID": "AtxHeader", 4 | "startOffset": 0, 5 | "endOffset": 0, 6 | "string": "#", 7 | "lineNumber": 1, 8 | "column": 1 9 | }, 10 | { 11 | "ID": "Whitespace", 12 | "startOffset": 1, 13 | "endOffset": 1, 14 | "string": " ", 15 | "lineNumber": 1, 16 | "column": 2 17 | }, 18 | { 19 | "ID": "Text", 20 | "startOffset": 2, 21 | "endOffset": 4, 22 | "string": "foo", 23 | "lineNumber": 1, 24 | "column": 3 25 | }, 26 | { 27 | "ID": "Whitespace", 28 | "startOffset": 5, 29 | "endOffset": 5, 30 | "string": "\n", 31 | "lineNumber": 1, 32 | "column": 6 33 | }, 34 | { 35 | "ID": "AtxHeader", 36 | "startOffset": 6, 37 | "endOffset": 7, 38 | "string": "##", 39 | "lineNumber": 2, 40 | "column": 1 41 | }, 42 | { 43 | "ID": "Whitespace", 44 | "startOffset": 8, 45 | "endOffset": 8, 46 | "string": " ", 47 | "lineNumber": 2, 48 | "column": 3 49 | }, 50 | { 51 | "ID": "Text", 52 | "startOffset": 9, 53 | "endOffset": 11, 54 | "string": "foo", 55 | "lineNumber": 2, 56 | "column": 4 57 | }, 58 | { 59 | "ID": "Whitespace", 60 | "startOffset": 12, 61 | "endOffset": 12, 62 | "string": "\n", 63 | "lineNumber": 2, 64 | "column": 7 65 | }, 66 | { 67 | "ID": "AtxHeader", 68 | "startOffset": 13, 69 | "endOffset": 15, 70 | "string": "###", 71 | "lineNumber": 3, 72 | "column": 1 73 | }, 74 | { 75 | "ID": "Whitespace", 76 | "startOffset": 16, 77 | "endOffset": 16, 78 | "string": " ", 79 | "lineNumber": 3, 80 | "column": 4 81 | }, 82 | { 83 | "ID": "Text", 84 | "startOffset": 17, 85 | "endOffset": 19, 86 | "string": "foo", 87 | "lineNumber": 3, 88 | "column": 5 89 | }, 90 | { 91 | "ID": "Whitespace", 92 | "startOffset": 20, 93 | "endOffset": 20, 94 | "string": "\n", 95 | "lineNumber": 3, 96 | "column": 8 97 | }, 98 | { 99 | "ID": "AtxHeader", 100 | "startOffset": 21, 101 | "endOffset": 24, 102 | "string": "####", 103 | "lineNumber": 4, 104 | "column": 1 105 | }, 106 | { 107 | "ID": "Whitespace", 108 | "startOffset": 25, 109 | "endOffset": 25, 110 | "string": " ", 111 | "lineNumber": 4, 112 | "column": 5 113 | }, 114 | { 115 | "ID": "Text", 116 | "startOffset": 26, 117 | "endOffset": 28, 118 | "string": "foo", 119 | "lineNumber": 4, 120 | "column": 6 121 | }, 122 | { 123 | "ID": "Whitespace", 124 | "startOffset": 29, 125 | "endOffset": 29, 126 | "string": "\n", 127 | "lineNumber": 4, 128 | "column": 9 129 | }, 130 | { 131 | "ID": "AtxHeader", 132 | "startOffset": 30, 133 | "endOffset": 34, 134 | "string": "#####", 135 | "lineNumber": 5, 136 | "column": 1 137 | }, 138 | { 139 | "ID": "Whitespace", 140 | "startOffset": 35, 141 | "endOffset": 35, 142 | "string": " ", 143 | "lineNumber": 5, 144 | "column": 6 145 | }, 146 | { 147 | "ID": "Text", 148 | "startOffset": 36, 149 | "endOffset": 38, 150 | "string": "foo", 151 | "lineNumber": 5, 152 | "column": 7 153 | }, 154 | { 155 | "ID": "Whitespace", 156 | "startOffset": 39, 157 | "endOffset": 39, 158 | "string": "\n", 159 | "lineNumber": 5, 160 | "column": 10 161 | }, 162 | { 163 | "ID": "AtxHeader", 164 | "startOffset": 40, 165 | "endOffset": 45, 166 | "string": "######", 167 | "lineNumber": 6, 168 | "column": 1 169 | }, 170 | { 171 | "ID": "Whitespace", 172 | "startOffset": 46, 173 | "endOffset": 46, 174 | "string": " ", 175 | "lineNumber": 6, 176 | "column": 7 177 | }, 178 | { 179 | "ID": "Text", 180 | "startOffset": 47, 181 | "endOffset": 49, 182 | "string": "foo", 183 | "lineNumber": 6, 184 | "column": 8 185 | }, 186 | { 187 | "ID": "Whitespace", 188 | "startOffset": 50, 189 | "endOffset": 50, 190 | "string": "\n", 191 | "lineNumber": 6, 192 | "column": 11 193 | }, 194 | { 195 | "ID": "EOF", 196 | "startOffset": 51, 197 | "endOffset": 51, 198 | "string": "", 199 | "lineNumber": 6, 200 | "column": 11 201 | } 202 | ] 203 | -------------------------------------------------------------------------------- /test/expect/03-section-atx-headings/testp_032.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ID": "AtxHeading", 4 | "Value": null, 5 | "PositionStart": { 6 | "Line": 1, 7 | "Column": 1, 8 | "Offset": 0 9 | }, 10 | "PositionEnd": { 11 | "Line": 1, 12 | "Column": 3, 13 | "Offset": 4 14 | }, 15 | "Children": [{ 16 | "ID": "Text", 17 | "Value": "foo", 18 | "PositionStart": { 19 | "Line": 1, 20 | "Column": 3, 21 | "Offset": 2 22 | }, 23 | "PositionEnd": { 24 | "Line": 1, 25 | "Column": 3, 26 | "Offset": 4 27 | }, 28 | "Children": [], 29 | "Level": 1 30 | }], 31 | "Level": 1 32 | }, 33 | { 34 | "ID": "AtxHeading", 35 | "Value": null, 36 | "PositionStart": { 37 | "Line": 2, 38 | "Column": 1, 39 | "Offset": 6 40 | }, 41 | "PositionEnd": { 42 | "Line": 2, 43 | "Column": 4, 44 | "Offset": 11 45 | }, 46 | "Children": [{ 47 | "ID": "Text", 48 | "Value": "foo", 49 | "PositionStart": { 50 | "Line": 2, 51 | "Column": 4, 52 | "Offset": 9 53 | }, 54 | "PositionEnd": { 55 | "Line": 2, 56 | "Column": 4, 57 | "Offset": 11 58 | }, 59 | "Children": [], 60 | "Level": 2 61 | }], 62 | "Level": 2 63 | }, 64 | { 65 | "ID": "AtxHeading", 66 | "Value": null, 67 | "PositionStart": { 68 | "Line": 3, 69 | "Column": 1, 70 | "Offset": 13 71 | }, 72 | "PositionEnd": { 73 | "Line": 3, 74 | "Column": 5, 75 | "Offset": 19 76 | }, 77 | "Children": [{ 78 | "ID": "Text", 79 | "Value": "foo", 80 | "PositionStart": { 81 | "Line": 3, 82 | "Column": 5, 83 | "Offset": 17 84 | }, 85 | "PositionEnd": { 86 | "Line": 3, 87 | "Column": 5, 88 | "Offset": 19 89 | }, 90 | "Children": [], 91 | "Level": 3 92 | }], 93 | "Level": 3 94 | }, 95 | { 96 | "ID": "AtxHeading", 97 | "Value": null, 98 | "PositionStart": { 99 | "Line": 4, 100 | "Column": 1, 101 | "Offset": 21 102 | }, 103 | "PositionEnd": { 104 | "Line": 4, 105 | "Column": 6, 106 | "Offset": 28 107 | }, 108 | "Children": [{ 109 | "ID": "Text", 110 | "Value": "foo", 111 | "PositionStart": { 112 | "Line": 4, 113 | "Column": 6, 114 | "Offset": 26 115 | }, 116 | "PositionEnd": { 117 | "Line": 4, 118 | "Column": 6, 119 | "Offset": 28 120 | }, 121 | "Children": [], 122 | "Level": 4 123 | }], 124 | "Level": 4 125 | }, 126 | { 127 | "ID": "AtxHeading", 128 | "Value": null, 129 | "PositionStart": { 130 | "Line": 5, 131 | "Column": 1, 132 | "Offset": 30 133 | }, 134 | "PositionEnd": { 135 | "Line": 5, 136 | "Column": 7, 137 | "Offset": 38 138 | }, 139 | "Children": [{ 140 | "ID": "Text", 141 | "Value": "foo", 142 | "PositionStart": { 143 | "Line": 5, 144 | "Column": 7, 145 | "Offset": 36 146 | }, 147 | "PositionEnd": { 148 | "Line": 5, 149 | "Column": 7, 150 | "Offset": 38 151 | }, 152 | "Children": [], 153 | "Level": 5 154 | }], 155 | "Level": 5 156 | }, 157 | { 158 | "ID": "AtxHeading", 159 | "Value": null, 160 | "PositionStart": { 161 | "Line": 6, 162 | "Column": 1, 163 | "Offset": 40 164 | }, 165 | "PositionEnd": { 166 | "Line": 6, 167 | "Column": 8, 168 | "Offset": 49 169 | }, 170 | "Children": [{ 171 | "ID": "Text", 172 | "Value": "foo", 173 | "PositionStart": { 174 | "Line": 6, 175 | "Column": 8, 176 | "Offset": 47 177 | }, 178 | "PositionEnd": { 179 | "Line": 6, 180 | "Column": 8, 181 | "Offset": 49 182 | }, 183 | "Children": [], 184 | "Level": 6 185 | }], 186 | "Level": 6 187 | } 188 | ] 189 | -------------------------------------------------------------------------------- /test/section_atx_headings.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const mem = std.mem; 3 | const json = std.json; 4 | const testing = std.testing; 5 | const assert = std.debug.assert; 6 | 7 | const testUtil = @import("util.zig"); 8 | 9 | const log = @import("../src/md/log.zig"); 10 | const Token = @import("../src/md/token.zig").Token; 11 | const TokenId = @import("../src/md/token.zig").TokenId; 12 | const Lexer = @import("../src/md/lexer.zig").Lexer; 13 | const Parser = @import("../src/md/parse.zig").Parser; 14 | 15 | var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 16 | const allocator = &arena.allocator; 17 | 18 | // "markdown": "# foo\n## foo\n### foo\n#### foo\n##### foo\n###### foo\n", 19 | // "html": "

foo

\n

foo

\n

foo

\n

foo

\n
foo
\n
foo
\n", 20 | test "Test Example 032" { 21 | const testNumber: u8 = 32; 22 | const parserInput = try testUtil.getTest(allocator, testNumber, testUtil.TestKey.markdown); 23 | testUtil.dumpTest(parserInput); 24 | var p = Parser.init(allocator); 25 | defer p.deinit(); 26 | _ = try p.parse(parserInput); 27 | log.Debug("Testing lexer"); 28 | const expectLexerJson = @embedFile("expect/03-section-atx-headings/testl_032.json"); 29 | if (try testUtil.compareJsonExpect(allocator, expectLexerJson, p.lex.tokens.items)) |ajson| { 30 | // log.Errorf("LEXER TEST FAILED! lexer tokens (in json):\n{}\n", .{ajson}); 31 | std.os.exit(1); 32 | } 33 | log.Debug("Testing parser"); 34 | const expectParserJson = @embedFile("expect/03-section-atx-headings/testp_032.json"); 35 | if (try testUtil.compareJsonExpect(allocator, expectParserJson, p.root.items)) |ajson| { 36 | // log.Errorf("PARSER TEST FAILED! parser tree (in json):\n{}\n", .{ajson}); 37 | std.os.exit(1); 38 | } 39 | log.Debug("Testing html translator"); 40 | const expectHtml = try testUtil.getTest(allocator, testNumber, testUtil.TestKey.html); 41 | defer allocator.free(expectHtml); 42 | if (try testUtil.compareHtmlExpect(allocator, expectHtml, &p.root)) |ahtml| { 43 | // log.Errorf("HTML TRANSLATE TEST FAILED! html:\n{}\n", .{ahtml}); 44 | std.os.exit(1); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/section_tabs.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const mem = std.mem; 3 | const json = std.json; 4 | const testing = std.testing; 5 | const assert = std.debug.assert; 6 | 7 | const testUtil = @import("util.zig"); 8 | 9 | const log = @import("../src/md/log.zig"); 10 | const Token = @import("../src/md/token.zig").Token; 11 | const TokenId = @import("../src/md/token.zig").TokenId; 12 | const Lexer = @import("../src/md/lexer.zig").Lexer; 13 | const Parser = @import("../src/md/parse.zig").Parser; 14 | 15 | var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 16 | const allocator = &arena.allocator; 17 | 18 | // "markdown": "\tfoo\tbaz\t\tbim\n", 19 | // "html": "
foo\tbaz\t\tbim\n
\n", 20 | test "Test Example 001" { 21 | const testNumber: u8 = 1; 22 | const parserInput = try testUtil.getTest(allocator, testNumber, testUtil.TestKey.markdown); 23 | testUtil.dumpTest(parserInput); 24 | var p = Parser.init(allocator); 25 | defer p.deinit(); 26 | _ = try p.parse(parserInput); 27 | log.Debug("Testing lexer"); 28 | const expectLexerJson = @embedFile("expect/01-section-tabs/testl_001.json"); 29 | if (try testUtil.compareJsonExpect(allocator, expectLexerJson, p.lex.tokens.items)) |ajson| { 30 | // log.Errorf("LEXER TEST FAILED! lexer tokens (in json):\n{}\n", .{ajson}); 31 | std.os.exit(1); 32 | } 33 | log.Debug("Testing parser"); 34 | const expectParserJson = @embedFile("expect/01-section-tabs/testp_001.json"); 35 | if (try testUtil.compareJsonExpect(allocator, expectParserJson, p.root.items)) |ajson| { 36 | // log.Errorf("PARSER TEST FAILED! parser tree (in json):\n{}\n", .{ajson}); 37 | std.os.exit(1); 38 | } 39 | log.Debug("Testing html translator"); 40 | const expectHtml = try testUtil.getTest(allocator, testNumber, testUtil.TestKey.html); 41 | defer allocator.free(expectHtml); 42 | if (try testUtil.compareHtmlExpect(allocator, expectHtml, &p.root)) |ahtml| { 43 | // log.Errorf("HTML TRANSLATE TEST FAILED! html:\n{}\n", .{ahtml}); 44 | std.os.exit(1); 45 | } 46 | } 47 | 48 | // "markdown": " \tfoo\tbaz\t\tbim\n", 49 | // "html": "
foo\tbaz\t\tbim\n
\n", 50 | test "Test Example 002" { 51 | const testNumber: u8 = 2; 52 | const parserInput = try testUtil.getTest(allocator, testNumber, testUtil.TestKey.markdown); 53 | testUtil.dumpTest(parserInput); 54 | var p = Parser.init(allocator); 55 | defer p.deinit(); 56 | _ = try p.parse(parserInput); 57 | log.Debug("Testing lexer"); 58 | const expectLexerJson = @embedFile("expect/01-section-tabs/testl_002.json"); 59 | if (try testUtil.compareJsonExpect(allocator, expectLexerJson, p.lex.tokens.items)) |ajson| { 60 | log.Errorf("LEXER TEST FAILED! lexer tokens (in json):\n{}\n", .{ajson}); 61 | std.os.exit(1); 62 | } 63 | log.Debug("Testing parser"); 64 | const expectParserJson = @embedFile("expect/01-section-tabs/testp_002.json"); 65 | if (try testUtil.compareJsonExpect(allocator, expectParserJson, p.root.items)) |ajson| { 66 | // log.Errorf("PARSER TEST FAILED! parser tree (in json):\n{}\n", .{ajson}); 67 | std.os.exit(1); 68 | } 69 | log.Debug("Testing html translator"); 70 | const expectHtml = try testUtil.getTest(allocator, testNumber, testUtil.TestKey.html); 71 | defer allocator.free(expectHtml); 72 | if (try testUtil.compareHtmlExpect(allocator, expectHtml, &p.root)) |ahtml| { 73 | // log.Errorf("HTML TRANSLATE TEST FAILED! html:\n{}\n", .{ahtml}); 74 | std.os.exit(1); 75 | } 76 | } 77 | 78 | // "markdown": " a\ta\n ὐ\ta\n", 79 | // "html": "
a\ta\nὐ\ta\n
\n", 80 | test "Test Example 003" { 81 | const testNumber: u8 = 3; 82 | const parserInput = try testUtil.getTest(allocator, testNumber, testUtil.TestKey.markdown); 83 | testUtil.dumpTest(parserInput); 84 | var p = Parser.init(allocator); 85 | defer p.deinit(); 86 | _ = try p.parse(parserInput); 87 | log.Debug("Testing lexer"); 88 | const expectLexerJson = @embedFile("expect/01-section-tabs/testl_003.json"); 89 | if (try testUtil.compareJsonExpect(allocator, expectLexerJson, p.lex.tokens.items)) |ajson| { 90 | // log.Errorf("LEXER TEST FAILED! lexer tokens (in json):\n{}\n", .{ajson}); 91 | std.os.exit(1); 92 | } 93 | log.Debug("Testing parser"); 94 | const expectParserJson = @embedFile("expect/01-section-tabs/testp_003.json"); 95 | if (try testUtil.compareJsonExpect(allocator, expectParserJson, p.root.items)) |ajson| { 96 | // log.Errorf("PARSER TEST FAILED! parser tree (in json):\n{}\n", .{ajson}); 97 | std.os.exit(1); 98 | } 99 | log.Debug("Testing html translator"); 100 | const expectHtml = try testUtil.getTest(allocator, testNumber, testUtil.TestKey.html); 101 | defer allocator.free(expectHtml); 102 | if (try testUtil.compareHtmlExpect(allocator, expectHtml, &p.root)) |ahtml| { 103 | // log.Errorf("HTML TRANSLATE TEST FAILED! html:\n{}\n", .{ahtml}); 104 | std.os.exit(1); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /test/util.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const fs = std.fs; 3 | const fmt = std.fmt; 4 | const mem = std.mem; 5 | const math = std.math; 6 | const json = std.json; 7 | const ChildProcess = std.ChildProcess; 8 | 9 | const log = @import("../src/md/log.zig"); 10 | const translate = @import("../src/md/translate.zig"); 11 | const Node = @import("../src/md/parse.zig").Node; 12 | 13 | const TestError = error{ 14 | TestNotFound, 15 | CouldNotCreateTempDirectory, 16 | DockerRunFailed, 17 | }; 18 | 19 | pub const TestKey = enum { 20 | markdown, 21 | html, 22 | }; 23 | 24 | const ValidationOutStream = struct { 25 | const Self = @This(); 26 | 27 | expected_remaining: []const u8, 28 | 29 | pub const OutStream = std.io.OutStream(*Self, Error, write); 30 | pub const Error = error{DifferentData}; 31 | 32 | fn init(exp: []const u8) Self { 33 | return .{ 34 | .expected_remaining = exp, 35 | }; 36 | } 37 | 38 | pub fn outStream(self: *Self) OutStream { 39 | return .{ .context = self }; 40 | } 41 | 42 | fn write(self: *Self, bytes: []const u8) Error!usize { 43 | if (self.expected_remaining.len < bytes.len) { 44 | std.debug.warn( 45 | \\====== expected this output: ========= 46 | \\{} 47 | \\======== instead found this: ========= 48 | \\{} 49 | \\======================================\n 50 | , .{ 51 | self.expected_remaining, 52 | bytes, 53 | }); 54 | return error.DifferentData; 55 | } 56 | if (!mem.eql(u8, self.expected_remaining[0..bytes.len], bytes)) { 57 | std.debug.warn( 58 | \\====== expected this output: ========= 59 | \\{} 60 | \\======== instead found this: ========= 61 | \\{} 62 | \\======================================\n 63 | , .{ 64 | self.expected_remaining[0..bytes.len], 65 | bytes, 66 | }); 67 | return error.DifferentData; 68 | } 69 | self.expected_remaining = self.expected_remaining[bytes.len..]; 70 | return bytes.len; 71 | } 72 | }; 73 | 74 | /// Caller owns returned memory 75 | pub fn getTest(allocator: *mem.Allocator, number: i32, key: TestKey) ![]const u8 { 76 | const cwd = fs.cwd(); 77 | // path is relative to test.zig in the project root 78 | const source = try cwd.readFileAlloc(allocator, "test/spec/commonmark_spec_0.29.json", math.maxInt(usize)); 79 | defer allocator.free(source); 80 | var json_parser = std.json.Parser.init(allocator, true); 81 | defer json_parser.deinit(); 82 | var json_tree = try json_parser.parse(source); 83 | defer json_tree.deinit(); 84 | const stdout = &std.io.getStdOut().outStream(); 85 | for (json_tree.root.Array.items) |value, i| { 86 | var example_num = value.Object.get("example").?.Integer; 87 | if (example_num == number) { 88 | return try allocator.dupe(u8, value.Object.get(@tagName(key)).?.String); 89 | } 90 | } 91 | return TestError.TestNotFound; 92 | } 93 | 94 | pub fn mktmp(allocator: *mem.Allocator) ![]const u8 { 95 | const cwd = try fs.path.resolve(allocator, &[_][]const u8{"."}); 96 | defer allocator.free(cwd); 97 | var out = try exec(allocator, cwd, true, &[_][]const u8{ "mktemp", "-d" }); 98 | defer allocator.free(out.stdout); 99 | defer allocator.free(out.stderr); 100 | // defer allocator.free(out); 101 | log.Debugf("mktemp return: {}\n", .{out}); 102 | return allocator.dupe(u8, std.mem.trim(u8, out.stdout, &std.ascii.spaces)); 103 | } 104 | 105 | pub fn writeFile(allocator: *mem.Allocator, absoluteDirectory: []const u8, fileName: []const u8, contents: []const u8) ![]const u8 { 106 | var filePath = try fs.path.join(allocator, &[_][]const u8{ absoluteDirectory, fileName }); 107 | log.Debugf("writeFile path: {}\n", .{filePath}); 108 | const file = try std.fs.createFileAbsolute(filePath, .{}); 109 | defer file.close(); 110 | try file.writeAll(contents); 111 | return filePath; 112 | } 113 | 114 | pub fn writeJson(allocator: *mem.Allocator, tempDir: []const u8, name: []const u8, value: anytype) ![]const u8 { 115 | var buf = std.ArrayList(u8).init(allocator); 116 | defer buf.deinit(); 117 | try json.stringify(value, json.StringifyOptions{ 118 | .whitespace = .{ 119 | .indent = .{ .Space = 4 }, 120 | .separator = true, 121 | }, 122 | }, buf.outStream()); 123 | return writeFile(allocator, tempDir, name, buf.items); 124 | } 125 | 126 | fn exec(allocator: *mem.Allocator, cwd: []const u8, expect_0: bool, argv: []const []const u8) !ChildProcess.ExecResult { 127 | const max_output_size = 100 * 1024; 128 | const result = ChildProcess.exec(.{ 129 | .allocator = allocator, 130 | .argv = argv, 131 | .cwd = cwd, 132 | .max_output_bytes = max_output_size, 133 | }) catch |err| { 134 | std.debug.warn("The following command failed:\n", .{}); 135 | // printCmd(cwd, argv); 136 | return err; 137 | }; 138 | // switch (result.term) { 139 | // .Exited => |code| { 140 | // if ((code != 0) == expect_0) { 141 | // std.debug.warn("The following command exited with error code {}:\n", .{code}); 142 | // // printCmd(cwd, argv); 143 | // std.debug.warn("stderr:\n{}\n", .{result.stderr}); 144 | // return error.CommandFailed; 145 | // } 146 | // }, 147 | // else => { 148 | // std.debug.warn("The following command terminated unexpectedly:\n", .{}); 149 | // // printCmd(cwd, argv); 150 | // std.debug.warn("stderr:\n{}\n", .{result.stderr}); 151 | // return error.CommandFailed; 152 | // }, 153 | // } 154 | return result; 155 | } 156 | 157 | pub fn debugPrintExecCommand(allocator: *mem.Allocator, arry: [][]const u8) !void { 158 | var cmd_buf = std.ArrayList(u8).init(allocator); 159 | defer cmd_buf.deinit(); 160 | for (arry) |a| { 161 | try cmd_buf.appendSlice(a); 162 | try cmd_buf.append(' '); 163 | } 164 | log.Debugf("exec cmd: {}\n", .{cmd_buf.items}); 165 | } 166 | 167 | pub fn dockerRunJsonDiff(allocator: *mem.Allocator, actualJson: []const u8, expectJson: []const u8) !void { 168 | const cwd = try fs.path.resolve(allocator, &[_][]const u8{"."}); 169 | defer allocator.free(cwd); 170 | var filemount = try std.mem.concat(allocator, u8, &[_][]const u8{ actualJson, ":", actualJson }); 171 | defer allocator.free(filemount); 172 | var file2mount = try std.mem.concat(allocator, u8, &[_][]const u8{ expectJson, ":", expectJson }); 173 | defer allocator.free(file2mount); 174 | 175 | // The long way around until there is a better way to compare json in Zig 176 | var cmd = &[_][]const u8{ "docker", "run", "-t", "-v", filemount, "-v", file2mount, "-w", cwd, "--rm", "bwowk/json-diff", "-C", expectJson, actualJson }; 177 | try debugPrintExecCommand(allocator, cmd); 178 | 179 | var diff = try exec(allocator, cwd, true, cmd); 180 | if (diff.term.Exited != 0) { 181 | log.Errorf("docker run failed:\n{}\n", .{diff.stdout}); 182 | return error.DockerRunFailed; 183 | } 184 | } 185 | 186 | /// compareJsonExpect tests parser output against a json test file containing the expected output 187 | /// - expected: The expected json output. Use @embedFile()! 188 | /// - value: The value to test against the expected json. This will be marshaled to json. 189 | /// - returns: An error or optional: null (on success) or "value" encoded as json on compare failure. 190 | pub fn compareJsonExpect(allocator: *mem.Allocator, expected: []const u8, value: anytype) !?[]const u8 { 191 | // check with zig stream validator 192 | var dumpBuf = std.ArrayList(u8).init(allocator); 193 | defer dumpBuf.deinit(); 194 | 195 | var stringyOpts = json.StringifyOptions{ 196 | .whitespace = .{ 197 | .indent = .{ .Space = 4 }, 198 | .separator = true, 199 | }, 200 | }; 201 | 202 | // human readable diff 203 | var tempDir = try mktmp(allocator); 204 | defer allocator.free(tempDir); 205 | 206 | var expectJsonPath = try writeFile(allocator, tempDir, "expect.json", expected); 207 | defer allocator.free(expectJsonPath); 208 | 209 | var actualJsonPath = try writeJson(allocator, tempDir, "actual.json", value); 210 | defer allocator.free(actualJsonPath); 211 | 212 | // FIXME: replace with zig json diff 213 | dockerRunJsonDiff(allocator, actualJsonPath, expectJsonPath) catch |err2| { 214 | try json.stringify(value, stringyOpts, dumpBuf.outStream()); 215 | return dumpBuf.toOwnedSlice(); 216 | }; 217 | return null; 218 | } 219 | 220 | /// compareHtmlExpect tests parser output against a json test file containing the expected output 221 | /// - expected: The expected html output. Use @embedFile()! 222 | /// - value: The translated parser output. 223 | /// - dumpHtml: If true, only the json value of "value" will be dumped to stdout. 224 | pub fn compareHtmlExpect(allocator: *std.mem.Allocator, expected: []const u8, value: *std.ArrayList(Node)) !?[]const u8 { 225 | var vos = ValidationOutStream.init(expected); 226 | var buf = std.ArrayList(u8).init(allocator); 227 | defer buf.deinit(); 228 | try translate.markdownToHtml(value, buf.outStream()); 229 | _ = vos.outStream().write(buf.items) catch |err| { 230 | return buf.items; 231 | }; 232 | return null; 233 | } 234 | 235 | pub fn dumpTest(input: []const u8) void { 236 | log.config(log.logger.Level.Debug, true); 237 | std.debug.warn("{}", .{"\n"}); 238 | log.Debugf("test:\n{}-- END OF TEST --\n", .{input}); 239 | } 240 | --------------------------------------------------------------------------------