├── .github └── workflows │ └── main.yml ├── .gitignore ├── LICENSE.txt ├── README.md ├── build.zig ├── build.zig.zon ├── dap-config.lua ├── lua ├── zigdown.lua └── zigdown │ ├── build.lua │ ├── render.lua │ └── utils.lua ├── plugin └── zigdown.lua ├── sample-render-html.png ├── sample-render.png ├── src ├── app │ ├── RawTTY.zig │ ├── lua_api.zig │ ├── main.zig │ ├── present.zig │ ├── serve.zig │ ├── serve │ │ └── markdown.zig │ ├── wasm │ │ ├── stdlib.c │ │ └── stdlib.zig │ └── wasm_main.zig ├── assets │ ├── assets.zig │ ├── html.zig │ ├── html │ │ └── style.css │ ├── img │ │ └── zig-zero.png │ ├── queries.zig │ └── queries │ │ ├── highlights-bash.scm │ │ ├── highlights-c.scm │ │ ├── highlights-cmake.scm │ │ ├── highlights-cpp.scm │ │ ├── highlights-json.scm │ │ ├── highlights-make.scm │ │ ├── highlights-python.scm │ │ ├── highlights-rust.scm │ │ ├── highlights-yaml.scm │ │ └── highlights-zig.scm └── lib │ ├── ast │ ├── blocks.zig │ ├── containers.zig │ ├── inlines.zig │ └── leaves.zig │ ├── console.zig │ ├── debug.zig │ ├── image.zig │ ├── lexer.zig │ ├── parser.zig │ ├── parsers │ ├── blocks.zig │ ├── inlines.zig │ └── utils.zig │ ├── render.zig │ ├── render │ ├── Renderer.zig │ ├── render_console.zig │ ├── render_format.zig │ └── render_html.zig │ ├── syntax.zig │ ├── test.zig │ ├── tokens.zig │ ├── ts_queries.zig │ ├── utils.zig │ ├── wasm.zig │ └── zigdown.zig ├── test ├── code.md ├── directive.md ├── html │ ├── demo.html │ ├── zigdown-wasm.wasm │ └── zigdown_wrapper.js ├── link.md ├── list.md ├── list2.md ├── mini.md ├── mononoki-Bold.ttf ├── mononoki-BoldItalic.ttf ├── mononoki-Italic.ttf ├── mononoki-Regular.ttf ├── out.html ├── quote.md ├── sample.md ├── sample2.md ├── table.md ├── test.lua ├── toc.md └── yaml.md └── tools ├── download-highlights.bash ├── fetch_queries.sh ├── run_demo.sh └── test_runner.zig /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | concurrency: 10 | # Cancels pending runs when a PR gets updated. 11 | group: ${{ github.head_ref || github.run_id }}-${{ github.actor }} 12 | cancel-in-progress: true 13 | 14 | permissions: 15 | # Sets permission policy for `GITHUB_TOKEN` 16 | contents: read 17 | 18 | jobs: 19 | tests: 20 | strategy: 21 | fail-fast: false 22 | runs-on: ubuntu-latest 23 | 24 | container: 25 | image: ghcr.io/jacobcrabill/alpine-zig:0.14.0 26 | 27 | steps: 28 | - uses: actions/checkout@v4 29 | with: 30 | fetch-depth: 0 # Change if you need git info 31 | 32 | - name: Build all targets 33 | run: zig build -Dtarget=x86_64-linux-musl 34 | 35 | - name: Test 36 | run: zig build test -Dtarget=x86_64-linux-musl 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.swp 3 | *.o 4 | *.so 5 | *.a 6 | zig-out/ 7 | .zig-cache/ 8 | .ccls-cache/ 9 | docs/ 10 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2024 Jacob Crabill 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the “Software”), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zigdown: Markdown toolset in Zig 2 | 3 | ![CI Status](https://github.com/JacobCrabill/zigdown/actions/workflows/main.yml/badge.svg) 4 | 5 | ```{toctree} 6 | 7 | ``` 8 | 9 | ![Zig is Awesome!](src/assets/img/zig-zero.png) 10 | 11 | Zigdown, inspired by [Glow](https://github.com/charmbracelet/glow) and 12 | [mdcat](https://github.com/swsnr/mdcat), is a tool to parse and render Markdown-like content to the 13 | terminal or to HTML. It can also serve up a directory of files to your browser like a psuedo-static 14 | web site, or present a set of files interactively as an in-terminal slide show. 15 | 16 | This will likely forever be a WIP, but it currently supports the the most common features of simple 17 | Markdown files. 18 | 19 | ```{warning} 20 | This is not a CommonMark-compliant Markdown parser, nor will it ever be one! 21 | ``` 22 | 23 | ## Features & Future Work 24 | 25 | ### Parser Features 26 | 27 | - [x] Headers 28 | - [x] Basic text formatting (**Bold**, _italic_, ~underline~) 29 | - I may change ~this~ back to strikethrough and add another syntax for underline 30 | - [x] Links 31 | - [x] Quote blocks 32 | - [x] Unordered lists 33 | - [x] Ordered lists 34 | - [x] Code blocks, including syntax highlighting using TreeSitter 35 | - [x] Task lists 36 | - [x] Tables 37 | - [x] Autolinks 38 | 39 | ### Renderer Features 40 | 41 | - [x] Console and HTML rendering 42 | - [x] Images (rendered to the console using the 43 | [Kitty graphics protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/)) 44 | - [x] Web-based images (fetch from URL & display in-terminal) 45 | - [x] (Clickable) Links 46 | - [x] Tables 47 | - [x] Automatic Table of Contents creation 48 | - [x] Neovim integration (Lua) 49 | - Optional: If you have Lua 5.1 system libraries, can build as a Lua plugin module 50 | - [x] Markdown formatter 51 | 52 | ### Future Work / Missing Pieces 53 | 54 | - [ ] Enabling TreeSitter parsers to be used in WASM modules 55 | - Requires filling in some libC stub functions (the TS parsers use quite a few functions from the 56 | C standard library that are not available in WASM) 57 | - [ ] Better handling of inline code spans & backtick strings 58 | - [ ] Character escaping 59 | - [ ] Complete NeoVim integration (w/ image rendering and auto-scrolling) 60 | - Requires writing a renderer in Lua using NeoVim APIs 61 | - [ ] [Link References](https://spec.commonmark.org/0.31.2/#link-reference-definition) 62 | - [ ] GitHub-flavored note/info boxes? (not a fan of the syntax, but such is life) 63 | - [ ] Color schemes for syntax highlighting 64 | 65 | ## Caveats 66 | 67 | Note that I am **not** planning to implement complete CommonMark specification support, or even full 68 | Markdown support by any definition. Rather, the goal is to support "nicely formatted" Markdown, 69 | making some simplifying assumptions about what constitutes a paragraph vs. a code block, for 70 | example. The "nicely formatted" caveat simplifies the parser somewhat, enabling easier extension for 71 | new features like special warnings, note boxes, and other custom directives. 72 | 73 | In addition to my "nicely formatted" caveat, I am also only interested in supporting a very common 74 | subset of all Markdown syntax, and ignoring anything I personally find useless or annoying to parse. 75 | 76 | ### Things I Will Not Support 77 | 78 | - Setext headings 79 | - Thematic breaks 80 | - Embedded HTML 81 | - Indent-based code blocks (as opposed to fenced code blocks) 82 | 83 | ## Usage 84 | 85 | The current version of Zig this code compiles with is 86 | [0.14.0](https://ziglang.org/download/0.14.0/zig-linux-x86_64-0.14.0.tar.xz). I highly recommend 87 | using the [Zig version manager](https://github.com/tristanisham/zvm) to install and manage various 88 | Zig versions. 89 | 90 | ```bash 91 | zig build run -- console test/sample.md 92 | zig build -l # List build options 93 | zig build -Dtarget=x86_64-linux-musl # Compile for x86-64 Linux using 94 | # statically-linked MUSL libC 95 | ``` 96 | 97 | `zig build` will create a `zigdown` binary at `zig-out/bin/zigdown`. Add `-Doptimize=ReleaseSafe` to 98 | enable optimizations while keeping safety checks and backtraces upon errors. The shorthand options 99 | `-Dsafe` and `-Dfast` also enable ReleaseSafe and ReleaseFast, respectively. 100 | 101 | ## Enabling Syntax Highlighting 102 | 103 | To enable syntax highlighting within code blocks, you must install the necessary TreeSitter language 104 | parsers and highlight queries for the languages you'd like to highlight. This can be done by 105 | building and installing each language into a location in your `$LD_LIBRARY_PATH` environment 106 | variable. 107 | 108 | ### Built-In Parsers 109 | 110 | Zigdown comes with a number of TreeSitter parsers and highlight queries built-in: 111 | 112 | - bash 113 | - C 114 | - C++ 115 | - CMake 116 | - JSON 117 | - Make 118 | - Python 119 | - Rust 120 | - YAML 121 | - Zig 122 | 123 | The parsers are downloaded from Github and the relevant source files are added to the build, and the 124 | queries are stored at `data/queries/`, which contain some fixes and improvements to the original 125 | highlighting queries. 126 | 127 | ### Installing Parsers Using Zigdown 128 | 129 | The Zigdown cli tool can also download and install parsers for you. For example, to download, build, 130 | and install the C and C++ parsers and their highlight queries: 131 | 132 | ```bash 133 | zigdown install-parsers c,cpp # Assumes both exist at github.com/tree-sitter on the 'master' branch 134 | zigdown install-parsers maxxnino:zig # Specify the Github user; still assumes the 'master' branch 135 | zigdown install-parsers tree-sitter:master:rust # Specify Github user, branch, and language 136 | ``` 137 | 138 | ### Installing Parsers Manually 139 | 140 | You can also install manually if Zigdown doesn't properly fetch the repo for you (or if the repo is 141 | not setup in a standard manner and requires custom setup). For example, to install the C++ parser 142 | from the default tree-sitter project on Github: 143 | 144 | ```bash 145 | #!/usr/bin/env bash 146 | 147 | # Ensure the TS_CONFIG_DIR is available 148 | export TS_CONFIG_DIR=$HOME/.config/tree-sitter/ 149 | mkdir -p ${TS_CONFIG_DIR}/parsers 150 | cd ${TS_CONFIG_DIR}/parsers 151 | 152 | # Clone and build a TreeSitter parser library 153 | git clone https://github.com/tree-sitter/tree-sitter-cpp 154 | cd tree-sitter-cpp 155 | make install PREFIX=$HOME/.local/ 156 | 157 | # Add the install directory to LD_LIBRARY_PATH (if not done so already) 158 | export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$HOME/.local/lib/ 159 | ``` 160 | 161 | In addition to having the parser libraries available for `dlopen`, you will also need the highlight 162 | queries. For this, use the provided bash script `./tools/fetch_queries.sh`. This will install the 163 | queries to `$TS_CONFIG_DIR/queries`, which defaults to `$HOME/.config/tree-sitter/queries`. 164 | 165 | ## Sample Renders 166 | 167 | ### Console 168 | 169 | ![Sample Console Render](sample-render.png) 170 | 171 | ### HTML 172 | 173 | ![Sample HTML Render](sample-render-html.png) 174 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .zigdown, 3 | .fingerprint = 0xec26eaf34f894e33, 4 | .version = "0.1.0", 5 | .minimum_zig_version = "0.14.0", 6 | .paths = .{ 7 | "lua", 8 | "plugin", 9 | "src", 10 | "tools", 11 | "build.zig", 12 | "build.zig.zon", 13 | }, 14 | .dependencies = .{ 15 | .stbi = .{ 16 | .url = "https://github.com/JacobCrabill/zig-stb-image/archive/5a650f9439b2b2d8ca5e498b349e25bb7308f7bb.tar.gz", 17 | .hash = "zig_stb_image-2.28.0-9dD1dTgdBQBDRqd8_8qNHlT62Ox8Gfh3qbbBtvbufjom", 18 | }, 19 | .plutosvg = .{ 20 | .url = "https://github.com/JacobCrabill/zig-plutosvg/archive/518e00df9412df30eea6a1934a7edd2658cdd2d9.tar.gz", 21 | .hash = "plutosvg-0.0.6-LyMxgbMuAADqe6_HVbOOBTtsmMVlb-ar-iOc7pRrPQYc", 22 | }, 23 | .flags = .{ 24 | .url = "https://github.com/n0s4/flags/archive/0e2491d8e6d2be38dc0c2ce8e103469886e468bb.tar.gz", 25 | .hash = "flags-0.10.0-a_9h3kR2AABNAfaPGRyOOGmWsfv12Hk6JQPTX4MM446s", 26 | }, 27 | .treez = .{ 28 | .url = "https://github.com/JacobCrabill/treez/archive/640de7530c4567a0cadf3f701d1c315724eed4c3.tar.gz", 29 | .hash = "treez-0.1.0-4JX9hQSlAAAR1_cQTapcK9qGwbVI4bHrnwFMGQOafXXL", 30 | }, 31 | .known_folders = .{ 32 | .url = "git+https://github.com/ziglibs/known-folders.git#aa24df42183ad415d10bc0a33e6238c437fc0f59", 33 | .hash = "known_folders-0.0.0-Fy-PJtLDAADGDOwYwMkVydMSTp_aN-nfjCZw6qPQ2ECL", 34 | }, 35 | 36 | // Individual TreeSitter language parsers 37 | .tree_sitter_bash = .{ 38 | .url = "https://github.com/tree-sitter/tree-sitter-bash/archive/49c31006d8307dcb12bc5770f35b6d5b9e2be68e.tar.gz", 39 | .hash = "N-V-__8AAG_cogDl9X3jxLCGR6dsBzkjlX8TYhtdCCvonOUr", 40 | }, 41 | .tree_sitter_c = .{ 42 | .url = "https://github.com/tree-sitter/tree-sitter-c/archive/e8841a6a9431b7365ac9055688429e1deb8db90f.tar.gz", 43 | .hash = "N-V-__8AALWcSAD2F6Vd1LSqa0js-e6kawfanpXMUnMhQeq6", 44 | }, 45 | .tree_sitter_cmake = .{ 46 | .url = "https://github.com/uyha/tree-sitter-cmake/archive/fe48221d4d9842d916d66b5e71ab3c6307ec28b3.tar.gz", 47 | .hash = "N-V-__8AACLOCQCV8GF5uI_qg4S3tygmCZMpxB9qk4L2evjb", 48 | }, 49 | .tree_sitter_cpp = .{ 50 | .url = "https://github.com/tree-sitter/tree-sitter-cpp/archive/f41b4f66a42100be405f96bdc4ebc4a61095d3e8.tar.gz", 51 | .hash = "N-V-__8AABV0FwF4Ttn0MFCCLOxdUUKUC07SL-wqYa-2ySus", 52 | }, 53 | .tree_sitter_json = .{ 54 | .url = "https://github.com/tree-sitter/tree-sitter-json/archive/4d770d31f732d50d3ec373865822fbe659e47c75.tar.gz", 55 | .hash = "N-V-__8AACVXAgCEl--zqcqoXlvXpEMk5BTS748bsLVslERc", 56 | }, 57 | .tree_sitter_make = .{ 58 | .url = "https://github.com/tree-sitter-grammars/tree-sitter-make/archive/5e9e8f8ff3387b0edcaa90f46ddf3629f4cfeb1d.tar.gz", 59 | .hash = "N-V-__8AAD6dEwA9hwePYcn8FdDAJEeYqCBBlYI2Oey6O5g2", 60 | }, 61 | .tree_sitter_python = .{ 62 | .url = "https://github.com/tree-sitter/tree-sitter-python/archive/de0c01e7102e755f6c2e1b3055ae6ca85f261a10.tar.gz", 63 | .hash = "N-V-__8AANVLPgC-yBA9l867GoeMn95l0Y_Q9CAzcizA7qlT", 64 | }, 65 | .tree_sitter_rust = .{ 66 | .url = "https://github.com/tree-sitter/tree-sitter-rust/archive/c447dcce961ac438aaeaf117347749fe7d1e8365.tar.gz", 67 | .hash = "N-V-__8AAJ3uaABprqh1hqE6xstGrAJWc4xL3u1HfJ6Plj6E", 68 | }, 69 | .tree_sitter_yaml = .{ 70 | .url = "https://github.com/tree-sitter-grammars/tree-sitter-yaml/archive/1805917414a9a8ba2473717fd69447277a175fae.tar.gz", 71 | .hash = "N-V-__8AABBcGwBNXXnYqPiNmGDO5T2k7__Tglt1pwNFI_YN", 72 | }, 73 | .tree_sitter_zig = .{ 74 | .url = "https://github.com/tree-sitter-grammars/tree-sitter-zig/archive/b670c8df85a1568f498aa5c8cae42f51a90473c0.tar.gz", 75 | .hash = "N-V-__8AAN6NXgCZxLUnqe4620P_FqHrEY8PHTA1-VlOn4yX", 76 | }, 77 | 78 | .ziglua = .{ 79 | .url = "https://github.com/natecraddock/ziglua/archive/5f19274632c94fc9c6ed6ab8506ac903d25c8303.tar.gz", 80 | .hash = "zlua-0.1.0-hGRpCxsXBQD2CZVDwzHsRkyMrydDpxADn0px0x1IraT8", 81 | .lazy = true, 82 | }, 83 | }, 84 | } 85 | -------------------------------------------------------------------------------- /dap-config.lua: -------------------------------------------------------------------------------- 1 | return { 2 | configurations = { 3 | zig = { 4 | { 5 | name = "Render README to stdout", 6 | type = 'gdb', 7 | request = 'launch', 8 | cwd = '${workspaceFolder}', 9 | stopOnEntry = false, 10 | program = 'zig-out/bin/zigdown', 11 | args = 'console README.md' 12 | }, 13 | }, 14 | }, 15 | 16 | adapters = {} 17 | } 18 | -------------------------------------------------------------------------------- /lua/zigdown.lua: -------------------------------------------------------------------------------- 1 | local utils = require "zigdown.utils" 2 | local build = require "zigdown.build" 3 | local render = require "zigdown.render" 4 | 5 | -- For luajit 2.1.0 (Based on Lua 5.1) 6 | -- Import the shared library containing our Lua API to Zigdown 7 | local plugin_root = utils.get_zigdown_root() 8 | vim.opt.runtimepath:append(plugin_root) 9 | 10 | -- Our Plugin Module Table 11 | local M = {} 12 | M.opts = {} 13 | M.root = plugin_root 14 | M.lua_module = M.root .. "/lua/zigdown_lua.so" 15 | M.zigdown_bin = M.root .. "/zig-out/bin/zigdown" 16 | M.use_lua_module = false 17 | 18 | -- Required version of the Zig compiler 19 | local zig_ver = "0.14.0" 20 | 21 | --- Setup the plugin with user-provided options 22 | function M.setup(opts) 23 | M.opts = opts or {} 24 | 25 | -- Check if the plugin has been built yet. Build it if not. 26 | if not utils.file_exists(M.zigdown_bin) then 27 | build.install(zig_ver, M.root, M.use_lua_module) 28 | end 29 | 30 | -- Setup any default values on options, if any 31 | end 32 | 33 | -- Render the current Markdown buffer to a new Terminal window 34 | function M.render_current_buffer() 35 | if not vim.tbl_contains({"markdown"}, vim.bo.filetype) then 36 | vim.notify("Can only render Markdown content!", vim.log.levels.WARN) 37 | return 38 | end 39 | 40 | if M.use_lua_module then 41 | -- Render the Markdown in-process using the Lua module 42 | -- Still a WIP to use the Lua module effectively 43 | -- Problem is not the rendering, but the dumping of the rendered output 44 | -- to a terminal to have the terminal render the ANSI escape sequences 45 | -- Building the Lua module also currently requires system libraries to 46 | -- be installed 47 | render.render_buffer(0) 48 | else 49 | -- Render the Markdown using an external process 50 | -- Runs the prebuilt 'zigdown' binary in a subshell to a new terminal 51 | render.render_file(vim.api.nvim_buf_get_name(0)) 52 | end 53 | end 54 | 55 | -- Clear the Vim autocommand group to cancel the render-on-save 56 | function M.cancel_auto_render() 57 | render.clear_autogroup() 58 | end 59 | 60 | -- Rebuild the zig code 61 | function M.install() 62 | build.install(zig_ver, M.root, M.use_lua_module) 63 | end 64 | 65 | return M 66 | -------------------------------------------------------------------------------- /lua/zigdown/build.lua: -------------------------------------------------------------------------------- 1 | local utils = require "zigdown.utils" 2 | 3 | local M = {} 4 | 5 | -- Mappings from NeoVim system strings to Zig system strings 6 | local os_patterns = { 7 | ["Windows"] = "windows", 8 | ["Linux"] = "linux", 9 | ["Darwin"] = "macos", 10 | } 11 | 12 | local file_patterns = { 13 | ["Windows"] = ".zip", 14 | ["Linux"] = ".tar.xz", 15 | ["Darwin"] = ".tar.xz", 16 | } 17 | 18 | local arch_patterns = { 19 | ["x86"] = "x86", 20 | ["x64"] = "x86_64", 21 | ["arm"] = "armv7a", 22 | ["arm64"] = "aarch64", 23 | } 24 | 25 | M.zigdown = nil 26 | M.curl_cmd = { "echo", "Hello" } 27 | M.tar_cmd = { "echo", "World" } 28 | M.build_cmd = {} 29 | M.build_dir = '' 30 | M.root_dir = '' 31 | M.zig_binary = '' 32 | M.tarball = '' 33 | M.load_lib = false 34 | M.build_jobid = nil 35 | 36 | function M.install_zig() 37 | vim.notify("Extracting archive: " .. table.concat(M.tar_cmd, " "), vim.log.levels.INFO) 38 | local tar_pid = vim.fn.jobstart(M.tar_cmd, { cwd = M.build_dir }) 39 | vim.fn.jobwait({tar_pid}) 40 | end 41 | 42 | -- Callback function for completion of 'zig build' 43 | function on_build_exit(obj) 44 | if obj.code ~= 0 then 45 | vim.schedule(function() 46 | vim.notify("Error building Zigdown!", vim.log.levels.ERROR) 47 | vim.notify("Build log:" .. obj.stdout, vim.log.levels.INFO) 48 | end) 49 | return 50 | end 51 | vim.schedule(function() 52 | vim.notify("Finished building Zigdown", vim.log.levels.INFO) 53 | end) 54 | 55 | if load_lib then 56 | M.zigdown = M.load_module() 57 | end 58 | 59 | -- Remove the archive after completion 60 | if vim.fn.filereadable(M.tarball) == 1 then 61 | vim.fn.delete(M.tarball) 62 | M.tarball = '' 63 | end 64 | M.build_jobid = nil 65 | end 66 | 67 | -- Build the Zigdown binary, and optionally the Lua plugin module 68 | ---@param load_lib boolean Load the Lua module 69 | function M.build_zigdown(load_lib) 70 | if M.build_jobid ~= nil then 71 | vim.notify("ZigdownRebuild already in progress - Please wait!", vim.log.levels.INFO) 72 | return 73 | end 74 | 75 | M.load_lib = load_lib 76 | vim.notify("Building zigdown using:" .. table.concat(M.build_cmd, " "), vim.log.levels.INFO) 77 | vim.notify("Compiling zigdown - Please wait...", vim.log.levels.INFO) 78 | 79 | local cmd = { M.zig_binary, "build", "-Doptimize=ReleaseFast" } 80 | if load_lib then 81 | table.insert(cmd, "-Dlua") 82 | end 83 | 84 | M.build_jobid = vim.system(cmd, { cwd = M.root_dir }, on_build_exit) 85 | end 86 | 87 | -- Attempt to load the zigdown_lua module we just compiled 88 | function M.load_module() 89 | if M.zigdown ~= nil then 90 | return M.zigdown 91 | end 92 | local path = utils.path_append(M.root_dir, "lua") 93 | package.cpath = package.cpath .. ';' .. path .. '/?.so' 94 | return require('zigdown_lua') 95 | end 96 | 97 | -- Build the plugin from source 98 | -- Install the required version of Zig to do so 99 | ---@param zig_ver string Version of Zig to use, e.g. "0.12.1" 100 | ---@param root string Root directory of the package to build 101 | ---@param load_lib boolean Load the build shared lib as a Lua module 102 | function M.install(zig_ver, root, load_lib) 103 | local raw_os = vim.loop.os_uname().sysname 104 | local raw_arch = jit.arch 105 | 106 | local os = os_patterns[raw_os] 107 | local ext = file_patterns[raw_os] 108 | local arch = arch_patterns[raw_arch] 109 | 110 | M.root_dir = root 111 | M.build_dir = utils.path_append(M.root_dir, "build") 112 | vim.fn.mkdir(M.build_dir, "p") 113 | 114 | -- Pattern is "zig---" for the final directory 115 | -- Plus for the archive being downloaded 116 | local base_url = "https://ziglang.org/download/" .. zig_ver .. "/" 117 | local target_triple = os .. "-" .. arch .. "-" .. zig_ver 118 | local output_dir = "zig-" .. target_triple 119 | M.tarball = output_dir .. ext 120 | local download_url = base_url .. M.tarball 121 | M.zig_binary = M.build_dir .. "/" .. output_dir .. "/zig" 122 | 123 | -- Download to a temporary file in Neovim's tmp directory 124 | M.curl_cmd = { "curl", "-sL", "-o", M.build_dir .. "/" .. M.tarball, download_url } 125 | M.tar_cmd = { "tar", "-xvf", M.tarball, "-C", M.build_dir } 126 | M.build_cmd = { M.zig_binary, "build", "-Doptimize=ReleaseSafe" } 127 | 128 | local callbacks = { 129 | on_sterr = vim.schedule_wrap(function(_, data, _) 130 | local out = table.concat(data, "\n") 131 | vim.notify(out, vim.log.levels.ERROR) 132 | end), 133 | on_exit = vim.schedule_wrap(function() 134 | -- Extract the zig compiler tarball 135 | M.install_zig() 136 | 137 | -- Build the zigdown project 138 | M.build_zigdown(load_lib) 139 | 140 | -- Now that the module is built, import it 141 | if load_lib then 142 | M.zigdown = M.load_module() 143 | end 144 | end), 145 | } 146 | 147 | vim.notify("Building zigdown; Please wait...", vim.log.levels.WARN) 148 | return vim.fn.jobstart(M.curl_cmd, callbacks) 149 | end 150 | 151 | return M 152 | 153 | -------------------------------------------------------------------------------- /lua/zigdown/render.lua: -------------------------------------------------------------------------------- 1 | local utils = require "zigdown.utils" 2 | local build = require "zigdown.build" 3 | 4 | local M = {} 5 | M.root = utils.parent_dir(utils.parent_dir(utils.script_dir())) 6 | 7 | local zigdown = nil 8 | local job_id = nil -- Job ID of render process (cat to term) 9 | 10 | local config = { 11 | src_win = nil, 12 | dest_win = nil, 13 | job_id = nil, 14 | src_buf = nil, 15 | dest_buf = nil, 16 | win_width = nil, 17 | } 18 | 19 | -- Render the file using a system command ('/path/to/zigdown -c filename') 20 | ---@param filename string The absolute path to the file to render 21 | function M.render_file(filename) 22 | -- If we don't already have a preview window open, open one 23 | config.src_win = vim.fn.win_getid() 24 | config.src_buf = vim.fn.bufnr() 25 | local wins = vim.api.nvim_list_wins() 26 | if #wins < 2 then 27 | vim.cmd('vsplit') 28 | wins = vim.api.nvim_list_wins() 29 | config.src_win = wins[1] 30 | end 31 | config.dest_win = wins[#wins] 32 | vim.api.nvim_set_current_win(config.dest_win) 33 | 34 | -- Create an autocmd group for the auto-update (live preview) 35 | vim.api.nvim_create_augroup("ZigdownGrp", { clear = true }) 36 | vim.api.nvim_create_autocmd("BufWritePost", { 37 | pattern = "*.md", 38 | command = ":Zigdown", 39 | group = "ZigdownGrp", 40 | }) 41 | 42 | -- Create a fresh buffer (delete existing if needed) 43 | if config.dest_buf ~= nil then 44 | vim.api.nvim_win_set_buf(config.dest_win, config.dest_buf) 45 | vim.cmd("Kwbd") 46 | end 47 | config.dest_buf = vim.api.nvim_create_buf(true, true) 48 | vim.api.nvim_win_set_buf(config.dest_win, config.dest_buf) 49 | vim.api.nvim_buf_attach(config.dest_buf, false, { 50 | on_detach = function() 51 | config.dest_buf = nil 52 | config.win_width = nil 53 | end, 54 | }) 55 | config.win_width = vim.api.nvim_win_get_width(config.dest_win) 56 | 57 | -- Create a tmp output dir (sorry, Linux only right now) 58 | local cbs = { 59 | on_exit = function() 60 | vim.api.nvim_set_current_win(config.dest_win) 61 | vim.api.nvim_set_current_buf(config.dest_buf) 62 | -- Rename the term window to a temp file with a consistent name 63 | vim.cmd("keepalt file zd-render") 64 | -- Return to the source window 65 | vim.api.nvim_set_current_win(config.src_win) 66 | end, 67 | } 68 | 69 | local zd_bin = utils.path_append(M.root, "zig-out/bin/zigdown") 70 | local zd_cmd = { zd_bin, "console", "-t", filename } 71 | if config.win_width ~= nil then 72 | table.insert(zd_cmd, "-w") 73 | table.insert(zd_cmd, math.min(config.win_width - 4, 100)) 74 | end 75 | job_id = vim.fn.termopen(zd_cmd, cbs) 76 | end 77 | 78 | 79 | -- Create a temporary file containing the given contents 80 | -- 'contents' must be a list containing the lines of the file 81 | local function create_tmp_file(contents) 82 | -- Dump the output to a tmp file 83 | local tmp = vim.fn.tempname() .. ".md" 84 | vim.fn.writefile(contents, tmp) 85 | return tmp 86 | end 87 | 88 | -- Get the contents of the given buffer as a single string with unix line endings 89 | local function buffer_to_string(bufnr) 90 | local content = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) 91 | return table.concat(content, "\n") 92 | end 93 | 94 | -- Display the rendered 'content' to the terminal buffer in 'wins.dest' 95 | function M.display_content(content) 96 | utils.stop_job(job_id) 97 | 98 | -- Create an autocmd group to automatically re-render the buffer upon save 99 | -- (Effectively a live preview pane) 100 | vim.api.nvim_create_augroup("ZigdownGrp", { clear = true }) 101 | vim.api.nvim_create_autocmd("BufWritePost", { 102 | pattern = "*.md", 103 | command = ":Zigdown", 104 | group = "ZigdownGrp", 105 | }) 106 | 107 | -- Create a fresh buffer (delete existing if needed) 108 | if config.dest_buf ~= nil then 109 | vim.api.nvim_win_set_buf(config.dest_win, config.dest_buf) 110 | vim.cmd("Kwbd") 111 | end 112 | vim.api.nvim_set_current_win(config.dest_win) 113 | config.dest_buf = vim.api.nvim_create_buf(true, true) 114 | vim.api.nvim_win_set_buf(config.dest_win, config.dest_buf) 115 | vim.api.nvim_buf_attach(config.dest_buf, false, { 116 | on_detach = function() 117 | config.dest_buf = nil 118 | end, 119 | }) 120 | 121 | -- Place the rendered output in a temp file so we can 'cat' it in a terminal buffer 122 | -- (We need the terminal buffer to do the ANSI rendering for us) 123 | local tmp_file = create_tmp_file(content) 124 | local cmd_args = { "cat", tmp_file } 125 | local cmd = table.concat(cmd_args, " ") 126 | 127 | -- Keep the render window open with a fixed name, delete the temp file, 128 | -- and switch back to the Markdown source window upon finishing the rendering 129 | local cbs = { 130 | on_exit = function() 131 | vim.api.nvim_set_current_win(config.dest_win) 132 | vim.api.nvim_set_current_buf(config.dest_buf) 133 | vim.cmd("keepalt file zd-render") 134 | if tmp_file ~= nil then 135 | vim.fn.delete(tmp_file) 136 | end 137 | -- Why does this not work? 138 | vim.print("Resetting to original window and buffer") 139 | vim.api.nvim_set_current_win(config.src_win) 140 | vim.api.nvim_set_current_buf(config.src_buf) 141 | end, 142 | } 143 | 144 | -- Execute the job 145 | job_id = vim.fn.termopen(cmd, cbs) 146 | 147 | vim.api.nvim_set_current_win(config.src_win) 148 | vim.api.nvim_set_current_buf(config.src_buf) 149 | end 150 | 151 | -- Render the given buffer using Zigdown as a Lua plugin 152 | -- The final "render" step cats the output to a terminal 153 | ---@param bufnr integer Source buffer index 154 | function M.render_buffer(bufnr) 155 | config.src_buf = bufnr 156 | if zigdown == nil then 157 | zigdown = build.load_module() 158 | end 159 | 160 | local wins = utils.setup_window_spilt(config.dest_win) 161 | config.src_win = wins.source 162 | config.dest_win = wins.dest 163 | local cols = vim.api.nvim_win_get_width(config.src_win) - 6 164 | 165 | local content = buffer_to_string(0) 166 | local output = zigdown.render_markdown(content, cols) 167 | 168 | M.display_content(vim.split(output, "\n")) 169 | end 170 | 171 | -- Clear the Zigdown autocommand group 172 | -- This cancels the automatic render-on-save 173 | function M.clear_autogroup() 174 | vim.api.nvim_create_augroup("ZigdownGrp", { clear = true }) 175 | end 176 | 177 | return M 178 | -------------------------------------------------------------------------------- /lua/zigdown/utils.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | -- Stop the running Vim background job 4 | ---@param job_id number|nil The Vim job ID to stop 5 | function M.stop_job(job_id) 6 | if job_id == nil then 7 | return 8 | end 9 | vim.fn.jobstop(job_id) 10 | end 11 | 12 | -- Check if filesystem is Windows or not 13 | ---@return boolean 14 | function M.is_windows() 15 | return package.config:sub(1, 1) == '\\' 16 | end 17 | 18 | -- Get the path separator for our system ('/' normally; '\' if Windows) 19 | ---@return string 20 | function M.get_path_separator() 21 | if M.is_windows() then 22 | return '\\' 23 | end 24 | return '/' 25 | end 26 | 27 | -- Append a file or folder to the existing path 28 | ---@param path string The base path 29 | ---@param new string The path to append to the base path 30 | ---@return string The full normalized path 31 | function M.path_append(path, new) 32 | return vim.fs.normalize(path .. M.get_path_separator() .. new) 33 | end 34 | 35 | -- Get the current script's directory 36 | -- Because this is a method on a table, its scope is the scope 37 | -- of the script calling the method 38 | ---@return string The path of the script calling the function 39 | function M.script_dir() 40 | local str = debug.getinfo(2, 'S').source:sub(2) 41 | if M.is_windows() then 42 | str = str:gsub('/', '\\') 43 | end 44 | return str:match('(.*' .. M.get_path_separator() .. ')') 45 | end 46 | 47 | -- Get the parent directory of the given [file or directory] 48 | -- If the given path ends with the path separator (e.g. /home/user/), 49 | -- then 'dirname' returns the same string but minus the trailing path separator. 50 | -- In these cases, we must call 'dirname' a 2nd time to get the true parent. 51 | ---@param file string Starting file or directory 52 | function M.parent_dir(file) 53 | local parent = vim.fs.dirname(file) 54 | if string.sub(file, -1) == '/' or string.sub(file, -1) == M.get_path_separator() then 55 | parent = vim.fs.dirname(parent) 56 | end 57 | return parent 58 | end 59 | 60 | -- Get the root of our Zigdown project 61 | function M.get_zigdown_root() 62 | return M.parent_dir(M.parent_dir(M.script_dir())) 63 | end 64 | 65 | -- Check if the given file exists 66 | function M.file_exists(name) 67 | local f = io.open(name, "r") 68 | return f ~= nil and io.close(f) 69 | end 70 | 71 | -- Create a vertical split if we don't already have one 72 | ---@return table: The source and destination windows of the new split view 73 | function M.setup_window_spilt(dest_win, dest_buf) 74 | if dest_win ~= nil then 75 | vim.api.nvim_win_close(dest_win, false) 76 | end 77 | -- If we don't already have a preview window open, open one 78 | local src_win = vim.api.nvim_get_current_win() 79 | local wins = vim.api.nvim_list_wins() 80 | if #wins < 2 then 81 | vim.cmd('vsplit') 82 | wins = vim.api.nvim_list_wins() 83 | end 84 | dest_win = wins[#wins] 85 | vim.api.nvim_set_current_win(dest_win) 86 | 87 | return { source = src_win, dest = dest_win } 88 | end 89 | 90 | return M 91 | -------------------------------------------------------------------------------- /plugin/zigdown.lua: -------------------------------------------------------------------------------- 1 | -- Expose relevant functions as user commands 2 | vim.api.nvim_create_user_command('Zigdown', function() require('zigdown').render_current_buffer() end, {}) 3 | vim.api.nvim_create_user_command('ZigdownCancel', function() require('zigdown').cancel_auto_render() end, {}) 4 | vim.api.nvim_create_user_command('ZigdownRebuild', function() require('zigdown').install() end, {}) 5 | -------------------------------------------------------------------------------- /sample-render-html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JacobCrabill/zigdown/33e64b0f27f7e4ef4565af83fd881b3c26087cb1/sample-render-html.png -------------------------------------------------------------------------------- /sample-render.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JacobCrabill/zigdown/33e64b0f27f7e4ef4565af83fd881b3c26087cb1/sample-render.png -------------------------------------------------------------------------------- /src/app/RawTTY.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | 4 | const Allocator = std.mem.Allocator; 5 | const ArrayList = std.ArrayList; 6 | const Dir = std.fs.Dir; 7 | const File = std.fs.File; 8 | const os = std.os; 9 | 10 | const Self = @This(); 11 | 12 | tty: File = undefined, 13 | orig_termios: std.c.termios = undefined, 14 | writer: std.io.Writer(File, File.WriteError, File.write) = undefined, 15 | 16 | pub fn init() !Self { 17 | // Store the original terminal settings for later 18 | // Apply the settings to enable raw TTY ('uncooked' terminal input) 19 | const tty = std.io.getStdIn(); 20 | 21 | var orig_termios: std.c.termios = undefined; 22 | _ = std.c.tcgetattr(tty.handle, &orig_termios); 23 | var raw = orig_termios; 24 | 25 | raw.lflag = std.c.tc_lflag_t{ 26 | .ECHO = false, 27 | .ICANON = false, 28 | .ISIG = false, 29 | .IEXTEN = false, 30 | }; 31 | 32 | raw.iflag = std.c.tc_iflag_t{ 33 | .IXON = false, 34 | .ICRNL = false, 35 | .BRKINT = false, 36 | .INPCK = false, 37 | .ISTRIP = false, 38 | }; 39 | 40 | raw.cc[@intFromEnum(std.c.V.TIME)] = 0; 41 | raw.cc[@intFromEnum(std.c.V.MIN)] = 1; 42 | _ = std.c.tcsetattr(tty.handle, .FLUSH, &raw); 43 | 44 | const writer = std.io.getStdOut().writer(); // tty.writer(); 45 | 46 | try writer.writeAll("\x1B[?25l"); // Hide the cursor 47 | try writer.writeAll("\x1B[s"); // Save cursor position 48 | try writer.writeAll("\x1B[?47h"); // Save screen 49 | try writer.writeAll("\x1B[?1049h"); // Enable alternative buffer 50 | 51 | return Self{ 52 | .tty = tty, 53 | .writer = writer, 54 | .orig_termios = orig_termios, 55 | }; 56 | } 57 | 58 | pub fn deinit(self: Self) void { 59 | _ = std.c.tcsetattr(self.tty.handle, .FLUSH, &self.orig_termios); 60 | 61 | self.writer.writeAll("\x1B[?1049l") catch {}; // Disable alternative buffer 62 | self.writer.writeAll("\x1B[?47l") catch {}; // Restore screen 63 | self.writer.writeAll("\x1B[u") catch {}; // Restore cursor position 64 | self.writer.writeAll("\x1B[?25h") catch {}; // Show the cursor 65 | 66 | self.tty.close(); 67 | } 68 | 69 | pub fn read(self: Self) u8 { 70 | while (true) { 71 | var buffer: [1]u8 = undefined; 72 | const nb = self.tty.read(&buffer) catch return 0; 73 | if (nb < 1) continue; 74 | return buffer[0]; 75 | } 76 | } 77 | 78 | fn moveCursor(self: Self, row: usize, col: usize) !void { 79 | _ = try self.writer.print("\x1B[{};{}H", .{ row + 1, col + 1 }); 80 | } 81 | -------------------------------------------------------------------------------- /src/app/lua_api.zig: -------------------------------------------------------------------------------- 1 | //! Registering a Zig function to be called from Lua 2 | //! This creates a shared library that can be imported from a Lua program, e.g.: 3 | //! > mylib = require('zig-mod') 4 | //! > print( mylib.adder(40, 2) ) 5 | //! 42 6 | 7 | const std = @import("std"); 8 | const zd = @import("zigdown"); 9 | 10 | // The code here is specific to Lua 5.1 11 | // This has been tested with LuaJIT 5.1, specifically 12 | pub const c = @cImport({ 13 | @cInclude("luaconf.h"); 14 | @cInclude("lua.h"); 15 | @cInclude("lualib.h"); 16 | @cInclude("lauxlib.h"); 17 | }); 18 | 19 | // It can be convenient to store a short reference to the Lua struct when 20 | // it is used multiple times throughout a file. 21 | const LuaState = c.lua_State; 22 | const FnReg = c.luaL_Reg; 23 | 24 | /// A Zig function called by Lua must accept a single ?*LuaState parameter and must 25 | /// return a c_int representing the number of return values pushed onto the stack 26 | export fn adder(lua: ?*LuaState) callconv(.C) c_int { 27 | const a = c.lua_tointeger(lua, 1); 28 | const b = c.lua_tointeger(lua, 2); 29 | c.lua_pushinteger(lua, a + b); 30 | return 1; 31 | } 32 | 33 | export fn render_markdown(lua: ?*LuaState) callconv(.C) c_int { 34 | // Markdown text to render 35 | var len: usize = 0; 36 | const input_a: [*c]const u8 = c.lua_tolstring(lua, 1, &len); 37 | const input: []const u8 = input_a[0..len]; 38 | const alloc = std.heap.page_allocator; 39 | 40 | // Number of columns to render (output width) 41 | //const cols: usize = @intCast(c.lua_tointeger(lua, 2)); 42 | const cols: usize = 90; 43 | 44 | // Parse the input text 45 | const opts = zd.parser.ParserOpts{ .copy_input = false, .verbose = false }; 46 | var parser = zd.Parser.init(alloc, opts); 47 | defer parser.deinit(); 48 | 49 | parser.parseMarkdown(input) catch @panic("Parse error!"); // TODO: better error handling? 50 | const md: zd.Block = parser.document; 51 | 52 | // Create a buffer to render into - Note that Lua creates its own copy internally 53 | var buffer = std.ArrayList(u8).init(alloc); 54 | defer buffer.deinit(); 55 | 56 | // Render the AST to the buffer 57 | // TODO: Configure the cwd for the renderer (For use with evaluating links/paths) 58 | // TODO: Configure the render width 59 | zd.render.render(.{ 60 | .alloc = alloc, 61 | .document = md, 62 | .document_dir = null, 63 | .out_stream = buffer.writer().any(), 64 | .width = @max(@min(cols, 120), 40), 65 | .method = .console, 66 | }) catch @panic("Render error!"); 67 | 68 | // Push the rendered string to the Lua stack 69 | c.lua_pushlstring(lua, @ptrCast(buffer.items), buffer.items.len); 70 | 71 | return 1; 72 | } 73 | 74 | /// I recommend using ZLS (the Zig language server) for autocompletion to help 75 | /// find relevant Lua function calls like pushstring, tostring, etc. 76 | export fn hello(lua: ?*LuaState) callconv(.C) c_int { 77 | c.lua_pushstring(lua, "Hello, LuaJIT!"); 78 | return 1; 79 | } 80 | 81 | /// Function registration struct for the 'adder' function 82 | const adder_reg: FnReg = .{ .name = "adder", .func = adder }; 83 | const hello_reg: FnReg = .{ .name = "hello", .func = hello }; 84 | const render_reg: FnReg = .{ .name = "render_markdown", .func = render_markdown }; 85 | 86 | /// The list of function registrations for our library 87 | /// Note that the last entry must be empty/null as a sentinel value to the luaL_register function 88 | const lib_fn_reg = [_]FnReg{ adder_reg, hello_reg, render_reg, FnReg{} }; 89 | 90 | /// Register the function with Lua using the special luaopen_x function 91 | /// This is the entrypoint into the library from a Lua script 92 | export fn luaopen_zigdown_lua(lua: ?*LuaState) callconv(.C) c_int { 93 | c.luaL_register(lua.?, "zigdown_lua", @ptrCast(&lib_fn_reg[0])); 94 | return 1; 95 | } 96 | -------------------------------------------------------------------------------- /src/app/present.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const zd = @import("zigdown"); 3 | const RawTTY = @import("RawTTY.zig"); 4 | 5 | const Allocator = std.mem.Allocator; 6 | const ArrayList = std.ArrayList; 7 | const Dir = std.fs.Dir; 8 | const File = std.fs.File; 9 | 10 | const gfx = zd.gfx; 11 | const cons = zd.cons; 12 | const Parser = zd.Parser; 13 | const ConsoleRenderer = zd.ConsoleRenderer; 14 | 15 | /// Where the files to render come from 16 | pub const Source = struct { 17 | /// The directory which 'dirname' is relative to 18 | dir: Dir = undefined, 19 | slides: ?File = null, 20 | root: []const u8 = undefined, 21 | }; 22 | 23 | /// Begin the slideshow using all slides within 'dir' at the sub-path 'dirname' 24 | /// 25 | /// alloc: The allocator to use for all file reading, parsing, and rendering 26 | /// source: Struct specifying the location of the slides (.md files) to render 27 | /// recurse: If true, all *.md files in all child directories of {dir}/{dirname} will be used 28 | pub fn present(alloc: Allocator, writer: std.io.AnyWriter, source: Source, recurse: bool) !void { 29 | const raw_tty = try RawTTY.init(); 30 | defer raw_tty.deinit(); 31 | 32 | // Store all of the Markdown file paths, in iterator order 33 | var slides = ArrayList([]const u8).init(alloc); 34 | defer { 35 | for (slides.items) |slide| { 36 | alloc.free(slide); 37 | } 38 | slides.deinit(); 39 | } 40 | 41 | if (source.slides) |file| { 42 | try loadSlidesFromFile(alloc, source.dir, file, &slides); 43 | } else { 44 | try loadSlidesFromDirectory(alloc, source.dir, recurse, &slides); 45 | 46 | // Sort the slides 47 | std.sort.heap([]const u8, slides.items, {}, cmpStr); 48 | } 49 | 50 | if (slides.items.len == 0) { 51 | errdefer std.debug.print("Error: No slides found!\n", .{}); 52 | return error.NoSlidesFound; 53 | } 54 | 55 | // Begin the presentation, using stdin to go forward/backward 56 | var i: usize = 0; 57 | var update: bool = true; 58 | var quit: bool = false; 59 | while (!quit) { 60 | if (update) { 61 | const slide: []const u8 = slides.items[i]; 62 | const file = try std.fs.openFileAbsolute(slide, .{}); 63 | defer file.close(); 64 | try renderFile(alloc, writer, source.root, file, i + 1, slides.items.len); 65 | update = false; 66 | } 67 | 68 | // Check for a keypress to advance to the next slide 69 | switch (raw_tty.read()) { 70 | 'n', 'j', 'l' => { // Next Slide 71 | if (i < slides.items.len - 1) { 72 | i += 1; 73 | update = true; 74 | } 75 | }, 76 | 'p', 'h', 'k' => { // Previous Slide 77 | if (i > 0) { 78 | i -= 1; 79 | update = true; 80 | } 81 | }, 82 | 'q' => { // Quit 83 | quit = true; 84 | }, 85 | 27 => { // Escape (0x1b) 86 | if (raw_tty.read() == 91) { // 0x5b (??) 87 | switch (raw_tty.read()) { 88 | 66, 67 => { // Down, Right -- Next Slide 89 | if (i < slides.items.len - 1) { 90 | i += 1; 91 | update = true; 92 | } 93 | }, 94 | 65, 68 => { // Up, Left -- Previous Slide 95 | if (i > 0) { 96 | i -= 1; 97 | update = true; 98 | } 99 | }, 100 | else => {}, 101 | } 102 | } 103 | }, 104 | else => {}, 105 | } 106 | } 107 | } 108 | 109 | /// Read a given Markdown file from a directory and render it to the terminal 110 | /// Also render a slide counter in the bottom-right corner 111 | /// The given directory is used as the 'root_dir' option for the renderer - 112 | /// this is used to determine the path to relative includes such as images 113 | /// and links 114 | fn renderFile(alloc: Allocator, writer: anytype, dir: []const u8, file: File, slide_no: usize, n_slides: usize) !void { 115 | var arena = std.heap.ArenaAllocator.init(alloc); 116 | defer arena.deinit(); 117 | 118 | // Read slide file 119 | const md_text = try file.readToEndAlloc(arena.allocator(), 1e9); 120 | 121 | // Parse slide 122 | var parser: Parser = Parser.init(arena.allocator(), .{ .copy_input = false, .verbose = false }); 123 | defer parser.deinit(); 124 | 125 | try parser.parseMarkdown(md_text); 126 | 127 | // Clear the screen 128 | // Get the terminal size; limit our width to that 129 | // Some tools like `fzf --preview` cause the getTerminalSize() to fail, so work around that 130 | // Kinda hacky, but :shrug: 131 | var columns: usize = 90; 132 | const tsize = gfx.getTerminalSize() catch gfx.TermSize{}; 133 | if (tsize.cols > 0) { 134 | columns = @min(tsize.cols, columns); 135 | } 136 | 137 | _ = try writer.write(cons.clear_screen); 138 | try writer.print(cons.set_row_col, .{ 0, 0 }); 139 | 140 | // Render slide 141 | const opts = ConsoleRenderer.RenderOpts{ 142 | .root_dir = dir, 143 | .indent = 2, 144 | .width = columns - 2, 145 | .out_stream = std.io.getStdOut().writer().any(), 146 | .max_image_cols = columns - 4, 147 | .termsize = tsize, 148 | }; 149 | var c_renderer = ConsoleRenderer.init(arena.allocator(), opts); 150 | defer c_renderer.deinit(); 151 | try c_renderer.renderBlock(parser.document); 152 | 153 | // Display slide number 154 | try writer.print(cons.set_row_col, .{ tsize.rows - 1, tsize.cols - 8 }); 155 | try writer.print("{d}/{d}", .{ slide_no, n_slides }); 156 | } 157 | 158 | /// Load all *.md files in the given directory; append their absolute paths to the 'slides' array 159 | /// dir: The directory to search 160 | /// recurse: If true, also recursively search all child directories of 'dir' 161 | /// slides: The array to append all slide filenames to 162 | fn loadSlidesFromDirectory(alloc: Allocator, dir: Dir, recurse: bool, slides: *ArrayList([]const u8)) !void { 163 | var iter = dir.iterate(); 164 | while (try iter.next()) |entry| { 165 | switch (entry.kind) { 166 | .file => { 167 | var path_buf: [std.fs.max_path_bytes]u8 = undefined; 168 | const realpath = dir.realpath(entry.name, &path_buf) catch |err| { 169 | std.debug.print("Error loading slide: {s}\n", .{entry.name}); 170 | return err; 171 | }; 172 | if (std.mem.eql(u8, ".md", std.fs.path.extension(realpath))) { 173 | std.debug.print("Adding slide: {s}\n", .{realpath}); 174 | const slide: []const u8 = try alloc.dupe(u8, realpath); 175 | try slides.append(slide); 176 | } 177 | }, 178 | .directory => { 179 | if (recurse) { 180 | const child_dir: Dir = try dir.openDir(entry.name, .{ .iterate = true }); 181 | try loadSlidesFromDirectory(alloc, child_dir, recurse, slides); 182 | } 183 | }, 184 | else => {}, 185 | } 186 | } 187 | } 188 | 189 | /// Load a list of slides to present from a single text file 190 | fn loadSlidesFromFile(alloc: Allocator, dir: Dir, file: File, slides: *ArrayList([]const u8)) !void { 191 | const buf = try file.readToEndAlloc(alloc, 1_000_000); 192 | defer alloc.free(buf); 193 | 194 | var lines = std.mem.splitScalar(u8, buf, '\n'); 195 | while (lines.next()) |name| { 196 | if (name.len < 1) break; 197 | 198 | var path_buf: [std.fs.max_path_bytes]u8 = undefined; 199 | const realpath = dir.realpath(name, &path_buf) catch |err| { 200 | std.debug.print("Error loading slide: {s}\n", .{name}); 201 | return err; 202 | }; 203 | if (std.mem.eql(u8, ".md", std.fs.path.extension(realpath))) { 204 | std.debug.print("Adding slide: {s}\n", .{realpath}); 205 | const slide: []const u8 = try alloc.dupe(u8, realpath); 206 | try slides.append(slide); 207 | } 208 | } 209 | } 210 | 211 | /// String comparator for standard ASCII ascending sort 212 | fn cmpStr(_: void, left: []const u8, right: []const u8) bool { 213 | const N = @min(left.len, right.len); 214 | for (0..N) |i| { 215 | if (left[i] > right[i]) 216 | return false; 217 | } 218 | 219 | if (left.len <= right.len) 220 | return true; 221 | 222 | return false; 223 | } 224 | -------------------------------------------------------------------------------- /src/app/serve.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const zd = @import("zigdown"); 4 | const md = @import("serve/markdown.zig"); 5 | const html = @import("assets").html; 6 | 7 | const Allocator = std.mem.Allocator; 8 | const ArrayList = std.ArrayList; 9 | const Dir = std.fs.Dir; 10 | 11 | const MimeMap = std.StringHashMap([]const u8); 12 | const RouteMap = std.StringHashMap(*const fn (r: *std.http.Server.Request) void); 13 | 14 | const log = std.log.scoped(.server); 15 | 16 | /// Configuration options to be passed in from main() 17 | pub const ServeOpts = struct { 18 | root_file: ?[]const u8 = null, 19 | root_directory: ?[]const u8 = null, 20 | port: u16 = 8000, 21 | }; 22 | 23 | const server_addr = "127.0.0.1"; 24 | 25 | /// Contains all data to be shared by all request handlers 26 | const Context = struct { 27 | alloc: Allocator = undefined, 28 | dir: Dir = undefined, 29 | dir_path: []const u8 = ".", 30 | file: ?[]const u8 = null, 31 | mimes: MimeMap = undefined, 32 | 33 | pub fn init(alloc: Allocator) !Context { 34 | return .{ 35 | .alloc = alloc, 36 | .mimes = try initMimeMap(alloc), 37 | }; 38 | } 39 | 40 | pub fn deinit(ctx: *Context) void { 41 | ctx.mimes.deinit(); 42 | } 43 | }; 44 | 45 | pub fn serve(alloc: std.mem.Allocator, config: ServeOpts) !void { 46 | var context = try Context.init(alloc); 47 | defer context.deinit(); 48 | context.dir = std.fs.cwd(); 49 | 50 | if (config.root_directory) |dir| { 51 | context.dir_path = dir; 52 | context.dir = try std.fs.cwd().openDir(dir, .{ .iterate = true }); 53 | } 54 | 55 | if (config.root_file) |file| { 56 | context.file = file; 57 | } 58 | 59 | // Parse the server address and start the server 60 | const address = std.net.Address.parseIp(server_addr, config.port) catch unreachable; 61 | var server = try address.listen(.{ .reuse_address = true }); 62 | defer server.deinit(); 63 | 64 | md.init(alloc, context.dir); 65 | 66 | var t_accept = try std.Thread.spawn(.{}, runServer, .{ &context, &server }); 67 | defer t_accept.join(); 68 | 69 | if (context.file) |file| { 70 | const url = try std.fmt.allocPrint(alloc, "http://localhost:{d}/{s}", .{ config.port, file }); 71 | defer alloc.free(url); 72 | var proc: std.process.Child = undefined; 73 | if (builtin.os.tag == .windows) { 74 | const argv = &[_][]const u8{ "start", url }; 75 | proc = std.process.Child.init(argv, alloc); 76 | } else { 77 | const argv = &[_][]const u8{ "xdg-open", url }; 78 | proc = std.process.Child.init(argv, alloc); 79 | } 80 | try proc.spawn(); 81 | } 82 | } 83 | 84 | /// Run the HTTP server forever 85 | fn runServer(context: *Context, server: *std.net.Server) !void { 86 | while (true) { 87 | const connection = try server.accept(); 88 | _ = std.Thread.spawn(.{}, accept, .{ context, connection }) catch |err| { 89 | std.log.err("unable to accept connection: {s}", .{@errorName(err)}); 90 | connection.stream.close(); 91 | continue; 92 | }; 93 | } 94 | } 95 | 96 | /// Accept a new connection request 97 | fn accept( 98 | context: *const Context, 99 | connection: std.net.Server.Connection, 100 | ) void { 101 | defer connection.stream.close(); 102 | 103 | var read_buffer: [8000]u8 = undefined; 104 | var server = std.http.Server.init(connection, &read_buffer); 105 | while (server.state == .ready) { 106 | var request = server.receiveHead() catch |err| switch (err) { 107 | error.HttpConnectionClosing => return, 108 | else => { 109 | std.log.err("closing http connection: {s}", .{@errorName(err)}); 110 | return; 111 | }, 112 | }; 113 | serveRequest(&request, context) catch |err| { 114 | std.log.err("unable to serve {s}: {s}", .{ request.head.target, @errorName(err) }); 115 | return; 116 | }; 117 | } 118 | } 119 | 120 | /// Serve an HTTP request 121 | fn serveRequest(request: *std.http.Server.Request, context: *const Context) !void { 122 | const path = request.head.target; 123 | 124 | if (std.mem.endsWith(u8, path, ".md")) { 125 | md.renderMarkdown(request); 126 | // try serveDocsFile(request, context, path, "text/html"); 127 | } else if (std.mem.indexOf(u8, path, "favicon")) |_| { 128 | try request.respond(html.favicon, .{ 129 | .extra_headers = &.{ 130 | .{ .name = "content-type", .value = "text/png" }, 131 | }, 132 | }); 133 | } else if (std.mem.eql(u8, path, "/style.css")) { 134 | try request.respond(html.style_css, .{ 135 | .extra_headers = &.{ 136 | .{ .name = "content-type", .value = "text/css" }, 137 | }, 138 | }); 139 | } else { 140 | serveFile(request, context) catch { 141 | try request.respond(html.error_page, .{ 142 | .extra_headers = &.{ 143 | .{ .name = "content-type", .value = "text/html" }, 144 | cache_control_header, 145 | }, 146 | }); 147 | }; 148 | } 149 | } 150 | 151 | /// Tell the browser not to cache the result, so that a simple page refresh 152 | /// will properly show any changes to that page 153 | const cache_control_header: std.http.Header = .{ 154 | .name = "cache-control", 155 | .value = "max-age=0, must-revalidate", 156 | }; 157 | 158 | fn serveFile( 159 | request: *std.http.Server.Request, 160 | context: *const Context, 161 | ) !void { 162 | std.debug.assert(std.mem.startsWith(u8, request.head.target, "/")); 163 | const path = request.head.target[1..]; 164 | 165 | const ftype: []const u8 = std.fs.path.extension(path); 166 | const content_type = context.mimes.get(ftype) orelse "text/plain"; 167 | 168 | const file_contents = try context.dir.readFileAlloc(context.alloc, path, 10 * 1024 * 1024); 169 | defer context.alloc.free(file_contents); 170 | 171 | try request.respond(file_contents, .{ 172 | .extra_headers = &.{ 173 | .{ .name = "content-type", .value = content_type }, 174 | cache_control_header, 175 | }, 176 | }); 177 | } 178 | 179 | /// Setup our basic MIME types map 180 | pub fn initMimeMap(alloc: Allocator) !MimeMap { 181 | var mimes = MimeMap.init(alloc); 182 | 183 | // Text Files 184 | try mimes.put(".html", "text/html"); 185 | try mimes.put(".css", "text/css"); 186 | 187 | // Scripts / executable code 188 | try mimes.put(".js", "application/javascript"); 189 | try mimes.put(".wasm", "application/wasm"); 190 | 191 | // Image Files 192 | try mimes.put(".jpg", "image/jpeg"); 193 | try mimes.put(".jpeg", "image/jpeg"); 194 | try mimes.put(".JPG", "image/jpeg"); 195 | try mimes.put(".JPEG", "image/jpeg"); 196 | try mimes.put(".png", "image/png"); 197 | try mimes.put(".PNG", "image/png"); 198 | 199 | return mimes; 200 | } 201 | -------------------------------------------------------------------------------- /src/app/serve/markdown.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const zap = @import("zap"); 3 | const zd = @import("zigdown"); 4 | const html = @import("assets").html; 5 | 6 | const Allocator = std.mem.Allocator; 7 | 8 | pub const Self = @This(); 9 | 10 | var alloc: Allocator = undefined; 11 | var root_dir: std.fs.Dir = undefined; 12 | 13 | pub fn init( 14 | a: std.mem.Allocator, 15 | dir: std.fs.Dir, 16 | ) void { 17 | alloc = a; 18 | root_dir = dir; 19 | } 20 | 21 | pub fn deinit() void {} 22 | 23 | /// Respond to the request with our predefined, generic error page 24 | fn sendErrorPage(r: *std.http.Server.Request, status: std.http.Status) void { 25 | r.respond(html.error_page, .{ 26 | .status = status, 27 | .extra_headers = &.{ 28 | .{ .name = "content-type", .value = "text/html" }, 29 | }, 30 | }) catch return; 31 | } 32 | 33 | /// Render a Markdown file to HTML and send it back as an HTTP response 34 | pub fn renderMarkdown(r: *std.http.Server.Request) void { 35 | const path = r.head.target; 36 | 37 | if (!std.mem.endsWith(u8, path, ".md")) { 38 | return; 39 | } 40 | std.log.debug("Rendering file: {s}", .{path}); 41 | 42 | const md_html = renderMarkdownImpl(path) orelse { 43 | sendErrorPage(r, .internal_server_error); 44 | return; 45 | }; 46 | defer alloc.free(md_html); 47 | 48 | const body_template = 49 | \\ 50 | \\ 51 | \\ {s} 52 | \\ 53 | ; 54 | const body = std.fmt.allocPrint(alloc, body_template, .{ html.style_css, md_html }) catch unreachable; 55 | defer alloc.free(body); 56 | r.respond(body, .{ 57 | .status = .created, 58 | .extra_headers = &.{ 59 | .{ .name = "content-type", .value = "text/html" }, 60 | }, 61 | }) catch return; 62 | } 63 | 64 | fn renderMarkdownImpl(path: []const u8) ?[]const u8 { 65 | // Determine the file to open 66 | const prefix = "/"; 67 | std.debug.assert(std.mem.startsWith(u8, path, prefix)); 68 | 69 | if (path.len <= prefix.len + 1) 70 | return null; 71 | 72 | const sub_path = path[prefix.len..]; 73 | 74 | // Open file 75 | var file: std.fs.File = root_dir.openFile(sub_path, .{ .mode = .read_only }) catch |err| { 76 | std.log.err("Error opening markdown file {s}: {any}", .{ sub_path, err }); 77 | return null; 78 | }; 79 | defer file.close(); 80 | 81 | const md_text = file.readToEndAlloc(alloc, 1_000_000) catch |err| { 82 | std.log.err("Error reading file: {any}", .{err}); 83 | return null; 84 | }; 85 | defer alloc.free(md_text); 86 | 87 | // Parse page 88 | var parser: zd.Parser = zd.Parser.init(alloc, .{ .copy_input = false, .verbose = false }); 89 | defer parser.deinit(); 90 | 91 | parser.parseMarkdown(md_text) catch |err| { 92 | std.log.err("Error parsing markdown file: {any}", .{err}); 93 | return null; 94 | }; 95 | 96 | // Create the output buffe catch returnr 97 | var buf = std.ArrayList(u8).init(alloc); 98 | defer buf.deinit(); 99 | 100 | // Render slide 101 | var h_renderer = zd.HtmlRenderer.init(buf.writer().any(), alloc); 102 | defer h_renderer.deinit(); 103 | 104 | h_renderer.renderBlock(parser.document) catch |err| { 105 | std.log.err("Error rendering HTML from markdown: {any}", .{err}); 106 | return null; 107 | }; 108 | 109 | return buf.toOwnedSlice() catch return null; 110 | } 111 | -------------------------------------------------------------------------------- /src/app/wasm/stdlib.c: -------------------------------------------------------------------------------- 1 | // This file implements a very simple allocator for external scanners running 2 | // in WASM. Allocation is just bumping a static pointer and growing the heap 3 | // as needed, and freeing is mostly a noop. But in the special case of freeing 4 | // the last-allocated pointer, we'll reuse that pointer again. 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | extern void tree_sitter_debug_message(const char *, size_t); 14 | 15 | #define PAGESIZE 0x10000 16 | #define MAX_HEAP_SIZE (4 * 1024 * 1024) 17 | 18 | // // TODO: May need a proper implementation! This is just a placeholder for now. 19 | // bool iswspace(int wc) { 20 | // const int wspace_chars[] = { ' ', '\t', '\n', '\r', 0x0B, 0x0C }; 21 | // for (size_t i = 0; i < sizeof(wspace_chars); i++) { 22 | // if (wc == wspace_chars[i]) 23 | // return true; 24 | // } 25 | // return false; 26 | // } 27 | // 28 | // // TODO: May need a proper implementation! This is just a placeholder for now. 29 | // bool iswalpha(int wc) { 30 | // const bool is_cap = (wc >= (int)'A' && wc <= (int)'Z'); 31 | // const bool is_lower = (wc >= (int)'a' && wc <= (int)'z'); 32 | // return is_cap || is_lower; 33 | // } 34 | // 35 | // bool iswalnum(int wc) { 36 | // const bool is_cap = (wc >= (int)'A' && wc <= (int)'Z'); 37 | // const bool is_lower = (wc >= (int)'a' && wc <= (int)'z'); 38 | // const bool is_number = (wc >= (int)'0' && wc <= (int)'9'); 39 | // return is_cap || is_lower || is_number; 40 | // } 41 | // 42 | // int strcmp(const char *s1, const char *s2) { 43 | // const unsigned char *p1 = ( const unsigned char * )s1; 44 | // const unsigned char *p2 = ( const unsigned char * )s2; 45 | // 46 | // while ( *p1 && *p1 == *p2 ) { 47 | // ++p1; ++p2; 48 | // } 49 | // 50 | // return *p1 - *p2; 51 | // } 52 | // 53 | // 54 | // int strncmp(const char *s1, const char *s2, size_t n) { 55 | // const unsigned char *p1 = ( const unsigned char * )s1; 56 | // const unsigned char *p2 = ( const unsigned char * )s2; 57 | // 58 | // size_t i = 0; 59 | // while ( *p1 && i < n && *p1 == *p2 ) { 60 | // ++p1; ++p2; ++i; 61 | // } 62 | // 63 | // return *p1 - *p2; 64 | // } 65 | 66 | int fprintf(FILE *stream, const char *format, ...) { 67 | // Is this even relevant for WASM? Probably not. 68 | return 0; 69 | } 70 | 71 | // void __assert_fail() { 72 | // // do nothing 73 | // } 74 | 75 | typedef struct { 76 | size_t size; 77 | char data[0]; 78 | } Region; 79 | 80 | static Region *heap_end = NULL; 81 | static Region *heap_start = NULL; 82 | static Region *next = NULL; 83 | 84 | // Get the region metadata for the given heap pointer. 85 | static inline Region *region_for_ptr(void *ptr) { 86 | return ((Region *)ptr) - 1; 87 | } 88 | 89 | // Get the location of the next region after the given region, 90 | // if the given region had the given size. 91 | static inline Region *region_after(Region *self, size_t len) { 92 | char *address = self->data + len; 93 | char *aligned = (char *)((uint32_t)(address + 3) & ~0x3); 94 | return (Region *)aligned; 95 | } 96 | 97 | static void *get_heap_end() { 98 | return (void *)(__builtin_wasm_memory_size(0) * PAGESIZE); 99 | } 100 | 101 | static int grow_heap(size_t size) { 102 | size_t new_page_count = ((size - 1) / PAGESIZE) + 1; 103 | return __builtin_wasm_memory_grow(0, new_page_count) != MAX_HEAP_SIZE; // ??? SIZE_MAX; ??? 104 | } 105 | 106 | // Clear out the heap, and move it to the given address. 107 | void reset_heap(void *new_heap_start) { 108 | heap_start = new_heap_start; 109 | next = new_heap_start; 110 | heap_end = get_heap_end(); 111 | } 112 | 113 | void *malloc(size_t size) { 114 | Region *region_end = region_after(next, size); 115 | 116 | if (region_end > heap_end) { 117 | if ((char *)region_end - (char *)heap_start > MAX_HEAP_SIZE) { 118 | return NULL; 119 | } 120 | if (!grow_heap(size)) return NULL; 121 | heap_end = get_heap_end(); 122 | } 123 | 124 | void *result = &next->data; 125 | next->size = size; 126 | next = region_end; 127 | 128 | return result; 129 | } 130 | 131 | void free(void *ptr) { 132 | if (ptr == NULL) return; 133 | 134 | Region *region = region_for_ptr(ptr); 135 | Region *region_end = region_after(region, region->size); 136 | 137 | // When freeing the last allocated pointer, re-use that 138 | // pointer for the next allocation. 139 | if (region_end == next) { 140 | next = region; 141 | } 142 | } 143 | 144 | void *calloc(size_t count, size_t size) { 145 | void *result = malloc(count * size); 146 | memset(result, 0, count * size); 147 | return result; 148 | } 149 | 150 | void *realloc(void *ptr, size_t new_size) { 151 | if (ptr == NULL) { 152 | return malloc(new_size); 153 | } 154 | 155 | Region *region = region_for_ptr(ptr); 156 | Region *region_end = region_after(region, region->size); 157 | 158 | // When reallocating the last allocated region, return 159 | // the same pointer, and skip copying the data. 160 | if (region_end == next) { 161 | next = region; 162 | return malloc(new_size); 163 | } 164 | 165 | void *result = malloc(new_size); 166 | memcpy(result, ®ion->data, region->size); 167 | return result; 168 | } 169 | -------------------------------------------------------------------------------- /src/app/wasm/stdlib.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const alloc = std.heap.wasm_allocator; 4 | 5 | export fn strcmp(s1: [*:0]const u8, s2: [*:0]const u8) i32 { 6 | return switch (std.mem.orderZ(u8, s1, s2)) { 7 | .eq => 0, 8 | .lt => -1, 9 | .gt => 1, 10 | }; 11 | } 12 | 13 | export fn strncmp(s1: [*:0]const u8, s2: [*:0]const u8, n: usize) i32 { 14 | var i: usize = 0; 15 | while (s1[i] == s2[i] and s1[i] != 0 and i < n) : (i += 1) {} 16 | return switch (std.math.order(s1[i], s2[i])) { 17 | .eq => 0, 18 | .lt => -1, 19 | .gt => 1, 20 | }; 21 | } 22 | 23 | export fn iswspace(wc: i32) bool { 24 | return std.ascii.isWhitespace(@intCast(wc)); 25 | } 26 | 27 | export fn iswalpha(wc: i32) bool { 28 | return std.ascii.isAlphabetic(@intCast(wc)); 29 | } 30 | 31 | export fn iswalnum(wc: i32) bool { 32 | return std.ascii.isAlphanumeric(@intCast(wc)); 33 | } 34 | 35 | export fn __assert_fail() void { 36 | @panic("Assertion failed!"); 37 | } 38 | 39 | export fn abort() void { 40 | @panic("abort"); 41 | } 42 | 43 | export fn fputc(c: i32, stream: *std.c.FILE) i32 { 44 | _ = stream; 45 | return c; 46 | } 47 | 48 | export fn putc(c: i32, stream: *std.c.FILE) i32 { 49 | _ = stream; 50 | return c; 51 | } 52 | 53 | export fn putchar(c: i32) i32 { 54 | return c; 55 | } 56 | 57 | export fn fputs(s: [*:0]const u8, stream: *std.c.FILE) i32 { 58 | _ = s; 59 | _ = stream; 60 | return 0; 61 | } 62 | 63 | export fn puts(s: [*:0]const u8) i32 { 64 | _ = s; 65 | return 0; 66 | } 67 | -------------------------------------------------------------------------------- /src/app/wasm_main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const zd = @import("zigdown"); 3 | const stdlib = @import("wasm/stdlib.zig"); 4 | const wasm = zd.wasm; 5 | 6 | const ArrayList = std.ArrayList; 7 | 8 | const alloc = std.heap.wasm_allocator; 9 | 10 | const Imports = wasm.Imports; 11 | const Console = wasm.Console; 12 | const Renderer = wasm.Renderer; 13 | 14 | export fn allocUint8(n: usize) [*]u8 { 15 | const slice = alloc.alloc(u8, n) catch @panic("Unable to allocate memory!"); 16 | return slice.ptr; 17 | } 18 | 19 | export fn renderToHtml(md_ptr: [*:0]u8) void { 20 | const md_text: []const u8 = std.mem.span(md_ptr); 21 | 22 | // Parse the input text 23 | const opts = zd.parser.ParserOpts{ 24 | .copy_input = false, 25 | .verbose = false, 26 | }; 27 | Console.log("Parsing: {s}\n", .{md_text}); 28 | var parser = zd.Parser.init(alloc, opts); 29 | defer parser.deinit(); 30 | parser.parseMarkdown(md_text) catch |err| { 31 | Console.log("[parse] Caught Zig error: {any}\n", .{err}); 32 | }; 33 | 34 | Console.log("Rendering...\n", .{}); 35 | 36 | var h_renderer = zd.HtmlRenderer.init(Renderer.writer.any(), alloc); 37 | defer h_renderer.deinit(); 38 | h_renderer.renderBlock(parser.document) catch |err| { 39 | Console.log("[render] Caught Zig error: {any}\n", .{err}); 40 | }; 41 | Renderer.log("", .{}); 42 | Console.log("Rendered!\n", .{}); 43 | } 44 | -------------------------------------------------------------------------------- /src/assets/assets.zig: -------------------------------------------------------------------------------- 1 | pub const queries = @import("queries.zig"); 2 | pub const html = @import("html.zig"); 3 | -------------------------------------------------------------------------------- /src/assets/html.zig: -------------------------------------------------------------------------------- 1 | pub const style_css = @embedFile("html/style.css"); 2 | 3 | pub const error_page = 4 | \\ 5 | \\ 6 | \\

