├── .gitattributes ├── .github ├── vscode-autoformat.gif ├── vscode.png └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.zig ├── build.zig.zon ├── editors ├── emacs │ └── README.md └── vscode │ ├── .gitignore │ ├── .vscode │ ├── extensions.json │ ├── launch.json │ ├── settings.json │ └── tasks.json │ ├── .vscodeignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── extension.ts │ ├── formatter.ts │ └── util.ts │ ├── superhtml.language-configuration.json │ ├── syntaxes │ ├── superhtml-derivative.tmLanguage.json │ └── superhtml.tmLanguage.json │ ├── tsconfig.json │ └── vscode.png ├── src ├── Ast.zig ├── cli.zig ├── cli │ ├── check.zig │ ├── fmt.zig │ ├── interface.zig │ ├── logging.zig │ ├── lsp.zig │ └── lsp │ │ ├── Document.zig │ │ └── logic.zig ├── css.zig ├── css │ ├── Ast.zig │ └── Tokenizer.zig ├── errors.zig ├── fuzz.zig ├── fuzz │ ├── afl.zig │ ├── astgen.zig │ └── cases │ │ ├── 12.html │ │ ├── 2.html │ │ ├── 3-01.html │ │ ├── 3.html │ │ ├── 4-01.html │ │ ├── 5-01.html │ │ ├── 6-01.html │ │ ├── 6-02.html │ │ ├── 77.html │ │ ├── round2 │ │ ├── 2.html │ │ └── 3.html │ │ └── round3 │ │ └── 2.html ├── html.zig ├── html │ ├── Ast.zig │ ├── Tokenizer.zig │ └── named_character_references.zig ├── root.zig ├── sitter.zig ├── template.zig ├── vm.zig └── wasm.zig └── tree-sitter-superhtml ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .npmignore ├── Cargo.toml ├── LICENSE ├── Makefile ├── Package.swift ├── README.md ├── binding.gyp ├── bindings ├── c │ ├── tree-sitter-html.h │ └── tree-sitter-html.pc.in ├── go │ ├── binding.go │ ├── binding_test.go │ └── go.mod ├── node │ ├── binding.cc │ ├── index.d.ts │ └── index.js ├── python │ └── tree_sitter_html │ │ ├── __init__.py │ │ ├── __init__.pyi │ │ ├── binding.c │ │ └── py.typed ├── rust │ ├── build.rs │ └── lib.rs └── swift │ └── TreeSitterHTML │ └── html.h ├── examples ├── deeply-nested-custom.html └── deeply-nested.html ├── grammar.js ├── package-lock.json ├── package.json ├── pyproject.toml ├── queries ├── highlights.scm └── injections.scm ├── setup.py ├── src ├── grammar.json ├── node-types.json ├── parser.c ├── scanner.c ├── tag.h └── tree_sitter │ ├── alloc.h │ ├── array.h │ └── parser.h └── test ├── corpus └── main.txt └── highlight ├── attributes.html ├── doctype.html ├── erroneous.html └── self-closing.html /.gitattributes: -------------------------------------------------------------------------------- 1 | *.zig text eol=lf 2 | *.zon text eol=lf -------------------------------------------------------------------------------- /.github/vscode-autoformat.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kristoff-it/superhtml/13f5a2221cb748bbe50ad702e89362afd5b925a7/.github/vscode-autoformat.gif -------------------------------------------------------------------------------- /.github/vscode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kristoff-it/superhtml/13f5a2221cb748bbe50ad702e89362afd5b925a7/.github/vscode.png -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: push 3 | jobs: 4 | deploy: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v4 8 | with: 9 | fetch-depth: 0 # Change if you need git info 10 | 11 | - name: Setup Zig 12 | uses: mlugg/setup-zig@v1 13 | with: 14 | version: 0.14.0 15 | 16 | - name: Test 17 | run: zig build test 18 | 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .zig-cache/ 2 | zig-cache/ 3 | zig-out/ 4 | release/ 5 | scratch 6 | 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Loris Cro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SuperHTML 2 | HTML Language Server and Templating Language Library 3 | 4 | 5 | ## HTML Language Server 6 | The Super CLI Tool offers **syntax checking** and **autoformatting** features for HTML files. 7 | 8 | The tool can be used either directly (for example by running it on save), or through a LSP client implementation. 9 | 10 | ``` 11 | $ superhtml 12 | Usage: superhtml COMMAND [OPTIONS] 13 | 14 | Commands: 15 | check Check documents for syntax errors 16 | interface, i Print a SuperHTML template's interface 17 | fmt Format documents 18 | lsp Start the Super LSP 19 | help Show this menu and exit 20 | version Print Super's version and exit 21 | 22 | General Options: 23 | --help, -h Print command specific usage 24 | ``` 25 | 26 | >[!WARNING] 27 | >SuperHTML currently only supports UTF8-encoded HTML and assumes HTML5 compliance (e.g. doesn't support XHTML, regardless of what you define the doctype to be). 28 | 29 | ### Diagnostics 30 | 31 | ![](.github/vscode.png) 32 | 33 | This language server is stricter than the HTML spec whenever it would prevent potential human errors from being reported. 34 | 35 | 36 | As an example, HTML allows for closing some tags implicitly. For example the following snippet is correct HTML. 37 | 38 | ```html 39 | 43 | ``` 44 | 45 | This will still be reported as an error by SuperHTML because otherwise the following snippet would have to be considered correct (while it's most probably a typo): 46 | 47 | ```html 48 |
  • item
  • 49 | ``` 50 | 51 | ### Autoformatting 52 | ![](.github/vscode-autoformat.gif) 53 | 54 | The autoformatter has two main ways of interacting with it in order to request for horizontal / vertical alignment. 55 | 56 | 1. Adding / removing whitespace between the **start tag** of an element and its content. 57 | 2. Adding / removing whitespace between the **last attribute** of a start tag and the closing `>`. 58 | 59 | 60 | #### Example of rule #1 61 | Before: 62 | ```html 63 |

    Foo

    64 | ``` 65 | 66 | After: 67 | ```html 68 |
    69 |

    Foo

    70 |
    71 | ``` 72 | 73 | ##### Reverse 74 | 75 | Before: 76 | ```html 77 |

    Foo

    78 |
    79 | ``` 80 | 81 | After: 82 | ```html 83 |

    Foo

    84 | ``` 85 | 86 | #### Example of rule #2 87 | Before: 88 | ```html 89 |
    90 | Foo 91 |
    92 | ``` 93 | 94 | After: 95 | ```html 96 |
    100 | Foo 101 |
    102 | ``` 103 | 104 | ##### Reverse 105 | 106 | Before: 107 | ```html 108 |
    111 | Foo 112 |
    113 | ``` 114 | 115 | After: 116 | ```html 117 |
    118 | Foo 119 |
    120 | ``` 121 | 122 | ### Editor support 123 | #### VSCode 124 | Install the [Super HTML VSCode extension](https://marketplace.visualstudio.com/items?itemName=LorisCro.super). 125 | 126 | #### Neovim 127 | 1. Download a prebuilt version of `superhtml` from the Releases section (or build it yourself). 128 | 2. Put `superhtml` in your `PATH`. 129 | 3. Configure `superhtml` for your chosen lsp 130 | 131 | - ##### [Neovim Built-In](https://neovim.io/doc/user/lsp.html#vim.lsp.start()) 132 | 133 | ```lua 134 | vim.api.nvim_create_autocmd("Filetype", { 135 | pattern = { "html", "shtml", "htm" }, 136 | callback = function() 137 | vim.lsp.start({ 138 | name = "superhtml", 139 | cmd = { "superhtml", "lsp" }, 140 | root_dir = vim.fs.dirname(vim.fs.find({".git"}, { upward = true })[1]) 141 | }) 142 | end 143 | }) 144 | ``` 145 | 146 | - ##### [LspZero](https://github.com/VonHeikemen/lsp-zero.nvim) 147 | 148 | ```lua 149 | local lsp = require("lsp-zero") 150 | 151 | require('lspconfig.configs').superhtml = { 152 | default_config = { 153 | name = 'superhtml', 154 | cmd = {'superhtml', 'lsp'}, 155 | filetypes = {'html', 'shtml', 'htm'}, 156 | root_dir = require('lspconfig.util').root_pattern('.git') 157 | } 158 | } 159 | 160 | lsp.configure('superhtml', {force_setup = true}) 161 | ``` 162 | 163 | #### Helix 164 | 165 | In versions later than `24.07` `superhtml` is supported out of the box, simply add executable to your `PATH`. 166 | 167 | For `24.07` and earlier, add to your `.config/helix/languages.toml`: 168 | ```toml 169 | [language-server.superhtml-lsp] 170 | command = "superhtml" 171 | args = ["lsp"] 172 | 173 | [[language]] 174 | name = "html" 175 | scope = "source.html" 176 | roots = [] 177 | file-types = ["html"] 178 | language-servers = [ "superhtml-lsp" ] 179 | ``` 180 | See https://helix-editor.com for more information on how to add new language servers. 181 | 182 | #### [Flow Control](https://github.com/neurocyte/flow) 183 | Already defaults to using SuperHTML, just add the executable to your `PATH`. 184 | 185 | #### Vim 186 | Vim should be able to parse the errors that `superhtml check [PATH]`. This 187 | means that you can use `:make` and the quickfix window to check for syntax 188 | errors. 189 | 190 | Set the `makeprg` to the following in your .vimrc: 191 | ``` 192 | " for any html file, a :make action will populate the quickfix menu 193 | autocmd filetype html setlocal makeprg=superhtml\ check\ % 194 | " if you want to use gq{motion} to format sections or the whole buffer (with gggqG) 195 | autocmd filetype html setlocal formatprg=superhtml\ fmt\ --stdin 196 | ``` 197 | 198 | #### Zed 199 | 200 | See [WeetHet/superhtml-zed](https://github.com/WeetHet/superhtml-zed). 201 | 202 | #### Other editors 203 | Follow your editor specific instructions on how to define a new Language Server for a given language / file format. 204 | 205 | *(Also feel free to contribute more specific instructions to this readme / add files under the `editors/` subdirectory).* 206 | 207 | ## Templating Language Library 208 | SuperHTML is also a HTML templating language. More on that soon. 209 | 210 | ## Contributing 211 | SuperHTML tracks the latest Zig release (0.13.0 at the moment of writing). 212 | 213 | ### Contributing to the HTML parser & LSP 214 | Contributing to the HTML parser and LSP doesn't require you to be familiar with the templating language, basically limiting the scope of what you have to worry about to: 215 | 216 | - `src/cli.zig` 217 | - `src/cli/` 218 | - `src/html/` 219 | 220 | In particular, you will care about `src/html/Tokenizer.zig` and `src/html/Ast.zig`. 221 | 222 | You can run `zig test src/html/Ast.zig` to run parser unit tests without needing to worry the rest of the project. 223 | 224 | Running `zig build` will compile the Super CLI tool, allowing you to also then test the LSP behavior directly from your favorite editor. 225 | 226 | The LSP will log in your cache directory so you can `tail -f ~/.cache/super/super.log` to see what happens with the LSP. 227 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .superhtml, 3 | .version = "0.4.0", 4 | .fingerprint = 0xc5e9aede3c1db363, 5 | .minimum_zig_version = "0.14.0-dev.3451+d8d2aa9af", 6 | .dependencies = .{ 7 | .lsp_kit = .{ 8 | .url = "git+https://github.com/kristoff-it/zig-lsp-kit#87ff3d537a0c852442e180137d9557711963802c", 9 | .hash = "lsp_kit-0.1.0-hAAxO9S9AADv_5D0iplASFtNCFXAPk54M0u-3jj2MRFk", 10 | }, 11 | .afl_kit = .{ 12 | .url = "git+https://github.com/kristoff-it/zig-afl-kit?ref=zig-0.14.0#1e9fcaa08361307d16a9bde82b4a7fd4560ce502", 13 | .hash = "afl_kit-0.1.0-uhOgGDkdAAALG16McR2B4b8QwRUQ2sa9XdgDTFXRWQTY", 14 | .lazy = true, 15 | }, 16 | .known_folders = .{ 17 | .url = "git+https://github.com/ziglibs/known-folders#aa24df42183ad415d10bc0a33e6238c437fc0f59", 18 | .hash = "known_folders-0.0.0-Fy-PJtLDAADGDOwYwMkVydMSTp_aN-nfjCZw6qPQ2ECL", 19 | }, 20 | .tracy = .{ 21 | .url = "git+https://github.com/kristoff-it/tracy#67d2d89e351048c76fc6d161e0ac09d8a831dc60", 22 | .hash = "tracy-0.0.0-4Xw-1pwwAABTfMgoDP1unCbZDZhJEfict7XCBGF6IdIn", 23 | }, 24 | .scripty = .{ 25 | .url = "git+https://github.com/kristoff-it/scripty#57056571abcc6fe69fcb171c10b0c9e5962f53b0", 26 | .hash = "scripty-0.1.0-LKK5O9jDAADwZbkwkzYcmtTD3xIStr1SNYWL0kcGf8sk", 27 | }, 28 | }, 29 | .paths = .{ 30 | "LICENSE", 31 | "build.zig", 32 | "build.zig.zon", 33 | "src", 34 | }, 35 | } 36 | -------------------------------------------------------------------------------- /editors/emacs/README.md: -------------------------------------------------------------------------------- 1 | # Adding SuperHTML to Emacs via Eglot 2 | 3 | With `eglot` being included with the core Emacs distribution since 4 | version 29, you can add various language servers, including SuperHTML, 5 | to Emacs fairly simply. Just ensure that `superhtml` is somewhere in 6 | your `$PATH` and you should be able to use one of the forms below with 7 | minimal modification. 8 | 9 | ## With `use-package` 10 | 11 | ```elisp 12 | (use-package eglot 13 | :defer t 14 | :hook ((web-mode . eglot-ensure) 15 | ;; Add more modes as needed 16 | ) 17 | :config 18 | ;; ... 19 | (add-to-list 'eglot-server-programs '((web-mode :language-id "html") . ("superhtml" "lsp")))) 20 | ``` 21 | 22 | ## Without `use-package` 23 | 24 | ```elisp 25 | (require 'eglot) 26 | (with-eval-after-load 'eglot 27 | (add-to-list 'eglot-server-programs 28 | `((web-mode :language-id "html") . ("superhtml" "lsp")))) 29 | ``` 30 | 31 | You can modify the `superhtml` path here as well. If you're not using 32 | `web-mode` then you'll also want to substitute your preferred 33 | mode. The `:language-id` property ensures that HTML is the 34 | content-type passed to the language server, as `eglot` will send the 35 | mode name (minus `-mode`) by default. 36 | -------------------------------------------------------------------------------- /editors/vscode/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | out/ 3 | wasm/ 4 | *.vsix 5 | -------------------------------------------------------------------------------- /editors/vscode/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint" 4 | ] 5 | } -------------------------------------------------------------------------------- /editors/vscode/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that launches the extension inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "runtimeExecutable": "${execPath}", 13 | "args": [ 14 | "--extensionDevelopmentPath=${workspaceFolder}" 15 | ], 16 | "outFiles": [ 17 | "${workspaceFolder}/out/**/*.js" 18 | ], 19 | "preLaunchTask": "Build Extension" 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /editors/vscode/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off", 11 | } -------------------------------------------------------------------------------- /editors/vscode/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Build Extension in Background", 8 | "group": "build", 9 | "type": "npm", 10 | "script": "watch", 11 | "problemMatcher": { 12 | "base": "$tsc-watch" 13 | }, 14 | "isBackground": true 15 | }, 16 | { 17 | "label": "Build Extension", 18 | "group": "build", 19 | "type": "npm", 20 | "script": "build", 21 | "problemMatcher": { 22 | "base": "$tsc" 23 | } 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /editors/vscode/.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | *.vsix 4 | src/** 5 | node_modules/** 6 | .gitignore 7 | -------------------------------------------------------------------------------- /editors/vscode/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "super" extension will be documented in this file. 4 | 5 | ## [v0.5.3] 6 | - Fixes remaining bug when formatting void elements vertically. 7 | 8 | ## [v0.5.2] 9 | - Starting from this release, a WASM-WASI build of SuperHTML is available on GitHub (in the Releases section) in case editors other than VSCode might watnt to bundle a wasm build of SuperHTML. 10 | 11 | - Fixed indentation bug when formatting void elements. 12 | 13 | ## [v0.5.1] 14 | - This is now a web extension that can be used with vscode.dev, etc. 15 | 16 | ## [v0.5.0] 17 | - Updated list of obsolete tags, it previously was based on an outdated HTML spec version. 18 | - The minor version of this extension is now aliged with the internal language server implementation version. 19 | 20 | ## [v0.3.0] 21 | Now the LSP server is bundled in the extension, no need for a separate download anymore. 22 | 23 | ## [v0.2.0] 24 | Introduced correct syntax highlighting grammar. 25 | 26 | ## [v0.1.3] 27 | Add 'path' setting for this extension to allow specifying location of the Super CLI executable manually. 28 | 29 | ## [v0.1.2] 30 | Override VSCode default autoformatting. 31 | 32 | ## [v0.1.1] 33 | Readme fixes 34 | 35 | ## [v0.1.0] 36 | - Initial release 37 | -------------------------------------------------------------------------------- /editors/vscode/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Loris Cro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /editors/vscode/README.md: -------------------------------------------------------------------------------- 1 | # SuperHTML VSCode LSP 2 | Language Server for HTML and SuperHTML Templates. 3 | 4 | ![](../../.github/vscode-autoformat.gif) 5 | 6 | 7 | # NOTE: This extension bundles the full language server 8 | 9 | But you can optionally also get the CLI tool so that you can access it outside of VSCode. 10 | For prebuilt binaries and more info: https://github.com/kristoff-it/superhtml 11 | 12 | 13 | ## Diagnostics 14 | 15 | ![](../../.github/vscode.png) 16 | 17 | This language server is stricter than the HTML spec whenever it would prevent potential human errors from being reported. 18 | 19 | 20 | As an example, HTML allows for closing some tags implicitly. For example the following snipped is correct HTML. 21 | 22 | ```html 23 | 27 | ``` 28 | 29 | This will still be reported as an error by SuperHTML because otherwise the following snippet would have to be considered correct (while it's much probably a typo): 30 | 31 | ```html 32 |

    Title

    33 | ``` 34 | 35 | ## Autoformatting 36 | 37 | The autoformatter has two main ways of interacting with it in order to request for horizontal / vertical alignment. 38 | 39 | 1. Adding / removing whitespace between the **start tag** of an element and its content. 40 | 2. Adding / removing whitespace between the **last attribute** of a start tag and the closing `>`. 41 | 42 | 43 | ### Example of rule #1 44 | Before: 45 | ```html 46 |

    Foo

    47 | ``` 48 | 49 | After: 50 | ```html 51 |
    52 |

    Foo

    53 |
    54 | ``` 55 | 56 | #### Reverse 57 | 58 | Before: 59 | ```html 60 |

    Foo

    61 |
    62 | ``` 63 | 64 | After: 65 | ```html 66 |

    Foo

    67 | ``` 68 | 69 | ### Example of rule #2 70 | Before: 71 | ```html 72 |
    73 | Foo 74 |
    75 | ``` 76 | 77 | After: 78 | ```html 79 |
    83 | Foo 84 |
    85 | ``` 86 | 87 | #### Reverse 88 | 89 | Before: 90 | ```html 91 |
    94 | Foo 95 |
    96 | ``` 97 | 98 | After: 99 | ```html 100 |
    101 | Foo 102 |
    103 | ``` 104 | 105 | -------------------------------------------------------------------------------- /editors/vscode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "super", 3 | "displayName": "SuperHTML", 4 | "description": "Language Server for HTML and SuperHTML Templates.", 5 | "repository": "https://github.com/kristoff-it/superhtml/", 6 | "publisher": "LorisCro", 7 | "version": "0.5.3", 8 | "engines": { 9 | "vscode": "^1.92.0" 10 | }, 11 | "categories": [ 12 | "Formatters" 13 | ], 14 | "activationEvents": [ 15 | "onLanguage:html" 16 | ], 17 | "contributes": { 18 | "configurationDefaults": { 19 | "[html]": { 20 | "editor.formatOnSave": true, 21 | "editor.defaultFormatter": "LorisCro.super", 22 | "files.eol": "\n" 23 | }, 24 | "[superhtml]": { 25 | "editor.formatOnSave": true, 26 | "editor.defaultFormatter": "LorisCro.super", 27 | "files.eol": "\n" 28 | } 29 | }, 30 | "languages": [ 31 | { 32 | "id": "superhtml", 33 | "aliases": [ 34 | "SuperHTML", 35 | "Super HTML", 36 | "superhtml", 37 | "shtml", 38 | "super" 39 | ], 40 | "extensions": [ 41 | ".shtml" 42 | ], 43 | "configuration": "./superhtml.language-configuration.json" 44 | } 45 | ], 46 | "grammars": [ 47 | { 48 | "scopeName": "text.superhtml.basic", 49 | "path": "./syntaxes/superhtml.tmLanguage.json", 50 | "embeddedLanguages": { 51 | "text.superhtml": "superhtml", 52 | "source.css": "css", 53 | "source.js": "javascript" 54 | }, 55 | "tokenTypes": { 56 | "meta.tag string.quoted": "other" 57 | } 58 | }, 59 | { 60 | "language": "superhtml", 61 | "scopeName": "text.superhtml.derivative", 62 | "path": "./syntaxes/superhtml-derivative.tmLanguage.json", 63 | "embeddedLanguages": { 64 | "text.superhtml": "superhtml", 65 | "source.css": "css", 66 | "source.js": "javascript", 67 | "source.python": "python", 68 | "source.smarty": "smarty" 69 | }, 70 | "tokenTypes": { 71 | "meta.tag string.quoted": "other" 72 | } 73 | } 74 | ] 75 | }, 76 | "main": "./out/extension", 77 | "browser": "./out/extension", 78 | "extensionDependencies": [ 79 | "ms-vscode.wasm-wasi-core" 80 | ], 81 | "devDependencies": { 82 | "@types/mocha": "^2.2.48", 83 | "@types/node": "^18.0.0", 84 | "@types/vscode": "^1.92.0", 85 | "@types/which": "^2.0.1", 86 | "@typescript-eslint/eslint-plugin": "^6.7.0", 87 | "@typescript-eslint/parser": "^6.7.0", 88 | "eslint": "^8.49.0", 89 | "vscode-test": "^1.4.0" 90 | }, 91 | "dependencies": { 92 | "@vscode/vsce": "^2.24.0", 93 | "@vscode/wasm-wasi-lsp": "^0.1.0-pre.7", 94 | "camelcase": "^7.0.1", 95 | "esbuild": "^0.12.1", 96 | "lodash-es": "^4.17.21", 97 | "lodash.debounce": "^4.0.8", 98 | "mkdirp": "^2.1.3", 99 | "vscode-languageclient": "^10.0.0-next.12", 100 | "which": "^3.0.0" 101 | }, 102 | "scripts": { 103 | "vscode:prepublish": "npm run build-base -- --minify", 104 | "build-base": "esbuild --bundle --external:vscode src/extension.ts --outdir=out --platform=node --format=cjs", 105 | "build": "npm run build-base -- --sourcemap", 106 | "watch": "npm run build-base -- --sourcemap --watch", 107 | "lint": "eslint . --ext .ts" 108 | } 109 | } -------------------------------------------------------------------------------- /editors/vscode/src/extension.ts: -------------------------------------------------------------------------------- 1 | import { 2 | // createStdioOptions, 3 | startServer 4 | } from '@vscode/wasm-wasi-lsp'; 5 | import { ProcessOptions, Stdio, Wasm } from '@vscode/wasm-wasi/v1'; 6 | import { ExtensionContext, languages, Uri, window, workspace } from 'vscode'; 7 | import { 8 | LanguageClient, 9 | LanguageClientOptions, 10 | ServerOptions 11 | } from 'vscode-languageclient/node'; 12 | import { SuperFormatProvider } from './formatter'; 13 | 14 | 15 | let client: LanguageClient; 16 | export async function activate(context: ExtensionContext) { 17 | const wasm: Wasm = await Wasm.load(); 18 | 19 | const channel = window.createOutputChannel('SuperHTML Language Server'); 20 | // The server options to run the WebAssembly language server. 21 | const serverOptions: ServerOptions = async () => { 22 | const options: ProcessOptions = { 23 | stdio: createStdioOptions(), 24 | // mountPoints: [{ kind: 'workspaceFolder' }] 25 | }; 26 | 27 | // Load the WebAssembly code 28 | const filename = Uri.joinPath( 29 | context.extensionUri, 30 | 'wasm', 31 | 'superhtml.wasm' 32 | ); 33 | const bits = await workspace.fs.readFile(filename); 34 | const module = await WebAssembly.compile(bits); 35 | 36 | // Create the wasm worker that runs the LSP server 37 | const process = await wasm.createProcess( 38 | 'superhtml', 39 | module, 40 | { initial: 160, maximum: 160, shared: false }, 41 | options 42 | ); 43 | 44 | // Hook stderr to the output channel 45 | const decoder = new TextDecoder('utf-8'); 46 | process.stderr!.onData(data => { 47 | channel.append(decoder.decode(data)); 48 | }); 49 | 50 | return startServer(process); 51 | }; 52 | 53 | const clientOptions: LanguageClientOptions = { 54 | documentSelector: [ 55 | { scheme: "file", language: 'html' }, 56 | { scheme: "file", language: 'superhtml' }, 57 | ], 58 | outputChannel: channel, 59 | }; 60 | 61 | client = new LanguageClient( 62 | "superhtml", 63 | "SuperHTML Language Server", 64 | serverOptions, 65 | clientOptions 66 | ); 67 | 68 | context.subscriptions.push( 69 | languages.registerDocumentFormattingEditProvider( 70 | [{ scheme: "file", language: "html" }], 71 | new SuperFormatProvider(client), 72 | ), 73 | languages.registerDocumentRangeFormattingEditProvider( 74 | [{ scheme: "file", language: "html" }], 75 | new SuperFormatProvider(client), 76 | ), 77 | languages.registerDocumentFormattingEditProvider( 78 | [{ scheme: "file", language: "superhtml" }], 79 | new SuperFormatProvider(client), 80 | ), 81 | languages.registerDocumentRangeFormattingEditProvider( 82 | [{ scheme: "file", language: "superhtml" }], 83 | new SuperFormatProvider(client), 84 | ), 85 | ); 86 | 87 | await client.start(); 88 | 89 | } 90 | 91 | export function deactivate(): Thenable | undefined { 92 | if (!client) { 93 | return undefined; 94 | } 95 | return client.stop(); 96 | } 97 | 98 | 99 | function createStdioOptions(): Stdio { 100 | return { 101 | in: { 102 | kind: 'pipeIn', 103 | }, 104 | out: { 105 | kind: 'pipeOut' 106 | }, 107 | err: { 108 | kind: 'pipeOut' 109 | } 110 | }; 111 | } 112 | -------------------------------------------------------------------------------- /editors/vscode/src/formatter.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { TextEdit } from "vscode"; 3 | 4 | import { 5 | DocumentFormattingRequest, 6 | DocumentRangeFormattingRequest, 7 | LanguageClient, 8 | TextDocumentIdentifier 9 | } from 'vscode-languageclient/node'; 10 | 11 | export class SuperFormatProvider implements vscode.DocumentFormattingEditProvider, vscode.DocumentRangeFormattingEditProvider { 12 | private _client: LanguageClient; 13 | 14 | constructor(client: LanguageClient) { 15 | this._client = client; 16 | } 17 | 18 | provideDocumentFormattingEdits(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken): vscode.ProviderResult { 19 | return this._client.sendRequest( 20 | DocumentFormattingRequest.type, 21 | { textDocument: TextDocumentIdentifier.create(document.uri.toString()), options: options }, 22 | token, 23 | ) as Promise; 24 | } 25 | 26 | provideDocumentRangeFormattingEdits(document: vscode.TextDocument, range: vscode.Range, options: vscode.FormattingOptions, token: vscode.CancellationToken): vscode.ProviderResult { 27 | return this._client.sendRequest( 28 | DocumentRangeFormattingRequest.type, 29 | { textDocument: TextDocumentIdentifier.create(document.uri.toString()), range: range, options: options }, 30 | token, 31 | ) as Promise; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /editors/vscode/src/util.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as os from "os"; 3 | import * as path from "path"; 4 | import { window, workspace } from "vscode"; 5 | import which from "which"; 6 | 7 | export const isWindows = process.platform === "win32"; 8 | 9 | export function getExePath(exePath: string | null, exeName: string, optionName: string): string { 10 | // Allow passing the ${workspaceFolder} predefined variable 11 | // See https://code.visualstudio.com/docs/editor/variables-reference#_predefined-variables 12 | if (exePath && exePath.includes("${workspaceFolder}")) { 13 | // We choose the first workspaceFolder since it is ambiguous which one to use in this context 14 | if (workspace.workspaceFolders && workspace.workspaceFolders.length > 0) { 15 | // older versions of Node (which VSCode uses) may not have String.prototype.replaceAll 16 | exePath = exePath.replace(/\$\{workspaceFolder\}/gm, workspace.workspaceFolders[0].uri.fsPath); 17 | } 18 | } 19 | 20 | if (!exePath) { 21 | exePath = which.sync(exeName, { nothrow: true }); 22 | } else if (exePath.startsWith("~")) { 23 | exePath = path.join(os.homedir(), exePath.substring(1)); 24 | } else if (!path.isAbsolute(exePath)) { 25 | exePath = which.sync(exePath, { nothrow: true }); 26 | } 27 | 28 | let message; 29 | if (!exePath) { 30 | message = `Could not find ${exeName} in PATH`; 31 | } else if (!fs.existsSync(exePath)) { 32 | message = `\`${optionName}\` ${exePath} does not exist` 33 | } else { 34 | try { 35 | fs.accessSync(exePath, fs.constants.R_OK | fs.constants.X_OK); 36 | return exePath; 37 | } catch { 38 | message = `\`${optionName}\` ${exePath} is not an executable`; 39 | } 40 | } 41 | window.showErrorMessage(message); 42 | throw Error(message); 43 | } 44 | 45 | export function getSuperPath(): string { 46 | const configuration = workspace.getConfiguration("super"); 47 | const superPath = configuration.get("path"); 48 | return getExePath(superPath, "super", "super.path"); 49 | } 50 | 51 | -------------------------------------------------------------------------------- /editors/vscode/superhtml.language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | "blockComment": [ 4 | "" 6 | ] 7 | }, 8 | "brackets": [ 9 | [ 10 | "" 12 | ], 13 | [ 14 | "{", 15 | "}" 16 | ], 17 | [ 18 | "(", 19 | ")" 20 | ] 21 | ], 22 | "autoClosingPairs": [ 23 | { 24 | "open": "{", 25 | "close": "}" 26 | }, 27 | { 28 | "open": "[", 29 | "close": "]" 30 | }, 31 | { 32 | "open": "(", 33 | "close": ")" 34 | }, 35 | { 36 | "open": "'", 37 | "close": "'" 38 | }, 39 | { 40 | "open": "\"", 41 | "close": "\"" 42 | }, 43 | { 44 | "open": "", 46 | "notIn": [ 47 | "comment", 48 | "string" 49 | ] 50 | } 51 | ], 52 | "surroundingPairs": [ 53 | { 54 | "open": "'", 55 | "close": "'" 56 | }, 57 | { 58 | "open": "\"", 59 | "close": "\"" 60 | }, 61 | { 62 | "open": "{", 63 | "close": "}" 64 | }, 65 | { 66 | "open": "[", 67 | "close": "]" 68 | }, 69 | { 70 | "open": "(", 71 | "close": ")" 72 | }, 73 | { 74 | "open": "<", 75 | "close": ">" 76 | } 77 | ], 78 | "colorizedBracketPairs": [], 79 | "folding": { 80 | "markers": { 81 | "start": "^\\s*", 82 | "end": "^\\s*" 83 | } 84 | }, 85 | "wordPattern": "(-?\\d*\\.\\d\\w*)|([^\\`\\~\\!\\@\\$\\^\\&\\*\\(\\)\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\'\\\"\\,\\.\\<\\>\\/\\s]+)", 86 | "onEnterRules": [ 87 | { 88 | "beforeText": { 89 | "pattern": "<(?!(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr))([_:\\w][_:\\w-.\\d]*)(?:(?:[^'\"/>]|\"[^\"]*\"|'[^']*')*?(?!\\/)>)[^<]*$", 90 | "flags": "i" 91 | }, 92 | "afterText": { 93 | "pattern": "^<\\/([_:\\w][_:\\w-.\\d]*)\\s*>", 94 | "flags": "i" 95 | }, 96 | "action": { 97 | "indent": "indentOutdent" 98 | } 99 | }, 100 | { 101 | "beforeText": { 102 | "pattern": "<(?!(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr))([_:\\w][_:\\w-.\\d]*)(?:(?:[^'\"/>]|\"[^\"]*\"|'[^']*')*?(?!\\/)>)[^<]*$", 103 | "flags": "i" 104 | }, 105 | "action": { 106 | "indent": "indent" 107 | } 108 | } 109 | ], 110 | "indentationRules": { 111 | "increaseIndentPattern": "<(?!\\?|(?:area|base|br|col|frame|hr|html|img|input|keygen|link|menuitem|meta|param|source|track|wbr)\\b|[^>]*\\/>)([-_\\.A-Za-z0-9]+)(?=\\s|>)\\b[^>]*>(?!.*<\\/\\1>)|)|\\{[^}\"']*$", 112 | "decreaseIndentPattern": "^\\s*(<\\/(?!html)[-_\\.A-Za-z0-9]+\\b[^>]*>|-->|\\})" 113 | } 114 | } -------------------------------------------------------------------------------- /editors/vscode/syntaxes/superhtml-derivative.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "information_for_contributors": [ 3 | "This file has been converted from https://github.com/textmate/html.tmbundle/blob/master/Syntaxes/HTML%20%28Derivative%29.tmLanguage", 4 | "If you want to provide a fix or improvement, please create a pull request against the original repository.", 5 | "Once accepted there, we are happy to receive an update request." 6 | ], 7 | "version": "https://github.com/textmate/html.tmbundle/commit/390c8870273a2ae80244dae6db6ba064a802f407", 8 | "name": "SuperHTML (Derivative)", 9 | "scopeName": "text.superhtml.derivative", 10 | "injections": { 11 | "R:text.superhtml - (comment.block, text.superhtml meta.embedded, meta.tag.*.*.html, meta.tag.*.*.*.html, meta.tag.*.*.*.*.html)": { 12 | "comment": "Uses R: to ensure this matches after any other injections.", 13 | "patterns": [ 14 | { 15 | "match": "<", 16 | "name": "invalid.illegal.bad-angle-bracket.html" 17 | } 18 | ] 19 | } 20 | }, 21 | "patterns": [ 22 | { 23 | "include": "text.superhtml.basic#core-minus-invalid" 24 | }, 25 | { 26 | "begin": "(]*)(?)", 36 | "endCaptures": { 37 | "1": { 38 | "name": "punctuation.definition.tag.end.html" 39 | } 40 | }, 41 | "name": "meta.tag.other.unrecognized.html.derivative", 42 | "patterns": [ 43 | { 44 | "include": "text.html.basic#attribute" 45 | } 46 | ] 47 | } 48 | ] 49 | } -------------------------------------------------------------------------------- /editors/vscode/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "NodeNext", 4 | "target": "ESNext", 5 | "outDir": "out", 6 | "esModuleInterop": true, 7 | "lib": [ 8 | "esnext" 9 | ], 10 | "sourceMap": true, 11 | "rootDir": "src", 12 | "moduleResolution": "NodeNext" 13 | }, 14 | "exclude": [ 15 | "node_modules", 16 | ".vscode-test" 17 | ] 18 | } -------------------------------------------------------------------------------- /editors/vscode/vscode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kristoff-it/superhtml/13f5a2221cb748bbe50ad702e89362afd5b925a7/editors/vscode/vscode.png -------------------------------------------------------------------------------- /src/cli.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const build_options = @import("build_options"); 4 | const known = @import("known_folders"); 5 | const super = @import("super"); 6 | const logging = @import("cli/logging.zig"); 7 | const interface_exe = @import("cli/interface.zig"); 8 | const check_exe = @import("cli/check.zig"); 9 | const fmt_exe = @import("cli/fmt.zig"); 10 | const lsp_exe = @import("cli/lsp.zig"); 11 | 12 | pub const known_folders_config = known.KnownFolderConfig{ 13 | .xdg_force_default = true, 14 | .xdg_on_mac = true, 15 | }; 16 | 17 | pub const std_options: std.Options = .{ 18 | .log_level = if (build_options.verbose_logging) 19 | .debug 20 | else 21 | std.log.default_level, 22 | .logFn = logging.logFn, 23 | }; 24 | 25 | var lsp_mode = false; 26 | 27 | pub fn panic( 28 | msg: []const u8, 29 | trace: ?*std.builtin.StackTrace, 30 | ret_addr: ?usize, 31 | ) noreturn { 32 | if (lsp_mode) { 33 | std.log.err("{s}\n\n{?}", .{ msg, trace }); 34 | } else { 35 | std.debug.print("{s}\n\n{?}", .{ msg, trace }); 36 | } 37 | blk: { 38 | const out = if (!lsp_mode) std.io.getStdErr() else logging.log_file orelse break :blk; 39 | const w = out.writer(); 40 | if (builtin.strip_debug_info) { 41 | w.print("Unable to dump stack trace: debug info stripped\n", .{}) catch return; 42 | break :blk; 43 | } 44 | const debug_info = std.debug.getSelfDebugInfo() catch |err| { 45 | w.print("Unable to dump stack trace: Unable to open debug info: {s}\n", .{@errorName(err)}) catch break :blk; 46 | break :blk; 47 | }; 48 | std.debug.writeCurrentStackTrace(w, debug_info, .no_color, ret_addr) catch |err| { 49 | w.print("Unable to dump stack trace: {s}\n", .{@errorName(err)}) catch break :blk; 50 | break :blk; 51 | }; 52 | } 53 | if (builtin.mode == .Debug) @breakpoint(); 54 | std.process.exit(1); 55 | } 56 | 57 | pub const Command = enum { 58 | check, 59 | interface, 60 | i, // alias for interface 61 | fmt, 62 | lsp, 63 | help, 64 | version, 65 | }; 66 | 67 | pub fn main() !void { 68 | var gpa_impl: std.heap.GeneralPurposeAllocator(.{}) = .{}; 69 | const gpa = gpa_impl.allocator(); 70 | 71 | logging.setup(gpa); 72 | 73 | const args = std.process.argsAlloc(gpa) catch oom(); 74 | defer std.process.argsFree(gpa, args); 75 | 76 | if (args.len < 2) fatalHelp(); 77 | 78 | const cmd = std.meta.stringToEnum(Command, args[1]) orelse { 79 | std.debug.print("unrecognized subcommand: '{s}'\n\n", .{args[1]}); 80 | fatalHelp(); 81 | }; 82 | 83 | if (cmd == .lsp) lsp_mode = true; 84 | 85 | _ = switch (cmd) { 86 | .check => check_exe.run(gpa, args[2..]), 87 | .interface, .i => interface_exe.run(gpa, args[2..]), 88 | .fmt => fmt_exe.run(gpa, args[2..]), 89 | .lsp => lsp_exe.run(gpa, args[2..]), 90 | .help => fatalHelp(), 91 | .version => printVersion(), 92 | } catch |err| fatal("unexpected error: {s}\n", .{@errorName(err)}); 93 | } 94 | 95 | fn fatal(comptime fmt: []const u8, args: anytype) noreturn { 96 | std.debug.print(fmt, args); 97 | std.process.exit(1); 98 | } 99 | 100 | fn oom() noreturn { 101 | fatal("oom\n", .{}); 102 | } 103 | 104 | fn printVersion() noreturn { 105 | std.debug.print("{s}\n", .{build_options.version}); 106 | std.process.exit(0); 107 | } 108 | 109 | fn fatalHelp() noreturn { 110 | fatal( 111 | \\Usage: superhtml COMMAND [OPTIONS] 112 | \\ 113 | \\Commands: 114 | \\ check Check documents for syntax errors 115 | \\ interface, i Print a SuperHTML template's interface 116 | \\ fmt Format documents 117 | \\ lsp Start the Super LSP 118 | \\ help Show this menu and exit 119 | \\ version Print Super's version and exit 120 | \\ 121 | \\General Options: 122 | \\ --help, -h Print command specific usage 123 | \\ 124 | \\ 125 | , .{}); 126 | } 127 | -------------------------------------------------------------------------------- /src/cli/check.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const super = @import("superhtml"); 3 | 4 | const FileType = enum { html, super }; 5 | 6 | pub fn run(gpa: std.mem.Allocator, args: []const []const u8) !void { 7 | const cmd = Command.parse(args); 8 | var any_error = false; 9 | switch (cmd.mode) { 10 | .stdin => { 11 | var buf = std.ArrayList(u8).init(gpa); 12 | try std.io.getStdIn().reader().readAllArrayList(&buf, super.max_size); 13 | const in_bytes = try buf.toOwnedSliceSentinel(0); 14 | 15 | try checkHtml(gpa, null, in_bytes); 16 | }, 17 | .stdin_super => { 18 | var buf = std.ArrayList(u8).init(gpa); 19 | try std.io.getStdIn().reader().readAllArrayList(&buf, super.max_size); 20 | const in_bytes = try buf.toOwnedSliceSentinel(0); 21 | 22 | try checkSuper(gpa, null, in_bytes); 23 | }, 24 | .paths => |paths| { 25 | // checkFile will reset the arena at the end of each call 26 | var arena_impl = std.heap.ArenaAllocator.init(gpa); 27 | for (paths) |path| { 28 | checkFile( 29 | &arena_impl, 30 | std.fs.cwd(), 31 | path, 32 | path, 33 | &any_error, 34 | ) catch |err| switch (err) { 35 | error.IsDir, error.AccessDenied => { 36 | checkDir( 37 | gpa, 38 | &arena_impl, 39 | path, 40 | &any_error, 41 | ) catch |dir_err| { 42 | std.debug.print("Error walking dir '{s}': {s}\n", .{ 43 | path, 44 | @errorName(dir_err), 45 | }); 46 | std.process.exit(1); 47 | }; 48 | }, 49 | else => { 50 | std.debug.print("Error while accessing '{s}': {s}\n", .{ 51 | path, @errorName(err), 52 | }); 53 | std.process.exit(1); 54 | }, 55 | }; 56 | } 57 | }, 58 | } 59 | 60 | if (any_error) { 61 | std.process.exit(1); 62 | } 63 | } 64 | 65 | fn checkDir( 66 | gpa: std.mem.Allocator, 67 | arena_impl: *std.heap.ArenaAllocator, 68 | path: []const u8, 69 | any_error: *bool, 70 | ) !void { 71 | var dir = try std.fs.cwd().openDir(path, .{ .iterate = true }); 72 | defer dir.close(); 73 | var walker = dir.walk(gpa) catch oom(); 74 | defer walker.deinit(); 75 | while (try walker.next()) |item| { 76 | switch (item.kind) { 77 | .file => { 78 | try checkFile( 79 | arena_impl, 80 | item.dir, 81 | item.basename, 82 | item.path, 83 | any_error, 84 | ); 85 | }, 86 | else => {}, 87 | } 88 | } 89 | } 90 | 91 | fn checkFile( 92 | arena_impl: *std.heap.ArenaAllocator, 93 | base_dir: std.fs.Dir, 94 | sub_path: []const u8, 95 | full_path: []const u8, 96 | any_error: *bool, 97 | ) !void { 98 | _ = any_error; 99 | defer _ = arena_impl.reset(.retain_capacity); 100 | const arena = arena_impl.allocator(); 101 | 102 | const file = try base_dir.openFile(sub_path, .{}); 103 | defer file.close(); 104 | 105 | const stat = try file.stat(); 106 | if (stat.kind == .directory) 107 | return error.IsDir; 108 | 109 | const file_type: FileType = blk: { 110 | const ext = std.fs.path.extension(sub_path); 111 | if (std.mem.eql(u8, ext, ".html") or 112 | std.mem.eql(u8, ext, ".htm")) 113 | { 114 | break :blk .html; 115 | } 116 | 117 | if (std.mem.eql(u8, ext, ".shtml")) { 118 | break :blk .super; 119 | } 120 | return; 121 | }; 122 | 123 | var buf = std.ArrayList(u8).init(arena); 124 | defer buf.deinit(); 125 | 126 | try file.reader().readAllArrayList(&buf, super.max_size); 127 | 128 | const in_bytes = try buf.toOwnedSliceSentinel(0); 129 | 130 | switch (file_type) { 131 | .html => try checkHtml( 132 | arena, 133 | full_path, 134 | in_bytes, 135 | ), 136 | .super => try checkSuper( 137 | arena, 138 | full_path, 139 | in_bytes, 140 | ), 141 | } 142 | } 143 | 144 | pub fn checkHtml( 145 | arena: std.mem.Allocator, 146 | path: ?[]const u8, 147 | code: [:0]const u8, 148 | ) !void { 149 | const ast = try super.html.Ast.init(arena, code, .html); 150 | if (ast.errors.len > 0) { 151 | try ast.printErrors(code, path, std.io.getStdErr().writer()); 152 | std.process.exit(1); 153 | } 154 | } 155 | 156 | fn checkSuper( 157 | arena: std.mem.Allocator, 158 | path: ?[]const u8, 159 | code: [:0]const u8, 160 | ) !void { 161 | const html = try super.html.Ast.init(arena, code, .superhtml); 162 | if (html.errors.len > 0) { 163 | try html.printErrors(code, path, std.io.getStdErr().writer()); 164 | std.process.exit(1); 165 | } 166 | 167 | const s = try super.Ast.init(arena, html, code); 168 | if (s.errors.len > 0) { 169 | try s.printErrors(code, path, std.io.getStdErr().writer()); 170 | std.process.exit(1); 171 | } 172 | } 173 | 174 | fn oom() noreturn { 175 | std.debug.print("Out of memory\n", .{}); 176 | std.process.exit(1); 177 | } 178 | 179 | const Command = struct { 180 | mode: Mode, 181 | 182 | const Mode = union(enum) { 183 | stdin, 184 | stdin_super, 185 | paths: []const []const u8, 186 | }; 187 | 188 | fn parse(args: []const []const u8) Command { 189 | var mode: ?Mode = null; 190 | 191 | var idx: usize = 0; 192 | while (idx < args.len) : (idx += 1) { 193 | const arg = args[idx]; 194 | if (std.mem.eql(u8, arg, "--help") or 195 | std.mem.eql(u8, arg, "-h")) 196 | { 197 | fatalHelp(); 198 | } 199 | 200 | if (std.mem.startsWith(u8, arg, "-")) { 201 | if (std.mem.eql(u8, arg, "--stdin") or 202 | std.mem.eql(u8, arg, "-")) 203 | { 204 | if (mode != null) { 205 | std.debug.print("unexpected flag: '{s}'\n", .{arg}); 206 | std.process.exit(1); 207 | } 208 | 209 | mode = .stdin; 210 | } else if (std.mem.eql(u8, arg, "--stdin-super")) { 211 | if (mode != null) { 212 | std.debug.print("unexpected flag: '{s}'\n", .{arg}); 213 | std.process.exit(1); 214 | } 215 | 216 | mode = .stdin_super; 217 | } else { 218 | std.debug.print("unexpected flag: '{s}'\n", .{arg}); 219 | std.process.exit(1); 220 | } 221 | } else { 222 | const paths_start = idx; 223 | while (idx < args.len) : (idx += 1) { 224 | if (std.mem.startsWith(u8, args[idx], "-")) { 225 | break; 226 | } 227 | } 228 | idx -= 1; 229 | 230 | if (mode != null) { 231 | std.debug.print( 232 | "unexpected path argument(s): '{s}'...\n", 233 | .{args[paths_start]}, 234 | ); 235 | std.process.exit(1); 236 | } 237 | 238 | const paths = args[paths_start .. idx + 1]; 239 | mode = .{ .paths = paths }; 240 | } 241 | } 242 | 243 | const m = mode orelse { 244 | std.debug.print("missing argument(s)\n\n", .{}); 245 | fatalHelp(); 246 | }; 247 | 248 | return .{ .mode = m }; 249 | } 250 | 251 | fn fatalHelp() noreturn { 252 | std.debug.print( 253 | \\Usage: super check PATH [PATH...] [OPTIONS] 254 | \\ 255 | \\ Checks for syntax errors. If PATH is a directory, it will 256 | \\ be searched recursively for HTML and SuperHTML files. 257 | \\ 258 | \\ Detected extensions: 259 | \\ HTML .html, .htm 260 | \\ SuperHTML .shtml 261 | \\ 262 | \\Options: 263 | \\ 264 | \\ --stdin Format bytes from stdin and output to stdout. 265 | \\ Mutually exclusive with other input arguments. 266 | \\ 267 | \\ --stdin-super Same as --stdin but for SuperHTML files. 268 | \\ 269 | \\ --help, -h Prints this help and exits. 270 | , .{}); 271 | 272 | std.process.exit(1); 273 | } 274 | }; 275 | -------------------------------------------------------------------------------- /src/cli/fmt.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const super = @import("superhtml"); 3 | 4 | const FileType = enum { html, super }; 5 | 6 | pub fn run(gpa: std.mem.Allocator, args: []const []const u8) !void { 7 | const cmd = Command.parse(args); 8 | var any_error = false; 9 | switch (cmd.mode) { 10 | .stdin => { 11 | var buf = std.ArrayList(u8).init(gpa); 12 | try std.io.getStdIn().reader().readAllArrayList(&buf, super.max_size); 13 | const in_bytes = try buf.toOwnedSliceSentinel(0); 14 | 15 | const out_bytes = try fmtHtml(gpa, null, in_bytes); 16 | try std.io.getStdOut().writeAll(out_bytes); 17 | }, 18 | .stdin_super => { 19 | var buf = std.ArrayList(u8).init(gpa); 20 | try std.io.getStdIn().reader().readAllArrayList(&buf, super.max_size); 21 | const in_bytes = try buf.toOwnedSliceSentinel(0); 22 | 23 | const out_bytes = try fmtSuper(gpa, null, in_bytes); 24 | try std.io.getStdOut().writeAll(out_bytes); 25 | }, 26 | .paths => |paths| { 27 | // checkFile will reset the arena at the end of each call 28 | var arena_impl = std.heap.ArenaAllocator.init(gpa); 29 | for (paths) |path| { 30 | formatFile( 31 | &arena_impl, 32 | cmd.check, 33 | std.fs.cwd(), 34 | path, 35 | path, 36 | &any_error, 37 | ) catch |err| switch (err) { 38 | error.IsDir, error.AccessDenied => { 39 | formatDir( 40 | gpa, 41 | &arena_impl, 42 | cmd.check, 43 | path, 44 | &any_error, 45 | ) catch |dir_err| { 46 | std.debug.print("Error walking dir '{s}': {s}\n", .{ 47 | path, 48 | @errorName(dir_err), 49 | }); 50 | std.process.exit(1); 51 | }; 52 | }, 53 | else => { 54 | std.debug.print("Error while accessing '{s}': {s}\n", .{ 55 | path, @errorName(err), 56 | }); 57 | std.process.exit(1); 58 | }, 59 | }; 60 | } 61 | }, 62 | } 63 | 64 | if (any_error) { 65 | std.process.exit(1); 66 | } 67 | } 68 | 69 | fn formatDir( 70 | gpa: std.mem.Allocator, 71 | arena_impl: *std.heap.ArenaAllocator, 72 | check: bool, 73 | path: []const u8, 74 | any_error: *bool, 75 | ) !void { 76 | var dir = try std.fs.cwd().openDir(path, .{ .iterate = true }); 77 | defer dir.close(); 78 | var walker = dir.walk(gpa) catch oom(); 79 | defer walker.deinit(); 80 | while (try walker.next()) |item| { 81 | switch (item.kind) { 82 | .file => { 83 | try formatFile( 84 | arena_impl, 85 | check, 86 | item.dir, 87 | item.basename, 88 | item.path, 89 | any_error, 90 | ); 91 | }, 92 | else => {}, 93 | } 94 | } 95 | } 96 | 97 | fn formatFile( 98 | arena_impl: *std.heap.ArenaAllocator, 99 | check: bool, 100 | base_dir: std.fs.Dir, 101 | sub_path: []const u8, 102 | full_path: []const u8, 103 | any_error: *bool, 104 | ) !void { 105 | defer _ = arena_impl.reset(.retain_capacity); 106 | const arena = arena_impl.allocator(); 107 | 108 | const file = try base_dir.openFile(sub_path, .{}); 109 | defer file.close(); 110 | 111 | const stat = try file.stat(); 112 | if (stat.kind == .directory) 113 | return error.IsDir; 114 | 115 | const file_type: FileType = blk: { 116 | const ext = std.fs.path.extension(sub_path); 117 | if (std.mem.eql(u8, ext, ".html") or 118 | std.mem.eql(u8, ext, ".htm")) 119 | { 120 | break :blk .html; 121 | } 122 | 123 | if (std.mem.eql(u8, ext, ".shtml")) { 124 | break :blk .super; 125 | } 126 | return; 127 | }; 128 | 129 | var buf = std.ArrayList(u8).init(arena); 130 | defer buf.deinit(); 131 | 132 | try file.reader().readAllArrayList(&buf, super.max_size); 133 | 134 | const in_bytes = try buf.toOwnedSliceSentinel(0); 135 | 136 | const out_bytes = switch (file_type) { 137 | .html => try fmtHtml( 138 | arena, 139 | full_path, 140 | in_bytes, 141 | ), 142 | .super => try fmtSuper( 143 | arena, 144 | full_path, 145 | in_bytes, 146 | ), 147 | }; 148 | 149 | if (std.mem.eql(u8, out_bytes, in_bytes)) return; 150 | 151 | const stdout = std.io.getStdOut().writer(); 152 | if (check) { 153 | any_error.* = true; 154 | try stdout.print("{s}\n", .{full_path}); 155 | return; 156 | } 157 | 158 | var af = try base_dir.atomicFile(sub_path, .{ .mode = stat.mode }); 159 | defer af.deinit(); 160 | 161 | try af.file.writeAll(out_bytes); 162 | try af.finish(); 163 | try stdout.print("{s}\n", .{full_path}); 164 | } 165 | 166 | pub fn fmtHtml( 167 | arena: std.mem.Allocator, 168 | path: ?[]const u8, 169 | code: [:0]const u8, 170 | ) ![]const u8 { 171 | const ast = try super.html.Ast.init(arena, code, .html); 172 | if (ast.errors.len > 0) { 173 | try ast.printErrors(code, path, std.io.getStdErr().writer()); 174 | std.process.exit(1); 175 | } 176 | 177 | return std.fmt.allocPrint(arena, "{}", .{ast.formatter(code)}); 178 | } 179 | 180 | fn fmtSuper( 181 | arena: std.mem.Allocator, 182 | path: ?[]const u8, 183 | code: [:0]const u8, 184 | ) ![]const u8 { 185 | const ast = try super.html.Ast.init(arena, code, .superhtml); 186 | if (ast.errors.len > 0) { 187 | try ast.printErrors(code, path, std.io.getStdErr().writer()); 188 | std.process.exit(1); 189 | } 190 | 191 | return std.fmt.allocPrint(arena, "{}", .{ast.formatter(code)}); 192 | } 193 | 194 | fn oom() noreturn { 195 | std.debug.print("Out of memory\n", .{}); 196 | std.process.exit(1); 197 | } 198 | 199 | const Command = struct { 200 | check: bool, 201 | mode: Mode, 202 | 203 | const Mode = union(enum) { 204 | stdin, 205 | stdin_super, 206 | paths: []const []const u8, 207 | }; 208 | 209 | fn parse(args: []const []const u8) Command { 210 | var check: bool = false; 211 | var mode: ?Mode = null; 212 | 213 | var idx: usize = 0; 214 | while (idx < args.len) : (idx += 1) { 215 | const arg = args[idx]; 216 | if (std.mem.eql(u8, arg, "--help") or 217 | std.mem.eql(u8, arg, "-h")) 218 | { 219 | fatalHelp(); 220 | } 221 | 222 | if (std.mem.eql(u8, arg, "--check")) { 223 | if (check) { 224 | std.debug.print("error: duplicate '--check' flag\n\n", .{}); 225 | std.process.exit(1); 226 | } 227 | 228 | check = true; 229 | continue; 230 | } 231 | 232 | if (std.mem.startsWith(u8, arg, "-")) { 233 | if (std.mem.eql(u8, arg, "--stdin") or 234 | std.mem.eql(u8, arg, "-")) 235 | { 236 | if (mode != null) { 237 | std.debug.print("unexpected flag: '{s}'\n", .{arg}); 238 | std.process.exit(1); 239 | } 240 | 241 | mode = .stdin; 242 | } else if (std.mem.eql(u8, arg, "--stdin-super")) { 243 | if (mode != null) { 244 | std.debug.print("unexpected flag: '{s}'\n", .{arg}); 245 | std.process.exit(1); 246 | } 247 | 248 | mode = .stdin_super; 249 | } else { 250 | std.debug.print("unexpected flag: '{s}'\n", .{arg}); 251 | std.process.exit(1); 252 | } 253 | } else { 254 | const paths_start = idx; 255 | while (idx < args.len) : (idx += 1) { 256 | if (std.mem.startsWith(u8, args[idx], "-")) { 257 | break; 258 | } 259 | } 260 | idx -= 1; 261 | 262 | if (mode != null) { 263 | std.debug.print( 264 | "unexpected path argument(s): '{s}'...\n", 265 | .{args[paths_start]}, 266 | ); 267 | std.process.exit(1); 268 | } 269 | 270 | const paths = args[paths_start .. idx + 1]; 271 | mode = .{ .paths = paths }; 272 | } 273 | } 274 | 275 | const m = mode orelse { 276 | std.debug.print("missing argument(s)\n\n", .{}); 277 | fatalHelp(); 278 | }; 279 | 280 | return .{ .check = check, .mode = m }; 281 | } 282 | 283 | fn fatalHelp() noreturn { 284 | std.debug.print( 285 | \\Usage: super fmt PATH [PATH...] [OPTIONS] 286 | \\ 287 | \\ Formats input paths inplace. If PATH is a directory, it will 288 | \\ be searched recursively for HTML and SuperHTML files. 289 | \\ 290 | \\ Detected extensions: 291 | \\ HTML .html, .htm 292 | \\ SuperHTML .shtml 293 | \\ 294 | \\Options: 295 | \\ 296 | \\ --stdin Format bytes from stdin and output to stdout. 297 | \\ Mutually exclusive with other input arguments. 298 | \\ 299 | \\ --stdin-super Same as --stdin but for SuperHTML files. 300 | \\ 301 | \\ --check List non-conforming files and exit with an 302 | \\ error if the list is not empty. 303 | \\ 304 | \\ --help, -h Prints this help and exits. 305 | , .{}); 306 | 307 | std.process.exit(1); 308 | } 309 | }; 310 | -------------------------------------------------------------------------------- /src/cli/interface.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const super = @import("superhtml"); 3 | 4 | const FileType = enum { html, super }; 5 | 6 | pub fn run(gpa: std.mem.Allocator, args: []const []const u8) !void { 7 | const cmd = Command.parse(args); 8 | switch (cmd.mode) { 9 | .stdin => { 10 | var buf = std.ArrayList(u8).init(gpa); 11 | try std.io.getStdIn().reader().readAllArrayList(&buf, super.max_size); 12 | const in_bytes = try buf.toOwnedSliceSentinel(0); 13 | 14 | const out_bytes = try renderInterface(gpa, null, in_bytes); 15 | try std.io.getStdOut().writeAll(out_bytes); 16 | }, 17 | .path => |path| { 18 | var arena_impl = std.heap.ArenaAllocator.init(gpa); 19 | const out_bytes = printInterfaceFromFile( 20 | &arena_impl, 21 | std.fs.cwd(), 22 | path, 23 | path, 24 | ) catch |err| switch (err) { 25 | error.IsDir => { 26 | std.debug.print("error: '{s}' is a directory\n\n", .{ 27 | path, 28 | }); 29 | std.process.exit(1); 30 | }, 31 | else => { 32 | std.debug.print("error while accessing '{s}': {}\n\n", .{ 33 | path, 34 | err, 35 | }); 36 | std.process.exit(1); 37 | }, 38 | }; 39 | 40 | try std.io.getStdOut().writeAll(out_bytes); 41 | }, 42 | } 43 | } 44 | 45 | fn printInterfaceFromFile( 46 | arena_impl: *std.heap.ArenaAllocator, 47 | base_dir: std.fs.Dir, 48 | sub_path: []const u8, 49 | full_path: []const u8, 50 | ) ![]const u8 { 51 | defer _ = arena_impl.reset(.retain_capacity); 52 | const arena = arena_impl.allocator(); 53 | 54 | const file = try base_dir.openFile(sub_path, .{}); 55 | defer file.close(); 56 | 57 | const stat = try file.stat(); 58 | if (stat.kind == .directory) 59 | return error.IsDir; 60 | 61 | var buf = std.ArrayList(u8).init(arena); 62 | defer buf.deinit(); 63 | 64 | try file.reader().readAllArrayList(&buf, super.max_size); 65 | 66 | const in_bytes = try buf.toOwnedSliceSentinel(0); 67 | 68 | return renderInterface(arena, full_path, in_bytes); 69 | } 70 | 71 | fn renderInterface( 72 | arena: std.mem.Allocator, 73 | path: ?[]const u8, 74 | code: [:0]const u8, 75 | ) ![]const u8 { 76 | const html_ast = try super.html.Ast.init(arena, code, .superhtml); 77 | if (html_ast.errors.len > 0) { 78 | try html_ast.printErrors(code, path, std.io.getStdErr().writer()); 79 | std.process.exit(1); 80 | } 81 | 82 | const s = try super.Ast.init(arena, html_ast, code); 83 | if (s.errors.len > 0) { 84 | try s.printErrors(code, path, std.io.getStdErr().writer()); 85 | std.process.exit(1); 86 | } 87 | 88 | return std.fmt.allocPrint(arena, "{}", .{ 89 | s.interfaceFormatter(html_ast, path), 90 | }); 91 | } 92 | 93 | fn oom() noreturn { 94 | std.debug.print("Out of memory\n", .{}); 95 | std.process.exit(1); 96 | } 97 | 98 | const Command = struct { 99 | mode: Mode, 100 | 101 | const Mode = union(enum) { 102 | stdin, 103 | path: []const u8, 104 | }; 105 | 106 | fn parse(args: []const []const u8) Command { 107 | var mode: ?Mode = null; 108 | 109 | var idx: usize = 0; 110 | while (idx < args.len) : (idx += 1) { 111 | const arg = args[idx]; 112 | if (std.mem.eql(u8, arg, "--help") or 113 | std.mem.eql(u8, arg, "-h")) 114 | { 115 | fatalHelp(); 116 | } 117 | 118 | if (std.mem.startsWith(u8, arg, "-")) { 119 | if (std.mem.eql(u8, arg, "--stdin") or 120 | std.mem.eql(u8, arg, "-")) 121 | { 122 | if (mode != null) { 123 | std.debug.print("unexpected flag: '{s}'\n", .{arg}); 124 | std.process.exit(1); 125 | } 126 | 127 | mode = .stdin; 128 | } else { 129 | std.debug.print("unexpected flag: '{s}'\n", .{arg}); 130 | std.process.exit(1); 131 | } 132 | } else { 133 | if (mode != null) { 134 | std.debug.print( 135 | "unexpected path argument: '{s}'...\n", 136 | .{args[idx]}, 137 | ); 138 | std.process.exit(1); 139 | } 140 | 141 | mode = .{ .path = args[idx] }; 142 | } 143 | } 144 | 145 | const m = mode orelse { 146 | std.debug.print("missing argument\n\n", .{}); 147 | fatalHelp(); 148 | }; 149 | 150 | return .{ .mode = m }; 151 | } 152 | 153 | fn fatalHelp() noreturn { 154 | std.debug.print( 155 | \\Usage: super i [FILE] [OPTIONS] 156 | \\ 157 | \\ Prints a SuperHTML template's interface. 158 | \\ 159 | \\Options: 160 | \\ 161 | \\ --stdin Read the template from stdin instead of 162 | \\ reading from a file. 163 | \\ 164 | \\ --help, -h Prints this help and exits. 165 | , .{}); 166 | 167 | std.process.exit(1); 168 | } 169 | }; 170 | -------------------------------------------------------------------------------- /src/cli/logging.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const build_options = @import("build_options"); 4 | const folders = @import("known_folders"); 5 | 6 | pub var log_file: ?std.fs.File = switch (builtin.target.os.tag) { 7 | .linux, .macos => std.io.getStdErr(), 8 | else => null, 9 | }; 10 | 11 | // const enabled_scopes = blk: { 12 | // const len = build_options.enabled_scopes.len; 13 | // const scopes: [len]@Type(.EnumLiteral) = undefined; 14 | // for (build_options.enabled_scopes, &scopes) |s, *e| { 15 | // e.* = @Type() 16 | // } 17 | // }; 18 | 19 | pub fn logFn( 20 | comptime level: std.log.Level, 21 | comptime scope: @Type(.enum_literal), 22 | comptime format: []const u8, 23 | args: anytype, 24 | ) void { 25 | if (build_options.enabled_scopes.len > 0) { 26 | inline for (build_options.enabled_scopes) |es| { 27 | if (comptime std.mem.eql(u8, es, @tagName(scope))) { 28 | break; 29 | } 30 | } else return; 31 | } 32 | 33 | const l = log_file orelse return; 34 | const scope_prefix = "(" ++ @tagName(scope) ++ "): "; 35 | const prefix = "[" ++ @tagName(level) ++ "] " ++ scope_prefix; 36 | 37 | std.debug.lockStdErr(); 38 | defer std.debug.unlockStdErr(); 39 | 40 | var buf_writer = std.io.bufferedWriter(l.writer()); 41 | buf_writer.writer().print(prefix ++ format ++ "\n", args) catch return; 42 | buf_writer.flush() catch return; 43 | } 44 | 45 | pub fn setup(gpa: std.mem.Allocator) void { 46 | std.debug.lockStdErr(); 47 | defer std.debug.unlockStdErr(); 48 | 49 | setupInternal(gpa) catch { 50 | log_file = null; 51 | }; 52 | } 53 | 54 | fn setupInternal(gpa: std.mem.Allocator) !void { 55 | const cache_base = try folders.open(gpa, .cache, .{}) orelse return error.Failure; 56 | try cache_base.makePath("super"); 57 | 58 | const log_path = "superhtml.log"; 59 | const file = try cache_base.createFile(log_path, .{ .truncate = false }); 60 | const end = try file.getEndPos(); 61 | try file.seekTo(end); 62 | 63 | log_file = file; 64 | } 65 | -------------------------------------------------------------------------------- /src/cli/lsp/Document.zig: -------------------------------------------------------------------------------- 1 | const Document = @This(); 2 | 3 | const std = @import("std"); 4 | const assert = std.debug.assert; 5 | const super = @import("superhtml"); 6 | 7 | const log = std.log.scoped(.lsp_document); 8 | 9 | language: super.Language, 10 | src: []const u8, 11 | html: super.html.Ast, 12 | super_ast: ?super.Ast = null, 13 | 14 | pub fn deinit(doc: *Document, gpa: std.mem.Allocator) void { 15 | doc.html.deinit(gpa); 16 | if (doc.super_ast) |s| s.deinit(gpa); 17 | } 18 | 19 | pub fn init( 20 | gpa: std.mem.Allocator, 21 | src: []const u8, 22 | language: super.Language, 23 | ) error{OutOfMemory}!Document { 24 | var doc: Document = .{ 25 | .src = src, 26 | .language = language, 27 | .html = try super.html.Ast.init(gpa, src, language), 28 | }; 29 | errdefer doc.html.deinit(gpa); 30 | 31 | if (language == .superhtml and doc.html.errors.len == 0) { 32 | const super_ast = try super.Ast.init(gpa, doc.html, src); 33 | errdefer super_ast.deinit(gpa); 34 | doc.super_ast = super_ast; 35 | } 36 | 37 | return doc; 38 | } 39 | 40 | pub fn reparse(doc: *Document, gpa: std.mem.Allocator) !void { 41 | doc.deinit(gpa); 42 | doc.html = try super.html.Ast.init(gpa, doc.src, doc.language); 43 | errdefer doc.html.deinit(gpa); 44 | 45 | if (doc.language == .superhtml and doc.html.errors.len == 0) { 46 | doc.super_ast = try super.Ast.init(gpa, doc.html, doc.src); 47 | } else { 48 | doc.super_ast = null; 49 | } 50 | 51 | return; 52 | } 53 | -------------------------------------------------------------------------------- /src/cli/lsp/logic.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const lsp = @import("lsp"); 3 | const super = @import("superhtml"); 4 | const lsp_namespace = @import("../lsp.zig"); 5 | const Handler = lsp_namespace.Handler; 6 | const getRange = lsp_namespace.getRange; 7 | const Document = @import("Document.zig"); 8 | 9 | const log = std.log.scoped(.ziggy_lsp); 10 | 11 | pub fn loadFile( 12 | self: *Handler, 13 | arena: std.mem.Allocator, 14 | new_text: [:0]const u8, 15 | uri: []const u8, 16 | language: super.Language, 17 | ) !void { 18 | errdefer @panic("error while loading document!"); 19 | 20 | var res: lsp.types.PublishDiagnosticsParams = .{ 21 | .uri = uri, 22 | .diagnostics = &.{}, 23 | }; 24 | 25 | const doc = try Document.init( 26 | self.gpa, 27 | new_text, 28 | language, 29 | ); 30 | 31 | log.debug("document init", .{}); 32 | 33 | const gop = try self.files.getOrPut(self.gpa, uri); 34 | errdefer _ = self.files.remove(uri); 35 | 36 | if (gop.found_existing) { 37 | gop.value_ptr.deinit(self.gpa); 38 | } else { 39 | gop.key_ptr.* = try self.gpa.dupe(u8, uri); 40 | } 41 | 42 | gop.value_ptr.* = doc; 43 | 44 | if (doc.html.errors.len != 0) { 45 | const diags = try arena.alloc(lsp.types.Diagnostic, doc.html.errors.len); 46 | for (doc.html.errors, diags) |err, *d| { 47 | const range = getRange(err.main_location, doc.src); 48 | d.* = .{ 49 | .range = range, 50 | .severity = .Error, 51 | .message = switch (err.tag) { 52 | .token => |t| @tagName(t), 53 | .ast => |t| @tagName(t), 54 | }, 55 | }; 56 | } 57 | res.diagnostics = diags; 58 | } else { 59 | if (doc.super_ast) |super_ast| { 60 | const diags = try arena.alloc( 61 | lsp.types.Diagnostic, 62 | super_ast.errors.len, 63 | ); 64 | for (super_ast.errors, diags) |err, *d| { 65 | const range = getRange(err.main_location, doc.src); 66 | d.* = .{ 67 | .range = range, 68 | .severity = .Error, 69 | .message = @tagName(err.kind), 70 | }; 71 | } 72 | res.diagnostics = diags; 73 | } 74 | } 75 | 76 | const msg = try self.server.sendToClientNotification( 77 | "textDocument/publishDiagnostics", 78 | res, 79 | ); 80 | 81 | defer self.gpa.free(msg); 82 | } 83 | -------------------------------------------------------------------------------- /src/css.zig: -------------------------------------------------------------------------------- 1 | pub const Tokenizer = @import("css/Tokenizer.zig"); 2 | pub const Ast = @import("css/Ast.zig"); 3 | 4 | test { 5 | _ = Tokenizer; 6 | _ = Ast; 7 | } 8 | -------------------------------------------------------------------------------- /src/errors.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const html = @import("html.zig"); 4 | const Span = @import("root.zig").Span; 5 | 6 | pub const ErrWriter = std.ArrayListUnmanaged(u8).Writer; 7 | 8 | /// Used to catch programming errors where a function fails to report 9 | /// correctly that an error has occurred. 10 | pub const Fatal = error{ 11 | /// The error has been fully reported. 12 | Fatal, 13 | 14 | /// There was an error while outputting to the error writer. 15 | ErrIO, 16 | 17 | /// There war an error while outputting to the out writer. 18 | OutIO, 19 | }; 20 | 21 | pub const FatalOOM = error{OutOfMemory} || Fatal; 22 | 23 | pub const FatalShow = Fatal || error{ 24 | /// The error has been reported but we should also print the 25 | /// interface of the template we are extending. 26 | FatalShowInterface, 27 | }; 28 | 29 | pub const FatalShowOOM = error{OutOfMemory} || FatalShow; 30 | 31 | pub fn report( 32 | writer: ErrWriter, 33 | template_name: []const u8, 34 | template_path: []const u8, 35 | bad_node: Span, 36 | src: []const u8, 37 | error_code: []const u8, 38 | comptime title: []const u8, 39 | comptime msg: []const u8, 40 | ) Fatal { 41 | try header(writer, title, msg); 42 | try diagnostic( 43 | writer, 44 | template_name, 45 | template_path, 46 | true, 47 | error_code, 48 | bad_node, 49 | src, 50 | ); 51 | return error.Fatal; 52 | } 53 | 54 | pub fn diagnostic( 55 | writer: ErrWriter, 56 | template_name: []const u8, 57 | template_path: []const u8, 58 | bracket_line: bool, 59 | note_line: []const u8, 60 | span: Span, 61 | src: []const u8, 62 | ) error{ErrIO}!void { 63 | const pos = span.range(src); 64 | const line_off = span.line(src); 65 | 66 | // trim spaces 67 | const line_trim_left = std.mem.trimLeft(u8, line_off.line, &std.ascii.whitespace); 68 | const start_trim_left = line_off.start + line_off.line.len - line_trim_left.len; 69 | 70 | const caret_len = span.end - span.start; 71 | const caret_spaces_len = span.start -| start_trim_left; 72 | 73 | const line_trim = std.mem.trimRight(u8, line_trim_left, &std.ascii.whitespace); 74 | 75 | var buf: [1024]u8 = undefined; 76 | 77 | const highlight = if (caret_len + caret_spaces_len < 1024) blk: { 78 | const h = buf[0 .. caret_len + caret_spaces_len]; 79 | @memset(h[0..caret_spaces_len], ' '); 80 | @memset(h[caret_spaces_len..][0..caret_len], '^'); 81 | break :blk h; 82 | } else ""; 83 | 84 | writer.print( 85 | \\ 86 | \\{s}{s}{s} 87 | \\({s}) {s}:{}:{}: 88 | \\ {s} 89 | \\ {s} 90 | \\ 91 | , .{ 92 | if (bracket_line) "[" else "", 93 | note_line, 94 | if (bracket_line) "]" else "", 95 | template_name, 96 | template_path, 97 | pos.start.row, 98 | pos.start.col, 99 | line_trim, 100 | highlight, 101 | }) catch return error.ErrIO; 102 | } 103 | 104 | pub fn header( 105 | writer: ErrWriter, 106 | comptime title: []const u8, 107 | comptime msg: []const u8, 108 | ) error{ErrIO}!void { 109 | writer.print( 110 | \\ 111 | \\---------- {s} ---------- 112 | \\ 113 | , .{title}) catch return error.ErrIO; 114 | writer.print(msg, .{}) catch return error.ErrIO; 115 | writer.print("\n", .{}) catch return error.ErrIO; 116 | } 117 | 118 | pub fn fatal( 119 | writer: ErrWriter, 120 | comptime fmt: []const u8, 121 | args: anytype, 122 | ) Fatal { 123 | writer.print(fmt, args) catch return error.ErrIO; 124 | return error.ErrIO; 125 | } 126 | -------------------------------------------------------------------------------- /src/fuzz.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const super = @import("superhtml"); 3 | 4 | pub const std_options: std.Options = .{ .log_level = .err }; 5 | 6 | /// This main function is meant to be used via black box fuzzers 7 | /// and/or to manually weed out test cases that are not valid anymore 8 | /// after fixing bugs. 9 | /// 10 | /// See fuzz/afl.zig for the AFL++ specific executable. 11 | pub fn main() !void { 12 | var gpa_impl: std.heap.GeneralPurposeAllocator(.{}) = .{}; 13 | const gpa = gpa_impl.allocator(); 14 | 15 | const stdin = std.io.getStdIn(); 16 | const src = try stdin.readToEndAlloc(gpa, std.math.maxInt(usize)); 17 | defer gpa.free(src); 18 | 19 | const ast = try super.html.Ast.init(gpa, src, .html); 20 | defer ast.deinit(gpa); 21 | 22 | if (ast.errors.len == 0) { 23 | try ast.render(src, std.io.null_writer); 24 | } 25 | } 26 | 27 | test "afl++ fuzz cases" { 28 | const cases: []const []const u8 = &.{ 29 | @embedFile("fuzz/cases/2.html"), 30 | @embedFile("fuzz/cases/3.html"), 31 | @embedFile("fuzz/cases/12.html"), 32 | @embedFile("fuzz/cases/round2/2.html"), 33 | @embedFile("fuzz/cases/round2/3.html"), 34 | @embedFile("fuzz/cases/round3/2.html"), 35 | @embedFile("fuzz/cases/77.html"), 36 | @embedFile("fuzz/cases/3-01.html"), 37 | @embedFile("fuzz/cases/4-01.html"), 38 | @embedFile("fuzz/cases/5-01.html"), 39 | @embedFile("fuzz/cases/6-01.html"), 40 | @embedFile("fuzz/cases/6-02.html"), 41 | }; 42 | 43 | for (cases) |c| { 44 | // std.debug.print("test: \n\n{s}\n\n", .{c}); 45 | const ast = try super.html.Ast.init(std.testing.allocator, c, .html); 46 | defer ast.deinit(std.testing.allocator); 47 | if (ast.errors.len == 0) { 48 | try ast.render(c, std.io.null_writer); 49 | } 50 | // ast.debug(c); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/fuzz/afl.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const super = @import("superhtml"); 3 | const astgen = @import("astgen.zig"); 4 | 5 | pub const std_options: std.Options = .{ .log_level = .err }; 6 | 7 | export fn zig_fuzz_init() void {} 8 | 9 | export fn zig_fuzz_test(buf: [*]u8, len: isize) void { 10 | var gpa_impl: std.heap.GeneralPurposeAllocator(.{}) = .{}; 11 | defer std.debug.assert(gpa_impl.deinit() == .ok); 12 | 13 | const gpa = gpa_impl.allocator(); 14 | const src = buf[0..@intCast(len)]; 15 | 16 | const html_ast = super.html.Ast.init(gpa, src, .superhtml) catch unreachable; 17 | defer html_ast.deinit(gpa); 18 | 19 | // if (html_ast.errors.len == 0) { 20 | // const super_ast = super.Ast.init(gpa, html_ast, src) catch unreachable; 21 | // defer super_ast.deinit(gpa); 22 | // } 23 | 24 | if (html_ast.errors.len == 0) { 25 | var out = std.ArrayList(u8).init(gpa); 26 | defer out.deinit(); 27 | html_ast.render(src, out.writer()) catch unreachable; 28 | 29 | eqlIgnoreWhitespace(src, out.items); 30 | 31 | var full_circle = std.ArrayList(u8).init(gpa); 32 | defer full_circle.deinit(); 33 | html_ast.render(out.items, full_circle.writer()) catch unreachable; 34 | 35 | std.debug.assert(std.mem.eql(u8, out.items, full_circle.items)); 36 | 37 | const super_ast = super.Ast.init(gpa, html_ast, src) catch unreachable; 38 | defer super_ast.deinit(gpa); 39 | } 40 | } 41 | 42 | export fn zig_fuzz_test_astgen(buf: [*]u8, len: isize) void { 43 | var gpa_impl: std.heap.GeneralPurposeAllocator(.{}) = .{}; 44 | const gpa = gpa_impl.allocator(); 45 | const astgen_src = buf[0..@intCast(len)]; 46 | 47 | const clamp: u32 = @min(20, astgen_src.len); 48 | const src = astgen.build(gpa, astgen_src[0..clamp]) catch unreachable; 49 | defer gpa.free(src); 50 | 51 | const html_ast = super.html.Ast.init(gpa, src, .superhtml) catch unreachable; 52 | defer html_ast.deinit(gpa); 53 | 54 | std.debug.assert(html_ast.errors.len == 0); 55 | 56 | const super_ast = super.Ast.init(gpa, html_ast, src) catch unreachable; 57 | defer super_ast.deinit(gpa); 58 | 59 | // if (html_ast.errors.len == 0) { 60 | // var out = std.ArrayList(u8).init(gpa); 61 | // defer out.deinit(); 62 | // html_ast.render(src, out.writer()) catch unreachable; 63 | 64 | // eqlIgnoreWhitespace(src, out.items); 65 | 66 | // var full_circle = std.ArrayList(u8).init(gpa); 67 | // defer full_circle.deinit(); 68 | // html_ast.render(out.items, full_circle.writer()) catch unreachable; 69 | 70 | // std.debug.assert(std.mem.eql(u8, out.items, full_circle.items)); 71 | 72 | // const super_ast = super.Ast.init(gpa, html_ast, src) catch unreachable; 73 | // defer super_ast.deinit(gpa); 74 | // } 75 | } 76 | 77 | fn eqlIgnoreWhitespace(a: []const u8, b: []const u8) void { 78 | var i: u32 = 0; 79 | var j: u32 = 0; 80 | 81 | while (i < a.len) : (i += 1) { 82 | const a_byte = a[i]; 83 | if (std.ascii.isWhitespace(a_byte)) continue; 84 | while (j < b.len) : (j += 1) { 85 | const b_byte = b[j]; 86 | if (std.ascii.isWhitespace(b_byte)) continue; 87 | 88 | if (a_byte != b_byte) { 89 | const a_span: super.Span = .{ .start = i, .end = i + 1 }; 90 | const b_span: super.Span = .{ .start = j, .end = j + 1 }; 91 | std.debug.panic("mismatch! {c} != {c} \na = {any}\nb={any}\n", .{ 92 | a_byte, 93 | b_byte, 94 | a_span.range(a), 95 | b_span.range(b), 96 | }); 97 | } 98 | } 99 | } 100 | } 101 | 102 | test "afl++ fuzz cases" { 103 | const cases: []const []const u8 = &.{ 104 | @embedFile("fuzz/2.html"), 105 | @embedFile("fuzz/3.html"), 106 | @embedFile("fuzz/12.html"), 107 | @embedFile("fuzz/round2/2.html"), 108 | @embedFile("fuzz/round2/3.html"), 109 | @embedFile("fuzz/round3/2.html"), 110 | @embedFile("fuzz/77.html"), 111 | @embedFile("fuzz/3-01.html"), 112 | @embedFile("fuzz/4-01.html"), 113 | @embedFile("fuzz/5-01.html"), 114 | }; 115 | 116 | for (cases) |c| { 117 | // std.debug.print("test: \n\n{s}\n\n", .{c}); 118 | const ast = try super.html.Ast.init(std.testing.allocator, c, .html); 119 | defer ast.deinit(std.testing.allocator); 120 | if (ast.errors.len == 0) { 121 | try ast.render(c, std.io.null_writer); 122 | } 123 | // ast.debug(c); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/fuzz/astgen.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const super = @import("superhtml"); 4 | 5 | const Op = enum(u8) { 6 | // add element 7 | n = 'n', 8 | // add element and give it a template attribute 9 | N = 'N', 10 | // add element 11 | s = 's', 12 | // add text node 13 | t = 't', 14 | // add comment node 15 | c = 'c', 16 | // add new element, enter it 17 | e = 'e', 18 | // add a new non-semantic void element 19 | E = 'E', 20 | // add an id attribute 21 | // (break if any 'u', 'c', or 't' was sent after the last 'e' or 'E') 22 | // (break if another attribute of the same kind was already added) 23 | i = 'i', 24 | // add non-semantic attribute to selected node 25 | // (break if any 'u', 'c', or 't' was sent after the last 'e' or 'E') 26 | a = 'a', 27 | // add loop attribute 28 | // (break if any 'u', 'c', or 't' was sent after the last 'e' or 'E') 29 | // (break if another attribute of the same kind was already added) 30 | l = 'l', 31 | // add an inline-loop attribute 32 | // (break if any 'u', 'c', or 't' was sent after the last 'e' or 'E') 33 | // (break if another attribute of the same kind was already added) 34 | L = 'L', 35 | // add an if attribute 36 | // (break if any 'u', 'c', or 't' was sent after the last 'e' or 'E') 37 | // (break if another attribute of the same kind was already added) 38 | f = 'f', 39 | // add an inline-if attribute 40 | // (break if any 'u', 'c', or 't' was sent after the last 'e' or 'E') 41 | // (break if another attribute of the same kind was already added) 42 | F = 'F', 43 | // add a var attribute 44 | // (break if any 'u', 'c', or 't' was sent after the last 'e' or 'E') 45 | // (break if another attribute of the same kind was already added) 46 | v = 'v', 47 | // add an empty attribute value 48 | // (break when not put in front of an attribute Op) 49 | x = 'x', 50 | // add a static non-scripted attribute value 51 | // (break when not put in front of an attribute Op) 52 | X = 'X', 53 | // add a scripted attribute value 54 | // (break when not put in front of an attribute Op) 55 | y = 'y', 56 | // add a unique non-scripted attribute value 57 | // (break when not put in front of an attribute Op) 58 | Y = 'Y', 59 | // select the parent element of the current element 60 | // (break when a top-level element is already selected) 61 | u = 'u', 62 | // add whitespace 63 | // (consecutive 'w' on the same element will cause a break) 64 | w = 'w', 65 | 66 | // noop 67 | _, 68 | }; 69 | 70 | const Element = struct { 71 | // a span of Ops that describes a list of attrs 72 | attrs: super.Span = .{ .start = 0, .end = 0 }, 73 | kind: Tag = .none, 74 | whitespace: bool = false, 75 | 76 | pub const Tag = enum { none, div, super, extend, br, comment, text }; 77 | 78 | pub fn commit( 79 | e: *Element, 80 | w: anytype, 81 | src: []const u8, 82 | ends: *std.ArrayList(Tag), 83 | ) !void { 84 | switch (e.kind) { 85 | .comment => { 86 | if (e.whitespace) try w.writeAll("\n"); 87 | try w.writeAll(""); 88 | e.* = .{}; 89 | return; 90 | }, 91 | .text => { 92 | if (e.whitespace) try w.writeAll("\n"); 93 | try w.writeAll("X"); 94 | e.* = .{}; 95 | return; 96 | }, 97 | .none => { 98 | e.* = .{}; 99 | return; 100 | }, 101 | .div, .super, .extend, .br => { 102 | if (e.whitespace) try w.writeAll("\n"); 103 | }, 104 | } 105 | 106 | try w.print("<{s}", .{@tagName(e.kind)}); 107 | defer { 108 | w.writeAll(">") catch unreachable; 109 | switch (e.kind) { 110 | .div => ends.append(e.kind) catch unreachable, 111 | .super, .br, .extend, .none, .text, .comment => {}, 112 | } 113 | e.* = .{}; 114 | } 115 | 116 | var has_id = false; 117 | var has_loop = false; 118 | var has_inl_loop = false; 119 | var has_if = false; 120 | var has_inl_if = false; 121 | var has_var = false; 122 | var idx = e.attrs.start; 123 | while (idx < e.attrs.end) : (idx += 1) { 124 | var attribute_was_added = true; 125 | const op: Op = @enumFromInt(src[idx]); 126 | switch (op) { 127 | .N => { 128 | try w.writeAll(" template='x'"); 129 | attribute_was_added = false; 130 | }, 131 | .a => try w.print(" a{}", .{idx}), 132 | .i => if (!has_id) { 133 | try w.writeAll(" id"); 134 | has_id = true; 135 | } else { 136 | return error.Break; 137 | }, 138 | .l => if (!has_loop) { 139 | try w.writeAll(" loop"); 140 | has_loop = true; 141 | } else { 142 | return error.Break; 143 | }, 144 | .L => if (!has_inl_loop) { 145 | try w.writeAll(" inline-loop"); 146 | has_inl_loop = true; 147 | } else { 148 | return error.Break; 149 | }, 150 | .f => if (!has_if) { 151 | try w.writeAll(" if"); 152 | has_if = true; 153 | } else { 154 | return error.Break; 155 | }, 156 | .F => if (!has_inl_if) { 157 | try w.writeAll(" inline-if"); 158 | has_inl_if = true; 159 | } else { 160 | return error.Break; 161 | }, 162 | .v => if (!has_var) { 163 | try w.writeAll(" var"); 164 | has_var = true; 165 | } else { 166 | return error.Break; 167 | }, 168 | .w => attribute_was_added = false, 169 | else => { 170 | return error.Break; 171 | }, 172 | } 173 | 174 | if (attribute_was_added and idx < e.attrs.end - 1) { 175 | idx += 1; 176 | const op_next: Op = @enumFromInt(src[idx]); 177 | switch (op_next) { 178 | .x => try w.writeAll("=''"), 179 | .X => try w.writeAll("='x'"), 180 | .y => try w.writeAll("='$'"), 181 | .Y => try w.print("='{}'", .{idx}), 182 | else => idx -= 1, 183 | } 184 | } 185 | } 186 | } 187 | }; 188 | 189 | pub fn build(gpa: std.mem.Allocator, src: []const u8) ![]const u8 { 190 | var out = std.ArrayList(u8).init(gpa); 191 | var ends = std.ArrayList(Element.Tag).init(gpa); 192 | const w = out.writer(); 193 | var current: Element = .{}; 194 | 195 | buildInternal(w, src, &ends, ¤t) catch |err| switch (err) { 196 | error.Break => {}, 197 | else => unreachable, 198 | }; 199 | 200 | current.commit(w, src, &ends) catch |err| switch (err) { 201 | error.Break => {}, 202 | else => unreachable, 203 | }; 204 | 205 | while (ends.popOrNull()) |kind| 206 | try w.print("", .{@tagName(kind)}); 207 | 208 | return out.items; 209 | } 210 | pub fn buildInternal( 211 | w: anytype, 212 | src: []const u8, 213 | ends: *std.ArrayList(Element.Tag), 214 | current: *Element, 215 | ) !void { 216 | for (src, 0..) |c, i| { 217 | const idx: u32 = @intCast(i); 218 | const op: Op = @enumFromInt(c); 219 | switch (op) { 220 | // add attribute 221 | .n => { 222 | try current.commit(w, src, ends); 223 | current.kind = .extend; 224 | }, 225 | // add attribute and give it a template attribute 226 | .N => { 227 | try current.commit(w, src, ends); 228 | current.kind = .extend; 229 | current.attrs = .{ .start = idx, .end = idx + 1 }; 230 | }, 231 | // add new element, enter it 232 | .e => { 233 | try current.commit(w, src, ends); 234 | current.kind = .div; 235 | }, 236 | // add a new non-semantic void element 237 | .E => { 238 | try current.commit(w, src, ends); 239 | current.kind = .br; 240 | }, 241 | // add element 242 | .s => { 243 | try current.commit(w, src, ends); 244 | current.kind = .super; 245 | }, 246 | // add element into the current element and give id 247 | // attribute to the parent if needed 248 | // (noop if any 'u', 'c', or 't' was sent after the last 'e' or 'E') 249 | // .S => switch (current.kind) { 250 | // .none, .comment, .text => continue, 251 | // .div, .super, .extend, .br => { 252 | // if (current.attrs.end == 0) { 253 | // current.attrs = .{ .start = idx, .end = idx + 1 }; 254 | // } else { 255 | // current.attrs.end = idx + 1; 256 | // } 257 | // try current.commit(w, src, ends); 258 | // current.kind = .super; 259 | // }, 260 | // }, 261 | // add text element 262 | .t => { 263 | try current.commit(w, src, ends); 264 | current.kind = .text; 265 | }, 266 | // add comment 267 | .c => { 268 | try current.commit(w, src, ends); 269 | current.kind = .comment; 270 | }, 271 | // attributes 272 | // (noop if any 'u', 'c', or 't' was sent after the last 'e' or 'E') 273 | .a, .l, .L, .f, .F, .v, .i, .x, .X, .y, .Y => switch (current.kind) { 274 | .none, .comment, .text => break, 275 | .div, .super, .extend, .br => { 276 | if (current.attrs.end == 0) { 277 | current.attrs = .{ .start = idx, .end = idx + 1 }; 278 | } else { 279 | current.attrs.end = idx + 1; 280 | } 281 | }, 282 | }, 283 | // select the parent element of the current element 284 | // (noop when a top-level element is already selected) 285 | .u => { 286 | try current.commit(w, src, ends); 287 | if (ends.popOrNull()) |kind| 288 | try w.print("", .{@tagName(kind)}) 289 | else 290 | break; 291 | }, 292 | // add whitespace 293 | // (consecutive 'w' on the same element are noops) 294 | .w => { 295 | if (current.whitespace) break; 296 | current.whitespace = true; 297 | }, 298 | 299 | // early return to avoid keeping "dead bytes" 300 | // in the active set of bytes, ideally improving 301 | // fuzzer performance 302 | else => break, 303 | } 304 | } 305 | } 306 | 307 | pub fn main() !void { 308 | var gpa_impl: std.heap.GeneralPurposeAllocator(.{}) = .{}; 309 | const gpa = gpa_impl.allocator(); 310 | 311 | const args = try std.process.argsAlloc(gpa); 312 | defer std.process.argsFree(gpa, args); 313 | 314 | if (args.len != 2) @panic("wrong number of arguments"); 315 | const src = args[1]; 316 | 317 | const out = try build(gpa, src); 318 | try std.io.getStdOut().writeAll(out); 319 | } 320 | -------------------------------------------------------------------------------- /src/fuzz/cases/12.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kristoff-it/superhtml/13f5a2221cb748bbe50ad702e89362afd5b925a7/src/fuzz/cases/12.html -------------------------------------------------------------------------------- /src/fuzz/cases/2.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kristoff-it/superhtml/13f5a2221cb748bbe50ad702e89362afd5b925a7/src/fuzz/cases/2.html -------------------------------------------------------------------------------- /src/fuzz/cases/3-01.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 | 6 | > 7 | 8 | 9 | 10 | 11 | 6 12 | -------------------------------------------------------------------------------- /src/fuzz/cases/3.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kristoff-it/superhtml/13f5a2221cb748bbe50ad702e89362afd5b925a7/src/fuzz/cases/3.html -------------------------------------------------------------------------------- /src/fuzz/cases/4-01.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kristoff-it/superhtml/13f5a2221cb748bbe50ad702e89362afd5b925a7/src/fuzz/cases/4-01.html -------------------------------------------------------------------------------- /src/fuzz/cases/5-01.html: -------------------------------------------------------------------------------- 1 | ]<t< 3 | 140 | 141 | 144 | 145 | 147 | 148 | --- 149 | 150 | (document 151 | (script_element 152 | (start_tag (tag_name)) 153 | (raw_text) 154 | (end_tag (tag_name))) 155 | (style_element 156 | (start_tag (tag_name)) 157 | (raw_text) 158 | (end_tag (tag_name))) 159 | (script_element 160 | (start_tag (tag_name)) 161 | (raw_text) 162 | (end_tag (tag_name)))) 163 | 164 | ================================== 165 | All-caps doctype 166 | ================================== 167 | 170 | --- 171 | 172 | (document 173 | (doctype)) 174 | 175 | ================================== 176 | Lowercase doctype 177 | ================================== 178 | 179 | --- 180 | 181 | (document 182 | (doctype)) 183 | 184 | ================================== 185 | LI elements without close tags 186 | ================================== 187 |
      188 |
    • One 189 |
    • Two 190 |
    191 | --- 192 | 193 | (document 194 | (element 195 | (start_tag (tag_name)) 196 | (element (start_tag (tag_name)) (text)) 197 | (element (start_tag (tag_name)) (text)) 198 | (end_tag (tag_name)))) 199 | 200 | ====================================== 201 | DT and DL elements without close tags 202 | ====================================== 203 |
    204 |
    Coffee 205 |
    Café 206 |
    Black hot drink 207 |
    Milk 208 |
    White cold drink 209 |
    210 | --- 211 | 212 | (document 213 | (element 214 | (start_tag (tag_name)) 215 | (element (start_tag (tag_name)) (text)) 216 | (element (start_tag (tag_name)) (text)) 217 | (element (start_tag (tag_name)) (text)) 218 | (element (start_tag (tag_name)) (text)) 219 | (element (start_tag (tag_name)) (text)) 220 | (end_tag (tag_name)))) 221 | 222 | ====================================== 223 | P elements without close tags 224 | ====================================== 225 |

    One 226 |

    Two
    227 |

    Three 228 |

    Four 229 |

    Five

    230 | --- 231 | 232 | (document 233 | (element (start_tag (tag_name)) (text)) 234 | (element (start_tag (tag_name)) (text) (end_tag (tag_name))) 235 | (element (start_tag (tag_name)) (text)) 236 | (element (start_tag (tag_name)) (text)) 237 | (element (start_tag (tag_name)) (text) (end_tag (tag_name)))) 238 | 239 | ====================================== 240 | Ruby annotation elements without close tags 241 | ====================================== 242 | とうきょう 243 | --- 244 | 245 | (document 246 | (element 247 | (start_tag (tag_name)) 248 | (text) 249 | (element (start_tag (tag_name)) (text)) 250 | (element (start_tag (tag_name)) (text)) 251 | (element (start_tag (tag_name)) (text)) 252 | (end_tag (tag_name)))) 253 | 254 | ======================================= 255 | COLGROUP elements without end tags 256 | ======================================= 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 |
    LimeLemonOrange
    267 | --- 268 | 269 | (document 270 | (element 271 | (start_tag (tag_name)) 272 | (element 273 | (start_tag (tag_name)) 274 | (element (start_tag 275 | (tag_name) 276 | (attribute (attribute_name) (quoted_attribute_value (attribute_value))))) 277 | (element (start_tag 278 | (tag_name) 279 | (attribute (attribute_name) (quoted_attribute_value (attribute_value)))))) 280 | (element 281 | (start_tag (tag_name)) 282 | (element (start_tag (tag_name)) (text) (end_tag (tag_name))) 283 | (element (start_tag (tag_name)) (text) (end_tag (tag_name))) 284 | (element (start_tag (tag_name)) (text) (end_tag (tag_name))) 285 | (end_tag (tag_name))) 286 | (end_tag (tag_name)))) 287 | 288 | ========================================= 289 | TR, TD, and TH elements without end tags 290 | ========================================= 291 | 292 | 293 | 296 |
    One 294 | Two 295 |
    Three 297 | Four 298 |
    299 | --- 300 | 301 | (document 302 | (element 303 | (start_tag (tag_name)) 304 | (element 305 | (start_tag (tag_name)) 306 | (element (start_tag (tag_name)) (text)) 307 | (element (start_tag (tag_name)) (text))) 308 | (element 309 | (start_tag (tag_name)) 310 | (element (start_tag (tag_name)) (text)) 311 | (element (start_tag (tag_name)) (text))) 312 | (end_tag (tag_name)))) 313 | 314 | ============================== 315 | Named entities in tag contents 316 | ============================== 317 | 318 |

    Lorem ipsum   dolor sit © amet.

    319 | --- 320 | 321 | (document 322 | (element 323 | (start_tag (tag_name)) 324 | (text) 325 | (entity) 326 | (text) 327 | (entity) 328 | (text) 329 | (end_tag (tag_name)))) 330 | 331 | ================================ 332 | Numeric entities in tag contents 333 | ================================ 334 | 335 |

    Lorem ipsum   dolor sit — amet.

    336 | --- 337 | 338 | (document 339 | (element 340 | (start_tag (tag_name)) 341 | (text) 342 | (entity) 343 | (text) 344 | (entity) 345 | (text) 346 | (end_tag (tag_name)))) 347 | 348 | ================================= 349 | Multiple entities in tag contents 350 | ================================= 351 | 352 |

    Lorem ipsum   dolor   sit   amet.

    353 | --- 354 | 355 | (document 356 | (element 357 | (start_tag (tag_name)) 358 | (text) 359 | (entity) 360 | (text) 361 | (entity) 362 | (text) 363 | (entity) 364 | (text) 365 | (end_tag (tag_name)))) 366 | 367 | ================================== 368 | Omitted end tags for html and head 369 | ================================== 370 | 371 | 372 | 373 | --- 374 | 375 | (document 376 | (doctype) 377 | (element 378 | (start_tag 379 | (tag_name)) 380 | (element 381 | (start_tag 382 | (tag_name))))) 383 | -------------------------------------------------------------------------------- /tree-sitter-superhtml/test/highlight/attributes.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 | 6 |
    Hello, World
    7 | 8 |
    Hello, World
    9 | 10 |
    Hello, World
    11 | 12 |
    Hello, World
    13 | 14 |
    Hello, World
    15 | 16 |
    Hello, World
    17 | 18 | 20 | 21 | @click="count++" 22 | 23 | 24 | :value="count" 25 | 26 | 27 | @value:modelValue="newValue => count = newValue" 28 | 29 | 30 | > 31 | 32 | 33 | 34 | 35 | 36 |
    37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /tree-sitter-superhtml/test/highlight/doctype.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /tree-sitter-superhtml/test/highlight/erroneous.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tree-sitter-superhtml/test/highlight/self-closing.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | --------------------------------------------------------------------------------