Apologies! An error occurred

7 | \\ 8 | ; 9 | 10 | pub const favicon = @embedFile("img/zig-zero.png"); 11 | -------------------------------------------------------------------------------- /src/assets/html/style.css: -------------------------------------------------------------------------------- 1 | /* -------- Extra Fonts -------- */ 2 | 3 | @font-face { 4 | font-family: "mononoki"; 5 | src: url("test/mononoki-Regular.ttf") format("truetype"); 6 | font-weight: normal; 7 | font-style: normal; 8 | font-display: swap; 9 | } 10 | @font-face { 11 | font-family: "mononoki"; 12 | src: url("test/mononoki-Bold.ttf") format("truetype"); 13 | font-weight: bold; 14 | font-style: normal; 15 | font-display: swap; 16 | } 17 | @font-face { 18 | font-family: "mononoki"; 19 | src: url("test/mononoki-Italic.ttf") format("truetype"); 20 | font-weight: normal; 21 | font-style: italic; 22 | font-display: swap; 23 | } 24 | @font-face { 25 | font-family: "mononoki"; 26 | src: url("test/mononoki-BoldItalic.ttf") format("truetype"); 27 | font-weight: bold; 28 | font-style: italic; 29 | font-display: swap; 30 | } 31 | 32 | /* -------- Markdown Render Styling -------- */ 33 | 34 | html, body { 35 | height: fit-content; 36 | overflow-wrap: break-word; 37 | } 38 | 39 | /* Set the basic style: bg color, fg color, typeface */ 40 | body { 41 | --color-rosewater: #f2d5cf; 42 | --color-flamingo: #eebebe; 43 | --color-pink: #f4b8e4; 44 | --color-mauve: #cda1e6; 45 | --color-red: #e78284; 46 | --color-maroon: #ea999c; 47 | --color-peach: #ef9f76; 48 | --color-yellow: #eaca60; 49 | --color-green: #96dd87; 50 | --color-teal: #81c8be; 51 | --color-sky: #99d1db; 52 | --color-sapphire: #85c1dc; 53 | --color-blue: #66aaff; 54 | --color-lavender: #babbf1; 55 | --color-text: #d6e0ff; 56 | --color-subtext1: #b5bfe2; 57 | --color-subtext0: #a5adce; 58 | --color-overlay2: #949cbb; 59 | --color-overlay1: #838ba7; 60 | --color-overlay0: #737994; 61 | --color-surface2: #626880; 62 | --color-surface1: #51576d; 63 | --color-surface0: #414559; 64 | --color-base: #303446; 65 | --color-mantle: #292c3c; 66 | --color-crust: #232634; 67 | 68 | background-image: none; 69 | background-color: var(--color-base); 70 | 71 | text-align: left; 72 | font-family: "Ubuntu Mono"; 73 | // font-family: mononoki; 74 | font-size: 20px; 75 | color: var(--color-text); 76 | 77 | --padding-vertical: clamp(1.5em, 5vh, 2.5em); 78 | margin: 0 auto; 79 | max-width: min(90ch, 100%); 80 | min-height: calc(100% - 2 * var(--padding-vertical)); 81 | padding: var(--padding-vertical) clamp(1.5em, 5vw, 2.5em); 82 | } 83 | 84 | *::selection { 85 | background: var(--color-blue); 86 | color: var(--color-base); 87 | } 88 | 89 | /* Title-style header (H1) */ 90 | .header { 91 | text-align: center; 92 | font-size: 24px; 93 | font-weight: bold; 94 | padding: 20px; 95 | } 96 | 97 | /* Basic centering of simple elements */ 98 | .center { 99 | display: block; 100 | margin-left: auto; 101 | margin-right: auto; 102 | width: 50%; 103 | } 104 | 105 | h1 { 106 | color: var(--color-blue); 107 | margin-top: 20px; 108 | margin-bottom: 0px; 109 | } 110 | 111 | h2 { 112 | color: var(--color-peach); 113 | margin-top: 20px; 114 | margin-bottom: 0px; 115 | } 116 | 117 | h3 { 118 | color: var(--color-green); 119 | margin-top: 16px; 120 | margin-bottom: 0px; 121 | } 122 | 123 | h4 { 124 | color: var(--color-mauve); 125 | margin-top: 16px; 126 | margin-bottom: 0px; 127 | } 128 | 129 | p { 130 | margin-top: 0px; 131 | margin-bottom: 12px; 132 | } 133 | 134 | ol, ul { 135 | margin-top: 0px; 136 | margin-bottom: 0px; 137 | } 138 | 139 | code { 140 | background-color: var(--color-mantle); 141 | color: var(--color-lavender); 142 | margin-top: 10px; 143 | margin-bottom: 10px; 144 | } 145 | 146 | .directive { 147 | border: 4px solid var(--color-red); 148 | border-radius: 10px; 149 | margin-top: 10px; 150 | margin-bottom: 10px; 151 | padding: 20px; 152 | background-color: var(--color-red); 153 | color: white; 154 | } 155 | 156 | /* ------------------------------------ 157 | * Syntax Highlighting 158 | * ------------------------------------ */ 159 | 160 | /** 161 | * TODO: keyword, constant, identifier, function, ... 162 | * Currently defined in syntax.zig; should instead define 163 | * classes here that get referenced there 164 | */ 165 | 166 | .code_block { 167 | color: var(--color-lavender); 168 | font-family: monospace; 169 | background-color: var(--color-mantle); 170 | padding: 0px 12px; 171 | margin-top: 10px; 172 | margin-bottom: 10px; 173 | } 174 | 175 | table { 176 | border-collapse: collapse; 177 | width: 100%; 178 | } 179 | 180 | tr { 181 | padding-top: 3px; 182 | padding-bottom: 3px; 183 | } 184 | 185 | td, th { 186 | padding-left: 6px; 187 | vertical-align: center; 188 | p { 189 | margin-top: 3px; 190 | margin-bottom: 3px; 191 | } 192 | } 193 | 194 | /* For normal (explicit) tables (as opposed to tables used for content alignment) */ 195 | 196 | .md_table { 197 | tr, td, th { 198 | border: 2px solid var(--color-subtext0); 199 | } 200 | th { 201 | background-color: var(--color-mantle); 202 | color: var(--color-green); 203 | } 204 | td { 205 | background-color: var(--color-surface0); 206 | } 207 | } 208 | 209 | /** 210 | * Bits from a a very nicely styled blog I found: 211 | * https://git.sr.ht/~ashie/blog/tree/master/item/assets/main.scss 212 | */ 213 | 214 | a { 215 | color: var(--color-sky); 216 | text-decoration: none; 217 | padding: 0.05em; 218 | 219 | &:visited { 220 | color: var(--color-mauve); 221 | 222 | &:hover { 223 | background: var(--color-mauve); 224 | } 225 | } 226 | 227 | &:hover { 228 | text-decoration: underline; 229 | background: var(--color-sky); 230 | color: var(--color-base); 231 | } 232 | } 233 | 234 | blockquote { 235 | background: var(--color-crust); 236 | color: var(--color-subtext1); 237 | margin: 0; 238 | margin-left: 0.75em; 239 | max-width: fit-content; 240 | padding: 0.5em; 241 | font-style: italic; 242 | margin: 0; 243 | 244 | i { 245 | font-style: normal; 246 | } 247 | } 248 | 249 | code { 250 | padding: 0.1ch 0.5ch; 251 | background: var(--color-crust); 252 | } 253 | 254 | li { 255 | margin: 0.25em 0; 256 | } 257 | 258 | ul sup { 259 | line-height: 0; 260 | vertical-align: super; 261 | } 262 | 263 | @counter-style checked-box { 264 | system: cyclic; 265 | symbols: "\2705"; // unicode code point 266 | suffix: " "; 267 | } 268 | @counter-style unchecked-box { 269 | system: cyclic; 270 | symbols: "\2B1C"; // unicode code point 271 | suffix: " "; 272 | } 273 | 274 | ul.task_list { 275 | list-style: none outside none; 276 | li.checked { 277 | list-style-type: "\2705 "; /* ✅ */ 278 | } 279 | li.unchecked { 280 | list-style-type: "\2B1C "; /* ⬜ */ 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /src/assets/img/zig-zero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JacobCrabill/zigdown/33e64b0f27f7e4ef4565af83fd881b3c26087cb1/src/assets/img/zig-zero.png -------------------------------------------------------------------------------- /src/assets/queries.zig: -------------------------------------------------------------------------------- 1 | pub const std = @import("std"); 2 | const config = @import("config"); 3 | 4 | /// Comptime function to turn our list of languages into a hash map of (language, query) pairs 5 | fn makeQuerymap(comptime languages: []const []const u8) std.StaticStringMap([]const u8) { 6 | const T = struct { []const u8, []const u8 }; 7 | var entries: [languages.len]T = undefined; 8 | inline for (languages, 0..) |lang, i| { 9 | entries[i] = .{ lang, @embedFile("queries/highlights-" ++ lang ++ ".scm") }; 10 | } 11 | return std.StaticStringMap([]const u8).initComptime(entries); 12 | } 13 | 14 | /// A map of (language, highlights query) for all built-in languages 15 | pub const builtin_queries = makeQuerymap(config.builtin_ts_parsers); 16 | -------------------------------------------------------------------------------- /src/assets/queries/highlights-bash.scm: -------------------------------------------------------------------------------- 1 | [ 2 | (string) 3 | (raw_string) 4 | (heredoc_body) 5 | (heredoc_start) 6 | ] @string 7 | 8 | (command_name) @function 9 | (command 10 | name: (command_name) @function) 11 | 12 | (command 13 | argument: (string) @string) 14 | 15 | (command 16 | argument: (word) @property) 17 | 18 | (variable_name) @property 19 | 20 | [ 21 | "case" 22 | "do" 23 | "done" 24 | "elif" 25 | "else" 26 | "esac" 27 | "export" 28 | "fi" 29 | "for" 30 | "function" 31 | "if" 32 | "in" 33 | "select" 34 | "then" 35 | "unset" 36 | "until" 37 | "while" 38 | ] @keyword 39 | 40 | (comment) @comment 41 | 42 | (function_definition name: (word) @function) 43 | 44 | (file_descriptor) @number 45 | 46 | [ 47 | (command_substitution) 48 | (process_substitution) 49 | (expansion) 50 | ] @embedded 51 | 52 | [ 53 | "$" 54 | "&&" 55 | ">" 56 | ">>" 57 | "<" 58 | "|" 59 | "||" 60 | "[" 61 | "[[" 62 | "]" 63 | "]]" 64 | ] @operator 65 | 66 | ( 67 | (command (_) @constant) 68 | (#match? @constant "^-") 69 | ) 70 | -------------------------------------------------------------------------------- /src/assets/queries/highlights-c.scm: -------------------------------------------------------------------------------- 1 | (identifier) @variable 2 | 3 | ((identifier) @constant 4 | (#match? @constant "^[A-Z][A-Z\\d_]*$")) 5 | 6 | "break" @keyword 7 | "case" @keyword 8 | "const" @keyword 9 | "continue" @keyword 10 | "default" @keyword 11 | "do" @keyword 12 | "else" @keyword 13 | "enum" @keyword 14 | "extern" @keyword 15 | "for" @keyword 16 | "if" @keyword 17 | "inline" @keyword 18 | "return" @keyword 19 | "sizeof" @keyword 20 | "static" @keyword 21 | "struct" @keyword 22 | "switch" @keyword 23 | "typedef" @keyword 24 | "union" @keyword 25 | "volatile" @keyword 26 | "while" @keyword 27 | 28 | "#define" @keyword 29 | "#elif" @keyword 30 | "#else" @keyword 31 | "#endif" @keyword 32 | "#if" @keyword 33 | "#ifdef" @keyword 34 | "#ifndef" @keyword 35 | "#include" @keyword 36 | (preproc_directive) @keyword 37 | 38 | "--" @operator 39 | "-" @operator 40 | "-=" @operator 41 | "->" @operator 42 | "=" @operator 43 | "!=" @operator 44 | "*" @operator 45 | "&" @operator 46 | "&&" @operator 47 | "+" @operator 48 | "++" @operator 49 | "+=" @operator 50 | "<" @operator 51 | "==" @operator 52 | ">" @operator 53 | "||" @operator 54 | 55 | "." @delimiter 56 | ";" @delimiter 57 | 58 | (string_literal) @string 59 | (system_lib_string) @string 60 | 61 | (null) @constant 62 | (number_literal) @number 63 | (char_literal) @number 64 | 65 | (field_identifier) @property 66 | (statement_identifier) @label 67 | (type_identifier) @type 68 | (primitive_type) @type 69 | (sized_type_specifier) @type 70 | 71 | (call_expression 72 | function: (identifier) @function) 73 | (call_expression 74 | function: (field_expression 75 | field: (field_identifier) @function)) 76 | (function_declarator 77 | declarator: (identifier) @function) 78 | (preproc_function_def 79 | name: (identifier) @function.special) 80 | 81 | (comment) @comment 82 | -------------------------------------------------------------------------------- /src/assets/queries/highlights-cmake.scm: -------------------------------------------------------------------------------- 1 | (normal_command 2 | (identifier) 3 | (argument_list 4 | (argument 5 | (unquoted_argument)) @constant) 6 | (#lua-match? @constant "^[%u@][%u%d_]+$")) 7 | 8 | [ 9 | (quoted_argument) 10 | (bracket_argument) 11 | ] @string 12 | 13 | (variable_ref) @none 14 | 15 | (variable) @variable 16 | 17 | [ 18 | (bracket_comment) 19 | (line_comment) 20 | ] @comment @spell 21 | 22 | (normal_command 23 | (identifier) @function) 24 | 25 | [ 26 | "ENV" 27 | "CACHE" 28 | ] @module 29 | 30 | [ 31 | "$" 32 | "{" 33 | "}" 34 | ] @punctuation.special 35 | 36 | [ 37 | "(" 38 | ")" 39 | ] @punctuation.bracket 40 | 41 | [ 42 | (function) 43 | (endfunction) 44 | (macro) 45 | (endmacro) 46 | ] @keyword.function 47 | 48 | [ 49 | (if) 50 | (elseif) 51 | (else) 52 | (endif) 53 | ] @keyword.conditional 54 | 55 | [ 56 | (foreach) 57 | (endforeach) 58 | (while) 59 | (endwhile) 60 | ] @keyword.repeat 61 | 62 | (normal_command 63 | (identifier) @keyword.repeat 64 | (#match? @keyword.repeat "^([cC][oO][nN][tT][iI][nN][uU][eE]|[bB][rR][eE][aA][kK])$")) 65 | 66 | (normal_command 67 | (identifier) @keyword.return 68 | (#match? @keyword.return "^[rR][eE][tT][uU][rR][nN]$")) 69 | 70 | (function_command 71 | (function) 72 | (argument_list 73 | . 74 | (argument) @function 75 | (argument)* @variable.parameter)) 76 | 77 | (macro_command 78 | (macro) 79 | (argument_list 80 | . 81 | (argument) @function.macro 82 | (argument)* @variable.parameter)) 83 | 84 | (block_def 85 | (block_command 86 | (block) @function.builtin 87 | (argument_list 88 | (argument 89 | (unquoted_argument) @constant)) 90 | (#any-of? @constant "SCOPE_FOR" "POLICIES" "VARIABLES" "PROPAGATE")) 91 | (endblock_command 92 | (endblock) @function.builtin)) 93 | 94 | ; 95 | ((argument) @boolean 96 | (#match? @boolean "^(1|[oO][nN]|[yY][eE][sS]|[tT][rR][uU][eE]|[yY]|0|[oO][fF][fF]|[nN][oO]|[fF][aA][lL][sS][eE]|[nN]|[iI][gG][nN][oO][rR][eE]|[nN][oO][tT][fF][oO][uU][nN][dD]|.*-[nN][oO][tT][fF][oO][uU][nN][dD])$")) 97 | 98 | ; 99 | (if_command 100 | (if) 101 | (argument_list 102 | (argument) @keyword.operator) 103 | (#any-of? @keyword.operator 104 | "NOT" "AND" "OR" "COMMAND" "POLICY" "TARGET" "TEST" "DEFINED" "IN_LIST" "EXISTS" "IS_NEWER_THAN" 105 | "IS_DIRECTORY" "IS_SYMLINK" "IS_ABSOLUTE" "MATCHES" "LESS" "GREATER" "EQUAL" "LESS_EQUAL" 106 | "GREATER_EQUAL" "STRLESS" "STRGREATER" "STREQUAL" "STRLESS_EQUAL" "STRGREATER_EQUAL" 107 | "VERSION_LESS" "VERSION_GREATER" "VERSION_EQUAL" "VERSION_LESS_EQUAL" "VERSION_GREATER_EQUAL")) 108 | 109 | (elseif_command 110 | (elseif) 111 | (argument_list 112 | (argument) @keyword.operator) 113 | (#any-of? @keyword.operator 114 | "NOT" "AND" "OR" "COMMAND" "POLICY" "TARGET" "TEST" "DEFINED" "IN_LIST" "EXISTS" "IS_NEWER_THAN" 115 | "IS_DIRECTORY" "IS_SYMLINK" "IS_ABSOLUTE" "MATCHES" "LESS" "GREATER" "EQUAL" "LESS_EQUAL" 116 | "GREATER_EQUAL" "STRLESS" "STRGREATER" "STREQUAL" "STRLESS_EQUAL" "STRGREATER_EQUAL" 117 | "VERSION_LESS" "VERSION_GREATER" "VERSION_EQUAL" "VERSION_LESS_EQUAL" "VERSION_GREATER_EQUAL")) 118 | 119 | (normal_command 120 | (identifier) @function.builtin 121 | (#match? @function.builtin 122 | "^([cC][mM][aA][kK][eE]_[hH][oO][sS][tT]_[sS][yY][sS][tT][eE][mM]_[iI][nN][fF][oO][rR][mM][aA][tT][iI][oO][nN]|[cC][mM][aA][kK][eE]_[lL][aA][nN][gG][uU][aA][gG][eE]|[cC][mM][aA][kK][eE]_[mM][iI][nN][iI][mM][uU][mM]_[rR][eE][qQ][uU][iI][rR][eE][dD]|[cC][mM][aA][kK][eE]_[pP][aA][rR][sS][eE]_[aA][rR][gG][uU][mM][eE][nN][tT][sS]|[cC][mM][aA][kK][eE]_[pP][aA][tT][hH]|[cC][mM][aA][kK][eE]_[pP][oO][lL][iI][cC][yY]|[cC][oO][nN][fF][iI][gG][uU][rR][eE]_[fF][iI][lL][eE]|[eE][xX][eE][cC][uU][tT][eE]_[pP][rR][oO][cC][eE][sS][sS]|[fF][iI][lL][eE]|[fF][iI][nN][dD]_[fF][iI][lL][eE]|[fF][iI][nN][dD]_[lL][iI][bB][rR][aA][rR][yY]|[fF][iI][nN][dD]_[pP][aA][cC][kK][aA][gG][eE]|[fF][iI][nN][dD]_[pP][aA][tT][hH]|[fF][iI][nN][dD]_[pP][rR][oO][gG][rR][aA][mM]|[fF][oO][rR][eE][aA][cC][hH]|[gG][eE][tT]_[cC][mM][aA][kK][eE]_[pP][rR][oO][pP][eE][rR][tT][yY]|[gG][eE][tT]_[dD][iI][rR][eE][cC][tT][oO][rR][yY]_[pP][rR][oO][pP][eE][rR][tT][yY]|[gG][eE][tT]_[fF][iI][lL][eE][nN][aA][mM][eE]_[cC][oO][mM][pP][oO][nN][eE][nN][tT]|[gG][eE][tT]_[pP][rR][oO][pP][eE][rR][tT][yY]|[iI][nN][cC][lL][uU][dD][eE]|[iI][nN][cC][lL][uU][dD][eE]_[gG][uU][aA][rR][dD]|[lL][iI][sS][tT]|[mM][aA][cC][rR][oO]|[mM][aA][rR][kK]_[aA][sS]_[aA][dD][vV][aA][nN][cC][eE][dD]|[mM][aA][tT][hH]|[mM][eE][sS][sS][aA][gG][eE]|[oO][pP][tT][iI][oO][nN]|[sS][eE][pP][aA][rR][aA][tT][eE]_[aA][rR][gG][uU][mM][eE][nN][tT][sS]|[sS][eE][tT]|[sS][eE][tT]_[dD][iI][rR][eE][cC][tT][oO][rR][yY]_[pP][rR][oO][pP][eE][rR][tT][iI][eE][sS]|[sS][eE][tT]_[pP][rR][oO][pP][eE][rR][tT][yY]|[sS][iI][tT][eE]_[nN][aA][mM][eE]|[sS][tT][rR][iI][nN][gG]|[uU][nN][sS][eE][tT]|[vV][aA][rR][iI][aA][bB][lL][eE]_[wW][aA][tT][cC][hH]|[aA][dD][dD]_[cC][oO][mM][pP][iI][lL][eE]_[dD][eE][fF][iI][nN][iI][tT][iI][oO][nN][sS]|[aA][dD][dD]_[cC][oO][mM][pP][iI][lL][eE]_[oO][pP][tT][iI][oO][nN][sS]|[aA][dD][dD]_[cC][uU][sS][tT][oO][mM]_[cC][oO][mM][mM][aA][nN][dD]|[aA][dD][dD]_[cC][uU][sS][tT][oO][mM]_[tT][aA][rR][gG][eE][tT]|[aA][dD][dD]_[dD][eE][fF][iI][nN][iI][tT][iI][oO][nN][sS]|[aA][dD][dD]_[dD][eE][pP][eE][nN][dD][eE][nN][cC][iI][eE][sS]|[aA][dD][dD]_[eE][xX][eE][cC][uU][tT][aA][bB][lL][eE]|[aA][dD][dD]_[lL][iI][bB][rR][aA][rR][yY]|[aA][dD][dD]_[lL][iI][nN][kK]_[oO][pP][tT][iI][oO][nN][sS]|[aA][dD][dD]_[sS][uU][bB][dD][iI][rR][eE][cC][tT][oO][rR][yY]|[aA][dD][dD]_[tT][eE][sS][tT]|[aA][uU][xX]_[sS][oO][uU][rR][cC][eE]_[dD][iI][rR][eE][cC][tT][oO][rR][yY]|[bB][uU][iI][lL][dD]_[cC][oO][mM][mM][aA][nN][dD]|[cC][rR][eE][aA][tT][eE]_[tT][eE][sS][tT]_[sS][oO][uU][rR][cC][eE][lL][iI][sS][tT]|[dD][eE][fF][iI][nN][eE]_[pP][rR][oO][pP][eE][rR][tT][yY]|[eE][nN][aA][bB][lL][eE]_[lL][aA][nN][gG][uU][aA][gG][eE]|[eE][nN][aA][bB][lL][eE]_[tT][eE][sS][tT][iI][nN][gG]|[eE][xX][pP][oO][rR][tT]|[fF][lL][tT][kK]_[wW][rR][aA][pP]_[uU][iI]|[gG][eE][tT]_[sS][oO][uU][rR][cC][eE]_[fF][iI][lL][eE]_[pP][rR][oO][pP][eE][rR][tT][yY]|[gG][eE][tT]_[tT][aA][rR][gG][eE][tT]_[pP][rR][oO][pP][eE][rR][tT][yY]|[gG][eE][tT]_[tT][eE][sS][tT]_[pP][rR][oO][pP][eE][rR][tT][yY]|[iI][nN][cC][lL][uU][dD][eE]_[dD][iI][rR][eE][cC][tT][oO][rR][iI][eE][sS]|[iI][nN][cC][lL][uU][dD][eE]_[eE][xX][tT][eE][rR][nN][aA][lL]_[mM][sS][pP][rR][oO][jJ][eE][cC][tT]|[iI][nN][cC][lL][uU][dD][eE]_[rR][eE][gG][uU][lL][aA][rR]_[eE][xX][pP][rR][eE][sS][sS][iI][oO][nN]|[iI][nN][sS][tT][aA][lL][lL]|[lL][iI][nN][kK]_[dD][iI][rR][eE][cC][tT][oO][rR][iI][eE][sS]|[lL][iI][nN][kK]_[lL][iI][bB][rR][aA][rR][iI][eE][sS]|[lL][oO][aA][dD]_[cC][aA][cC][hH][eE]|[pP][rR][oO][jJ][eE][cC][tT]|[rR][eE][mM][oO][vV][eE]_[dD][eE][fF][iI][nN][iI][tT][iI][oO][nN][sS]|[sS][eE][tT]_[sS][oO][uU][rR][cC][eE]_[fF][iI][lL][eE][sS]_[pP][rR][oO][pP][eE][rR][tT][iI][eE][sS]|[sS][eE][tT]_[tT][aA][rR][gG][eE][tT]_[pP][rR][oO][pP][eE][rR][tT][iI][eE][sS]|[sS][eE][tT]_[tT][eE][sS][tT][sS]_[pP][rR][oO][pP][eE][rR][tT][iI][eE][sS]|[sS][oO][uU][rR][cC][eE]_[gG][rR][oO][uU][pP]|[tT][aA][rR][gG][eE][tT]_[cC][oO][mM][pP][iI][lL][eE]_[dD][eE][fF][iI][nN][iI][tT][iI][oO][nN][sS]|[tT][aA][rR][gG][eE][tT]_[cC][oO][mM][pP][iI][lL][eE]_[fF][eE][aA][tT][uU][rR][eE][sS]|[tT][aA][rR][gG][eE][tT]_[cC][oO][mM][pP][iI][lL][eE]_[oO][pP][tT][iI][oO][nN][sS]|[tT][aA][rR][gG][eE][tT]_[iI][nN][cC][lL][uU][dD][eE]_[dD][iI][rR][eE][cC][tT][oO][rR][iI][eE][sS]|[tT][aA][rR][gG][eE][tT]_[lL][iI][nN][kK]_[dD][iI][rR][eE][cC][tT][oO][rR][iI][eE][sS]|[tT][aA][rR][gG][eE][tT]_[lL][iI][nN][kK]_[lL][iI][bB][rR][aA][rR][iI][eE][sS]|[tT][aA][rR][gG][eE][tT]_[lL][iI][nN][kK]_[oO][pP][tT][iI][oO][nN][sS]|[tT][aA][rR][gG][eE][tT]_[pP][rR][eE][cC][oO][mM][pP][iI][lL][eE]_[hH][eE][aA][dD][eE][rR][sS]|[tT][aA][rR][gG][eE][tT]_[sS][oO][uU][rR][cC][eE][sS]|[tT][rR][yY]_[cC][oO][mM][pP][iI][lL][eE]|[tT][rR][yY]_[rR][uU][nN]|[cC][tT][eE][sS][tT]_[bB][uU][iI][lL][dD]|[cC][tT][eE][sS][tT]_[cC][oO][nN][fF][iI][gG][uU][rR][eE]|[cC][tT][eE][sS][tT]_[cC][oO][vV][eE][rR][aA][gG][eE]|[cC][tT][eE][sS][tT]_[eE][mM][pP][tT][yY]_[bB][iI][nN][aA][rR][yY]_[dD][iI][rR][eE][cC][tT][oO][rR][yY]|[cC][tT][eE][sS][tT]_[mM][eE][mM][cC][hH][eE][cC][kK]|[cC][tT][eE][sS][tT]_[rR][eE][aA][dD]_[cC][uU][sS][tT][oO][mM]_[fF][iI][lL][eE][sS]|[cC][tT][eE][sS][tT]_[rR][uU][nN]_[sS][cC][rR][iI][pP][tT]|[cC][tT][eE][sS][tT]_[sS][lL][eE][eE][pP]|[cC][tT][eE][sS][tT]_[sS][tT][aA][rR][tT]|[cC][tT][eE][sS][tT]_[sS][uU][bB][mM][iI][tT]|[cC][tT][eE][sS][tT]_[tT][eE][sS][tT]|[cC][tT][eE][sS][tT]_[uU][pP][dD][aA][tT][eE]|[cC][tT][eE][sS][tT]_[uU][pP][lL][oO][aA][dD])$")) 123 | 124 | (normal_command 125 | (identifier) @_function 126 | (argument_list 127 | . 128 | (argument) @variable) 129 | (#match? @_function "^[sS][eE][tT]$")) 130 | 131 | (normal_command 132 | (identifier) @_function 133 | (#match? @_function "^[sS][eE][tT]$") 134 | (argument_list 135 | . 136 | (argument) 137 | ((argument) @_cache @keyword.modifier 138 | . 139 | (argument) @_type @type 140 | (#any-of? @_cache "CACHE") 141 | (#any-of? @_type "BOOL" "FILEPATH" "PATH" "STRING" "INTERNAL")))) 142 | 143 | (normal_command 144 | (identifier) @_function 145 | (#match? @_function "^[uU][nN][sS][eE][tT]$") 146 | (argument_list 147 | . 148 | (argument) 149 | (argument) @keyword.modifier 150 | (#any-of? @keyword.modifier "CACHE" "PARENT_SCOPE"))) 151 | 152 | (normal_command 153 | (identifier) @_function 154 | (#match? @_function "^[lL][iI][sS][tT]$") 155 | (argument_list 156 | . 157 | (argument) @constant 158 | (#any-of? @constant "LENGTH" "GET" "JOIN" "SUBLIST" "FIND") 159 | . 160 | (argument) @variable 161 | (argument) @variable .)) 162 | 163 | (normal_command 164 | (identifier) @_function 165 | (#match? @_function "^[lL][iI][sS][tT]$") 166 | (argument_list 167 | . 168 | (argument) @constant 169 | . 170 | (argument) @variable 171 | (#any-of? @constant 172 | "APPEND" "FILTER" "INSERT" "POP_BACK" "POP_FRONT" "PREPEND" "REMOVE_ITEM" "REMOVE_AT" 173 | "REMOVE_DUPLICATES" "REVERSE" "SORT"))) 174 | 175 | (normal_command 176 | (identifier) @_function 177 | (#match? @_function "^[lL][iI][sS][tT]$") 178 | (argument_list 179 | . 180 | (argument) @_transform @constant 181 | . 182 | (argument) @variable 183 | . 184 | (argument) @_action @constant 185 | (#eq? @_transform "TRANSFORM") 186 | (#any-of? @_action "APPEND" "PREPEND" "TOUPPER" "TOLOWER" "STRIP" "GENEX_STRIP" "REPLACE"))) 187 | 188 | (normal_command 189 | (identifier) @_function 190 | (#match? @_function "^[lL][iI][sS][tT]$") 191 | (argument_list 192 | . 193 | (argument) @_transform @constant 194 | . 195 | (argument) @variable 196 | . 197 | (argument) @_action @constant 198 | . 199 | (argument)? @_selector @constant 200 | (#eq? @_transform "TRANSFORM") 201 | (#any-of? @_action "APPEND" "PREPEND" "TOUPPER" "TOLOWER" "STRIP" "GENEX_STRIP" "REPLACE") 202 | (#any-of? @_selector "AT" "FOR" "REGEX"))) 203 | 204 | (normal_command 205 | (identifier) @_function 206 | (#match? @_function "^[lL][iI][sS][tT]$") 207 | (argument_list 208 | . 209 | (argument) @_transform @constant 210 | (argument) @constant 211 | . 212 | (argument) @variable 213 | (#eq? @_transform "TRANSFORM") 214 | (#eq? @constant "OUTPUT_VARIABLE"))) 215 | 216 | (escape_sequence) @string.escape 217 | 218 | ((source_file 219 | . 220 | (line_comment) @keyword.directive @nospell) 221 | (#lua-match? @keyword.directive "^#!/")) 222 | -------------------------------------------------------------------------------- /src/assets/queries/highlights-cpp.scm: -------------------------------------------------------------------------------- 1 | ; Functions 2 | 3 | (call_expression 4 | function: (qualified_identifier 5 | name: (identifier) @function)) 6 | 7 | (template_function 8 | name: (identifier) @function) 9 | 10 | (template_method 11 | name: (field_identifier) @function) 12 | 13 | (template_function 14 | name: (identifier) @function) 15 | 16 | (function_declarator 17 | declarator: (qualified_identifier 18 | name: (identifier) @function)) 19 | 20 | (function_declarator 21 | declarator: (field_identifier) @function) 22 | 23 | (function_declarator 24 | declarator: (identifier) @function) 25 | 26 | [ 27 | "delete" 28 | "new" 29 | ] @function.builtin 30 | 31 | ; Types 32 | 33 | ((namespace_identifier) @type 34 | (#match? @type "^[A-Z]")) 35 | 36 | (auto) @type 37 | 38 | (primitive_type) @type 39 | (type_identifier) @type 40 | 41 | (number_literal) @number 42 | 43 | ; Constants 44 | 45 | (this) @variable.builtin 46 | (null "nullptr" @constant) 47 | 48 | ; Keywords 49 | 50 | [ 51 | "class" 52 | "enum" 53 | "struct" 54 | "union" 55 | ] @keyword.type 56 | 57 | [ 58 | "throw" 59 | "try" 60 | "catch" 61 | ] @keyword.exception 62 | 63 | [ 64 | "if" 65 | "else" 66 | "switch" 67 | "case" 68 | ] @keyword.conditional 69 | 70 | [ 71 | "continue" 72 | "do" 73 | "while" 74 | "for" 75 | "break" 76 | ] @keyword.repeat 77 | 78 | [ 79 | "const" 80 | "constexpr" 81 | "constinit" 82 | "consteval" 83 | "explicit" 84 | "extern" 85 | "inline" 86 | "mutable" 87 | "override" 88 | "private" 89 | "protected" 90 | "public" 91 | "static" 92 | "template" 93 | "volatile" 94 | "virtual" 95 | ] @keyword.modifier 96 | 97 | "return" @keyword.return 98 | 99 | [ 100 | "and" 101 | "or" 102 | ] @keyword.operator 103 | 104 | [ 105 | "co_await" 106 | "co_return" 107 | "co_yield" 108 | "final" 109 | "friend" 110 | "namespace" 111 | "noexcept" 112 | "typename" 113 | "typedef" 114 | "using" 115 | "concept" 116 | "requires" 117 | "sizeof" 118 | "using" 119 | ] @keyword 120 | 121 | (preproc_directive) @keyword.directive 122 | 123 | ; Strings 124 | 125 | (string_literal) @string 126 | (system_lib_string) @string 127 | (raw_string_literal) @string 128 | 129 | [ 130 | "--" 131 | "-" 132 | "-=" 133 | "->" 134 | "=" 135 | "!=" 136 | "*" 137 | "&" 138 | "&&" 139 | "&=" 140 | "+" 141 | "++" 142 | "+=" 143 | "<" 144 | "<<" 145 | "==" 146 | ">" 147 | ">>" 148 | "|" 149 | "||" 150 | "|=" 151 | ] @operator 152 | 153 | [ 154 | "[" 155 | "]" 156 | "{" 157 | "}" 158 | "(" 159 | ")" 160 | ] @punctuation.bracket 161 | 162 | [ 163 | "." 164 | "::" 165 | ";" 166 | ] @punctuation.delimiter 167 | 168 | (comment) @comment 169 | -------------------------------------------------------------------------------- /src/assets/queries/highlights-json.scm: -------------------------------------------------------------------------------- 1 | (pair 2 | key: (_) @string.special.key) 3 | 4 | (string) @string 5 | 6 | (number) @number 7 | 8 | [ 9 | (null) 10 | (true) 11 | (false) 12 | ] @constant.builtin 13 | 14 | (escape_sequence) @escape 15 | 16 | (comment) @comment 17 | 18 | [ 19 | "[" 20 | "]" 21 | "{" 22 | "}" 23 | ] @operator 24 | -------------------------------------------------------------------------------- /src/assets/queries/highlights-make.scm: -------------------------------------------------------------------------------- 1 | [ 2 | "(" 3 | ")" 4 | "{" 5 | "}" 6 | ] @punctuation.bracket 7 | 8 | [ 9 | ":" 10 | "&:" 11 | "::" 12 | "|" 13 | ";" 14 | "\"" 15 | "'" 16 | "," 17 | ] @punctuation.delimiter 18 | 19 | [ 20 | "$" 21 | "$$" 22 | ] @punctuation.special 23 | 24 | (automatic_variable 25 | [ "@" "%" "<" "?" "^" "+" "/" "*" "D" "F"] @punctuation.special) 26 | 27 | (automatic_variable 28 | "/" @error . ["D" "F"]) 29 | 30 | [ 31 | "=" 32 | ":=" 33 | "::=" 34 | "?=" 35 | "+=" 36 | "!=" 37 | "@" 38 | "-" 39 | "+" 40 | ] @operator 41 | 42 | [ 43 | (text) 44 | (string) 45 | (raw_text) 46 | ] @string 47 | 48 | (variable_assignment (word) @string) 49 | 50 | [ 51 | "ifeq" 52 | "ifneq" 53 | "ifdef" 54 | "ifndef" 55 | "else" 56 | "endif" 57 | "if" 58 | "or" ; boolean functions are conditional in make grammar 59 | "and" 60 | ] @conditional 61 | 62 | "foreach" @repeat 63 | 64 | [ 65 | "define" 66 | "endef" 67 | "vpath" 68 | "undefine" 69 | "export" 70 | "unexport" 71 | "override" 72 | "private" 73 | ; "load" 74 | ] @keyword 75 | 76 | [ 77 | "include" 78 | "sinclude" 79 | "-include" 80 | ] @include 81 | 82 | [ 83 | "subst" 84 | "patsubst" 85 | "strip" 86 | "findstring" 87 | "filter" 88 | "filter-out" 89 | "sort" 90 | "word" 91 | "words" 92 | "wordlist" 93 | "firstword" 94 | "lastword" 95 | "dir" 96 | "notdir" 97 | "suffix" 98 | "basename" 99 | "addsuffix" 100 | "addprefix" 101 | "join" 102 | "wildcard" 103 | "realpath" 104 | "abspath" 105 | "call" 106 | "eval" 107 | "file" 108 | "value" 109 | "shell" 110 | ] @keyword.function 111 | 112 | [ 113 | "error" 114 | "warning" 115 | "info" 116 | ] @exception 117 | 118 | ;; Variable 119 | (variable_assignment 120 | name: (word) @constant) 121 | 122 | (variable_reference 123 | (word) @constant) 124 | 125 | (comment) @comment 126 | 127 | ((word) @clean @string.regex 128 | (#match? @clean "[%\*\?]")) 129 | 130 | (function_call 131 | function: "error" 132 | (arguments (text) @text.danger)) 133 | 134 | (function_call 135 | function: "warning" 136 | (arguments (text) @text.warning)) 137 | 138 | (function_call 139 | function: "info" 140 | (arguments (text) @text.note)) 141 | 142 | ;; Install Command Categories 143 | ;; Others special variables 144 | ;; Variables Used by Implicit Rules 145 | [ 146 | "VPATH" 147 | ".RECIPEPREFIX" 148 | ] @constant.builtin 149 | 150 | (variable_assignment 151 | name: (word) @clean @constant.builtin 152 | (#match? @clean "^(AR|AS|CC|CXX|CPP|FC|M2C|PC|CO|GET|LEX|YACC|LINT|MAKEINFO|TEX|TEXI2DVI|WEAVE|CWEAVE|TANGLE|CTANGLE|RM|ARFLAGS|ASFLAGS|CFLAGS|CXXFLAGS|COFLAGS|CPPFLAGS|FFLAGS|GFLAGS|LDFLAGS|LDLIBS|LFLAGS|YFLAGS|PFLAGS|RFLAGS|LINTFLAGS|PRE_INSTALL|POST_INSTALL|NORMAL_INSTALL|PRE_UNINSTALL|POST_UNINSTALL|NORMAL_UNINSTALL|MAKEFILE_LIST|MAKE_RESTARTS|MAKE_TERMOUT|MAKE_TERMERR|\.DEFAULT_GOAL|\.RECIPEPREFIX|\.EXTRA_PREREQS)$")) 153 | 154 | (variable_reference 155 | (word) @clean @constant.builtin 156 | (#match? @clean "^(AR|AS|CC|CXX|CPP|FC|M2C|PC|CO|GET|LEX|YACC|LINT|MAKEINFO|TEX|TEXI2DVI|WEAVE|CWEAVE|TANGLE|CTANGLE|RM|ARFLAGS|ASFLAGS|CFLAGS|CXXFLAGS|COFLAGS|CPPFLAGS|FFLAGS|GFLAGS|LDFLAGS|LDLIBS|LFLAGS|YFLAGS|PFLAGS|RFLAGS|LINTFLAGS|PRE_INSTALL|POST_INSTALL|NORMAL_INSTALL|PRE_UNINSTALL|POST_UNINSTALL|NORMAL_UNINSTALL|MAKEFILE_LIST|MAKE_RESTARTS|MAKE_TERMOUT|MAKE_TERMERR|\.DEFAULT_GOAL|\.RECIPEPREFIX|\.EXTRA_PREREQS\.VARIABLES|\.FEATURES|\.INCLUDE_DIRS|\.LOADED)$")) 157 | 158 | ;; Standart targets 159 | (targets 160 | (word) @constant.macro 161 | (#match? @constant.macro "^(all|install|install-html|install-dvi|install-pdf|install-ps|uninstall|install-strip|clean|distclean|mostlyclean|maintainer-clean|TAGS|info|dvi|html|pdf|ps|dist|check|installcheck|installdirs)$")) 162 | 163 | (targets 164 | (word) @constant.macro 165 | (#match? @constant.macro "^(all|install|install-html|install-dvi|install-pdf|install-ps|uninstall|install-strip|clean|distclean|mostlyclean|maintainer-clean|TAGS|info|dvi|html|pdf|ps|dist|check|installcheck|installdirs)$")) 166 | 167 | ;; Builtin targets 168 | (targets 169 | (word) @constant.macro 170 | (#match? @constant.macro "^\.(PHONY|SUFFIXES|DEFAULT|PRECIOUS|INTERMEDIATE|SECONDARY|SECONDEXPANSION|DELETE_ON_ERROR|IGNORE|LOW_RESOLUTION_TIME|SILENT|EXPORT_ALL_VARIABLES|NOTPARALLEL|ONESHELL|POSIX)$")) 171 | 172 | -------------------------------------------------------------------------------- /src/assets/queries/highlights-python.scm: -------------------------------------------------------------------------------- 1 | ; Identifier naming conventions 2 | 3 | (identifier) @variable 4 | 5 | ((identifier) @constructor 6 | (#match? @constructor "^[A-Z]")) 7 | 8 | ((identifier) @constant 9 | (#match? @constant "^[A-Z][A-Z_]*$")) 10 | 11 | ; Function calls 12 | 13 | (decorator) @function 14 | 15 | (call 16 | function: (attribute attribute: (identifier) @function.method)) 17 | (call 18 | function: (identifier) @function) 19 | 20 | ; Builtin functions 21 | 22 | ((call 23 | function: (identifier) @function.builtin) 24 | (#match? 25 | @function.builtin 26 | "^(abs|all|any|ascii|bin|bool|breakpoint|bytearray|bytes|callable|chr|classmethod|compile|complex|delattr|dict|dir|divmod|enumerate|eval|exec|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|isinstance|issubclass|iter|len|list|locals|map|max|memoryview|min|next|object|oct|open|ord|pow|print|property|range|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|vars|zip|__import__)$")) 27 | 28 | ; Function definitions 29 | 30 | (function_definition 31 | name: (identifier) @function) 32 | 33 | (attribute attribute: (identifier) @property) 34 | (type (identifier) @type) 35 | 36 | ; Literals 37 | 38 | [ 39 | (none) 40 | (true) 41 | (false) 42 | ] @constant.builtin 43 | 44 | [ 45 | (integer) 46 | (float) 47 | ] @number 48 | 49 | (comment) @comment 50 | (string) @string 51 | (escape_sequence) @escape 52 | 53 | (interpolation 54 | "{" @punctuation.special 55 | "}" @punctuation.special) @embedded 56 | 57 | [ 58 | "-" 59 | "-=" 60 | "!=" 61 | "*" 62 | "**" 63 | "**=" 64 | "*=" 65 | "/" 66 | "//" 67 | "//=" 68 | "/=" 69 | "&" 70 | "&=" 71 | "%" 72 | "%=" 73 | "^" 74 | "^=" 75 | "+" 76 | "->" 77 | "+=" 78 | "<" 79 | "<<" 80 | "<<=" 81 | "<=" 82 | "<>" 83 | "=" 84 | ":=" 85 | "==" 86 | ">" 87 | ">=" 88 | ">>" 89 | ">>=" 90 | "|" 91 | "|=" 92 | "~" 93 | "@=" 94 | "and" 95 | "in" 96 | "is" 97 | "not" 98 | "or" 99 | "is not" 100 | "not in" 101 | ] @operator 102 | 103 | [ 104 | "as" 105 | "assert" 106 | "async" 107 | "await" 108 | "break" 109 | "class" 110 | "continue" 111 | "def" 112 | "del" 113 | "elif" 114 | "else" 115 | "except" 116 | "exec" 117 | "finally" 118 | "for" 119 | "from" 120 | "global" 121 | "if" 122 | "import" 123 | "lambda" 124 | "nonlocal" 125 | "pass" 126 | "print" 127 | "raise" 128 | "return" 129 | "try" 130 | "while" 131 | "with" 132 | "yield" 133 | "match" 134 | "case" 135 | ] @keyword 136 | -------------------------------------------------------------------------------- /src/assets/queries/highlights-rust.scm: -------------------------------------------------------------------------------- 1 | ; Identifiers 2 | 3 | (type_identifier) @type 4 | (primitive_type) @type.builtin 5 | (field_identifier) @property 6 | 7 | ; Identifier conventions 8 | 9 | ; Assume all-caps names are constants 10 | ((identifier) @constant 11 | (#match? @constant "^[A-Z][A-Z\\d_]+$'")) 12 | 13 | ; Assume uppercase names are enum constructors 14 | ((identifier) @constructor 15 | (#match? @constructor "^[A-Z]")) 16 | 17 | ; Assume that uppercase names in paths are types 18 | ((scoped_identifier 19 | path: (identifier) @type) 20 | (#match? @type "^[A-Z]")) 21 | ((scoped_identifier 22 | path: (scoped_identifier 23 | name: (identifier) @type)) 24 | (#match? @type "^[A-Z]")) 25 | ((scoped_type_identifier 26 | path: (identifier) @type) 27 | (#match? @type "^[A-Z]")) 28 | ((scoped_type_identifier 29 | path: (scoped_identifier 30 | name: (identifier) @type)) 31 | (#match? @type "^[A-Z]")) 32 | 33 | ; Assume all qualified names in struct patterns are enum constructors. (They're 34 | ; either that, or struct names; highlighting both as constructors seems to be 35 | ; the less glaring choice of error, visually.) 36 | (struct_pattern 37 | type: (scoped_type_identifier 38 | name: (type_identifier) @constructor)) 39 | 40 | ; Function calls 41 | 42 | (call_expression 43 | function: (identifier) @function) 44 | (call_expression 45 | function: (field_expression 46 | field: (field_identifier) @function.method)) 47 | (call_expression 48 | function: (scoped_identifier 49 | "::" 50 | name: (identifier) @function)) 51 | 52 | (generic_function 53 | function: (identifier) @function) 54 | (generic_function 55 | function: (scoped_identifier 56 | name: (identifier) @function)) 57 | (generic_function 58 | function: (field_expression 59 | field: (field_identifier) @function.method)) 60 | 61 | (macro_invocation 62 | macro: (identifier) @function.macro 63 | "!" @function.macro) 64 | 65 | ; Function definitions 66 | 67 | (function_item (identifier) @function) 68 | (function_signature_item (identifier) @function) 69 | 70 | (line_comment) @comment 71 | (block_comment) @comment 72 | 73 | (line_comment (doc_comment)) @comment.documentation 74 | (block_comment (doc_comment)) @comment.documentation 75 | 76 | "(" @punctuation.bracket 77 | ")" @punctuation.bracket 78 | "[" @punctuation.bracket 79 | "]" @punctuation.bracket 80 | "{" @punctuation.bracket 81 | "}" @punctuation.bracket 82 | 83 | (type_arguments 84 | "<" @punctuation.bracket 85 | ">" @punctuation.bracket) 86 | (type_parameters 87 | "<" @punctuation.bracket 88 | ">" @punctuation.bracket) 89 | 90 | "::" @punctuation.delimiter 91 | ":" @punctuation.delimiter 92 | "." @punctuation.delimiter 93 | "," @punctuation.delimiter 94 | ";" @punctuation.delimiter 95 | 96 | (parameter (identifier) @variable.parameter) 97 | 98 | (lifetime (identifier) @label) 99 | 100 | "as" @keyword 101 | "async" @keyword 102 | "await" @keyword 103 | "break" @keyword 104 | "const" @keyword 105 | "continue" @keyword 106 | "default" @keyword 107 | "dyn" @keyword 108 | "else" @keyword 109 | "enum" @keyword 110 | "extern" @keyword 111 | "fn" @keyword 112 | "for" @keyword 113 | "if" @keyword 114 | "impl" @keyword 115 | "in" @keyword 116 | "let" @keyword 117 | "loop" @keyword 118 | "macro_rules!" @keyword 119 | "match" @keyword 120 | "mod" @keyword 121 | "move" @keyword 122 | "pub" @keyword 123 | "ref" @keyword 124 | "return" @keyword 125 | "static" @keyword 126 | "struct" @keyword 127 | "trait" @keyword 128 | "type" @keyword 129 | "union" @keyword 130 | "unsafe" @keyword 131 | "use" @keyword 132 | "where" @keyword 133 | "while" @keyword 134 | "yield" @keyword 135 | (crate) @keyword 136 | (mutable_specifier) @keyword 137 | (use_list (self) @keyword) 138 | (scoped_use_list (self) @keyword) 139 | (scoped_identifier (self) @keyword) 140 | (super) @keyword 141 | 142 | (self) @variable.builtin 143 | 144 | (char_literal) @string 145 | (string_literal) @string 146 | (raw_string_literal) @string 147 | 148 | (boolean_literal) @constant.builtin 149 | (integer_literal) @constant.builtin 150 | (float_literal) @constant.builtin 151 | 152 | (escape_sequence) @escape 153 | 154 | (attribute_item) @attribute 155 | (inner_attribute_item) @attribute 156 | 157 | "*" @operator 158 | "&" @operator 159 | "'" @operator 160 | -------------------------------------------------------------------------------- /src/assets/queries/highlights-yaml.scm: -------------------------------------------------------------------------------- 1 | ;; keys 2 | (block_mapping_pair 3 | key: (flow_node [(double_quote_scalar) (single_quote_scalar)] @variable)) 4 | (block_mapping_pair 5 | key: (flow_node (plain_scalar (string_scalar) @keyword))) 6 | 7 | ;; keys within inline {} blocks 8 | (flow_mapping 9 | (_ key: (flow_node [(double_quote_scalar) (single_quote_scalar)] @variable))) 10 | (flow_mapping 11 | (_ key: (flow_node (plain_scalar (string_scalar) @variable)))) 12 | 13 | ;; values 14 | (block_mapping_pair 15 | value: (flow_node (plain_scalar (string_scalar) @string))) 16 | (block_mapping_pair 17 | value: (block_node (block_scalar) @string)) 18 | 19 | ;; strings, numbers, bools 20 | [(double_quote_scalar) (single_quote_scalar) (block_scalar)] @string 21 | [(null_scalar) (boolean_scalar)] @constant.builtin 22 | [(integer_scalar) (float_scalar)] @number 23 | 24 | ["[" "]" "{" "}"] @punctuation.bracket 25 | ["," "-" ":" "?" ">" "|"] @punctuation.delimiter 26 | ["*" "&" "---" "..."] @punctuation.special 27 | 28 | (escape_sequence) @escape 29 | 30 | (comment) @comment 31 | [(anchor_name) (alias_name)] @function 32 | (yaml_directive) @type 33 | 34 | (tag) @type 35 | (tag_handle) @type 36 | (tag_prefix) @string 37 | (tag_directive) @property 38 | -------------------------------------------------------------------------------- /src/assets/queries/highlights-zig.scm: -------------------------------------------------------------------------------- 1 | ; Variables 2 | 3 | (identifier) @variable 4 | 5 | ; Parameters 6 | 7 | (parameter 8 | name: (identifier) @variable.parameter) 9 | 10 | ; Types 11 | 12 | (parameter 13 | type: (identifier) @type) 14 | 15 | ((identifier) @type 16 | (#lua-match? @type "^[A-Z_][a-zA-Z0-9_]*")) 17 | 18 | (variable_declaration 19 | (identifier) @type 20 | "=" 21 | [ 22 | (struct_declaration) 23 | (enum_declaration) 24 | (union_declaration) 25 | (opaque_declaration) 26 | ]) 27 | 28 | [ 29 | (builtin_type) 30 | "anyframe" 31 | ] @type.builtin 32 | 33 | ; Constants 34 | 35 | ((identifier) @constant 36 | (#lua-match? @constant "^[A-Z][A-Z_0-9]+$")) 37 | 38 | [ 39 | "null" 40 | "unreachable" 41 | "undefined" 42 | ] @constant.builtin 43 | 44 | (field_expression 45 | . 46 | member: (identifier) @constant) 47 | 48 | (enum_declaration 49 | (container_field 50 | type: (identifier) @constant)) 51 | 52 | ; Labels 53 | 54 | (block_label (identifier) @label) 55 | 56 | (break_label (identifier) @label) 57 | 58 | ; Fields 59 | 60 | (field_initializer 61 | . 62 | (identifier) @variable.member) 63 | 64 | (field_expression 65 | (_) 66 | member: (identifier) @variable.member) 67 | 68 | (container_field 69 | name: (identifier) @variable.member) 70 | 71 | (initializer_list 72 | (assignment_expression 73 | left: (field_expression 74 | . 75 | member: (identifier) @variable.member))) 76 | 77 | ; Functions 78 | 79 | (builtin_identifier) @function.builtin 80 | 81 | (call_expression 82 | function: (identifier) @function.call) 83 | 84 | (call_expression 85 | function: (field_expression 86 | member: (identifier) @function.call)) 87 | 88 | (function_declaration 89 | name: (identifier) @function) 90 | 91 | ; Modules 92 | 93 | (variable_declaration 94 | (identifier) @module 95 | (builtin_function 96 | (builtin_identifier) @keyword.import 97 | (#any-of? @keyword.import "@import" "@cImport"))) 98 | 99 | ; Builtins 100 | 101 | [ 102 | "c" 103 | "..." 104 | ] @variable.builtin 105 | 106 | ((identifier) @variable.builtin 107 | (#eq? @variable.builtin "_")) 108 | 109 | (calling_convention 110 | (identifier) @variable.builtin) 111 | 112 | ; Keywords 113 | 114 | [ 115 | "asm" 116 | "defer" 117 | "errdefer" 118 | "test" 119 | "error" 120 | "const" 121 | "var" 122 | ] @keyword 123 | 124 | [ 125 | "struct" 126 | "union" 127 | "enum" 128 | "opaque" 129 | ] @keyword.type 130 | 131 | [ 132 | "async" 133 | "await" 134 | "suspend" 135 | "nosuspend" 136 | "resume" 137 | ] @keyword.coroutine 138 | 139 | "fn" @keyword.function 140 | 141 | [ 142 | "and" 143 | "or" 144 | "orelse" 145 | ] @keyword.operator 146 | 147 | "return" @keyword.return 148 | 149 | [ 150 | "if" 151 | "else" 152 | "switch" 153 | ] @keyword.conditional 154 | 155 | [ 156 | "for" 157 | "while" 158 | "break" 159 | "continue" 160 | ] @keyword.repeat 161 | 162 | [ 163 | "usingnamespace" 164 | "export" 165 | ] @keyword.import 166 | 167 | [ 168 | "try" 169 | "catch" 170 | ] @keyword.exception 171 | 172 | [ 173 | "volatile" 174 | "allowzero" 175 | "noalias" 176 | "addrspace" 177 | "align" 178 | "callconv" 179 | "linksection" 180 | "pub" 181 | "inline" 182 | "noinline" 183 | "extern" 184 | "comptime" 185 | "packed" 186 | "threadlocal" 187 | ] @keyword.modifier 188 | 189 | ; Operator 190 | 191 | [ 192 | "=" 193 | "*=" 194 | "*%=" 195 | "*|=" 196 | "/=" 197 | "%=" 198 | "+=" 199 | "+%=" 200 | "+|=" 201 | "-=" 202 | "-%=" 203 | "-|=" 204 | "<<=" 205 | "<<|=" 206 | ">>=" 207 | "&=" 208 | "^=" 209 | "|=" 210 | "!" 211 | "~" 212 | "-" 213 | "-%" 214 | "&" 215 | "==" 216 | "!=" 217 | ">" 218 | ">=" 219 | "<=" 220 | "<" 221 | "&" 222 | "^" 223 | "|" 224 | "<<" 225 | ">>" 226 | "<<|" 227 | "+" 228 | "++" 229 | "+%" 230 | "-%" 231 | "+|" 232 | "-|" 233 | "*" 234 | "/" 235 | "%" 236 | "**" 237 | "*%" 238 | "*|" 239 | "||" 240 | ".*" 241 | ".?" 242 | "?" 243 | ".." 244 | ] @operator 245 | 246 | ; Literals 247 | 248 | (character) @character 249 | 250 | ([ 251 | (string) 252 | (multiline_string) 253 | ] @string 254 | (#set! "priority" 95)) 255 | 256 | (integer) @number 257 | 258 | (float) @number.float 259 | 260 | (boolean) @boolean 261 | 262 | (escape_sequence) @string.escape 263 | 264 | ; Punctuation 265 | 266 | [ 267 | "[" 268 | "]" 269 | "(" 270 | ")" 271 | "{" 272 | "}" 273 | ] @punctuation.bracket 274 | 275 | [ 276 | ";" 277 | "." 278 | "," 279 | ":" 280 | "=>" 281 | "->" 282 | ] @punctuation.delimiter 283 | 284 | (payload "|" @punctuation.bracket) 285 | 286 | ; Comments 287 | 288 | (comment) @comment @spell 289 | 290 | ((comment) @comment.documentation 291 | (#lua-match? @comment.documentation "^//!")) 292 | -------------------------------------------------------------------------------- /src/lib/ast/containers.zig: -------------------------------------------------------------------------------- 1 | /// containers.zig 2 | /// Container Block type implementations 3 | const std = @import("std"); 4 | 5 | /// Containers are Blocks which contain other Blocks 6 | pub const ContainerType = enum(u8) { 7 | Document, // The Document is the root container 8 | Quote, 9 | List, // Can only contain ListItems 10 | ListItem, // Can only be contained by a List 11 | Table, // Can only be contained by a Document; Can only contain Paragraphs 12 | }; 13 | 14 | pub const ContainerData = union(ContainerType) { 15 | Document: void, 16 | Quote: void, 17 | List: List, 18 | ListItem: ListItem, 19 | Table: Table, 20 | }; 21 | 22 | /// List blocks contain only ListItems 23 | /// However, we will use the base Container type's 'children' field to 24 | /// store the list items for simplicity, as the ListItems are Container blocks 25 | /// which can hold any kind of Block. 26 | pub const List = struct { 27 | pub const Kind = enum { 28 | unordered, 29 | ordered, 30 | task, 31 | }; 32 | kind: Kind = .unordered, 33 | start: usize = 1, // Starting number, if ordered list 34 | }; 35 | 36 | /// Single ListItem - Only needed for Task lists 37 | pub const ListItem = struct { 38 | checked: bool = false, // 󰄱 or 󰄵 39 | }; 40 | 41 | /// Table 42 | pub const Table = struct { 43 | ncol: usize = 0, 44 | row: usize = 0, 45 | }; 46 | -------------------------------------------------------------------------------- /src/lib/ast/inlines.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const utils = @import("../utils.zig"); 3 | const debug = @import("../debug.zig"); 4 | const tokens = @import("../tokens.zig"); 5 | 6 | const ArrayList = std.ArrayList; 7 | const Allocator = std.mem.Allocator; 8 | 9 | const TextStyle = utils.TextStyle; 10 | const Token = tokens.Token; 11 | const printIndent = utils.printIndent; 12 | 13 | /// Inlines are considered Phrasing content 14 | /// Phrasing content represents the text in a document, and its markup 15 | pub const InlineType = enum(u8) { 16 | autolink, 17 | codespan, 18 | image, 19 | linebreak, 20 | link, 21 | text, 22 | }; 23 | 24 | pub const InlineData = union(InlineType) { 25 | autolink: Autolink, 26 | codespan: Codespan, 27 | image: Image, 28 | linebreak: void, 29 | link: Link, 30 | text: Text, 31 | 32 | pub fn init(kind: InlineType) InlineData { 33 | switch (kind) { 34 | .Autolink => return InlineData{ .autolink = .{} }, 35 | .Codespan => return InlineData{ .codespan = .{} }, 36 | .Link => return InlineData{ .link = .{} }, 37 | .Linebreak => return InlineData{ .linebreak = .{} }, 38 | .Image => return InlineData{ .image = .{} }, 39 | .Text => return InlineData{ .text = .{} }, 40 | } 41 | } 42 | 43 | pub fn deinit(self: *InlineData) void { 44 | switch (self.*) { 45 | .link => |*l| l.deinit(), 46 | .image => |*i| i.deinit(), 47 | .text => |*t| t.deinit(), 48 | .codespan => |*c| c.deinit(), 49 | .autolink => |*a| a.deinit(), 50 | else => {}, 51 | } 52 | } 53 | 54 | pub fn print(self: InlineData, depth: u8) void { 55 | switch (self) { 56 | .codespan, .linebreak => { 57 | printIndent(depth); 58 | debug.print("Inline {s}\n", .{@tagName(self)}); 59 | }, 60 | .link => |link| { 61 | printIndent(depth); 62 | debug.print("Link:\n", .{}); 63 | for (link.text.items) |text| { 64 | text.print(depth + 1); 65 | } 66 | }, 67 | inline else => |item| { 68 | item.print(depth); 69 | }, 70 | } 71 | } 72 | }; 73 | 74 | pub const Inline = struct { 75 | const Self = @This(); 76 | alloc: Allocator, 77 | open: bool = true, 78 | content: InlineData = undefined, 79 | 80 | pub fn init(alloc: Allocator, kind: InlineType) !Inline { 81 | return .{ 82 | .alloc = alloc, 83 | .content = InlineData.init(kind), 84 | }; 85 | } 86 | 87 | pub fn initWithContent(alloc: Allocator, content: InlineData) Inline { 88 | return .{ 89 | .alloc = alloc, 90 | .content = content, 91 | }; 92 | } 93 | 94 | pub fn deinit(self: *Inline) void { 95 | self.content.deinit(); 96 | } 97 | 98 | pub fn print(self: Inline, depth: u8) void { 99 | self.content.print(depth); 100 | } 101 | }; 102 | 103 | /// Section of formatted text (single style) 104 | /// Example: "plain text" or "**bold text**" 105 | pub const Text = struct { 106 | alloc: ?Allocator = null, 107 | style: TextStyle = TextStyle{}, 108 | text: []const u8 = undefined, // The Text is assumed to own the string if 'alloc' is not null 109 | line: usize = 0, // Line number where this text appears 110 | col: usize = 0, // Column number where this text starts 111 | 112 | pub fn print(self: Text, depth: u8) void { 113 | printIndent(depth); 114 | debug.print("Text: '{s}' [line: {d}, col: {d}]\n", .{ self.text, self.line, self.col }); 115 | // printIndent(depth); 116 | // debug.print("Style: ", .{}); 117 | // if (self.style.fg_color) |fg| { 118 | // debug.print("fg: {s}", .{@tagName(fg)}); 119 | // } 120 | // if (self.style.bg_color) |bg| { 121 | // debug.print("bg: {s},", .{@tagName(bg)}); 122 | // } 123 | // inline for (@typeInfo(TextStyle).@"struct".fields) |field| { 124 | // const T: type = @TypeOf(@field(self.style, field.name)); 125 | // if (T == bool) { 126 | // if (@field(self.style, field.name)) { 127 | // debug.print("{s}", .{field.name}); 128 | // } 129 | // } 130 | // } 131 | // debug.print("\n", .{}); 132 | } 133 | 134 | pub fn deinit(self: *Text) void { 135 | if (self.alloc) |alloc| { 136 | alloc.free(self.text); 137 | } 138 | } 139 | }; 140 | 141 | /// Hyperlink 142 | pub const Link = struct { 143 | alloc: Allocator, 144 | url: []const u8, 145 | text: ArrayList(Text), 146 | heap_url: bool = false, // Whether the URL string has been Heap-allocated 147 | 148 | pub fn init(alloc: Allocator) Link { 149 | return .{ 150 | .alloc = alloc, 151 | .url = undefined, 152 | .text = ArrayList(Text).init(alloc), 153 | }; 154 | } 155 | 156 | pub fn deinit(self: *Link) void { 157 | for (self.text.items) |*text| { 158 | text.deinit(); 159 | } 160 | self.text.deinit(); 161 | 162 | if (self.heap_url and self.url.len > 0) 163 | self.alloc.free(self.url); 164 | } 165 | 166 | pub fn print(self: Link, depth: u8) void { 167 | printIndent(depth); 168 | //debug.print("Link to {s}\n", .{self.url}); 169 | debug.print("Link:\n", .{}); 170 | for (self.text.items) |text| { 171 | text.print(depth + 1); 172 | } 173 | } 174 | }; 175 | 176 | /// Raw text codespan 177 | pub const Codespan = struct { 178 | const Self = @This(); 179 | alloc: ?Allocator = null, 180 | text: []const u8 = "", 181 | 182 | pub fn deinit(self: *Self) void { 183 | if (self.alloc) |alloc| { 184 | alloc.free(self.text); 185 | self.alloc = null; 186 | } 187 | } 188 | }; 189 | 190 | /// Image Link 191 | pub const Image = struct { 192 | alloc: Allocator, 193 | src: []const u8, 194 | alt: ArrayList(Text), 195 | kind: Kind = .local, // Local file, or web URL 196 | format: Format = .other, 197 | heap_src: bool = false, // Whether the src string has been heap-allocated 198 | 199 | pub const Format = enum(u8) { 200 | /// PNG file that can be directly sent to the terminal with the Kitty Graphics Protocol 201 | png, 202 | /// JPEG file that can be loaded with stb_image and sent to the terminal as raw RGB pixels 203 | jpeg, 204 | /// SVG file that can be converted to a PNG 205 | svg, 206 | /// Some other image type we will attempt to load using stb_image 207 | other, 208 | }; 209 | 210 | pub const Kind = enum(u8) { 211 | local, 212 | web, 213 | }; 214 | 215 | pub fn init(alloc: Allocator) Image { 216 | return .{ 217 | .alloc = alloc, 218 | .src = "", 219 | .alt = ArrayList(Text).init(alloc), 220 | }; 221 | } 222 | 223 | pub fn deinit(self: *Image) void { 224 | for (self.alt.items) |*text| { 225 | text.deinit(); 226 | } 227 | self.alt.deinit(); 228 | 229 | if (self.heap_src and self.src.len > 0) 230 | self.alloc.free(self.src); 231 | } 232 | 233 | pub fn print(self: Image, depth: u8) void { 234 | printIndent(depth); 235 | debug.print("Image: {s}\n", .{self.src}); 236 | for (self.alt.items) |text| { 237 | text.print(depth + 1); 238 | } 239 | } 240 | }; 241 | 242 | /// Auto-link 243 | pub const Autolink = struct { 244 | alloc: Allocator, 245 | url: []const u8, 246 | heap_url: bool = false, // Whether the url string has been heap-allocated 247 | 248 | pub fn print(self: Autolink, depth: u8) void { 249 | printIndent(depth); 250 | debug.print("Autolink: {s}\n", .{self.url}); 251 | } 252 | 253 | pub fn deinit(self: *Autolink) void { 254 | if (self.heap_url and self.url.len > 0) { 255 | self.alloc.free(self.url); 256 | } 257 | } 258 | }; 259 | -------------------------------------------------------------------------------- /src/lib/ast/leaves.zig: -------------------------------------------------------------------------------- 1 | /// leaves.zig 2 | /// Leaf Block type implementations 3 | const std = @import("std"); 4 | 5 | const tokens = @import("../tokens.zig"); 6 | const utils = @import("../utils.zig"); 7 | const debug = @import("../debug.zig"); 8 | const inlines = @import("inlines.zig"); 9 | 10 | const Allocator = std.mem.Allocator; 11 | const ArrayList = std.ArrayList; 12 | 13 | const Inline = inlines.Inline; 14 | const InlineType = inlines.InlineType; 15 | 16 | const Text = inlines.Text; 17 | const Link = inlines.Link; 18 | // const Image = inlines.Image; 19 | 20 | const printIndent = utils.printIndent; 21 | 22 | /// All types of Leaf blocks that can be contained in Container blocks 23 | pub const LeafType = enum(u8) { 24 | Break, 25 | Code, 26 | Heading, 27 | Paragraph, 28 | // Reference, 29 | }; 30 | 31 | /// The type-specific content of a Leaf block 32 | pub const LeafData = union(LeafType) { 33 | Break: void, 34 | Code: Code, 35 | Heading: Heading, 36 | Paragraph: void, 37 | // Reference: Reference, 38 | 39 | pub fn deinit(self: *LeafData) void { 40 | switch (self.*) { 41 | .Heading => |*h| h.deinit(), 42 | .Code => |*c| c.deinit(), 43 | inline else => {}, 44 | } 45 | } 46 | 47 | pub fn print(self: LeafData, depth: u8) void { 48 | switch (self) { 49 | .Heading => |h| h.print(depth), 50 | .Code => |c| c.print(depth), 51 | inline else => {}, 52 | } 53 | } 54 | }; 55 | 56 | /// A heading with associated level 57 | pub const Heading = struct { 58 | alloc: Allocator = undefined, 59 | level: u8 = 1, 60 | text: []const u8 = undefined, 61 | 62 | pub fn init(alloc: Allocator) Heading { 63 | return .{ .alloc = alloc }; 64 | } 65 | 66 | pub fn deinit(h: *Heading) void { 67 | h.alloc.free(h.text); 68 | } 69 | 70 | pub fn print(h: Heading, depth: u8) void { 71 | printIndent(depth); 72 | debug.print("[H{d}] '{s}'\n", .{ h.level, h.text }); 73 | } 74 | }; 75 | 76 | /// Raw code or other preformatted content 77 | /// TODO: Split into "Code" and "Directive" 78 | pub const Code = struct { 79 | alloc: Allocator = undefined, 80 | // The opening tag, e.g. "```", that has to be matched to end the block 81 | opener: ?[]const u8 = null, 82 | tag: ?[]const u8 = null, 83 | directive: ?[]const u8 = null, 84 | text: ?[]const u8 = "", 85 | 86 | pub fn init(alloc: Allocator) Code { 87 | return .{ .alloc = alloc }; 88 | } 89 | 90 | pub fn deinit(c: *Code) void { 91 | if (c.tag) |tag| c.alloc.free(tag); 92 | if (c.text) |text| c.alloc.free(text); 93 | } 94 | 95 | pub fn print(c: Code, depth: u8) void { 96 | printIndent(depth); 97 | var tag: []const u8 = ""; 98 | var text: []const u8 = ""; 99 | if (c.tag) |ctag| tag = ctag; 100 | if (c.text) |ctext| text = ctext; 101 | debug.print("tag: '{s}'; body:\n{s}\n", .{ tag, text }); 102 | } 103 | }; 104 | -------------------------------------------------------------------------------- /src/lib/console.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const utils = @import("utils.zig"); 3 | 4 | const AnyWriter = std.io.AnyWriter; 5 | 6 | const Color = utils.Color; 7 | const Style = utils.Style; 8 | const TextStyle = utils.TextStyle; 9 | 10 | // ANSI terminal escape character 11 | pub const ansi = [1]u8{0x1b}; 12 | 13 | // ANSI Reset command (clear formatting) 14 | pub const ansi_end = ansi ++ "[m"; 15 | 16 | // ANSI cursor movements 17 | pub const move_up = ansi ++ "[{d}A"; 18 | pub const move_down = ansi ++ "[{d}B"; 19 | pub const move_right = ansi ++ "[{d}C"; 20 | pub const move_left = ansi ++ "[{d}D"; 21 | pub const move_setcol = ansi ++ "[{d}G"; 22 | pub const move_home = ansi ++ "[0G"; 23 | 24 | pub const set_col = ansi ++ "[{d}G"; 25 | pub const set_row_col = ansi ++ "[{d};{d}H"; // Row, Column 26 | 27 | pub const save_position = ansi ++ "[s"; 28 | pub const restore_position = ansi ++ "[u"; 29 | 30 | // ANSI Clear Screen Command 31 | pub const clear_screen_end = ansi ++ "[0J"; // Clear from cursor to end of screen 32 | pub const clear_screen_beg = ansi ++ "[1J"; // Clear from cursor to beginning of screen 33 | pub const clear_screen = ansi ++ "[2J"; // Clear entire screen 34 | 35 | // ANSI Clear Line Command 36 | pub const clear_line_end = ansi ++ "[0K"; // Clear from cursor to end of line 37 | pub const clear_line_beg = ansi ++ "[1K"; // Clear from cursor to beginning of line 38 | pub const clear_line = ansi ++ "[2K"; // Clear entire line 39 | 40 | // ==================================================== 41 | // ANSI display codes (colors, styles, etc.) 42 | // ---------------------------------------------------- 43 | 44 | // Basic Background Colors 45 | pub const bg_black = ansi ++ "[40m"; 46 | pub const bg_red = ansi ++ "[41m"; 47 | pub const bg_green = ansi ++ "[42m"; 48 | pub const bg_yellow = ansi ++ "[43m"; 49 | pub const bg_blue = ansi ++ "[44m"; 50 | pub const bg_magenta = ansi ++ "[45m"; 51 | pub const bg_cyan = ansi ++ "[46m"; 52 | pub const bg_white = ansi ++ "[47m"; 53 | pub const bg_default = ansi ++ "[49m"; 54 | 55 | // Extended Background Colors 56 | pub const bg_dark_yellow = ansi ++ "[48;5;178m"; 57 | pub const bg_purple_grey = ansi ++ "[48;2;170;130;250m"; // #aa82fa 58 | pub const bg_dark_grey = ansi ++ "[48;2;64;64;64m"; // #404040 59 | pub const bg_dark_red = ansi ++ "[48;2;128;32;32m"; // #802020 60 | pub const bg_rgb_blue = ansi ++ "[48;2;120;141;216m"; // #788dd8 61 | pub const bg_rgb_orange = ansi ++ "[48;2;255;151;0m"; // #ff9700 62 | pub const bg_rgb_coral = ansi ++ "[48;2;215;100;155m"; // #d7649b 63 | 64 | pub const bg_rgb_fmt = ansi ++ "[48;{d};{d};{d}m"; 65 | 66 | // Basic Foreground Colors 67 | pub const fg_black = ansi ++ "[30m"; 68 | pub const fg_red = ansi ++ "[31m"; 69 | pub const fg_green = ansi ++ "[32m"; 70 | pub const fg_yellow = ansi ++ "[33m"; 71 | pub const fg_blue = ansi ++ "[34m"; 72 | pub const fg_magenta = ansi ++ "[35m"; 73 | pub const fg_cyan = ansi ++ "[36m"; 74 | pub const fg_white = ansi ++ "[37m"; 75 | pub const fg_default = ansi ++ "[39m"; 76 | 77 | // Extended Foreground Colors 78 | pub const fg_dark_yellow = ansi ++ "[38;5;178m"; 79 | pub const fg_purple_grey = ansi ++ "[38;2;170;130;250m"; // #aa82fa 80 | pub const fg_dark_grey = ansi ++ "[38;2;112;112;112m"; // #707070 81 | pub const fg_dark_red = ansi ++ "[38;2;128;32;32m"; // #802020 82 | pub const fg_rgb_blue = ansi ++ "[38;2;120;141;216m"; // #788dd8 83 | pub const fg_rgb_orange = ansi ++ "[38;2;255;151;0m"; // #ff9700 84 | pub const fg_rgb_coral = ansi ++ "[38;2;215;100;155m"; // #d7649b 85 | 86 | pub const fg_rgb_fmt = ansi ++ "[38;{d};{d};{d}m"; 87 | 88 | // 24-Bit Coloring 89 | // Format strings which take 3 u8's for (r, g, b) 90 | pub const fg_rgb = ansi ++ "[38;2;{d};{d};{d}m"; 91 | pub const bg_rgb = ansi ++ "[48;2;{d};{d};{d}m"; 92 | 93 | // Typeface Formatting 94 | pub const text_bold = ansi ++ "[1m"; 95 | pub const text_italic = ansi ++ "[3m"; 96 | pub const text_underline = ansi ++ "[4m"; 97 | pub const text_blink = ansi ++ "[5m"; 98 | pub const text_fastblink = ansi ++ "[6m"; 99 | pub const text_reverse = ansi ++ "[7m"; 100 | pub const text_hide = ansi ++ "[8m"; 101 | pub const text_strike = ansi ++ "[9m"; 102 | 103 | pub const end_bold = ansi ++ "[22m"; 104 | pub const end_italic = ansi ++ "[23m"; 105 | pub const end_underline = ansi ++ "[24m"; 106 | pub const end_blink = ansi ++ "[25m"; 107 | pub const end_reverse = ansi ++ "[27m"; 108 | pub const end_hide = ansi ++ "[28m"; 109 | pub const end_strike = ansi ++ "[29m"; 110 | 111 | pub const hyperlink = ansi ++ "]8;;"; 112 | pub const link_end = ansi ++ "\\"; 113 | 114 | pub fn getFgColor(color: Color) []const u8 { 115 | return switch (color) { 116 | .Black => fg_black, 117 | .Red => fg_red, 118 | .Green => fg_green, 119 | .Yellow => fg_yellow, 120 | .Blue => fg_blue, 121 | .Cyan => fg_cyan, 122 | .White => fg_white, 123 | .Magenta => fg_magenta, 124 | .DarkYellow => fg_dark_yellow, 125 | .PurpleGrey => fg_purple_grey, 126 | .DarkGrey => fg_dark_grey, 127 | .DarkRed => fg_dark_red, 128 | .Orange => fg_rgb_orange, 129 | .Coral => fg_rgb_coral, 130 | .Default => fg_default, 131 | }; 132 | } 133 | 134 | pub fn getBgColor(color: Color) []const u8 { 135 | return switch (color) { 136 | .Black => bg_black, 137 | .Red => bg_red, 138 | .Green => bg_green, 139 | .Yellow => bg_yellow, 140 | .Blue => bg_blue, 141 | .Cyan => bg_cyan, 142 | .White => bg_white, 143 | .Magenta => bg_magenta, 144 | .DarkYellow => bg_dark_yellow, 145 | .PurpleGrey => bg_purple_grey, 146 | .DarkGrey => bg_dark_grey, 147 | .DarkRed => bg_dark_red, 148 | .Orange => bg_rgb_orange, 149 | .Coral => bg_rgb_coral, 150 | .Default => bg_default, 151 | }; 152 | } 153 | 154 | /// Configure the terminal to start printing with the given foreground color 155 | pub fn startFgColor(stream: AnyWriter, color: Color) void { 156 | stream.print("{s}", .{getFgColor(color)}) catch unreachable; 157 | } 158 | 159 | /// Configure the terminal to start printing with the given background color 160 | pub fn startBgColor(stream: AnyWriter, color: Color) void { 161 | stream.print("{s}", .{getBgColor(color)}) catch unreachable; 162 | } 163 | 164 | /// Configure the terminal to start printing with the given (single) style 165 | pub fn startStyle(stream: AnyWriter, style: Style) void { 166 | switch (style) { 167 | .Bold => stream.print(text_bold, .{}) catch unreachable, 168 | .Italic => stream.print(text_italic, .{}) catch unreachable, 169 | .Underline => stream.print(text_underline, .{}) catch unreachable, 170 | .Blink => stream.print(text_blink, .{}) catch unreachable, 171 | .FastBlink => stream.print(text_fastblink, .{}) catch unreachable, 172 | .Reverse => stream.print(text_reverse, .{}) catch unreachable, 173 | .Hide => stream.print(text_hide, .{}) catch unreachable, 174 | .Strike => stream.print(text_strike, .{}) catch unreachable, 175 | } 176 | } 177 | 178 | /// Configure the terminal to start printing one or more styles with color 179 | pub fn startStyles(stream: AnyWriter, style: TextStyle) void { 180 | if (style.bold) stream.print(text_bold, .{}) catch unreachable; 181 | if (style.italic) stream.print(text_italic, .{}) catch unreachable; 182 | if (style.underline) stream.print(text_underline, .{}) catch unreachable; 183 | if (style.blink) stream.print(text_blink, .{}) catch unreachable; 184 | if (style.fastblink) stream.print(text_fastblink, .{}) catch unreachable; 185 | if (style.reverse) stream.print(text_reverse, .{}) catch unreachable; 186 | if (style.hide) stream.print(text_hide, .{}) catch unreachable; 187 | if (style.strike) stream.print(text_strike, .{}) catch unreachable; 188 | 189 | if (style.fg_color) |fg_color| { 190 | startFgColor(stream, fg_color); 191 | } 192 | 193 | if (style.bg_color) |bg_color| { 194 | startBgColor(stream, bg_color); 195 | } 196 | } 197 | 198 | /// Reset all style in the terminal 199 | pub fn resetStyle(stream: AnyWriter) void { 200 | stream.print(ansi_end, .{}) catch unreachable; 201 | } 202 | 203 | /// Print the text using the given color 204 | pub fn printColor(stream: AnyWriter, color: Color, comptime fmt: []const u8, args: anytype) void { 205 | startFgColor(stream, color); 206 | stream.print(fmt, args) catch unreachable; 207 | resetStyle(stream); 208 | } 209 | 210 | /// Print the text using the given style description 211 | pub fn printStyled(stream: AnyWriter, style: TextStyle, comptime fmt: []const u8, args: anytype) void { 212 | startStyles(stream, style); 213 | stream.print(fmt, args) catch unreachable; 214 | resetStyle(stream); 215 | } 216 | 217 | // ==================================================== 218 | // Assemble our suite of box-drawing Unicode characters 219 | // ---------------------------------------------------- 220 | 221 | // Styles 222 | // 223 | // Sharp: Round: Double: Bold: 224 | // ┌─┬─┐ ╭─┬─╮ ╔═╦═╗ ┏━┳━┓ 225 | // ├─┼─┤ ├─┼─┤ ╠═╬═╣ ┣━╋━┫ 226 | // └─┴─┘ ╰─┴─╯ ╚═╩═╝ ┗━┻━┛ 227 | 228 | // "base class" for all our box-drawing character sets 229 | pub const Box = struct { 230 | hb: []const u8 = undefined, // horizontal bar 231 | vb: []const u8 = undefined, // vertical bar 232 | tl: []const u8 = undefined, // top-left 233 | tr: []const u8 = undefined, // top-right 234 | bl: []const u8 = undefined, // bottom-left 235 | br: []const u8 = undefined, // bottom-right 236 | lj: []const u8 = undefined, // left junction 237 | tj: []const u8 = undefined, // top junction 238 | rj: []const u8 = undefined, // right junction 239 | bj: []const u8 = undefined, // bottom junction 240 | cj: []const u8 = undefined, // center junction 241 | }; 242 | 243 | // Dummy style using plain ASCII characters 244 | pub const DummyBox = Box{ 245 | .hb = '-', 246 | .vb = '|', 247 | .tl = '/', 248 | .tr = '\\', 249 | .bl = '\\', 250 | .br = '/', 251 | .lj = '+', 252 | .tj = '+', 253 | .rj = '+', 254 | .bj = '+', 255 | .cj = '+', 256 | }; 257 | 258 | // Thin single-lined box with sharp corners 259 | pub const SharpBox = Box{ 260 | .hb = "─", 261 | .vb = "│", 262 | .tl = "┌", 263 | .tr = "┐", 264 | .bl = "└", 265 | .br = "┘", 266 | .lj = "├", 267 | .tj = "┬", 268 | .rj = "┤", 269 | .bj = "┴", 270 | .cj = "┼", 271 | }; 272 | 273 | // Thin single-lined box with rounded corners 274 | pub const RoundedBox = Box{ 275 | .hb = "─", 276 | .vb = "│", 277 | .tl = "╭", 278 | .tr = "╮", 279 | .bl = "╰", 280 | .br = "╯", 281 | .lj = "├", 282 | .tj = "┬", 283 | .rj = "┤", 284 | .bj = "┴", 285 | .cj = "┼", 286 | }; 287 | 288 | // Thin double-lined box with sharp corners 289 | pub const DoubleBox = Box{ 290 | .hb = "═", 291 | .vb = "║", 292 | .tl = "╔", 293 | .tr = "╗", 294 | .bl = "╚", 295 | .br = "╝", 296 | .lj = "╠", 297 | .tj = "╦", 298 | .rj = "╣", 299 | .bj = "╩", 300 | .cj = "╬", 301 | }; 302 | 303 | // Thick single-lined box with sharp corners 304 | pub const BoldBox = Box{ 305 | .hb = "━", 306 | .vb = "┃", 307 | .tl = "┏", 308 | .tr = "┓", 309 | .bl = "┗", 310 | .br = "┛", 311 | .lj = "┣", 312 | .tj = "┳", 313 | .rj = "┫", 314 | .bj = "┻", 315 | .cj = "╋", 316 | }; 317 | -------------------------------------------------------------------------------- /src/lib/debug.zig: -------------------------------------------------------------------------------- 1 | /// Debugging-related functionality such as logging and error reporting. 2 | const std = @import("std"); 3 | const builtin = @import("builtin"); 4 | const cons = @import("console.zig"); 5 | const wasm = @import("wasm.zig"); 6 | 7 | const Token = @import("tokens.zig").Token; 8 | 9 | /// Global debug stream instance. 10 | /// Intended to be set once from main() via setStream(). 11 | var stream: ?std.io.AnyWriter = null; 12 | 13 | /// Set the global debug output stream. 14 | /// 15 | /// This can be, for example, a buffered writer for use in tests. 16 | pub fn setStream(out_stream: std.io.AnyWriter) void { 17 | stream = out_stream; 18 | } 19 | 20 | /// Get the global debug output stream. 21 | /// 22 | /// This should be used by all debug printing, e.g. from Block types. 23 | pub fn getStream() std.io.AnyWriter { 24 | if (stream == null) { 25 | @branchHint(.cold); 26 | if (!wasm.is_wasm) { 27 | stream = std.io.getStdErr().writer().any(); 28 | } else { 29 | @panic("Unable to debug.print in WASM!"); 30 | } 31 | } 32 | return stream.?; 33 | } 34 | 35 | pub fn print(comptime fmt: []const u8, args: anytype) void { 36 | getStream().print(fmt, args) catch @panic("Unable to write to debug stream!"); 37 | } 38 | 39 | pub fn errorReturn(comptime src: std.builtin.SourceLocation, comptime fmt: []const u8, args: anytype) !void { 40 | switch (builtin.cpu.arch) { 41 | .wasm32, .wasm64 => return error.ParseError, 42 | else => {}, 43 | } 44 | cons.printStyled(getStream(), .{ .fg_color = .Red, .bold = true }, "{s}-{d}: ERROR: ", .{ src.fn_name, src.line }); 45 | cons.printStyled(getStream(), .{ .bold = true }, fmt, args); 46 | print("\n", .{}); 47 | return error.ParseError; 48 | } 49 | 50 | pub fn errorMsg(comptime src: std.builtin.SourceLocation, comptime fmt: []const u8, args: anytype) void { 51 | switch (builtin.cpu.arch) { 52 | .wasm32, .wasm64 => return, 53 | else => {}, 54 | } 55 | cons.printStyled(getStream(), .{ .fg_color = .Red, .bold = true }, "{s}-{d}: ERROR: ", .{ src.fn_name, src.line }); 56 | cons.printStyled(getStream(), .{ .bold = true }, fmt, args); 57 | print("\n", .{}); 58 | } 59 | 60 | /// Helper struct to log debug messages (normal host-cpu logger) 61 | pub const Logger = struct { 62 | const Self = @This(); 63 | depth: usize = 0, 64 | enabled: bool = true, 65 | 66 | pub fn log(self: Self, comptime fmt: []const u8, args: anytype) void { 67 | self.doIndent(); 68 | self.raw(fmt, args); 69 | } 70 | 71 | pub fn raw(self: Self, comptime fmt: []const u8, args: anytype) void { 72 | if (self.enabled) { 73 | if (wasm.is_wasm) { 74 | wasm.Console.log(fmt, args); 75 | } else { 76 | print(fmt, args); 77 | } 78 | } 79 | } 80 | 81 | pub fn printTypes(self: Self, tokens: []const Token, indent: bool) void { 82 | if (indent) self.doIndent(); 83 | for (tokens) |tok| { 84 | self.raw("{s}, ", .{@tagName(tok.kind)}); 85 | } 86 | self.raw("\n", .{}); 87 | } 88 | 89 | pub fn printText(self: Self, tokens: []const Token, indent: bool) void { 90 | if (indent) self.doIndent(); 91 | self.raw("\"", .{}); 92 | for (tokens) |tok| { 93 | if (tok.kind == .BREAK) { 94 | self.raw("\\n", .{}); 95 | continue; 96 | } 97 | self.raw("{s}", .{tok.text}); 98 | } 99 | self.raw("\"\n", .{}); 100 | } 101 | 102 | fn doIndent(self: Self) void { 103 | var i: usize = 0; 104 | while (i < self.depth) : (i += 1) { 105 | self.raw("│ ", .{}); 106 | } 107 | } 108 | }; 109 | -------------------------------------------------------------------------------- /src/lib/image.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const stb = @import("stb_image"); 3 | const builtin = @import("builtin"); 4 | 5 | const debug = @import("debug.zig"); 6 | const utils = @import("utils.zig"); 7 | 8 | const Allocator = std.mem.Allocator; 9 | const File = std.fs.File; 10 | const Dir = std.fs.Dir; 11 | const Base64Encoder = std.base64.standard.Encoder; 12 | 13 | const os = std.os; 14 | const linux = std.os.linux; 15 | const posix = std.posix; 16 | const windows = std.os.windows; 17 | 18 | const esc: [1]u8 = [1]u8{0x1b}; // ANSI escape code 19 | 20 | const CHUNK_SIZE: usize = 4096; 21 | const NROW: usize = 40; 22 | const NCOL: usize = 120; 23 | 24 | /// Transmission medium options per the Kitty Terminal Graphics Protocol 25 | const Medium = enum(u8) { 26 | RGB = 24, 27 | RGBA = 32, 28 | PNG = 100, 29 | }; 30 | 31 | const GraphicsError = error{ 32 | FileIsNotPNG, 33 | WrongNumberOfChannels, 34 | WriteError, 35 | }; 36 | 37 | /// Check if the file is a valid PNG 38 | pub fn isPNG(data: []const u8) bool { 39 | if (data.len < 8) return false; 40 | 41 | const png_header: []const u8 = &([8]u8{ 137, 80, 78, 71, 13, 10, 26, 10 }); 42 | if (std.mem.startsWith(u8, data, png_header)) 43 | return true; 44 | 45 | return false; 46 | } 47 | 48 | /// Send a PNG image file to the terminal using the Kitty terminal graphics protocol 49 | /// 'width' and 'height' are in terms of terminal cells, not pixels! 50 | pub fn sendImagePNG(stream: anytype, alloc: Allocator, bytes: []const u8, width: ?usize, height: ?usize) !void { 51 | // Read the image into memory 52 | // Check that the image is a PNG file 53 | if (!isPNG(bytes)) { 54 | return error.FileIsNotPNG; 55 | } 56 | 57 | // Encode the image data as base64 58 | const blen = Base64Encoder.calcSize(bytes.len); 59 | const b64buf = try alloc.alloc(u8, blen); 60 | defer alloc.free(b64buf); 61 | 62 | const data = Base64Encoder.encode(b64buf, bytes); 63 | 64 | // Send the image data in 4kB chunks 65 | var pos: usize = 0; 66 | var i: usize = 0; 67 | while (pos < data.len) { 68 | const chunk_end = @min(pos + CHUNK_SIZE, data.len); 69 | const chunk = data[pos..chunk_end]; 70 | const last_chunk: bool = (chunk_end == data.len); 71 | try sendImageChunkPNG(stream, chunk, last_chunk, width, height); 72 | pos = chunk_end; 73 | i += 1; 74 | } 75 | } 76 | 77 | /// Send an image file to the terminal as raw RGB pixel data using the Kitty terminal graphics protocol 78 | pub fn sendImageRGB(stream: anytype, alloc: Allocator, bytes: []const u8, width: ?usize, height: ?usize) !void { 79 | // Read the image into memory 80 | var img: stb.Image = try stb.load_image_from_memory(bytes); 81 | defer img.deinit(); 82 | 83 | if (img.nchan != 3) 84 | return error.WrongNumberOfChannels; 85 | 86 | const size: usize = @intCast(img.width * img.height * img.nchan); 87 | const rgb: []u8 = img.data[0..size]; 88 | 89 | // Encode the image data as base64 90 | const blen = Base64Encoder.calcSize(rgb.len); 91 | const b64buf = try alloc.alloc(u8, blen); 92 | defer alloc.free(b64buf); 93 | 94 | const data = Base64Encoder.encode(b64buf, rgb); 95 | 96 | // Send the image data in 4kB chunks 97 | var pos: usize = 0; 98 | var i: usize = 0; 99 | while (pos < data.len) { 100 | const chunk_end = @min(pos + CHUNK_SIZE, data.len); 101 | const chunk = data[pos..chunk_end]; 102 | const last_chunk: bool = (chunk_end == data.len); 103 | try sendImageChunkRGB( 104 | stream, 105 | chunk, 106 | last_chunk, 107 | width, 108 | height, 109 | @intCast(img.width), 110 | @intCast(img.height), 111 | ); 112 | pos = chunk_end; 113 | i += 1; 114 | } 115 | } 116 | 117 | /// Send raw RGB image data to the terminal using the Kitty terminal graphics protocol 118 | pub fn sendImageRGB2(stream: anytype, alloc: Allocator, img: *const stb.Image, width: ?usize, height: ?usize) !void { 119 | const size: usize = @intCast(img.width * img.height * img.nchan); 120 | const rgb: []u8 = img.data[0..size]; 121 | 122 | // Encode the image data as base64 123 | const blen = Base64Encoder.calcSize(rgb.len); 124 | const b64buf = try alloc.alloc(u8, blen); 125 | defer alloc.free(b64buf); 126 | 127 | const data = Base64Encoder.encode(b64buf, rgb); 128 | 129 | // Send the image data in 4kB chunks 130 | var pos: usize = 0; 131 | var i: usize = 0; 132 | while (pos < data.len) { 133 | const chunk_end = @min(pos + CHUNK_SIZE, data.len); 134 | const chunk = data[pos..chunk_end]; 135 | const last_chunk: bool = (chunk_end == data.len); 136 | try sendImageChunkRGB( 137 | stream, 138 | chunk, 139 | last_chunk, 140 | width, 141 | height, 142 | @intCast(img.width), 143 | @intCast(img.height), 144 | ); 145 | pos = chunk_end; 146 | i += 1; 147 | } 148 | } 149 | 150 | /// Send a chunk of PNG image data in a single '_G' command 151 | fn sendImageChunkPNG(stream: anytype, data: []const u8, last_chunk: bool, width: ?usize, height: ?usize) !void { 152 | var m: u8 = 1; 153 | if (last_chunk) 154 | m = 0; 155 | 156 | const ncol = width orelse NCOL; 157 | const nrow = height orelse NROW; 158 | 159 | // TODO: Need to manually scale the image to preserve the aspect ratio 160 | // Kitty's 'icat' kitten uses ImageMagick to do the scaling - outputs to temporary file 161 | // and sends that instead of the original file 162 | _ = try stream.write(esc ++ "_G"); 163 | try stream.print("c={d},r={d},a=T,f={d},m={d}", .{ ncol, nrow, @intFromEnum(Medium.PNG), m }); 164 | 165 | if (data.len > 0) { 166 | // Send the image payload 167 | _ = try stream.write(";"); 168 | _ = try stream.write(data); 169 | } 170 | 171 | // Finish the command 172 | _ = try stream.write(esc ++ "\\"); 173 | } 174 | 175 | /// Send a chunk of RGB image data in a single '_G' command 176 | fn sendImageChunkRGB( 177 | stream: anytype, 178 | data: []const u8, 179 | last_chunk: bool, 180 | display_width: ?usize, 181 | display_height: ?usize, 182 | img_width: usize, 183 | img_height: usize, 184 | ) !void { 185 | const m: u8 = if (last_chunk) 0 else 1; 186 | const ncol = display_width orelse NCOL; 187 | const nrow = display_height orelse NROW; 188 | 189 | // TODO: Need to manually scale the image to preserve the aspect ratio 190 | // Kitty's 'icat' kitten uses ImageMagick to do the scaling - outputs to temporary file 191 | // and sends that instead of the original file 192 | _ = try stream.write(esc ++ "_G"); 193 | try stream.print("s={d},v={d},c={d},r={d},a=T,f={d},m={d}", .{ 194 | img_width, 195 | img_height, 196 | ncol, 197 | nrow, 198 | @intFromEnum(Medium.RGB), 199 | m, 200 | }); 201 | 202 | if (data.len > 0) { 203 | // Send the image payload 204 | _ = try stream.write(";"); 205 | _ = try stream.write(data); 206 | } 207 | 208 | // Finish the command 209 | _ = try stream.write(esc ++ "\\"); 210 | } 211 | 212 | pub const TermSize = struct { 213 | rows: usize = 150, // Number of rows in window 214 | cols: usize = 90, // Number of columns in window 215 | width: usize = 900, // Width of window in pixels 216 | height: usize = 3300, // Height of window in pixels 217 | }; 218 | 219 | pub fn getTerminalSize() !TermSize { 220 | if (builtin.os.tag == .linux) { 221 | var wsz: posix.winsize = undefined; 222 | const TIOCGWINSZ: usize = 21523; 223 | const stdout_fd: posix.fd_t = 0; 224 | 225 | if (linux.ioctl(stdout_fd, TIOCGWINSZ, @intFromPtr(&wsz)) == 0) { 226 | // Some terminals may report invalid sizes (0) 227 | if (wsz.col == 0 or wsz.row == 0 or wsz.xpixel == 0 or wsz.ypixel == 0) { 228 | return error.InvalidTerminalSize; 229 | } 230 | return TermSize{ 231 | .rows = wsz.row, 232 | .cols = wsz.col, 233 | .width = wsz.xpixel, 234 | .height = wsz.ypixel, 235 | }; 236 | } 237 | 238 | return error.SystemCallFailed; 239 | } 240 | 241 | if (builtin.os.tag == .windows) { 242 | var binfo: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined; 243 | const stdio_h = try windows.GetStdHandle(windows.STD_OUTPUT_HANDLE); 244 | 245 | if (windows.kernel32.GetConsoleScreenBufferInfo(stdio_h, &binfo) > 0) { 246 | const cols: i32 = binfo.srWindow.Right - binfo.srWindow.Left + 1; 247 | const rows: i32 = binfo.srWindow.Bottom - binfo.srWindow.Top + 1; 248 | return TermSize{ .rows = @intCast(rows), .cols = @intCast(cols) }; 249 | } 250 | 251 | return error.SystemCallFailed; 252 | } 253 | 254 | return error.UnsupportedSystem; 255 | } 256 | 257 | test "Get window size" { 258 | _ = getTerminalSize() catch return error.SkipZigTest; 259 | } 260 | 261 | // I don't know why this can't be run as a test... 262 | // 'zig test src/image.zig' works, but 'zig build test-image' just hangs 263 | test "Display image" { 264 | const alloc = std.testing.allocator; 265 | var buf = std.ArrayList(u8).init(alloc); 266 | defer buf.deinit(); 267 | const stream = buf.writer().any(); 268 | debug.setStream(stream); 269 | debug.print("Rendering Zero the Ziguana here:\n", .{}); 270 | const bytes = try utils.readFile(alloc, "src/assets/img/zig-zero.png"); 271 | defer alloc.free(bytes); 272 | try sendImagePNG(stream, alloc, bytes, 100, 60); 273 | debug.print("\n--------------------------------\n", .{}); 274 | // TODO: Can check for expected output in buf.items if I really want to 275 | } 276 | 277 | pub fn main() !void { 278 | const alloc = std.heap.page_allocator; 279 | const args = try std.process.argsAlloc(alloc); 280 | defer std.process.argsFree(alloc, args); 281 | const stdout = std.io.getStdOut().writer(); 282 | 283 | if (args.len < 2) { 284 | try stdout.print("Expected .png filename\n", .{}); 285 | std.process.exit(1); 286 | } 287 | 288 | var width: ?usize = null; 289 | var height: ?usize = null; 290 | if (args.len >= 4) { 291 | width = try std.fmt.parseInt(usize, args[2], 10); 292 | height = try std.fmt.parseInt(usize, args[3], 10); 293 | } 294 | 295 | // The image can be shifted right by padding spaces 296 | // (The image is drawn from the top-left starting at the current cursor location) 297 | //try stdout.print(" ", .{}); 298 | const bytes = try utils.readFile(alloc, args[1]); 299 | if (std.mem.endsWith(u8, args[1], ".png")) { 300 | try sendImagePNG(stdout, alloc, bytes, width, height); 301 | } else { 302 | try sendImageRGB(stdout, alloc, bytes, width, height); 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /src/lib/lexer.zig: -------------------------------------------------------------------------------- 1 | /// lexer.zig 2 | /// Markdown lexer. Processes Markdown text into a list of tokens. 3 | const std = @import("std"); 4 | 5 | const con = @import("console.zig"); 6 | const debug = @import("debug.zig"); 7 | const toks = @import("tokens.zig"); 8 | const utils = @import("utils.zig"); 9 | 10 | const Allocator = std.mem.Allocator; 11 | const GPA = std.heap.GeneralPurposeAllocator; 12 | 13 | /// Common types from the Zigdown namespace 14 | const TokenType = toks.TokenType; 15 | const Token = toks.Token; 16 | const TokenList = toks.TokenList; 17 | 18 | /// Convert Markdown text into a stream of tokens 19 | pub const Lexer = struct { 20 | data: []const u8 = undefined, 21 | cursor: usize = 0, 22 | src: toks.SourceLocation = toks.SourceLocation{}, 23 | 24 | /// Store the text and reset the cursor position 25 | pub fn setText(self: *Lexer, text: []const u8) void { 26 | self.data = text; 27 | self.cursor = 0; 28 | } 29 | 30 | /// Tokenize the given input text 31 | pub fn tokenize(self: *Lexer, alloc: Allocator, text: []const u8) !std.ArrayList(Token) { 32 | self.setText(text); 33 | 34 | var tokens = std.ArrayList(Token).init(alloc); 35 | var token = self.next(); 36 | try tokens.append(token); 37 | while (token.kind != .EOF) { 38 | token = self.next(); 39 | try tokens.append(token); 40 | } 41 | return tokens; 42 | } 43 | 44 | /// Consume the remainder of the current line and return if a newline was found 45 | pub fn eatLine(self: *Lexer) bool { 46 | const end_opt: ?usize = std.mem.indexOfScalarPos(u8, self.data, self.cursor, '\n'); 47 | if (end_opt) |end| { 48 | self.cursor = end + 1; 49 | self.row.row += 1; 50 | self.src.col = 0; 51 | return true; 52 | } else { 53 | self.cursor = self.data.len; 54 | return false; 55 | } 56 | } 57 | 58 | /// Consume the next token in the text 59 | pub fn next(self: *Lexer) Token { 60 | if (self.cursor > self.data.len) { 61 | return toks.Eof; 62 | } else if (self.cursor == self.data.len) { 63 | self.cursor += 1; 64 | self.src.col += 1; 65 | return toks.Eof; 66 | } 67 | 68 | // Apply each of our tokenizers to the current text 69 | inline for (toks.Tokenizers) |tokenizer| { 70 | const text = self.data[self.cursor..]; 71 | if (tokenizer.peek(text)) |token| { 72 | self.cursor += token.text.len; 73 | 74 | const tok = Token{ 75 | .kind = token.kind, 76 | .text = token.text, 77 | .src = self.src, 78 | }; 79 | 80 | if (token.kind == .BREAK) { 81 | self.src.row += 1; 82 | self.src.col = 0; 83 | } else { 84 | self.src.col += token.text.len; 85 | } 86 | 87 | return tok; 88 | } 89 | } 90 | 91 | // If all else fails, return UNKNOWN 92 | const token = Token{ 93 | .kind = .UNKNOWN, 94 | .text = self.data[self.cursor .. self.cursor + 1], 95 | .src = self.src, 96 | }; 97 | self.cursor += 1; 98 | return token; 99 | } 100 | }; 101 | 102 | ////////////////////////////////////////////////////////// 103 | // Tests 104 | ////////////////////////////////////////////////////////// 105 | 106 | test "test lexer" { 107 | const test_input = 108 | \\# Heading 1 109 | \\## Heading Two 110 | \\ 111 | \\Text _italic_ **bold** ___bold_italic___ 112 | \\~underline~ 113 | ; 114 | 115 | const expected_tokens = [_]Token{ 116 | .{ .kind = .HASH, .text = "#" }, 117 | .{ .kind = .SPACE, .text = " " }, 118 | .{ .kind = .WORD, .text = "Heading" }, 119 | .{ .kind = .SPACE, .text = " " }, 120 | .{ .kind = .DIGIT, .text = "1" }, 121 | .{ .kind = .BREAK, .text = "\n" }, 122 | .{ .kind = .HASH, .text = "#" }, 123 | .{ .kind = .HASH, .text = "#" }, 124 | .{ .kind = .SPACE, .text = " " }, 125 | .{ .kind = .WORD, .text = "Heading" }, 126 | .{ .kind = .SPACE, .text = " " }, 127 | .{ .kind = .WORD, .text = "Two" }, 128 | .{ .kind = .BREAK, .text = "\n" }, 129 | .{ .kind = .BREAK, .text = "\n" }, 130 | .{ .kind = .WORD, .text = "Text" }, 131 | .{ .kind = .SPACE, .text = " " }, 132 | .{ .kind = .USCORE, .text = "_" }, 133 | .{ .kind = .WORD, .text = "italic" }, 134 | .{ .kind = .USCORE, .text = "_" }, 135 | .{ .kind = .SPACE, .text = " " }, 136 | .{ .kind = .BOLD, .text = "**" }, 137 | .{ .kind = .WORD, .text = "bold" }, 138 | .{ .kind = .BOLD, .text = "**" }, 139 | .{ .kind = .SPACE, .text = " " }, 140 | .{ .kind = .EMBOLD, .text = "___" }, 141 | .{ .kind = .WORD, .text = "bold" }, 142 | .{ .kind = .USCORE, .text = "_" }, 143 | .{ .kind = .WORD, .text = "italic" }, 144 | .{ .kind = .EMBOLD, .text = "___" }, 145 | .{ .kind = .BREAK, .text = "\n" }, 146 | .{ .kind = .TILDE, .text = "~" }, 147 | .{ .kind = .WORD, .text = "underline" }, 148 | .{ .kind = .TILDE, .text = "~" }, 149 | }; 150 | 151 | var lex = Lexer{}; 152 | lex.setText(test_input); 153 | 154 | for (expected_tokens) |token| { 155 | try compareTokens(token, lex.next()); 156 | } 157 | 158 | try std.testing.expectEqual(TokenType.EOF, lex.next().kind); 159 | } 160 | 161 | /// Compare an expected token to a token from the Lexer 162 | fn compareTokens(expected: Token, actual: Token) !void { 163 | std.testing.expectEqual(expected.kind, actual.kind) catch |err| { 164 | debug.print("Expected {any} ({s}), got {any} ({s})\n", .{ expected.kind, expected.text, actual.kind, actual.text }); 165 | return err; 166 | }; 167 | 168 | std.testing.expect(std.mem.eql(u8, expected.text, actual.text)) catch |err| { 169 | debug.print("Expected '{s}', got '{s}'\n", .{ expected.text, actual.text }); 170 | return err; 171 | }; 172 | } 173 | -------------------------------------------------------------------------------- /src/lib/parser.zig: -------------------------------------------------------------------------------- 1 | pub const ParserOpts = @import("parsers/utils.zig").ParserOpts; 2 | pub const Parser = @import("parsers/blocks.zig").Parser; 3 | pub const InlineParser = @import("parsers/inlines.zig").InlineParser; 4 | 5 | ////////////////////////////////////////////////////////// 6 | // Tests 7 | ////////////////////////////////////////////////////////// 8 | 9 | test "All Renderer Tests" { 10 | @import("std").testing.refAllDecls(@This()); 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/render.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const blocks = @import("ast/blocks.zig"); 3 | const gfx = @import("image.zig"); 4 | 5 | const Allocator = std.mem.Allocator; 6 | 7 | pub const Renderer = @import("render/Renderer.zig"); 8 | 9 | pub const HtmlRenderer = @import("render/render_html.zig").HtmlRenderer; 10 | pub const ConsoleRenderer = @import("render/render_console.zig").ConsoleRenderer; 11 | pub const FormatRenderer = @import("render/render_format.zig").FormatRenderer; 12 | 13 | /// Supported rendering methods 14 | pub const RenderMethod = enum(u8) { 15 | console, 16 | html, 17 | format, 18 | }; 19 | 20 | /// Generic rendering options mostly applicable to all renderers 21 | pub const RenderOptions = struct { 22 | alloc: Allocator, 23 | method: RenderMethod = .console, 24 | document: blocks.Block, 25 | out_stream: std.io.AnyWriter, 26 | document_dir: ?[]const u8 = null, 27 | width: ?usize = null, 28 | }; 29 | 30 | /// Render a Markdown document 31 | pub fn render(opts: RenderOptions) !void { 32 | var arena = std.heap.ArenaAllocator.init(opts.alloc); 33 | defer arena.deinit(); // Could do this, but no reason to do so 34 | 35 | switch (opts.method) { 36 | .html => { 37 | var h_renderer = HtmlRenderer.init(opts.out_stream, arena.allocator()); 38 | defer h_renderer.deinit(); 39 | try h_renderer.renderBlock(opts.document); 40 | }, 41 | .console => { 42 | // Get the terminal size; limit our width to that 43 | // Some tools like `fzf --preview` cause the getTerminalSize() to fail, so work around that 44 | // Kinda hacky, but :shrug: 45 | var columns: usize = 90; 46 | const tsize = gfx.getTerminalSize() catch gfx.TermSize{}; 47 | if (opts.width) |width| { 48 | columns = width; 49 | } else if (tsize.cols > 0) { 50 | columns = @min(tsize.cols, columns); 51 | } 52 | 53 | const render_opts = ConsoleRenderer.RenderOpts{ 54 | .out_stream = opts.out_stream, 55 | .root_dir = opts.document_dir, 56 | .indent = 2, 57 | .width = columns, 58 | .max_image_cols = columns - 4, 59 | .termsize = tsize, 60 | }; 61 | var c_renderer = ConsoleRenderer.init(arena.allocator(), render_opts); 62 | defer c_renderer.deinit(); 63 | try c_renderer.renderBlock(opts.document); 64 | }, 65 | .format => { 66 | const render_opts = FormatRenderer.RenderOpts{ 67 | .out_stream = opts.out_stream, 68 | .root_dir = opts.document_dir, 69 | .indent = 0, 70 | .width = opts.width orelse 90, 71 | }; 72 | var formatter = FormatRenderer.init(arena.allocator(), render_opts); 73 | defer formatter.deinit(); 74 | try formatter.renderBlock(opts.document); 75 | }, 76 | } 77 | } 78 | 79 | ////////////////////////////////////////////////////////// 80 | // Tests 81 | ////////////////////////////////////////////////////////// 82 | 83 | test "All Renderer Tests" { 84 | std.testing.refAllDecls(@This()); 85 | } 86 | -------------------------------------------------------------------------------- /src/lib/render/Renderer.zig: -------------------------------------------------------------------------------- 1 | /// Generic Renderer interface. 2 | /// Can be used in the future to easily enable new renderer types. 3 | const std = @import("std"); 4 | 5 | const blocks = @import("../ast/blocks.zig"); 6 | 7 | /// The type erased pointer to the renderer implementation 8 | ptr: *anyopaque, 9 | 10 | /// Virtual function table of the renderer implementation 11 | vtable: *const VTable, 12 | 13 | pub const VTable = struct { 14 | /// Generic interface to render a Markdown document 15 | render: *const fn (*anyopaque, document: Block) RenderError!void, 16 | 17 | /// Generic interface to render a Markdown document 18 | deinit: *const fn (*anyopaque) void, 19 | }; 20 | 21 | pub const RenderError = SystemError || AnyWriter.Error || Block.Error; 22 | 23 | const Block = blocks.Block; 24 | const AnyWriter = std.io.AnyWriter; 25 | 26 | const SystemError = error{ 27 | OutOfMemory, 28 | DiskQuota, 29 | FileTooBig, 30 | InputOutput, 31 | NoSpaceLeft, 32 | DeviceBusy, 33 | InvalidArgument, 34 | AccessDenied, 35 | BrokenPipe, 36 | SystemResources, 37 | OperationAborted, 38 | NotOpenForWriting, 39 | LockViolation, 40 | WouldBlock, 41 | ConnectionResetByPeer, 42 | Unexpected, 43 | SystemError, 44 | }; 45 | -------------------------------------------------------------------------------- /src/lib/syntax.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const treez = @import("treez"); 3 | const builtin = @import("builtin"); 4 | 5 | const cons = @import("console.zig"); 6 | const debug = @import("debug.zig"); 7 | const gfx = @import("image.zig"); 8 | const ts_queries = @import("ts_queries.zig"); 9 | const utils = @import("utils.zig"); 10 | const wasm = @import("wasm.zig"); 11 | 12 | const ArrayList = std.ArrayList; 13 | const Allocator = std.mem.Allocator; 14 | 15 | pub const Range = struct { 16 | color: utils.Color, 17 | content: []const u8, 18 | newline: bool = false, 19 | }; 20 | 21 | // Capture group name -> Color 22 | // List taken from neovim's runtime/doc/treesitter.txt 23 | const highlights_map = std.StaticStringMap(utils.Color).initComptime(.{ 24 | .{ "variable", .White }, // various variable names 25 | .{ "variable.builtin", .Yellow }, // built-in variable names (e.g. `this`, `self`) 26 | .{ "variable.parameter", .Red }, // parameters of a function 27 | .{ "variable.parameter.builtin", .Blue }, // special parameters (e.g. `_`, `it`) 28 | .{ "variable.member", .Red }, // object and struct fields 29 | .{ "constant", .DarkYellow }, // constant identifiers 30 | .{ "constant.builtin", .Magenta }, // built-in constant values 31 | .{ "constant.macro", .DarkYellow }, // constants defined by the preprocessor 32 | .{ "module", .Yellow }, // modules or namespaces 33 | .{ "module.builtin", .Blue }, // built-in modules or namespaces 34 | .{ "label", .Magenta }, // `GOTO` and other labels (e.g. `label:` in C), including heredoc labels 35 | .{ "string", .Green }, // string literals 36 | .{ "string.documentation", .Green }, // string documenting code (e.g. Python docstrings) 37 | .{ "string.regexp", .Blue }, // regular expressions 38 | .{ "string.escape", .Cyan }, // escape sequences 39 | .{ "string.special", .Blue }, // other special strings (e.g. dates) 40 | .{ "string.special.symbol", .Red }, // symbols or atoms 41 | .{ "string.special.path", .Blue }, // filenames 42 | .{ "string.special.url", .Blue }, // URIs (e.g. hyperlinks) 43 | .{ "character", .Green }, // character literals 44 | .{ "character.special", .Magenta }, // special characters (e.g. wildcards) 45 | .{ "boolean", .DarkYellow }, // boolean literals 46 | .{ "number", .DarkYellow }, // numeric literals 47 | .{ "number.float", .DarkYellow }, // floating-point number literals 48 | .{ "type", .Yellow }, // type or class definitions and annotations 49 | .{ "type.builtin", .Yellow }, // built-in types 50 | .{ "type.definition", .Yellow }, // identifiers in type definitions (e.g. `typedef ` in C) 51 | .{ "attribute", .Magenta }, // attribute annotations (e.g. Python decorators, Rust lifetimes) 52 | .{ "attribute.builtin", .Blue }, // builtin annotations (e.g. `@property` in Python) 53 | .{ "property", .DarkGrey }, // the key in key/value pairs 54 | .{ "function", .Blue }, // function definitions 55 | .{ "function.builtin", .Yellow }, // built-in functions 56 | .{ "function.call", .Blue }, // function calls 57 | .{ "function.macro", .Blue }, // preprocessor macros 58 | .{ "function.method", .Blue }, // method definitions 59 | .{ "function.method.call", .Blue }, // method calls 60 | .{ "constructor", .Yellow }, // constructor calls and definitions 61 | .{ "operator", .Cyan }, // symbolic operators (e.g. `+`, `*`) 62 | .{ "keyword", .Magenta }, // keywords not fitting into specific categories 63 | .{ "keyword.coroutine", .Magenta }, // keywords related to coroutines (e.g. `go` in Go, `async/await` in Python) 64 | .{ "keyword.function", .Magenta }, // keywords that define a function (e.g. `func` in Go, `def` in Python) 65 | .{ "keyword.operator", .Magenta }, // operators that are English words (e.g. `and`, `or`) 66 | .{ "keyword.import", .Magenta }, // keywords for including modules (e.g. `import`, `from` in Python) 67 | .{ "keyword.type", .Magenta }, // keywords defining composite types (e.g. `struct`, `enum`) 68 | .{ "keyword.modifier", .Magenta }, // keywords defining type modifiers (e.g. `const`, `static`, `public`) 69 | .{ "keyword.repeat", .Magenta }, // keywords related to loops (e.g. `for`, `while`) 70 | .{ "keyword.return", .Magenta }, // keywords like `return` and `yield` 71 | .{ "keyword.debug", .Magenta }, // keywords related to debugging 72 | .{ "keyword.exception", .Magenta }, // keywords related to exceptions (e.g. `throw`, `catch`) 73 | .{ "keyword.conditional", .Magenta }, // keywords related to conditionals (e.g. `if`, `else`) 74 | .{ "keyword.conditional.ternary", .Magenta }, // ernary operator (e.g. `?`, `:`) 75 | .{ "keyword.directive", .Magenta }, // various preprocessor directives and shebangs 76 | .{ "keyword.directive.define", .Magenta }, // preprocessor definition directives 77 | .{ "punctuation.delimiter", .White }, // delimiters (e.g. `;`, `.`, `,`) 78 | .{ "punctuation.bracket", .Magenta }, // brackets (e.g. `()`, `{}`, `[]`) 79 | .{ "punctuation.special", .White }, // special symbols (e.g. `{}` in string interpolation) 80 | .{ "comment", .Coral }, // line and block comments 81 | // TODO: background coloring - See treesitter.txt 82 | .{ "comment.documentation", .Coral }, // comments documenting code 83 | .{ "comment.error", .Red }, // error-type comments (e.g. `ERROR`, `FIXME`, `DEPRECATED`) 84 | .{ "comment.warning", .Yellow }, // warning-type comments (e.g. `WARNING`, `FIX`, `HACK`) 85 | .{ "comment.todo", .Blue }, // todo-type comments (e.g. `TODO`, `WIP`) 86 | .{ "comment.note", .Cyan }, // note-type comments (e.g. `NOTE`, `INFO`, `XXX`) 87 | .{ "markup.strong", .Yellow }, // bold text 88 | // TODO: italic, bold, etc. style 89 | .{ "markup.italic", .White }, // italic text 90 | .{ "markup.strikethrough", .White }, // struck-through text 91 | .{ "markup.underline", .White }, // underlined text (only for literal underline markup!) 92 | // TODO: Review rotated heading colors 93 | .{ "markup.heading", .Red }, // headings, titles (including markers) 94 | .{ "markup.heading.1", .Red }, // top-level heading 95 | .{ "markup.heading.2", .Magenta }, // section heading 96 | .{ "markup.heading.3", .Blue }, // subsection heading 97 | .{ "markup.heading.4", .Green }, // and so on 98 | .{ "markup.heading.5", .Cyan }, // and so forth 99 | .{ "markup.heading.6", .White }, // six levels ought to be enough for anybody 100 | .{ "markup.quote", .Blue }, // block quotes 101 | .{ "markup.math", .Blue }, // math environments (e.g. `$ ... $` in LaTeX) 102 | .{ "markup.link", .White }, // text references, footnotes, citations, etc. 103 | .{ "markup.link.label", .Blue }, // link, reference descriptions 104 | .{ "markup.link.url", .Magenta }, // URL-style links 105 | .{ "markup.raw", .Green }, // literal or verbatim text (e.g. inline code) 106 | .{ "markup.raw.block", .Green }, // literal or verbatim text as a stand-alone block 107 | .{ "markup.list", .Red }, // list markers 108 | .{ "markup.list.checked", .Magenta }, // checked todo-style list markers 109 | .{ "markup.list.unchecked", .White }, // unchecked todo-style list markers 110 | // TODO: these actually use extra colors - mint green, pink, light blue 111 | .{ "diff.plus", .Green }, // added text (for diff files) 112 | .{ "diff.minus", .Red }, // deleted text (for diff files) 113 | .{ "diff.delta", .Cyan }, // changed text (for diff files) 114 | .{ "tag", .Red }, // XML-style tag names (e.g. in XML, HTML, etc.) 115 | .{ "tag.builtin", .Blue }, // XML-style tag names (e.g. HTML5 tags) 116 | .{ "tag.attribute", .DarkGrey }, // XML-style tag attributes 117 | .{ "tag.delimiter", .White }, // XML-style tag delimiters 118 | }); 119 | 120 | /// Get the highlight color for a specific capture group 121 | /// TODO: Load from JSON, possibly on a per-language basis 122 | /// TODO: Setup RGB color schemes and a Vim-style subset of highlight groups 123 | pub fn getHighlightFor(label: []const u8) ?utils.Color { 124 | return highlights_map.get(label); 125 | } 126 | 127 | /// Get the TreeSitter language parser, either built-in at compile time, 128 | /// or dynamically loaded from a shared library 129 | fn getLanguage(_: Allocator, language: []const u8) ?*const treez.Language { 130 | if (ts_queries.builtin_languages.get(language)) |pair| { 131 | return pair.language; 132 | } 133 | 134 | if (wasm.is_wasm or builtin.os.tag == .windows) return null; 135 | 136 | return treez.Language.loadFromDynLib(language) catch { 137 | // debug.print("Error loading {s} language: {any}\n", .{ language, err }); 138 | return null; 139 | }; 140 | } 141 | 142 | /// Use TreeSitter to parse the code block and apply colors 143 | /// Returns a slice of Ranges assigning colors to ranges of text within the given code 144 | pub fn getHighlights(alloc: Allocator, code: []const u8, lang_name: []const u8) ![]Range { 145 | if (wasm.is_wasm) return error.WasmNotSupported; 146 | 147 | // De-alias the language if needed 148 | const language = ts_queries.alias(lang_name) orelse lang_name; 149 | 150 | const lang: ?*const treez.Language = getLanguage(alloc, language); 151 | 152 | // Get the highlights query 153 | const highlights_opt: ?[]const u8 = ts_queries.get(alloc, language); 154 | defer if (highlights_opt) |h| alloc.free(h); 155 | 156 | if (lang != null and highlights_opt != null) { 157 | const tlang = lang.?; 158 | const highlights = highlights_opt.?; 159 | 160 | var parser = try treez.Parser.create(); 161 | defer parser.destroy(); 162 | 163 | try parser.setLanguage(tlang); 164 | 165 | const tree = try parser.parseString(null, code); 166 | defer tree.destroy(); 167 | 168 | const query = try treez.Query.create(tlang, highlights); 169 | defer query.destroy(); 170 | 171 | const cursor = try treez.Query.Cursor.create(); 172 | defer cursor.destroy(); 173 | 174 | cursor.execute(query, tree.getRootNode()); 175 | 176 | // For simplicity, append each range as we iterate the matches 177 | // Any ranges not falling into a match will be set to the "Default" color 178 | var ranges = ArrayList(Range).init(alloc); 179 | defer ranges.deinit(); 180 | 181 | var idx: usize = 0; 182 | while (cursor.nextMatch()) |match| { 183 | for (match.captures()) |capture| { 184 | const node: treez.Node = capture.node; 185 | const start = node.getStartByte(); 186 | const end = node.getEndByte(); 187 | const capture_name = query.getCaptureNameForId(capture.id); 188 | const content = code[start..end]; 189 | const color = getHighlightFor(capture_name) orelse .Default; 190 | 191 | if (start > idx) { 192 | // We've missed something in between captures 193 | try splitByLines(&ranges, color, code[idx..start]); 194 | } 195 | 196 | if (end > idx) { 197 | try splitByLines(&ranges, color, content); 198 | idx = end; 199 | } 200 | } 201 | } 202 | 203 | if (idx < code.len) { 204 | // We've missed something un-captured at the end; probably a '}\n' 205 | // Skip the traiilng newline if it's present 206 | var trailing_content = code[idx..]; 207 | if (trailing_content.len > 1 and std.mem.endsWith(u8, trailing_content, "\n")) { 208 | trailing_content = trailing_content[0 .. trailing_content.len - 1]; 209 | } 210 | try splitByLines(&ranges, .Default, trailing_content); 211 | } 212 | 213 | return ranges.toOwnedSlice(); 214 | } 215 | 216 | return error.LangNotFound; 217 | } 218 | 219 | /// Split a range of content by newlines 220 | /// This allows the renderer to easily know when the current source line needs to end 221 | fn splitByLines(ranges: *ArrayList(Range), color: utils.Color, content: []const u8) !void { 222 | var split_content = content; 223 | while (std.mem.indexOf(u8, split_content, "\n")) |l_end| { 224 | try ranges.append(.{ .color = color, .content = split_content[0..l_end], .newline = true }); 225 | 226 | if (l_end + 1 < split_content.len) { 227 | split_content = split_content[l_end + 1 ..]; 228 | } else { 229 | split_content = ""; 230 | break; 231 | } 232 | } 233 | 234 | if (split_content.len > 0) { 235 | try ranges.append(.{ .color = color, .content = split_content }); 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/lib/test.zig: -------------------------------------------------------------------------------- 1 | test "All Tests" { 2 | _ = @import("lexer.zig"); 3 | _ = @import("parser.zig"); 4 | _ = @import("ast/blocks.zig"); 5 | _ = @import("render.zig"); 6 | _ = @import("image.zig"); 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/tokens.zig: -------------------------------------------------------------------------------- 1 | /// tokens.zig 2 | /// Defines all possible Markdown tokens used by Zigdown. 3 | const std = @import("std"); 4 | const Allocator = std.mem.Allocator; 5 | const ArrayList = std.ArrayList; 6 | 7 | const utils = @import("utils.zig"); 8 | const debug = @import("debug.zig"); 9 | 10 | pub const TokenType = enum { 11 | EOF, 12 | WORD, 13 | DIGIT, 14 | INDENT, 15 | SPACE, 16 | BREAK, 17 | HASH, 18 | DIRECTIVE, 19 | CODE_INLINE, 20 | PLUS, 21 | MINUS, 22 | STAR, 23 | USCORE, 24 | TILDE, 25 | PERIOD, 26 | COMMA, 27 | EQUAL, 28 | BANG, 29 | QUERY, 30 | AT, 31 | DOLLAR, 32 | PERCENT, 33 | CARET, 34 | AND, 35 | LT, 36 | GT, 37 | LPAREN, 38 | RPAREN, 39 | LBRACK, 40 | RBRACK, 41 | LCURLY, 42 | RCURLY, 43 | SLASH, 44 | BSLASH, 45 | PIPE, 46 | BOLD, 47 | EMBOLD, 48 | UNKNOWN, 49 | }; 50 | 51 | pub const SourceLocation = struct { 52 | row: usize = 0, 53 | col: usize = 0, 54 | }; 55 | 56 | pub const Token = struct { 57 | kind: TokenType = TokenType.EOF, 58 | text: []const u8 = undefined, 59 | src: SourceLocation = undefined, 60 | }; 61 | 62 | pub const TokenList = ArrayList(Token); 63 | 64 | pub const Eof = Token{ .kind = .EOF, .text = "" }; 65 | 66 | /// For future use with parsing obscure syntax in links 67 | const Precedence = enum(u8) { 68 | LOWEST, 69 | EMPHASIS, 70 | BRACKET, 71 | TICK, 72 | BSLASH, 73 | }; 74 | 75 | /// Generic parser for a multi-character token from a list of possible characters 76 | /// Greedily accepts as many characters as it can 77 | pub fn AnyOfTokenizer(comptime chars: []const u8, comptime kind: TokenType) type { 78 | return struct { 79 | pub fn peek(text: []const u8) ?Token { 80 | if (text.len == 0) 81 | return null; 82 | 83 | var i: usize = 0; 84 | while (std.mem.indexOfScalar(u8, chars, text[i]) != null) : (i += 1) {} 85 | if (i == 0) return null; 86 | return Token{ 87 | .kind = kind, 88 | .text = text[0..i], 89 | }; 90 | } 91 | }; 92 | } 93 | 94 | /// Generic parser for single-character tokens 95 | /// Parse the u8 'char' as token type 'kind' 96 | pub fn SingularTokenizer(comptime char: u8, comptime kind: TokenType) type { 97 | return struct { 98 | pub fn peek(text: []const u8) ?Token { 99 | if (text.len > 0 and text[0] == char) { 100 | return Token{ 101 | .kind = kind, 102 | .text = text[0..1], 103 | }; 104 | } 105 | return null; 106 | } 107 | }; 108 | } 109 | 110 | /// Generic parser for multi-character literals 111 | pub fn LiteralTokenizer(comptime delim: []const u8, comptime kind: TokenType) type { 112 | return struct { 113 | pub fn peek(text: []const u8) ?Token { 114 | if (text.len >= delim.len and std.mem.startsWith(u8, text, delim)) { 115 | return Token{ 116 | .kind = kind, 117 | .text = text[0..delim.len], 118 | }; 119 | } 120 | 121 | return null; 122 | } 123 | }; 124 | } 125 | 126 | /// Parser for a generic word 127 | pub const WordTokenizer = struct { 128 | pub fn peek(text: []const u8) ?Token { 129 | var end = text.len; // TODO: Should be '0'? 130 | for (text, 0..) |c, i| { 131 | if (!std.ascii.isASCII(c) or std.ascii.isWhitespace(c) or utils.isPunctuation(c)) { 132 | end = i; 133 | break; 134 | } 135 | } 136 | 137 | if (end > 0) { 138 | return Token{ 139 | .kind = TokenType.WORD, 140 | .text = text[0..end], 141 | }; 142 | } 143 | 144 | return null; 145 | } 146 | }; 147 | 148 | /// Parser for a directive token 149 | pub const DirectiveTokenizer = struct { 150 | pub fn peek(text: []const u8) ?Token { 151 | if (text.len == 0) return null; 152 | 153 | var end: usize = 0; 154 | for (text, 0..) |c, i| { 155 | if (c != '`') { 156 | end = i; 157 | break; 158 | } 159 | } 160 | 161 | // We only match 3 or more '`' characters 162 | if (end > 2) { 163 | return Token{ 164 | .kind = TokenType.DIRECTIVE, 165 | .text = text[0..end], 166 | }; 167 | } 168 | 169 | return null; 170 | } 171 | }; 172 | 173 | /// Collect all available tokenizers 174 | pub const Tokenizers = .{ 175 | LiteralTokenizer("\r\n", TokenType.BREAK), 176 | LiteralTokenizer("\n", TokenType.BREAK), 177 | LiteralTokenizer("\t", TokenType.INDENT), 178 | LiteralTokenizer("***", TokenType.EMBOLD), 179 | LiteralTokenizer("_**", TokenType.EMBOLD), 180 | LiteralTokenizer("**_", TokenType.EMBOLD), 181 | LiteralTokenizer("*__", TokenType.EMBOLD), 182 | LiteralTokenizer("__*", TokenType.EMBOLD), 183 | LiteralTokenizer("___", TokenType.EMBOLD), 184 | LiteralTokenizer("**", TokenType.BOLD), 185 | LiteralTokenizer("__", TokenType.BOLD), 186 | DirectiveTokenizer, 187 | SingularTokenizer('`', TokenType.CODE_INLINE), 188 | SingularTokenizer(' ', TokenType.SPACE), 189 | SingularTokenizer('#', TokenType.HASH), 190 | SingularTokenizer('*', TokenType.STAR), 191 | SingularTokenizer('_', TokenType.USCORE), 192 | SingularTokenizer('~', TokenType.TILDE), 193 | SingularTokenizer('+', TokenType.PLUS), 194 | SingularTokenizer('-', TokenType.MINUS), 195 | SingularTokenizer('<', TokenType.LT), 196 | SingularTokenizer('>', TokenType.GT), 197 | SingularTokenizer('.', TokenType.PERIOD), 198 | SingularTokenizer(',', TokenType.COMMA), 199 | SingularTokenizer('=', TokenType.EQUAL), 200 | SingularTokenizer('!', TokenType.BANG), 201 | SingularTokenizer('?', TokenType.QUERY), 202 | SingularTokenizer('@', TokenType.AT), 203 | SingularTokenizer('$', TokenType.DOLLAR), 204 | SingularTokenizer('%', TokenType.PERCENT), 205 | SingularTokenizer('^', TokenType.CARET), 206 | SingularTokenizer('&', TokenType.AND), 207 | SingularTokenizer('(', TokenType.LPAREN), 208 | SingularTokenizer(')', TokenType.RPAREN), 209 | SingularTokenizer('[', TokenType.LBRACK), 210 | SingularTokenizer(']', TokenType.RBRACK), 211 | SingularTokenizer('{', TokenType.LCURLY), 212 | SingularTokenizer('}', TokenType.RCURLY), 213 | SingularTokenizer('/', TokenType.SLASH), 214 | SingularTokenizer('\\', TokenType.BSLASH), 215 | SingularTokenizer('|', TokenType.PIPE), 216 | AnyOfTokenizer("0123456789", TokenType.DIGIT), 217 | WordTokenizer, 218 | }; 219 | 220 | /////////////////////////////////////////////////////////////////////////////// 221 | // Utility Functions 222 | /////////////////////////////////////////////////////////////////////////////// 223 | 224 | pub fn typeStr(kind: TokenType) []const u8 { 225 | return switch (kind) { 226 | .EOF => "EOF", 227 | .WORD => "WORD", 228 | .DIGIT => "DIGIT", 229 | .INDENT => "INDENT", 230 | .SPACE => "SPACE", 231 | .BREAK => "BREAK", 232 | .HASH => "HASH", 233 | .DIRECTIVE => "DIRECTIVE", 234 | .CODE_INLINE => "CODE_INLINE", 235 | .PLUS => "PLUS", 236 | .MINUS => "MINUS", 237 | .STAR => "STAR", 238 | .USCORE => "USCORE", 239 | .TILDE => "TILDE", 240 | .PERIOD => "PERIOD", 241 | .COMMA => "COMMA", 242 | .EQUAL => "EQUAL", 243 | .BANG => "BANG", 244 | .QUERY => "QUERY", 245 | .AT => "AT", 246 | .DOLLAR => "DOLLAR", 247 | .PERCENT => "PERCENT", 248 | .CARET => "CARET", 249 | .AND => "AND", 250 | .LT => "LT", 251 | .GT => "GT", 252 | .LPAREN => "LPAREN", 253 | .RPAREN => "RPAREN", 254 | .LBRACK => "LBRACK", 255 | .RBRACK => "RBRACK", 256 | .LCURLY => "LCURLY", 257 | .RCURLY => "RCURLY", 258 | .SLASH => "SLASH", 259 | .BSLASH => "BSLASH", 260 | .PIPE => "PIPE", 261 | .BOLD => "BOLD", 262 | .EMBOLD => "EMBOLD", 263 | .UNKNOWN => "UNKNOWN", 264 | }; 265 | } 266 | 267 | pub fn printTypes(tokens: []const Token) void { 268 | for (tokens) |tok| { 269 | debug.print("{s}, ", .{typeStr(tok.kind)}); 270 | } 271 | debug.print("\n", .{}); 272 | } 273 | 274 | pub fn printText(tokens: []const Token) void { 275 | debug.print("\"", .{}); 276 | for (tokens) |tok| { 277 | if (tok.kind == .BREAK) { 278 | debug.print("\\n", .{}); 279 | continue; 280 | } 281 | debug.print("{s}", .{tok.text}); 282 | } 283 | debug.print("\"\n", .{}); 284 | } 285 | 286 | /// Concatenate the raw text of each token into a single string 287 | pub fn concatWords(alloc: Allocator, tokens: []const Token) ![]const u8 { 288 | var words = ArrayList([]const u8).init(alloc); 289 | defer words.deinit(); 290 | 291 | for (tokens) |tok| { 292 | try words.append(tok.text); 293 | } 294 | 295 | return try std.mem.concat(alloc, u8, words.items); 296 | } 297 | -------------------------------------------------------------------------------- /src/lib/wasm.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | /// Global flag for building for WASM or not 4 | pub const is_wasm = switch (@import("builtin").cpu.arch) { 5 | .wasm32, .wasm64 => true, 6 | else => false, 7 | }; 8 | 9 | /// Functions defined in JavaScript and imported via WebAssembly 10 | pub const Imports = struct { 11 | extern fn jsConsoleLogWrite(ptr: [*]const u8, len: usize) void; 12 | extern fn jsConsoleLogFlush() void; 13 | extern fn jsHtmlBufferWrite(ptr: [*]const u8, len: usize) void; 14 | extern fn jsHtmlBufferFlush() void; 15 | }; 16 | 17 | /// Provides console logging functionality in the browser 18 | pub const Console = struct { 19 | pub const Logger = struct { 20 | fn writeFn(bytes: []const u8) anyerror!usize { 21 | Imports.jsConsoleLogWrite(bytes.ptr, bytes.len); 22 | return bytes.len; 23 | } 24 | 25 | /// Returns an AnyWriter suitable for use in the Zigdown render APIs 26 | pub inline fn any(self: *const Logger) std.io.AnyWriter { 27 | return .{ 28 | .context = self, 29 | .writeFn = typeErasedWriteFn, 30 | }; 31 | } 32 | 33 | fn typeErasedWriteFn(_: *const anyopaque, bytes: []const u8) anyerror!usize { 34 | return writeFn(bytes); 35 | } 36 | }; 37 | const logger = Logger{}; 38 | 39 | /// Write formatted data to the JS buffer 40 | pub fn write(bytes: []const u8) void { 41 | logger.any().write(bytes) catch return; 42 | } 43 | 44 | /// Write formatted data to the JS buffer 45 | pub fn print(comptime format: []const u8, args: anytype) void { 46 | logger.any().print(format, args) catch return; 47 | } 48 | 49 | /// Flush the stream (tell JS to dump the buffer to the console) 50 | pub fn flush() void { 51 | Imports.jsConsoleLogFlush(); 52 | } 53 | 54 | /// Write to the JS buffer and immediately flush the stream 55 | pub fn log(comptime format: []const u8, args: anytype) void { 56 | Console.print(format, args); 57 | Console.flush(); 58 | } 59 | }; 60 | 61 | pub const Renderer = struct { 62 | pub const Impl = struct { 63 | fn writeFn(bytes: []const u8) anyerror!usize { 64 | Imports.jsHtmlBufferWrite(bytes.ptr, bytes.len); 65 | return bytes.len; 66 | } 67 | 68 | /// Returns an AnyWriter suitable for use in the Zigdown render APIs 69 | pub inline fn any(self: *const Impl) std.io.AnyWriter { 70 | return .{ 71 | .context = self, 72 | .writeFn = typeErasedWriteFn, 73 | }; 74 | } 75 | 76 | fn typeErasedWriteFn(_: *const anyopaque, bytes: []const u8) anyerror!usize { 77 | return Impl.writeFn(bytes); 78 | } 79 | }; 80 | pub const writer = Impl{}; 81 | 82 | pub fn log(comptime format: []const u8, args: anytype) void { 83 | writer.any().print(format, args) catch return; 84 | Imports.jsHtmlBufferFlush(); 85 | } 86 | 87 | pub fn flush() void { 88 | Imports.jsConsoleLogFlush(); 89 | } 90 | }; 91 | -------------------------------------------------------------------------------- /src/lib/zigdown.zig: -------------------------------------------------------------------------------- 1 | /// Package up the entire Zigdown library 2 | 3 | // Expose dependencies to downstream consumers 4 | pub const stbi = @import("stb_image"); 5 | pub const flags = @import("flags"); 6 | 7 | // Expose public namespaces for building docs 8 | pub const assets = @import("assets"); 9 | pub const cons = @import("console.zig"); 10 | pub const debug = @import("debug.zig"); 11 | pub const lexer = @import("lexer.zig"); 12 | pub const parser = @import("parser.zig"); 13 | pub const render = @import("render.zig"); 14 | pub const tokens = @import("tokens.zig"); 15 | pub const utils = @import("utils.zig"); 16 | pub const blocks = @import("ast/blocks.zig"); 17 | pub const gfx = @import("image.zig"); 18 | pub const ts_queries = @import("ts_queries.zig"); 19 | pub const wasm = @import("wasm.zig"); 20 | 21 | // Global public Zigdown types 22 | // pub const Markdown = markdown.Markdown; 23 | pub const Block = blocks.Block; 24 | pub const Container = blocks.Container; 25 | pub const Leaf = blocks.Leaf; 26 | pub const Token = tokens.Token; 27 | pub const TokenType = tokens.TokenType; 28 | pub const Parser = parser.Parser; 29 | pub const Lexer = lexer.Lexer; 30 | pub const ConsoleRenderer = render.ConsoleRenderer; 31 | pub const HtmlRenderer = render.HtmlRenderer; 32 | pub const FormatRenderer = render.FormatRenderer; 33 | -------------------------------------------------------------------------------- /test/code.md: -------------------------------------------------------------------------------- 1 | # Code Parsing Test 2 | 3 | `inline` code 4 | 5 | ## CPP Code Block 6 | 7 | ```cpp 8 | #include /* Comment */ 9 | int main() { 10 | printf("Hello, World!\n"); 11 | const int64_t foo = 1234 + 5678; 12 | return foo; 13 | } 14 | ``` 15 | 16 | ```c++ 17 | #include // Comment 18 | int main() { 19 | std::cout << "Hello, World!" << std::endl; 20 | } 21 | ``` 22 | 23 | ```c 24 | #include 25 | int main() { 26 | printf("Hello, World!\n"); 27 | return 0; 28 | } 29 | ``` 30 | 31 | ```zig 32 | const std = @import("std"); 33 | pub fn main() !void { 34 | std.debug.print("Hello, World!\n", .{}); 35 | } 36 | ``` 37 | 38 | ## JSON `ugh` 39 | 40 | ```json 41 | { 42 | "foo": "bar", 43 | "baz": 2 44 | } 45 | ``` 46 | 47 | ## BASH, YAML, None 48 | 49 | ```bash 50 | echo -e "Hello, World!" 51 | ``` 52 | 53 | ```yaml 54 | # TODO: I need to fork the current tree-sitter-yaml Github repo to make it work 55 | root: 56 | foo: "bar" 57 | baz: hello 58 | bash: | 59 | echo -e Hello, World! 60 | ``` 61 | 62 | ```make 63 | # Default target 64 | all: foo bar baz 65 | 66 | clean: 67 | rm foo bar baz 68 | ``` 69 | 70 | ```cmake 71 | cmake_minimum_required(VERSION 3.20 FATAL_ERROR) 72 | set(FOO "${BAR}" CACHE "Description" FORCE) 73 | option(DO_STUFF "Do some stuff" ON) 74 | ``` 75 | 76 | ``` 77 | code with no language set 78 | ``` 79 | -------------------------------------------------------------------------------- /test/directive.md: -------------------------------------------------------------------------------- 1 | ```cpp 2 | // normal code here 3 | ``` 4 | 5 | ```{warning} 6 | special warning directive! 7 | This has a very looooong line which should end up being wrapped. 8 | I want that, so that I can see what happens at the right-aligned trailer column foo ber lsdkhf 9 | of the wrapper box... 10 | ``` 11 | -------------------------------------------------------------------------------- /test/html/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Minimal zig-wasm 6 | 7 | 8 | 22 | 23 | 24 | 25 |
Rendered Markdown will appear here
26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /test/html/zigdown-wasm.wasm: -------------------------------------------------------------------------------- 1 | ../../zig-out/bin/zigdown-wasm.wasm -------------------------------------------------------------------------------- /test/html/zigdown_wrapper.js: -------------------------------------------------------------------------------- 1 | const text_decoder = new TextDecoder(); 2 | const text_encoder = new TextEncoder(); 3 | let console_log_buffer = ""; 4 | let html_render_buffer = ""; 5 | 6 | const placeholder_markdown = 7 | ['# Live WASM Markdown Previewer', 8 | '', 9 | '```{toc}', 10 | '```', 11 | 12 | 'Enter Markdown above; it will be rendered here in real-time in the browser', 13 | 'using the WebAssembly version of [Zigdown](https://github.com/JacobCrabill/zigdown).', 14 | '', 15 | '## Demo', 16 | '', 17 | '### Table of Contents', 18 | '', 19 | 'See the Table of Contents rendered above; this can be auto-generated with:', 20 | '', 21 | '````markdown', 22 | '```{toc}', 23 | '```', 24 | '````', 25 | 26 | 'Code blocks are supported, but compiling TreeSitter for WASM is still a WIP,', 27 | 'so no syntax highlighting is possible (yet).', 28 | '', 29 | '### Links', 30 | '', 31 | 'Links like [this](jcrabill.dev) can be added like `[text](url)`', 32 | '', 33 | '### Images', 34 | '', 35 | 'Images can be added like `![alt-text](src-uri)`:', 36 | '', 37 | '![sample](zig-zero.png)'].join('\n'); 38 | 39 | let wasm = { 40 | instance: undefined, 41 | 42 | init: function (obj) { 43 | this.instance = obj.instance; 44 | }, 45 | 46 | // Get a string from WASM memory into JavaScript 47 | getString: function (ptr, len) { 48 | const memory = this.instance.exports.memory; 49 | return text_decoder.decode(new Uint8Array(memory.buffer, ptr, len)); 50 | }, 51 | 52 | // Encode a string from JavaScript into a utf-8 string in WASM memory 53 | encodeString: function (string) { 54 | const memory = this.instance.exports.memory; 55 | const allocUint8 = this.instance.exports.allocUint8; 56 | const buffer = text_encoder.encode(string); 57 | const pointer = allocUint8(buffer.length + 1); // ask Zig to allocate memory 58 | const slice = new Uint8Array( 59 | memory.buffer, // memory exported from Zig 60 | pointer, 61 | buffer.length + 1 62 | ); 63 | // Copy encoded string into allocated buffer 64 | slice.set(buffer); 65 | slice[buffer.length] = 0; // null byte to null-terminate the string 66 | return pointer; 67 | }, 68 | 69 | renderToHtml: function (md) { 70 | // Encode the Markdown text from JavaScript into WASM memory 71 | const md_ptr = this.encodeString(md); 72 | this.instance.exports.renderToHtml(md_ptr); 73 | // Zig will call jsHtmlBufferWrite() and jsConsoleLogFlush() 74 | }, 75 | }; 76 | 77 | let importObject = { 78 | env: { 79 | jsConsoleLogWrite: function (ptr, len) { 80 | console_log_buffer += wasm.getString(ptr, len); 81 | }, 82 | jsConsoleLogFlush: function () { 83 | console.log(console_log_buffer); 84 | console_log_buffer = ""; 85 | }, 86 | jsHtmlBufferWrite: function (ptr, len) { 87 | html_render_buffer += wasm.getString(ptr, len); 88 | }, 89 | jsHtmlBufferFlush: function () { 90 | let rbox = document.getElementById("renderbox"); 91 | rbox.innerHTML = html_render_buffer; 92 | html_render_buffer = ""; 93 | } 94 | } 95 | }; 96 | 97 | function renderFromInput() { 98 | const text = document.getElementById("input_box").value 99 | if (text == "") { 100 | wasm.renderToHtml(placeholder_markdown); 101 | } else { 102 | wasm.renderToHtml(document.getElementById("input_box").value); 103 | } 104 | } 105 | 106 | async function bootstrap() { 107 | wasm.init(await WebAssembly.instantiateStreaming(fetch("zigdown-wasm.wasm"), importObject)); 108 | 109 | wasm.renderToHtml(placeholder_markdown); 110 | 111 | let input = document.getElementById("input_box"); 112 | input.addEventListener("input", renderFromInput, false); 113 | } 114 | 115 | bootstrap(); 116 | -------------------------------------------------------------------------------- /test/link.md: -------------------------------------------------------------------------------- 1 | Here's a link: 2 | 3 | [ Link Text ](https://flow-state.photos) Did that work? Lmk! 4 | 5 | [Bad link (b/c this)](https://google.com) 6 | 7 | Autolink: 8 | -------------------------------------------------------------------------------- /test/list.md: -------------------------------------------------------------------------------- 1 | # List 1 2 | 3 | 22. *item* 1.1 4 | - sub-item 2.1 5 | - sub-item 2.2 6 | - grandchild item 1 7 | - grandchild item 2 8 | - sub-item 2.3 9 | ```cpp 10 | int main() 11 | { 12 | { 13 | printf("Hello, World!\n"); 14 | } 15 | } 16 | ``` 17 | - `foo` 18 | > bar 19 | 01. item 1.2 20 | 21 | # List 2 22 | 23 | 1. foo 24 | 1. bar > foooo 25 | > qyotee uh-oh 26 | - fsdfshdfhs 27 | - fskff 28 | - fhskfdhgdgs 29 | - bfensrsn nkblerb b kpsi rb hioprbsn ri bai b;airbh ;aib a;sibnb aab ais baib rs rcxv u 30 | w 31 | ```cpp 32 | int main() 33 | { 34 | printf("Hello, World!\n"); 35 | } 36 | ``` 37 | 1. baz 38 | 1. wtf 39 | > quote 40 | 41 | # List 3 42 | 43 | 1. Task List 44 | - [ ] Task Item 45 | - [x] Checked Item * sub-item of task list 46 | 1. Not a Task List 47 | - [x] 48 | - ^ not a task item b/c empty 49 | -------------------------------------------------------------------------------- /test/list2.md: -------------------------------------------------------------------------------- 1 | 1. one 2 | - two 3 | 1. 4 | 1. ^ empty 5 | -------------------------------------------------------------------------------- /test/mini.md: -------------------------------------------------------------------------------- 1 | # Heading 1 2 | 3 | ## Heading 2 4 | 5 | ### Heading 3 6 | 7 | #### Heading 4 8 | 9 | Foo **Bar _baz_**. ~Hi!~ 10 | 11 | > > Double-nested ~Quote~ ...which supports multiple lines 12 | 13 | Image: ![Some Image](image-source.png) 14 | 15 | Link: [Click Me!](https://google.com) 16 | 17 | 1. Numlist 18 | - With child list 19 | 1. item 20 | 21 | - And now a list! 22 | - more items 23 | 24 | ```c++ 25 | Some raw code here... 26 | And some more here. 27 | ``` 28 | -------------------------------------------------------------------------------- /test/mononoki-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JacobCrabill/zigdown/33e64b0f27f7e4ef4565af83fd881b3c26087cb1/test/mononoki-Bold.ttf -------------------------------------------------------------------------------- /test/mononoki-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JacobCrabill/zigdown/33e64b0f27f7e4ef4565af83fd881b3c26087cb1/test/mononoki-BoldItalic.ttf -------------------------------------------------------------------------------- /test/mononoki-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JacobCrabill/zigdown/33e64b0f27f7e4ef4565af83fd881b3c26087cb1/test/mononoki-Italic.ttf -------------------------------------------------------------------------------- /test/mononoki-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JacobCrabill/zigdown/33e64b0f27f7e4ef4565af83fd881b3c26087cb1/test/mononoki-Regular.ttf -------------------------------------------------------------------------------- /test/out.html: -------------------------------------------------------------------------------- 1 | 2 |

Heading 1

3 |
4 | Plain text with bold and italic styles (and bold_italic) and no line breaks unless theprevious line is...
5 | blank like above
6 |
    7 |
  • 8 | Unordered list
  • 9 |
  • 10 | nested list
  • 11 |
  • 12 |
  • 13 |
  • 14 | ^ Empty list item
  • 15 |
  • 16 | with formatting!
  • 17 |
18 |
19 |
    20 |
  1. Ordered list
  2. 21 |
  3. 22 |
  4. Item 2
  5. 23 |
  6. Item 3
  7. 24 |
25 |
26 |

Quotes

27 |
28 | 29 |
Quote Block
30 |
31 | 32 |
33 |
34 | 35 |
> With line breaks and Formatting.
36 |
37 |
38 | A C++ code block.
39 | 40 |

41 | int main() {
42 |   std::cout << "Hello, world!" << std::endl;
43 | }
44 | 
45 |
46 |

Images

47 |
48 | Link Text
49 |
50 | Note: hologram.nvim somehow renders this properly(ish) within the NeoVim buffer, but mdcat andimage_cat don't (even though they work in plain terminal). What's the difference between howHologram does it vs. sending the raw graphics protocol data? 51 | -------------------------------------------------------------------------------- /test/quote.md: -------------------------------------------------------------------------------- 1 | # Header 2 | 3 | > spaces 4 | 5 | break 6 | 7 | > QuoteBlock 8 | > 9 | > With multiple paragraphs and such 10 | -------------------------------------------------------------------------------- /test/sample.md: -------------------------------------------------------------------------------- 1 | # Heading 1 2 | 3 | ```{toc} 4 | ``` 5 | 6 | ## Heading 2 7 | 8 | ### Heading 3 9 | 10 | #### Heading 4 11 | 12 | Plain text with **bold** and _italic_ styles (and **_bold_italic_**) or ~underlined **and bold _or 13 | italic_**~. 14 | 15 | - Unordered list 16 | - Works as you'd expect 17 | 1. Nested numbered list 18 | 1. Numbers auto-increment 19 | - more nesting! 20 | - > nested quote 21 | - `list` with `lots of code` to test `inline code` being `wrapped` on `line breaks` `foo` `bar` 22 | 1. Numbers _continue_ to auto-increment 23 | - 24 | - ^ Empty list item 25 | - with **_simple_ formatting**! 26 | 27 | ## Quotes 28 | 29 | > Quote Block 30 | > 31 | > > With line breaks and **_Formatting_**. 32 | > 33 | > - Lists inside quotes 34 | 35 | ## Code Blocks 36 | 37 | A C code block with syntax highlighting. 38 | 39 | ```c 40 | #include "stdio.h" 41 | 42 | // Comment 43 | int main() { 44 | printf("Hello, world!\n"); 45 | } 46 | ``` 47 | 48 | ```yaml 49 | root: 50 | node: 51 | - key1: value 52 | - key2: "string" 53 | - key3: 1.234 54 | ``` 55 | 56 | # Images 57 | 58 | ![Image Alt](../src/assets/img/zig-zero.png) [Click Me!](https://google.com) 59 | 60 | ![Sample](https://flow-state.photos/wp-content/uploads/2024/05/DSC07974-export-400x284.jpg) 61 | 62 | Rendered to the console using the 63 | [Kitty graphics protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/). 64 | 65 | Note: hologram.nvim somehow renders this properly(ish) within the NeoVim buffer, but mdcat and 66 | image_cat don't (even though they work in plain terminal). What's the difference between how 67 | Hologram does it vs. sending the raw graphics protocol data? 68 | 69 | # Here's a big code block! 70 | 71 | ```zig 72 | const std = @import("std"); 73 | const ArrayList = std.ArrayList; 74 | 75 | pub const Range = struct { 76 | color: utils.Color, 77 | content: []const u8, 78 | newline: bool = false, 79 | }; 80 | 81 | // Capture Name: number 82 | const highlights_map = std.StaticStringMap(utils.Color).initComptime(.{ 83 | .{ "number", .Yellow }, 84 | }); 85 | 86 | /// Get the highlight color for a specific capture group 87 | pub fn getHighlightFor(label: []const u8) ?utils.Color { 88 | return highlights_map.get(label); 89 | } 90 | 91 | for (ranges) |range| { 92 | if (range.content.len > 0) { 93 | self.print("{s}", .{ utils.colorToCss(range.color), range.content }); 94 | } 95 | } 96 | ``` 97 | 98 | [Back to top](#heading-1) 99 | -------------------------------------------------------------------------------- /test/sample2.md: -------------------------------------------------------------------------------- 1 | # Zigdown: Markdown parser in Zig 2 | 3 | ```{toc} 4 | ``` 5 | 6 | ```{warning} 7 | This is not a CommonMark-compliant Markdown parser, nor will it ever be one! 8 | ``` 9 | 10 | ## Features 11 | 12 | - Console and HTML rendering 13 | - Headers, **Basic** _text_ ~formatting~ (Clickable) [Links](google.com) 14 | - Quote blocks, Unordered lists, Ordered lists 15 | - Code blocks - Including syntax highlighting using TreeSitter 16 | - Images (rendered to the console using the 17 | [Kitty graphics protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/) 18 | - Neovim integration 19 | 20 | ## Usage 21 | 22 | The current version of Zig this code compiles with is 23 | [0.13.0](https://ziglang.org/builds/zig-linux-x86_64-0.13.0.tar.xz). 24 | 25 | ```bash 26 | #!/usr/bin/env bash 27 | zig build run -- -c test/sample.md # Build and run on sample file 28 | zig build -l # List build options 29 | zig build -Dtarget=x86_64-linux-musl # Compile for x86-64 Linux using 30 | # statically-linked MUSL libC 31 | ``` 32 | 33 | `zig build` will create a `zigdown` binary at `zig-out/bin/zigdown`. Add `-Doptimize=ReleaseSafe` to 34 | enable optimizations while keeping safety checks and backtraces upon errors. The shorthand options 35 | `-Dsafe` and `-Dfast` also enable ReleaseSafe and ReleaseFast, respectively. 36 | 37 | ## Sample Render 38 | 39 | ![Sample Render](../sample-render.png) 40 | -------------------------------------------------------------------------------- /test/table.md: -------------------------------------------------------------------------------- 1 | # Tables 2 | 3 | **foo** (_bar baz_) 4 | 5 | | First row | 2nd cell | 3rd cell | 6 | | :--------------------------------------------------------------- | :--------------------- | -------- | 7 | | The **2nd row** (_header row_) sets the alignment of each column | same number of columns | foo | 8 | | Another row | same number of columns | bar | 9 | 10 | ## Table 2 11 | 12 | | Foo | Bar | Baz | 13 | | --- | --- | ----- | 14 | | one | two | three | 15 | 16 | ## Table 3 17 | 18 | | h1 | h2 | h3 | h4 | 19 | | :-- |---|----| :----- | 20 | | lorem ipsum dolor | sit amet, consectetur | adipiscing elit. | Ut sit amet luctus felis. | 21 | | lorem ipsum dolor | sit amet, consectetur | adipiscing elit. | Ut sit amet luctus felis. | 22 | -------------------------------------------------------------------------------- /test/test.lua: -------------------------------------------------------------------------------- 1 | -- For luajit 2.1.0 (Based on Lua 5.1) 2 | -- Compatible with NeoVim's built-in Lua 3 | -- 4 | -- From the Zigdown root directory, run as: 5 | -- $ luajit test/test.lua 6 | package.cpath = package.cpath .. ';./lua/?.so' 7 | local mylib = require('zigdown_lua') 8 | 9 | print(mylib.adder(40, 2)) 10 | print(mylib.hello()) 11 | 12 | local test_md = "# Hello, World!\n\nTest line\n\n- Test List\n" 13 | print(mylib.render_markdown(test_md)) 14 | -------------------------------------------------------------------------------- /test/toc.md: -------------------------------------------------------------------------------- 1 | # Table of Contents 2 | 3 | ```{toc} 4 | ``` 5 | 6 | ## Lists (2) 7 | 8 | ### Sub Heading (3) 9 | 10 | Here's a list that should look the same as the TOC: 11 | 12 | - Table of Contents 13 | - Lists (2) 14 | - Sub Heading (3) 15 | - More Content (2) 16 | - Even More Content!! (3) 17 | 18 | ## More Content (2) 19 | 20 | ### Even More Content!! (3) 21 | -------------------------------------------------------------------------------- /test/yaml.md: -------------------------------------------------------------------------------- 1 | ```yaml 2 | foo${i}: 3 | bar: baz 4 | buzz: 1234 5 | barf: | 6 | This is a block node. 7 | The highlight should go on for a while. 8 | # comment ... 9 | ``` 10 | -------------------------------------------------------------------------------- /tools/download-highlights.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | QUERY_DIR="$(git rev-parse --show-toplevel)/tree-sitter/queries/" 4 | 5 | function fetch_query() { 6 | lang=${1} 7 | echo "Fetching highlights query for ${lang}" 8 | wget https://raw.githubusercontent.com/tree-sitter/tree-sitter-${lang}/master/queries/highlights.scm \ 9 | -O ${QUERY_DIR}highlights-${lang}.scm 10 | } 11 | 12 | fetch_query c 13 | fetch_query cpp 14 | fetch_query zig 15 | fetch_query bash 16 | fetch_query python 17 | fetch_query json 18 | fetch_query rust 19 | fetch_query toml 20 | -------------------------------------------------------------------------------- /tools/fetch_queries.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ROOT_DIR=$(git rev-parse --show-toplevel) 3 | 4 | # Fetch the TreeSitter queries for some common langauges 5 | # These will be saved to either $TS_CONFIG_DIR/queries if the environment variable 6 | # TS_CONFIG_DIR exists; otherwise they will be saved to ~/.config/tree-sitter/queries. 7 | pushd ${ROOT_DIR} >/dev/null 8 | 9 | zig build fetch-queries -- "c" "cpp" "rust" "python" "bash" "json" "toml" "maxxnino:zig" 10 | 11 | popd >/dev/null 12 | -------------------------------------------------------------------------------- /tools/run_demo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | SCRIPT_DIR=$(dirname $(realpath ${BASH_SOURCE[0]})) 3 | HTML_DIR=$(realpath ${SCRIPT_DIR}/../test/html/) 4 | 5 | cd ${HTML_DIR} 6 | python3 -m http.server # Then navigate to localhost:8000:demo.html 7 | -------------------------------------------------------------------------------- /tools/test_runner.zig: -------------------------------------------------------------------------------- 1 | //! Custom test runner that displays all test results 2 | //! Courtesy of: https://gist.github.com/karlseguin/c6bea5b35e4e8d26af6f81c22cb5d76b 3 | const std = @import("std"); 4 | const builtin = @import("builtin"); 5 | 6 | const Allocator = std.mem.Allocator; 7 | 8 | const BORDER = "=" ** 80; 9 | 10 | // use in custom panic handler 11 | var current_test: ?[]const u8 = null; 12 | 13 | pub fn main() !void { 14 | var mem: [8192]u8 = undefined; 15 | var fba = std.heap.FixedBufferAllocator.init(&mem); 16 | 17 | const allocator = fba.allocator(); 18 | 19 | const env = Env.init(allocator); 20 | defer env.deinit(allocator); 21 | 22 | var slowest = SlowTracker.init(allocator, 5); 23 | defer slowest.deinit(); 24 | 25 | var pass: usize = 0; 26 | var fail: usize = 0; 27 | var skip: usize = 0; 28 | var leak: usize = 0; 29 | 30 | const printer = Printer.init(); 31 | printer.fmt("\r\x1b[0K", .{}); // beginning of line and clear to end of line 32 | 33 | for (builtin.test_functions, 0..) |t, idx| { 34 | if (isSetup(t)) { 35 | current_test = friendlyName(t.name); 36 | t.func() catch |err| { 37 | printer.status(.fail, "\n[{d}] setup \"{s}\" failed: {}\n", .{ idx, t.name, err }); 38 | return err; 39 | }; 40 | } 41 | } 42 | 43 | for (builtin.test_functions, 0..) |t, idx| { 44 | if (isSetup(t) or isTeardown(t)) { 45 | continue; 46 | } 47 | 48 | var status = Status.pass; 49 | slowest.startTiming(); 50 | 51 | const is_unnamed_test = isUnnamed(t); 52 | if (env.filter) |f| { 53 | if (!is_unnamed_test and std.mem.indexOf(u8, t.name, f) == null) { 54 | continue; 55 | } 56 | } 57 | 58 | const friendly_name = friendlyName(t.name); 59 | current_test = friendly_name; 60 | std.testing.allocator_instance = .{}; 61 | const result = t.func(); 62 | current_test = null; 63 | 64 | if (is_unnamed_test) { 65 | continue; 66 | } 67 | 68 | const ns_taken = slowest.endTiming(friendly_name); 69 | 70 | if (std.testing.allocator_instance.deinit() == .leak) { 71 | leak += 1; 72 | printer.status(.fail, "\n{s}\n[{d}] \"{s}\" - Memory Leak\n{s}\n", .{ BORDER, idx, friendly_name, BORDER }); 73 | } 74 | 75 | if (result) |_| { 76 | pass += 1; 77 | } else |err| switch (err) { 78 | error.SkipZigTest => { 79 | skip += 1; 80 | status = .skip; 81 | }, 82 | else => { 83 | status = .fail; 84 | fail += 1; 85 | printer.status(.fail, "\n{s}\n[{d}] \"{s}\" - {s}\n{s}\n", .{ BORDER, idx, friendly_name, @errorName(err), BORDER }); 86 | if (@errorReturnTrace()) |trace| { 87 | std.debug.dumpStackTrace(trace.*); 88 | } 89 | if (env.fail_first) { 90 | break; 91 | } 92 | }, 93 | } 94 | 95 | if (env.verbose) { 96 | const ms = @as(f64, @floatFromInt(ns_taken)) / 1_000_000.0; 97 | printer.status(status, "[{d}] {s} ({d:.2}ms)\n", .{ idx, friendly_name, ms }); 98 | } else { 99 | printer.status(status, "[{d}]", .{idx}); 100 | } 101 | } 102 | 103 | for (builtin.test_functions) |t| { 104 | if (isTeardown(t)) { 105 | current_test = friendlyName(t.name); 106 | t.func() catch |err| { 107 | printer.status(.fail, "\nteardown \"{s}\" failed: {}\n", .{ t.name, err }); 108 | return err; 109 | }; 110 | } 111 | } 112 | 113 | const total_tests = pass + fail; 114 | const status = if (fail == 0) Status.pass else Status.fail; 115 | printer.status(status, "\n{d} of {d} test{s} passed\n", .{ pass, total_tests, if (total_tests != 1) "s" else "" }); 116 | if (skip > 0) { 117 | printer.status(.skip, "{d} test{s} skipped\n", .{ skip, if (skip != 1) "s" else "" }); 118 | } 119 | if (leak > 0) { 120 | printer.status(.fail, "{d} test{s} leaked\n", .{ leak, if (leak != 1) "s" else "" }); 121 | } 122 | printer.fmt("\n", .{}); 123 | try slowest.display(printer); 124 | printer.fmt("\n", .{}); 125 | std.posix.exit(if (fail == 0) 0 else 1); 126 | } 127 | 128 | fn friendlyName(name: []const u8) []const u8 { 129 | var it = std.mem.splitScalar(u8, name, '.'); 130 | while (it.next()) |value| { 131 | if (std.mem.eql(u8, value, "test")) { 132 | const rest = it.rest(); 133 | return if (rest.len > 0) rest else name; 134 | } 135 | } 136 | return name; 137 | } 138 | 139 | const Printer = struct { 140 | out: std.fs.File.Writer, 141 | 142 | fn init() Printer { 143 | return .{ 144 | .out = std.io.getStdErr().writer(), 145 | }; 146 | } 147 | 148 | fn fmt(self: Printer, comptime format: []const u8, args: anytype) void { 149 | std.fmt.format(self.out, format, args) catch unreachable; 150 | } 151 | 152 | fn status(self: Printer, s: Status, comptime format: []const u8, args: anytype) void { 153 | const color = switch (s) { 154 | .pass => "\x1b[32m", 155 | .fail => "\x1b[31m", 156 | .skip => "\x1b[33m", 157 | else => "", 158 | }; 159 | const out = self.out; 160 | out.writeAll(color) catch @panic("writeAll failed?!"); 161 | std.fmt.format(out, format, args) catch @panic("std.fmt.format failed?!"); 162 | self.fmt("\x1b[0m", .{}); 163 | } 164 | }; 165 | 166 | const Status = enum { 167 | pass, 168 | fail, 169 | skip, 170 | text, 171 | }; 172 | 173 | const SlowTracker = struct { 174 | const SlowestQueue = std.PriorityDequeue(TestInfo, void, compareTiming); 175 | max: usize, 176 | slowest: SlowestQueue, 177 | timer: std.time.Timer, 178 | 179 | fn init(allocator: Allocator, count: u32) SlowTracker { 180 | const timer = std.time.Timer.start() catch @panic("failed to start timer"); 181 | var slowest = SlowestQueue.init(allocator, {}); 182 | slowest.ensureTotalCapacity(count) catch @panic("OOM"); 183 | return .{ 184 | .max = count, 185 | .timer = timer, 186 | .slowest = slowest, 187 | }; 188 | } 189 | 190 | const TestInfo = struct { 191 | ns: u64, 192 | name: []const u8, 193 | }; 194 | 195 | fn deinit(self: SlowTracker) void { 196 | self.slowest.deinit(); 197 | } 198 | 199 | fn startTiming(self: *SlowTracker) void { 200 | self.timer.reset(); 201 | } 202 | 203 | fn endTiming(self: *SlowTracker, test_name: []const u8) u64 { 204 | var timer = self.timer; 205 | const ns = timer.lap(); 206 | 207 | var slowest = &self.slowest; 208 | 209 | if (slowest.count() < self.max) { 210 | // Capacity is fixed to the # of slow tests we want to track 211 | // If we've tracked fewer tests than this capacity, than always add 212 | slowest.add(TestInfo{ .ns = ns, .name = test_name }) catch @panic("failed to track test timing"); 213 | return ns; 214 | } 215 | 216 | { 217 | // Optimization to avoid shifting the dequeue for the common case 218 | // where the test isn't one of our slowest. 219 | const fastest_of_the_slow = slowest.peekMin() orelse unreachable; 220 | if (fastest_of_the_slow.ns > ns) { 221 | // the test was faster than our fastest slow test, don't add 222 | return ns; 223 | } 224 | } 225 | 226 | // the previous fastest of our slow tests, has been pushed off. 227 | _ = slowest.removeMin(); 228 | slowest.add(TestInfo{ .ns = ns, .name = test_name }) catch @panic("failed to track test timing"); 229 | return ns; 230 | } 231 | 232 | fn display(self: *SlowTracker, printer: Printer) !void { 233 | var slowest = self.slowest; 234 | const count = slowest.count(); 235 | printer.fmt("Slowest {d} test{s}: \n", .{ count, if (count != 1) "s" else "" }); 236 | while (slowest.removeMinOrNull()) |info| { 237 | const ms = @as(f64, @floatFromInt(info.ns)) / 1_000_000.0; 238 | printer.fmt(" {d:.2}ms\t{s}\n", .{ ms, info.name }); 239 | } 240 | } 241 | 242 | fn compareTiming(context: void, a: TestInfo, b: TestInfo) std.math.Order { 243 | _ = context; 244 | return std.math.order(a.ns, b.ns); 245 | } 246 | }; 247 | 248 | const Env = struct { 249 | verbose: bool, 250 | fail_first: bool, 251 | filter: ?[]const u8, 252 | 253 | fn init(allocator: Allocator) Env { 254 | return .{ 255 | .verbose = readEnvBool(allocator, "TEST_VERBOSE", true), 256 | .fail_first = readEnvBool(allocator, "TEST_FAIL_FIRST", false), 257 | .filter = readEnv(allocator, "TEST_FILTER"), 258 | }; 259 | } 260 | 261 | fn deinit(self: Env, allocator: Allocator) void { 262 | if (self.filter) |f| { 263 | allocator.free(f); 264 | } 265 | } 266 | 267 | fn readEnv(allocator: Allocator, key: []const u8) ?[]const u8 { 268 | const v = std.process.getEnvVarOwned(allocator, key) catch |err| { 269 | if (err == error.EnvironmentVariableNotFound) { 270 | return null; 271 | } 272 | std.log.warn("failed to get env var {s} due to err {}", .{ key, err }); 273 | return null; 274 | }; 275 | return v; 276 | } 277 | 278 | fn readEnvBool(allocator: Allocator, key: []const u8, deflt: bool) bool { 279 | const value = readEnv(allocator, key) orelse return deflt; 280 | defer allocator.free(value); 281 | return std.ascii.eqlIgnoreCase(value, "true"); 282 | } 283 | }; 284 | 285 | pub fn panic(msg: []const u8, error_return_trace: ?*std.builtin.StackTrace, ret_addr: ?usize) noreturn { 286 | if (current_test) |ct| { 287 | std.debug.print("\x1b[31m{s}\npanic running \"{s}\"\n{s}\x1b[0m\n", .{ BORDER, ct, BORDER }); 288 | } 289 | _ = error_return_trace; 290 | std.debug.defaultPanic(msg, ret_addr); 291 | } 292 | 293 | fn isUnnamed(t: std.builtin.TestFn) bool { 294 | const marker = ".test_"; 295 | const test_name = t.name; 296 | const index = std.mem.indexOf(u8, test_name, marker) orelse return false; 297 | _ = std.fmt.parseInt(u32, test_name[index + marker.len ..], 10) catch return false; 298 | return true; 299 | } 300 | 301 | fn isSetup(t: std.builtin.TestFn) bool { 302 | return std.mem.endsWith(u8, t.name, "tests:beforeAll"); 303 | } 304 | 305 | fn isTeardown(t: std.builtin.TestFn) bool { 306 | return std.mem.endsWith(u8, t.name, "tests:afterAll"); 307 | } 308 | --------------------------------------------------------------------------------