├── hbml.json ├── nr outline.txt ├── docs ├── .gitignore ├── book │ └── compress │ │ └── HBML Tutorial.pdf ├── src │ ├── commands │ │ ├── init.md │ │ ├── README.md │ │ ├── lint.md │ │ ├── reverse.md │ │ └── build.md │ ├── macros │ │ ├── README.md │ │ ├── 4.scopes.md │ │ ├── 5.std.md │ │ ├── 1.simple.md │ │ ├── 2.children.md │ │ └── 3.conditionals.md │ ├── get_start │ │ ├── README.md │ │ ├── setup.md │ │ └── first_file.md │ ├── SUMMARY.md │ ├── intro.md │ └── imports.md ├── book.toml └── theme │ ├── HBML.sublime-syntax │ └── theme.tmtheme ├── test ├── raw_text.txt ├── insert_test.hbml ├── importable_macros.hbml ├── reverse.tests.mjs ├── parser.tests.mjs └── macros.tests.mjs ├── .gitattributes ├── src ├── error.js ├── reverse_web.js ├── commands │ ├── init.js │ ├── fs_prelude.js │ ├── reverse.js │ ├── build.js │ └── lint.js ├── parser │ ├── macros.js │ ├── util.js │ ├── parser.js │ ├── at-commands.js │ └── main.js ├── bench.js ├── index.js ├── config_parse.js ├── constants.js └── token.js ├── highlight ├── README.md ├── HBML.sublime-syntax └── mode-hbml.js ├── .github └── workflows │ └── test.yml ├── package.json ├── LICENSE.MD ├── Gruntfile.cjs ├── usage.md ├── README.md └── bundle └── hbml_browser.min.js /hbml.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nr outline.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | book/html 2 | -------------------------------------------------------------------------------- /test/raw_text.txt: -------------------------------------------------------------------------------- 1 | Voluptates fugit ut temporibus odit sint. -------------------------------------------------------------------------------- /test/insert_test.hbml: -------------------------------------------------------------------------------- 1 | --test > "Hello, world!" 2 | #check > "Other words" -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /docs/book/compress/HBML Tutorial.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heyitsdoodler/hbml/HEAD/docs/book/compress/HBML Tutorial.pdf -------------------------------------------------------------------------------- /docs/src/commands/init.md: -------------------------------------------------------------------------------- 1 | # Init 2 | 3 | The `hbml init` command will initialise a directory (by default the current one) with the standard HBML project structure. 4 | -------------------------------------------------------------------------------- /test/importable_macros.hbml: -------------------------------------------------------------------------------- 1 | --external-macro > "Hello, world!" 2 | --external-macro2 > p > "Hello, world!" 3 | --external-macro3 { :consume > :child :external-macro } -------------------------------------------------------------------------------- /docs/src/commands/README.md: -------------------------------------------------------------------------------- 1 | # CLI Commands 2 | 3 | This chapter contains the different CLI commands and how to use them. If you prefer the CLI help info, you can always run `hbml -h` for help on a command 4 | -------------------------------------------------------------------------------- /src/error.js: -------------------------------------------------------------------------------- 1 | export class Error { 2 | constructor(desc, file, ln, col) { 3 | this.desc = desc 4 | this.file = file 5 | this.ln = ln 6 | this.col = col 7 | } 8 | 9 | toString() { 10 | return `${this.desc} ${this.file} ${this.ln}:${this.col}` 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /highlight/README.md: -------------------------------------------------------------------------------- 1 | # Syntax highlighting 2 | 3 | This directory includes files relevant for syntax highlighting. 4 | 5 | For browser syntax highlighting we have: 6 | - [`mode-hbml.js`](mode-hbml.js) for ACE (ajax/ace) 7 | - [`HBML.sublime-syntax`](HBML.sublime-syntax) Sublime syntax highlighting 8 | -------------------------------------------------------------------------------- /docs/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | title = "HBML Tutorial" 3 | description = "A HBML tutorial and documentation by the HBML devs" 4 | authors = ["heyitsdoodler", "nxe (RosiePuddles)"] 5 | language = "en" 6 | [output.html] 7 | [output.compress] 8 | subtitle = "How to use HBML from simple markdown to complex macros" 9 | highlight = "no-node" -------------------------------------------------------------------------------- /docs/src/macros/README.md: -------------------------------------------------------------------------------- 1 | # Macros 2 | 3 | HBML allows you to simplify your code with macros. Macros are scoped and mutable and have standard defaults available to use. In this chapter, we'll introduce the basics of macros and how to use them, then we'll delve into more complex macros with conditionals that really give macros their power. 4 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: ["main"] 5 | pull_request: 6 | branches: ["main"] 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-node@v3 13 | with: 14 | node-version: 16 15 | - run: npm ci 16 | - run: npm test -------------------------------------------------------------------------------- /docs/src/get_start/README.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | First off, you'll need to have HBML installed: 4 | ```bash 5 | npm install hbml -g 6 | ``` 7 | 8 | To check this, you can run `hbml -h` and you should get the HBML help message 9 | 10 | ## What we'll cover 11 | 12 | In this chapter we'll cover: 13 | - Making your first HBML file 14 | - Setting up a project 15 | - Building and linting HBML 16 | - Including dependencies and local files 17 | -------------------------------------------------------------------------------- /docs/src/commands/lint.md: -------------------------------------------------------------------------------- 1 | # Lint 2 | 3 | The `hbml lint` command will lint files or a project. 4 | 5 | This command can either be run as `hbml lint project` to build your project (see [the project section](../get_start/setup.md) for more info on projects); or with some number of files or directories you want to build. 6 | 7 | When running in non-project mode, you can specify various flags to alter the build process. These flags are ignored in project mode. 8 | -------------------------------------------------------------------------------- /docs/src/commands/reverse.md: -------------------------------------------------------------------------------- 1 | # Reverse 2 | 3 | The `hbml reverse` command allows you to convert HTML back into HBML. 4 | 5 | The command accepts a list of paths, an optional output prefix, and a verbose output option. 6 | 7 | It uses a HTML parser to tokenise the input which is then converted to HBML tokens and turned into linted HBML with the default linting options with one change to make elements with one child prefer the arrow operator over using brackets. 8 | -------------------------------------------------------------------------------- /docs/src/commands/build.md: -------------------------------------------------------------------------------- 1 | # Build 2 | 3 | The `hbml build` command will build files or a project into HTML. 4 | 5 | This command can either be run as `hbml build project` to build your project (see [the project section](../get_start/setup.md) for more info on projects); or with some number of files or directories you want to build. 6 | 7 | When running in non-project mode, you can specify various flags to alter the build process. These flags are ignored in project mode. 8 | -------------------------------------------------------------------------------- /docs/src/macros/4.scopes.md: -------------------------------------------------------------------------------- 1 | # Scopes 2 | 3 | Scopes are important in discussing macros and understanding how they work can be very useful. 4 | 5 | A scope is a section of HBML that has access to the same macros and is either the root scope or under an element. 6 | 7 | For example, if we had 8 | ```hbml 9 | --macro1 > "Hello" 10 | { 11 | --macro2 > "World" 12 | } 13 | ``` 14 | 15 | then we have two scopes; the root scope, and the scope inside the brackets. Because the macro `:macro2` is defined in the brackets, it's not usable outside of them because it "doesn't exist". 16 | -------------------------------------------------------------------------------- /docs/src/get_start/setup.md: -------------------------------------------------------------------------------- 1 | # Projects 2 | 3 | Very rarely will you be working with single HBML files in isolation. It's much more common for you to be using them in a project with multiple HBML files. 4 | 5 | Because of this, you can use HBML alongside a project just like normal. 6 | 7 | To do this, run `hbml init` in the project directory. This will create a `hbml.json` file with the default value below 8 | 9 | ```json 10 | { 11 | "build": { 12 | "src": ["."], 13 | "output": "html" 14 | }, 15 | "lint": { 16 | "src": ["."], 17 | "output": "html" 18 | } 19 | } 20 | ``` 21 | -------------------------------------------------------------------------------- /docs/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Introduction](intro.md) 4 | - [Getting started](get_start/README.md) 5 | - [Your first file](get_start/first_file.md) 6 | - [Projects](get_start/setup.md) 7 | - [Macros](macros/README.md) 8 | - [Simple macros](macros/1.simple.md) 9 | - [Macro children](macros/2.children.md) 10 | - [Macro conditionals](macros/3.conditionals.md) 11 | - [Scopes](macros/4.scopes.md) 12 | - [Standard macros](macros/5.std.md) 13 | - [Importing files](imports.md) 14 | - [Commands](commands/README.md) 15 | - [`build`](commands/build.md) 16 | - [`lint`](commands/lint.md) 17 | - [`init`](commands/init.md) 18 | - [`reverse`](commands/reverse.md) 19 | -------------------------------------------------------------------------------- /docs/src/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Welcome to the HBML book! In this book we cover the entirety of HBML from simple files to complex macros and external dependencies 4 | 5 | But to start, we need to introduce some useful terms we use: 6 | - **Void elements** are HBML elements that can't have anything in it. For example, the `meta` tag is a void element 7 | - The **arrow operator** is the `>` character. This is used after a tag, which can be empty 8 | - A **child element** is any element that's nested inside another element called the **parent element**. For example in `div { h1 > "Example" }`, the `div` is the parent element and the `h1` is the child element in respect to each other 9 | - **Implicit tags** are tags that are defined implicitly. For example `{ "Hello World!" }` is actually `div { "Hello World!" }` just that the `div` tag is implicit 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hbml", 3 | "description": "HBML builder and linter", 4 | "version": "0.1.0", 5 | "repository": "github:heyitsdoodler/hbml", 6 | "main": "src/index.js", 7 | "files": [ 8 | "src/index.js", 9 | "src/config_parse.js", 10 | "src/constants.js", 11 | "src/error.js", 12 | "src/token.js", 13 | "src/parser", 14 | "src/commands/", 15 | "package.json" 16 | ], 17 | "dependencies": { 18 | "ajv": "8.11.2", 19 | "chalk": "5.1.2", 20 | "configstore": "6.0.0", 21 | "minimist": "1.2.7", 22 | "posthtml-parser": "0.11.0" 23 | }, 24 | "devDependencies": { 25 | "benchmark": "2.1.4", 26 | "grunt": "1.5.3", 27 | "grunt-contrib-uglify": "5.2.2", 28 | "grunt-regex-replace": "0.4.0", 29 | "mocha": "10.2.0" 30 | }, 31 | "bin": { 32 | "hbml": "src/index.js" 33 | }, 34 | "scripts": { 35 | "nodemon": "nodemon -e js,hbml src/index.js build src/test.hbml", 36 | "bench": "node src/bench.js", 37 | "test": "mocha", 38 | "build": "grunt" 39 | }, 40 | "license": "MIT", 41 | "type": "module" 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE.MD: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Ayden Hodgins-de Jonge 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 | -------------------------------------------------------------------------------- /Gruntfile.cjs: -------------------------------------------------------------------------------- 1 | const fs = require("fs") 2 | 3 | module.exports = function(grunt) { 4 | // Project configuration. 5 | grunt.initConfig({ 6 | pkg: grunt.file.readJSON('package.json'), 7 | uglify: { 8 | options: { 9 | banner: '/*! <%= pkg.name %> <%= pkg.version %> <%= grunt.template.today("yyyy-mm-dd HH:MM") %> */\n' 10 | }, 11 | build: { 12 | src: [ 13 | 'src/config_parser.js', 'src/constants.js', 'src/token.js', 'src/error.js', 14 | 'src/reverse_web.js', 'src/parser/*.js' 15 | ], 16 | dest: 'bundle/hbml_browser.min.js' 17 | } 18 | }, 19 | "regex-replace": { 20 | default: { 21 | src: ['bundle/hbml_browser.min.js'], 22 | actions: [ 23 | { 24 | name: "imports1", 25 | search: 'import [^ ]+ from"[^"]+";', 26 | replace: '', 27 | flags: 'g' 28 | }, 29 | { 30 | name: "imports2", 31 | search: 'import\{[^}]+}from"[^"]+";', 32 | replace: '', 33 | flags: 'g' 34 | }, 35 | { 36 | name: "exports", 37 | search: 'export\{[^}]+};', 38 | replace: 'export\{fullStringify,snippet,full\};', 39 | flags: 'g' 40 | } 41 | ] 42 | } 43 | } 44 | }); 45 | 46 | // Load the plugin that provides the "uglify" task. 47 | grunt.loadNpmTasks('grunt-contrib-uglify'); 48 | grunt.loadNpmTasks('grunt-regex-replace'); 49 | 50 | // Default task(s). 51 | grunt.registerTask('default', ['uglify', 'regex-replace']); 52 | }; -------------------------------------------------------------------------------- /docs/src/macros/5.std.md: -------------------------------------------------------------------------------- 1 | # Standard macros 2 | 3 | This section is a for checking what macros you have by default. These are the standard library macros. 4 | 5 | The headings below tell you when you have access to the macros. 6 | 7 | ## All the time 8 | 9 | | Macro | What it does | 10 | |---------|------------------------------------------------------------------------------------------------------------------------------------| 11 | | `:root` | Expands into required tags when building HTML files. Used for the root of a HBML project. Has the default attribute of `lang='en'` | 12 | 13 | ## In macro definitions 14 | 15 | | Macro | What it does | 16 | |----------------|-------------------------------------------------------------------------------------------------| 17 | | `:child` | Returns the next child element under a macro call or an empty string if none are left | 18 | | `:children` | Returns all the remaining children under a macro call or en empty string if none are left | 19 | | `:consume` | Returns the expanded element(s) under it if the required number of child elements are available | 20 | | `:consume-all` | Recursively runs `:consume` until it can't be run again | 21 | 22 | 23 | -------------------------------------------------------------------------------- /docs/src/macros/1.simple.md: -------------------------------------------------------------------------------- 1 | # Simple macros 2 | 3 | The simplest type of macro are replacement macros. These don't consume any elements, and are very simple to define. 4 | 5 | We always define macros the same way 6 | ```hbml 7 | --macro_name { 8 | // What the macro expands into 9 | } 10 | ``` 11 | 12 | You can also use the arrow operator if it expands into a single element such as if your macro will expand into a string or table. 13 | 14 | ## Calling macros 15 | 16 | To call a macro, all you need to do is type a colon (`:`) then the name of the macro you want to call. For example, in all HBML files, you'll see the `:root` macro which expands into required HTML attributes. 17 | 18 | ## Examples 19 | 20 | ### Page text in macros 21 | 22 | One use for simple macros like this is putting the text of a page into macros to make writing the page a bit cleaner. To do this you might have something like the following 23 | 24 | ```hbml 25 | --main_header > "Place your page header here" 26 | --main_body { 27 | p > "Some text here" 28 | img[src="photo.png"] 29 | } 30 | ``` 31 | 32 | ### Head tags 33 | 34 | Large projects often require using lots of external scripts (FontAwesome, JS libraries, font scripts, etc.). And for most of the files you use, there will be duplicated sections in the `head` section of your page. So, you could define a macro that includes all the repeated `head` sections and then call that in your pages 35 | 36 | ```hbml 37 | --head-base { 38 | script[src="script1.js"] 39 | link[rel='stylesheet' href='style.css'] 40 | } 41 | ``` 42 | -------------------------------------------------------------------------------- /src/reverse_web.js: -------------------------------------------------------------------------------- 1 | import {Token} from "./token.js"; 2 | import {CONFIG_DEFAULTS} from "./constants.js"; 3 | 4 | const convert = (e) => { 5 | if (e.nodeName === "#text") if (e.data.trim() === "") { return null } else return e.data 6 | if (e.nodeName === "#comment") return new Token("c t", {}, {value: e.nodeValue}, []) 7 | let children = [] 8 | e.childNodes.forEach((c) => { 9 | const r = convert(c) 10 | if (r !== null) children.push(r) 11 | }) 12 | let attrs = {} 13 | for (let i = 0; i < e.attributes.length; i++) { 14 | let value = e.attributes.item(i).nodeValue 15 | attrs[e.attributes.item(i).nodeName] = value === "" ? true : value 16 | } 17 | return new Token(e.localName, attrs, {}, children) 18 | } 19 | const lint_opts = {...CONFIG_DEFAULTS, 'lint.config.element_preference': "arrow", 'lint.config.remove_empty': true} 20 | 21 | export const snippet = (src) => { 22 | let children = [] 23 | new DOMParser().parseFromString(src, "text/html") 24 | .body.childNodes.forEach((c) => { 25 | const r = convert(c) 26 | if (r !== null) children.push(r) 27 | }) 28 | return children.map((t) => typeof t === "object" ? t.lint(0, false, lint_opts) : t).join("") 29 | } 30 | 31 | export const full = (src) => { 32 | let children = [] 33 | const res = new DOMParser().parseFromString(src, "text/html") 34 | const html = res.head.parentElement 35 | html.childNodes.forEach((c) => { 36 | const r = convert(c) 37 | if (r !== null) children.push(r) 38 | }) 39 | return new Token(":root", html.lang !== "en" ? {lang: html.lang} : {}, {}, children).lint(0, false, lint_opts) 40 | } 41 | -------------------------------------------------------------------------------- /src/commands/init.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import chalk from "chalk"; 3 | import npath from "path"; 4 | 5 | export const init_runner = (args) => { 6 | if (args["h"] !== undefined || args["help"] !== undefined) { 7 | init_help() 8 | } 9 | if (args["_"] !== undefined || args["_"].length > 1) { 10 | console.log(chalk.red(`Too many arguments provided! Expected at most 1! See 'hbml lint -h' for help`)) 11 | process.exit(1) 12 | } 13 | const path = args["_"] ? args["_"][0] : process.cwd() 14 | delete args["_"] 15 | if (Object.keys(args).length > 0) { 16 | console.log(chalk.red(`Too many flags provided! Expected none! See 'hbml lint -h' for help`)) 17 | process.exit(1) 18 | } 19 | init(path) 20 | } 21 | 22 | const init_default_hbml_config = `{ 23 | build: { 24 | src: ["hbml"] 25 | output: "html" 26 | } 27 | lint: { 28 | src: ["hbml"] 29 | } 30 | }` 31 | 32 | const init = (path) => { 33 | // make sure the path exists 34 | if (!fs.existsSync(path)) fs.mkdirSync(path, {recursive: true}) 35 | fs.writeFileSync(npath.join(path, "hbml.json"), init_default_hbml_config) 36 | if (!fs.existsSync(npath.join(path, "html"))) fs.mkdirSync(npath.join(path, "html")) 37 | if (!fs.existsSync(npath.join(path, "hbml"))) fs.mkdirSync(npath.join(path, "hbml")) 38 | } 39 | 40 | const init_help = () => { 41 | console.log(`Usage: hbml init ([project dir]) 42 | 43 | Initialises a HBML project in a directory 44 | 45 | ${chalk.bold(chalk.underline('Arguments:'))} 46 | [project dir] Optional directory to initialise the project in. Defaults to current directory`); 47 | process.exit() 48 | } 49 | -------------------------------------------------------------------------------- /docs/src/macros/2.children.md: -------------------------------------------------------------------------------- 1 | # Macro children 2 | 3 | Macros can do more than just turn into text. If macros have child elements, you can make use of them in the macro. 4 | 5 | When you define a macro, you have access to the `:child` and `:children` macros in the definition. The `:child` macro will get the next child element under the macro call or, if no child elements are left, an empty string. The `:children` macro will get all the remaining elements under the macro, or an empty string of there aren't anymore. 6 | 7 | The best way to show this is to see some examples, so that's what we've done 8 | 9 | ## Examples 10 | 11 | ### A list macro 12 | 13 | This example is a bit pointless considering how simple it is to define a list, but there we are 14 | 15 | ```hbml 16 | --list { 17 | ul { 18 | li > :child 19 | li > :child 20 | li > :child 21 | } 22 | } 23 | ``` 24 | 25 | ### Elements in a block 26 | 27 | If you need everything on your page to be nested inside several items to make sure it's formatted correctly, you might use a macro for that 28 | 29 | ```hbml 30 | --block > .class1 > .class2 > .class3 > :children 31 | ``` 32 | 33 | Notice how you don't have to surround `:children` in brackets. Because it looks like one element, we treat `:children` as one element when reading macro definitions. When it gets expanded, all the child elements will be grouped under `.class3` as you would expect. 34 | 35 | ## An important note 36 | 37 | When using `:child`, it may be the case that some child elements remain un-used. If and when this happens, a warning or error is raised about it depending on your build set-up. 38 | -------------------------------------------------------------------------------- /src/parser/macros.js: -------------------------------------------------------------------------------- 1 | import {Token, Macro} from "../token.js" 2 | 3 | /** 4 | * Get macro by name. Name does not include `:` before the macro call 5 | * @param self {Parser} Parser instance 6 | * @param name {string} Macro name (*without* colon prefix) 7 | * @return {{ok: ((Macro | function) | null), err: (null | string)}} 8 | */ 9 | export const get_macro = (self, name) => { 10 | let macro = undefined 11 | let index_to_check = self.macros.length 12 | while (index_to_check > 0) { 13 | index_to_check-- 14 | const possible = self.macros[index_to_check][name] 15 | if (possible) { 16 | macro = possible 17 | break 18 | } 19 | } 20 | if (macro === undefined) return {ok: null, err: `Unknown macro :${name}`} 21 | return {ok: macro, err: null} 22 | } 23 | 24 | /** 25 | * Parse a macro definition 26 | * @param self {Parser} Parser instance 27 | * @return {{ok: (Token[] | null), err: (null | string)}} 28 | */ 29 | export const parse_macro_def = (self) => { 30 | let {ok, err} = self.parse_inner("div", false, true) 31 | if (err) return {ok: null, err: err} 32 | // get required macro info 33 | const count_res = ok[0].count_child() 34 | let m_ok = count_res.tok 35 | const previous = self.get_macro(m_ok.type) 36 | if (previous.ok && previous.ok.void !== count_res.isVoid) return {ok: null, err: "Macro redefinitions must preserve voidness"} 37 | self.macros[self.macros.length - 1][m_ok.type] = new Macro(m_ok.children, count_res.isVoid, m_ok.type, {file: self.path, col: self.col, line: self.ln}) 38 | self.update_src() 39 | return self.isBuild ? {ok: null, err: null} : {ok: [new Token(`--${ok[0].type}`, ok[0].attributes, {...ok[0].additional, void: m_ok.children.length === 0}, m_ok.children)], err: null} 40 | } 41 | -------------------------------------------------------------------------------- /src/parser/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility functions for the {@link Parser parser} 3 | */ 4 | 5 | /** 6 | * Return the next character in the input string 7 | * @param self {Parser} Parser instance 8 | * @return {string} 9 | */ 10 | export const next = (self) => { return self.src[self.index] } 11 | 12 | /** 13 | * Return `true` if there are remaining characters in the input string 14 | * @param self {Parser} Parser instance 15 | * @return {boolean} 16 | */ 17 | export const remaining = (self) => { return self.index < self.src.length } 18 | 19 | /** 20 | * Move over spaces and tabs; then update the source string 21 | * @param self {Parser} Parser instance 22 | */ 23 | export const st = (self) => { 24 | while (self.remaining()) { 25 | if (self.next() === " " || self.next() === "\t") { 26 | self.col++ 27 | self.index++ 28 | } else { 29 | self.update_src() 30 | break 31 | } 32 | } 33 | if (!self.remaining()) self.update_src() 34 | } 35 | 36 | /** 37 | * Move over spaces, tabs, and newlines; then update the source string 38 | * @param self {Parser} Parser instance 39 | */ 40 | export const stn = (self) => { 41 | while (self.remaining()) { 42 | if (self.next() === " " || self.next() === "\t") { 43 | self.col++ 44 | self.index++ 45 | } else if (self.next() === "\n") { 46 | self.col = 1 47 | self.index++ 48 | self.ln++ 49 | } else { 50 | self.update_src() 51 | break 52 | } 53 | } 54 | if (!self.remaining()) self.update_src() 55 | } 56 | 57 | /** 58 | * Slices {@link Parser.src source} and resets {@link Parser.index index} 59 | * @param self {Parser} Parser instance 60 | */ 61 | export const update_src = (self) => { 62 | self.src = self.src.slice(self.index) 63 | self.index = 0 64 | } 65 | -------------------------------------------------------------------------------- /docs/src/imports.md: -------------------------------------------------------------------------------- 1 | # Simple import files 2 | 3 | HBML allows you to separate your projects into multiple files and import them later. We steal more syntax from CSS and use define importing files like this 4 | 5 | ```hbml 6 | @import path/to/file 7 | ``` 8 | 9 | The path you give can optionally end in `.hbml`. If it does not, the builder will add it automatically for you. The path can also be a URL if you want to use an external macro library. 10 | 11 | When you import a file, you only import the macros in the root of the file. These macros behave the same as if they were defined in the file you import them into. So, if you import a file with the macro `:example` in, you can use `:example` in your file just the same. 12 | 13 | ## Collisions 14 | 15 | When you import macros, the builder will check for collisions, meaning macros with the same name. If it finds any, it'll give you an error. Otherwise, it adds the macros into the current scope. You can, however, redefine macros manually without raising an error. This has been done for two reasons: 16 | 1. When importing a file you're not immediately presented with every macro in it and you might accidentally overwrite a macro you needed by importing one with the same name. If you intend to do this, you can easily get rid of the error with namespaces 17 | 2. Macro importing can sometimes include useful macros that work most of the time but will occasionally need changing. We recommend this change be done manually to avoid an error and that you can use the macros the same without the need for namespaces 18 | 19 | ## Namespaces 20 | 21 | When importing a file, you can optionally specify a namespace for all the macros in that file. If you do, any macro used from that file will need to be prefaced with the namespace of that file. 22 | 23 | For example, if `a.hbml` contains `:example`, you could import it as `@import a namespace-a` and then use `:example` from `a.hbml` as `:namespace-a:example`. 24 | -------------------------------------------------------------------------------- /src/bench.js: -------------------------------------------------------------------------------- 1 | import Benchmark from "benchmark" 2 | import {fullStringify as fsv2} from "./parser.js"; 3 | 4 | let suite = new Benchmark.Suite("HBML parser benchmark") 5 | 6 | const test_str = ` 7 | :root[lang="en-CA"] { 8 | head { 9 | meta[charset="UTF-8"] 10 | meta[ 11 | name="viewport" 12 | content="width=device-width, initial-scale=1" 13 | ] 14 | 15 | title > "Title of webpage" 16 | style > " 17 | body { 18 | font-family: 'Roboto', sans-serif; 19 | } 20 | 21 | .highlight { 22 | background-color: yellow; 23 | } 24 | " 25 | } 26 | body { 27 | 28 | /* Various inline headings */ 29 | 30 | section { 31 | h1 > "Heading 1" 32 | h2 > "Heading 2" 33 | h3 > "Heading 3" 34 | h4 > "Heading 4" 35 | h5 > "Heading 5" 36 | h6 > "Heading 6" 37 | } 38 | 39 | 40 | /* Divs and classes with multiple children */ 41 | 42 | div { 43 | p { "A paragraph inside a div" } 44 | p > "This also works for a single piece of text" 45 | } 46 | 47 | .layer-1 > .layer-2 > .layer-3 { 48 | "Text directly inside the .layer-3 div" 49 | 50 | br 51 | > "Some text inside an implicit div" 52 | div > "Other text inside an explicit div" 53 | br 54 | 55 | "Text inside the .layer-3 div after the child elements" 56 | } 57 | 58 | a[href="./"] > "Clickable link" 59 | 60 | /* Combination of inlining and multiple children */ 61 | 62 | p {"Text with a highlighted part " span.highlight>"right here" " followed by more text"} 63 | 64 | /* Testing different strings */ 65 | div { 66 | "Double quote string" br 'Single quote string' br \`Back tick string\` 67 | } 68 | /* some strange but technically allowable syntax */ 69 | a[at1="1" 70 | at2="2"] 71 | } 72 | } 73 | ` 74 | 75 | suite.add("current", () => { 76 | fsv2(test_str) 77 | }) 78 | .on("cycle", (event) => { 79 | console.log(String(event.target)); 80 | }) 81 | .run({"async": true}) 82 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import chalk from "chalk"; 4 | import minimist from "minimist" 5 | import fs from "fs"; 6 | import path from "path"; 7 | import {fileURLToPath} from 'url'; 8 | 9 | const version = JSON.parse(fs.readFileSync(path.join(fileURLToPath(import.meta.url), "..", "..", "package.json"), 'utf8')).version 10 | 11 | import {build_runner} from "./commands/build.js"; 12 | import {lint_runner} from "./commands/lint.js"; 13 | import {reverse_runner, reverse} from "./commands/reverse.js"; 14 | import {init_runner} from "./commands/init.js"; 15 | import {Parser, fullStringify} from "./parser/parser.js"; 16 | 17 | const project = process.argv[3] === "project" 18 | let args = minimist(process.argv.slice(project ? 4 : 3)) 19 | args = process.argv.length > (project ? 4 : 3) ? args : {} 20 | switch (process.argv[2]) { 21 | case undefined: 22 | console.log(chalk.red("No command given! Try running 'hbml -h' to see help messages")); 23 | process.exit(1) 24 | break 25 | case "help": 26 | case "-h": 27 | case "--help": 28 | help() 29 | break; 30 | case "-V": 31 | case "--version": 32 | console.log(`HBML version ${version}`) 33 | break; 34 | case "build": 35 | build_runner(args, project) 36 | break 37 | case "lint": 38 | lint_runner(args, project) 39 | break 40 | case "reverse": 41 | if (project) args["project"] = true 42 | reverse_runner(args) 43 | break 44 | case "init": 45 | if (project) args["project"] = true 46 | init_runner(args) 47 | break 48 | default: 49 | console.log(chalk.red(`Unknown command ${process.argv[2]}! Try running 'hbml -h' to see available commands`)); 50 | process.exit(1) 51 | } 52 | 53 | function help() { 54 | console.log(`${chalk.green(chalk.bold(chalk.underline('HBML')))} 55 | A command line interface for HBML 56 | 57 | Usage: hbml 58 | 59 | ${chalk.bold(chalk.underline('Commands:'))} 60 | build Build the project or file into HTML 61 | lint Lint the project or file 62 | init Initiate a new HBML project 63 | reverse Reverse HTML into HBML 64 | help Shows this message 65 | 66 | ${chalk.bold(chalk.underline('Options:'))} 67 | -V,--version 68 | Prints version information 69 | -h,--help 70 | Shows this message`) 71 | process.exit() 72 | } 73 | 74 | export {Parser, fullStringify, reverse} 75 | -------------------------------------------------------------------------------- /src/commands/fs_prelude.js: -------------------------------------------------------------------------------- 1 | import npath from "path"; 2 | import fs from "fs"; 3 | import chalk from "chalk"; 4 | 5 | /** 6 | * Expand paths and directories into absolute path objects (read and write) with a write path prefix for relative paths 7 | * and directories 8 | * @param paths {string[]} Path and directory array to expand 9 | * @param prefix {string} Write path prefix for relative paths 10 | * @param pre {string} File extension for read file to look for 11 | * @param post {string} File extension for write file to look for 12 | * @return {{ok: {read: string, write: string}[], err: {path: string, type: string}[]}} 13 | */ 14 | export const expand_paths = (paths, prefix, pre = ".hbml", post = ".html") => { 15 | let out = [] 16 | let err = [] 17 | const cwd = process.cwd() 18 | const inner = (path, underDir) => { 19 | if (fs.existsSync(path)) { 20 | const isAbs = npath.isAbsolute(path) 21 | if (fs.lstatSync(path).isDirectory()) { 22 | fs.readdirSync(path).forEach((p) => inner(npath.join(path, p), true)) 23 | } else { 24 | if (!path.endsWith(pre)) { if (!underDir) err.push({ 25 | path: npath.join(isAbs ? "" : cwd, path), 26 | type: "file type" 27 | }) } 28 | else { 29 | out.push({ 30 | read: npath.join(isAbs ? "" : cwd, path), 31 | write: npath.join(isAbs ? "" : npath.join(cwd, prefix), path.slice(0, -pre.length) + post) 32 | }) 33 | } 34 | } 35 | } else { 36 | err.push({path: path, type: "not found"}) 37 | } 38 | } 39 | paths.forEach((p) => inner(p, false)) 40 | return {ok: out, err: err} 41 | } 42 | 43 | /** 44 | * Log {@link expand_paths} error result 45 | * @param errs {{path: string, type: string}[]} Error array 46 | * @param allow {Object} Allow argument object 47 | */ 48 | export const log_ep_err = (errs, allow) => { 49 | let breaking = false 50 | errs.forEach(({path, type}) => { 51 | switch (type) { 52 | case "file type": 53 | console.log(chalk.yellow(`Cannot build non-HBML files into HTML (${path})`)); 54 | break 55 | case "not found": 56 | if (allow.not_found) { 57 | console.log(chalk.yellow(`Unable to read ${path}! Skipping over it`)) 58 | } else { 59 | console.log(chalk.red(`Unable to read file ${path}! Stopping!\nTo skip over missing files, pass the -s=not_found flag`)) 60 | breaking = true 61 | } 62 | } 63 | }) 64 | if (breaking) process.exit(1) 65 | } 66 | -------------------------------------------------------------------------------- /src/config_parse.js: -------------------------------------------------------------------------------- 1 | import Ajv from "ajv/dist/jtd.js"; 2 | import fs from "fs"; 3 | import path from "path" 4 | import {CONFIG_DEFAULTS} from "./constants.js"; 5 | 6 | /** 7 | * HBML.json config file schema 8 | * @type {Object} 9 | */ 10 | const schema = { 11 | "optionalProperties": { 12 | "lint": { 13 | "optionalProperties": { 14 | "src": {"elements": {"type": "string"}}, 15 | "output": {"type": "string"}, 16 | "config": { 17 | "optionalProperties": { 18 | "indent": { 19 | "properties": { 20 | "character": {"enum": [" ", "\t"]} 21 | }, 22 | "optionalProperties": { 23 | "count": {"type": "uint8"} 24 | } 25 | }, 26 | "pre_tag_space": {"type": "uint8"}, 27 | "post_tag_space": {"type": "uint8"}, 28 | "inline_same_line": {"type": "boolean"}, 29 | "keep_implicit": {"type": "boolean"}, 30 | "void_inline": {"type": "boolean"}, 31 | "element_preference": {"enum": ["bracket", "arrow", "preserve"]}, 32 | "remove_empty": {"type": "boolean"} 33 | } 34 | } 35 | } 36 | }, 37 | "build": { 38 | "optionalProperties": { 39 | "src": {"elements": {"type": "string"}}, 40 | "output": {"type": "string"}, 41 | "allow": { 42 | "optionalProperties": { 43 | "not_found": {"type": "boolean"}, 44 | "write": {"type": "boolean"}, 45 | "parse": {"type": "boolean"} 46 | } 47 | } 48 | } 49 | } 50 | } 51 | } 52 | 53 | /** 54 | * Flattens an object to make sure it's all single depth. 55 | * For example `flatten({a: 1, b: {c: 2, d: 3}}) === {'a': 1, 'b.c': 2, 'b.d': 3}` 56 | * @param a{Object} Object to flatten 57 | * @return {Object} Flattened object 58 | */ 59 | const flatten = (a) => Object.entries(a).reduce((q, [k, v]) => ({ 60 | ...q, 61 | ...(v && typeof v === 'object' && !Array.isArray(v) ? Object.entries(flatten(v)).reduce((p, [j, i]) => ({ 62 | ...p, 63 | [k + '.' + j]: i 64 | }), {}) : {[k]: v}) 65 | }), {}); 66 | 67 | /** 68 | * Get the config file as an object. Looks for the config file in the current working directory 69 | * @return {{ok: (Object | null), err: (String | null)}} 70 | */ 71 | export const getConfig = () => { 72 | const ajv = new Ajv() 73 | const parse = ajv.compileParser(schema) 74 | 75 | const conf_path = path.join(process.cwd(), "hbml.json") 76 | if (!fs.existsSync(conf_path)) return {ok: null, err: "Config file hbml.json not found in cwd"} 77 | const parsed = parse(fs.readFileSync(conf_path).toString()) 78 | if (parsed === undefined) return {ok: null, err: parse.message} 79 | return {ok: {...CONFIG_DEFAULTS, ...flatten(parsed)}, err: null} 80 | } 81 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Constants used by the parser 3 | */ 4 | 5 | /** 6 | * Characters that delimit text 7 | */ 8 | export const LITERAL_DELIMITERS = "\"'`" 9 | 10 | /** 11 | * Elements that are not allowed to have contents 12 | */ 13 | export const VOID_ELEMENTS = [ 14 | "area", 15 | "base", 16 | "br", 17 | "col", 18 | "embed", 19 | "hr", 20 | "img", 21 | "input", 22 | "link", 23 | "meta", 24 | "param", 25 | "source", 26 | "track", 27 | "wbr", 28 | "!DOCTYPE", 29 | ":child", ":children" 30 | ] 31 | 32 | /** 33 | * Elements that cause their children to become spans instead of divs when no element is specified 34 | */ 35 | export const INLINE_ELEMENTS = [ 36 | "abbr", 37 | "acronym", 38 | "audio", 39 | "b", 40 | "bdi", 41 | "bdo", 42 | "big", 43 | "br", 44 | "button", 45 | "canvas", 46 | "cite", 47 | "code", 48 | "data", 49 | "datalist", 50 | "del", 51 | "dfn", 52 | "em", 53 | "embed", 54 | "i", 55 | "iframe", 56 | "img", 57 | "input", 58 | "ins", 59 | "kbd", 60 | "label", 61 | "map", 62 | "mark", 63 | "meter", 64 | "noscript", 65 | "object", 66 | "output", 67 | "picture", 68 | "progress", 69 | "q", 70 | "ruby", 71 | "s", 72 | "samp", 73 | "script", 74 | "select", 75 | "slot", 76 | "small", 77 | "span", 78 | "strong", 79 | "sub", 80 | "sup", 81 | "svg", 82 | "template", 83 | "textarea", 84 | "time", 85 | "u", 86 | "tt", 87 | "var", 88 | "video", 89 | "wbr" 90 | ] 91 | 92 | /** 93 | * Attributes that can only have one value or appear once 94 | */ 95 | export const UNIQUE_ATTRS = [ 96 | "lang", 97 | "id" 98 | ] 99 | 100 | /** 101 | * Built-in macro names that aren't defined as standard macros 102 | * @type {string[]} 103 | */ 104 | export const BUILTIN_MACROS = [ 105 | ":child", ":children", ":consume", ":consume-all" 106 | ] 107 | 108 | /** 109 | * Default allow values 110 | * @type {{parse: boolean, not_found: boolean, write: boolean}} 111 | */ 112 | export const DEFAULT_ALLOW = {write: false, not_found: false, parse: false} 113 | 114 | /** 115 | * Default config values 116 | * @type {Object} 117 | */ 118 | export const CONFIG_DEFAULTS = { 119 | 'lint.src': ['/'], 120 | 'lint.output': '/', 121 | 'lint.allow.not_found': false, 122 | 'lint.allow.write': false, 123 | 'lint.allow.parse': false, 124 | 'lint.config.indent.character': '\t', 125 | 'lint.config.indent.count': 1, 126 | 'lint.config.pre_tag_space': 1, 127 | 'lint.config.post_tag_space': 1, 128 | 'lint.config.inline_same_line': true, 129 | 'lint.config.keep_implicit': true, 130 | 'lint.config.void_inline': true, 131 | 'lint.config.element_preference': "preserve", 132 | 'lint.config.remove_empty': false, 133 | 'build.src': ['/'], 134 | 'build.output': 'html', 135 | 'build.allow.not_found': false, 136 | 'build.allow.write': false, 137 | 'build.allow.parse': false 138 | } 139 | -------------------------------------------------------------------------------- /docs/theme/HBML.sublime-syntax: -------------------------------------------------------------------------------- 1 | %YAML 1.2 2 | --- 3 | # See http://www.sublimetext.com/docs/syntax.html 4 | file_extensions: 5 | - hbml 6 | scope: source.hbml 7 | verion: 2 8 | first_line_match: '(:root)|(@import[ ]+[^\s]+([ ]+[^\.#\[\{>}]"`\s]+)?)' 9 | variables: 10 | tag: "[^#\\. \\t\\n\"'`/>\\{\\[\\}\\]]+" 11 | contexts: 12 | main: 13 | - include: strings 14 | - match: "(@import) +([^ \\t\\n]+) +({{tag}})" 15 | captures: 16 | 1: keyword.control.import.hbml 17 | 2: support.constant.hbml 18 | 3: entity.name.namespace.hbml 19 | - match: '(@import) +([^\s]+)' 20 | captures: 21 | 1: keyword.control.import.hbml 22 | 2: support.constant.hbml 23 | - match: '(@insert) +([^\s]+)' 24 | captures: 25 | 1: keyword.control.import.hbml 26 | 2: support.constant.hbml 27 | 28 | - match: '//' 29 | scope: punctuation.definition.comment.hbml 30 | push: 31 | - meta_scope: comment.line.hbml 32 | - match: $ 33 | pop: true 34 | 35 | - match: '/\*' 36 | scope: punctuation.definition.comment.hbml 37 | push: 38 | - meta_scope: comment.block.hbml 39 | - match: '\*/' 40 | scope: punctuation.definition.comment.hbml 41 | pop: true 42 | 43 | - match: '\-\-{{tag}}' 44 | scope: entity.name.function.constructor.hbml 45 | 46 | - match: ':?{{tag}}' 47 | scope: entity.name.tag.hbml 48 | - include: id 49 | - include: classes 50 | - match: '\[' 51 | scope: punctuation.section.brackets.hbml 52 | push: attributes 53 | 54 | strings: 55 | - match: '"' 56 | scope: punctuation.definition.string.begin.hbml 57 | push: 58 | - meta_scope: string.quoted.hbml 59 | - match: '\\.' 60 | scope: constant.character.escape.hbml 61 | - match: '"' 62 | scope: punctuation.definition.string.end.hbml 63 | pop: true 64 | - match: "'" 65 | scope: punctuation.definition.string.begin.hbml 66 | push: 67 | - meta_scope: string.quoted.hbml 68 | - match: '\\.' 69 | scope: constant.character.escape.hbml 70 | - match: "'" 71 | scope: punctuation.definition.string.end.hbml 72 | pop: true 73 | - match: '`' 74 | scope: punctuation.definition.string.begin.hbml 75 | push: 76 | - meta_scope: string.quoted.hbml 77 | - match: '\\.' 78 | scope: constant.character.escape.hbml 79 | - match: '`' 80 | scope: punctuation.definition.string.end.hbml 81 | pop: true 82 | 83 | id: 84 | - match: '(#)({{tag}})' 85 | captures: 86 | 1: source.hbml 87 | 2: entity.name.tag.hbml 88 | push: classes 89 | - include: classes 90 | 91 | classes: 92 | - match: '(\.)({{tag}})' 93 | captures: 94 | 1: source.hbml 95 | 2: entity.name.tag.hbml 96 | - match: ' *\[' 97 | scope: punctuation.section.brackets.hbml 98 | pop: true 99 | push: attributes 100 | - match: ' ' 101 | pop: true 102 | 103 | attributes: 104 | - match: '\]' 105 | scope: punctuation.section.brackets.hbml 106 | pop: true 107 | - match: "[^\\s\\t\\n'\"`=]+" 108 | scope: entity.other.attribute-name 109 | push: 110 | - match: '\s*=\s*' 111 | pop: true 112 | push: 113 | - include: strings 114 | - match: '[ \]]' 115 | pop: true 116 | - match: '[^ \t\n\]]+' 117 | scope: string.hbml 118 | pop: true -------------------------------------------------------------------------------- /highlight/HBML.sublime-syntax: -------------------------------------------------------------------------------- 1 | %YAML 1.2 2 | --- 3 | # See http://www.sublimetext.com/docs/syntax.html 4 | file_extensions: 5 | - hbml 6 | scope: source.hbml 7 | verion: 2 8 | first_line_match: '(:root)|(@import[ ]+[^\s]+([ ]+[^\.#\[\{>}]"`\s]+)?)' 9 | variables: 10 | tag: "[^#\\. \\t\\n\"'`/>\\{\\[\\}\\]]+" 11 | contexts: 12 | main: 13 | - include: strings 14 | - match: "(@import) +([^ \\t\\n]+) +({{tag}})" 15 | captures: 16 | 1: keyword.control.import.hbml 17 | 2: support.constant.hbml 18 | 3: entity.name.namespace.hbml 19 | - match: '(@import) +([^\s]+)' 20 | captures: 21 | 1: keyword.control.import.hbml 22 | 2: support.constant.hbml 23 | - match: '(@insert) +([^\s]+)' 24 | captures: 25 | 1: keyword.control.import.hbml 26 | 2: support.constant.hbml 27 | 28 | - match: '//' 29 | scope: punctuation.definition.comment.hbml 30 | push: 31 | - meta_scope: comment.line.hbml 32 | - match: $ 33 | pop: true 34 | 35 | - match: '/\*' 36 | scope: punctuation.definition.comment.hbml 37 | push: 38 | - meta_scope: comment.block.hbml 39 | - match: '\*/' 40 | scope: punctuation.definition.comment.hbml 41 | pop: true 42 | 43 | - match: '\-\-{{tag}}' 44 | scope: entity.name.function.constructor.hbml 45 | 46 | - match: ':?{{tag}}' 47 | scope: entity.name.tag.hbml 48 | - include: id 49 | - include: classes 50 | - match: '\[' 51 | scope: punctuation.section.brackets.hbml 52 | push: attributes 53 | 54 | strings: 55 | - match: '"' 56 | scope: punctuation.definition.string.begin.hbml 57 | push: 58 | - meta_scope: string.quoted.hbml 59 | - match: '\\.' 60 | scope: constant.character.escape.hbml 61 | - match: '"' 62 | scope: punctuation.definition.string.end.hbml 63 | pop: true 64 | - match: "'" 65 | scope: punctuation.definition.string.begin.hbml 66 | push: 67 | - meta_scope: string.quoted.hbml 68 | - match: '\\.' 69 | scope: constant.character.escape.hbml 70 | - match: "'" 71 | scope: punctuation.definition.string.end.hbml 72 | pop: true 73 | - match: '`' 74 | scope: punctuation.definition.string.begin.hbml 75 | push: 76 | - meta_scope: string.quoted.hbml 77 | - match: '\\.' 78 | scope: constant.character.escape.hbml 79 | - match: '`' 80 | scope: punctuation.definition.string.end.hbml 81 | pop: true 82 | 83 | id: 84 | - match: '(#)({{tag}})' 85 | captures: 86 | 1: source.hbml 87 | 2: entity.name.tag.hbml 88 | push: classes 89 | - include: classes 90 | 91 | classes: 92 | - match: '(\.)({{tag}})' 93 | captures: 94 | 1: source.hbml 95 | 2: entity.name.tag.hbml 96 | - match: ' *\[' 97 | scope: punctuation.section.brackets.hbml 98 | pop: true 99 | push: attributes 100 | - match: ' ' 101 | pop: true 102 | 103 | attributes: 104 | - match: '\]' 105 | scope: punctuation.section.brackets.hbml 106 | pop: true 107 | - match: "[^\\s\\t\\n'\"`=]+" 108 | scope: entity.other.attribute-name 109 | push: 110 | - match: '\s*=\s*' 111 | pop: true 112 | push: 113 | - include: strings 114 | - match: '[ \]]' 115 | pop: true 116 | - match: '[^ \t\n\]]+' 117 | scope: string.hbml 118 | pop: true -------------------------------------------------------------------------------- /test/reverse.tests.mjs: -------------------------------------------------------------------------------- 1 | import {strictEqual as equal} from "assert" 2 | import {reverse} from "../src/commands/reverse.js"; 3 | 4 | const p = (src) => { 5 | const {ok, err} = reverse(src) 6 | return err ? err : ok.trim() 7 | } 8 | 9 | describe("Reverse tests", () => { 10 | it('Empty tags', () => { 11 | equal(p(`
`), `div`) 12 | equal(p(``), `span`) 13 | equal(p(``), `b`) 14 | }); 15 | 16 | it('Inline tags / Single child nesting chain', () => { 17 | equal(p(`Text`), `b > "Text"`) 18 | equal(p(`
Text
`), `div > "Text"`) 19 | equal(p(`
`), `div > span`) 20 | equal(p(`
Text
`), `div > span > "Text"`) 21 | }); 22 | 23 | it('Multi child nesting chain' , () => { 24 | equal(p(`
Text
Text
`), `b {\n\tdiv > "Text"\n\t"Text"\n}`) 25 | }); 26 | 27 | it("Root macro", () => { 28 | equal(p(``), `:root`) 29 | equal(p(``), `:root[lang="en-CA"]`) 30 | }) 31 | 32 | it("Large example", () => { 33 | equal(p(`Title of webpage

Heading 1

Heading 2

Heading 3

Heading 4

Heading 5
Heading 6

A paragraph inside a div

This also works for a single piece of text

Text directly inside the .layer-3 div
Some text inside an implicit div
Other text inside an explicit div

Text inside the .layer-3 div after the child elements
Clickable link

Text with a highlighted part right here followed by more text

Double quote string
Single quote string
Back tick string
`), 34 | `:root[lang="en-CA"] {\n\thead {\n\t\tmeta[charset="UTF-8"]\n\t\tmeta[name="viewport" content="width=device-width, initial-scale=1"]\n\t\ttitle > "Title of webpage"\n\t\tstyle > "body {\n\t\tfont-family: 'Roboto', sans-serif;\n\t}\n\n\t.highlight {\n\t\tbackground-color: yellow;\n\t}"\n\t}\n\tbody {\n\t\t/* Various inline headings */\n\t\tsection {\n\t\t\th1 > "Heading 1"\n\t\t\th2 > "Heading 2"\n\t\t\th3 > "Heading 3"\n\t\t\th4 > "Heading 4"\n\t\t\th5 > "Heading 5"\n\t\t\th6 > "Heading 6"\n\t\t}\n\t\t/* Divs and classes with multiple children */\n\t\tdiv {\n\t\t\tp > "A paragraph inside a div"\n\t\t\tp > "This also works for a single piece of text"\n\t\t}\n\t\tdiv.layer-1 > div.layer-2 > div.layer-3 {\n\t\t\t"Text directly inside the .layer-3 div"\n\t\t\tbr\n\t\t\tdiv > "Some text inside an implicit div"\n\t\t\tdiv > "Other text inside an explicit div"\n\t\t\tbr\n\t\t\t"Text inside the .layer-3 div after the child elements"\n\t\t}\n\t\ta[href="./"] > "Clickable link"\n\t\t/* Combination of inlining and multiple children */\n\t\tp {\n\t\t\t"Text with a highlighted part "\n\t\t\tspan.highlight > "right here"\n\t\t\t" followed by more text"\n\t\t}\n\t\t/* Testing different strings */\n\t\tdiv {\n\t\t\t"Double quote string"\n\t\t\tbr\n\t\t\t"Single quote string"\n\t\t\tbr\n\t\t\t"Back tick string"\n\t\t}\n\t\t/* some strange but technically allowable syntax */\n\t\ta[at1="1" at2="2"]\n\t}\n}`) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /docs/src/get_start/first_file.md: -------------------------------------------------------------------------------- 1 | # Your first HBML file 2 | 3 | To start off, let's make a file called `hello_world.hbml`. In this file we'll start off with a default template 4 | ```hbml 5 | :root { 6 | head { 7 | title > "Hello from HBML!" 8 | } 9 | body { 10 | /* Your first HBML file */ 11 | h1 > "Hello from a HBML file!" 12 | } 13 | } 14 | ``` 15 | 16 | But what does this mean? 17 | 18 | You may notice that the structure is pretty similar to a simple HTML file which is intentional. HBML is built on HTML with brackets not tags. In this sense, writing in HBML is functionally no different to writing in HTML, but hopefully a nicer experience. 19 | 20 | But what are the `>` characters? If you have something with one element inside it, you can use the arrow operator. This is the same as putting the element after the arrow in brackets, but easier to read. 21 | 22 | You'll also have noticed the comment line just before the `h1`. Comments in HBML are indicated by a starting `/*` and closing `*/` which can be on different lines. We discuss comments more when we talk about building HBML. 23 | 24 | ## IDs 25 | 26 | An important part of HTML is tag IDs. In HBML we use the `#` character to indicate an ID. For example `h1#mainHeader` would be the same as `

` in HTML. 27 | 28 | IDs are placed after a tag type and are optional 29 | 30 | ## Classes 31 | 32 | Classes are also a very important part of any webpage. In the style of CSS selectors, to specify a class on a tag we use the `.` character. For example `div.header` would be equivalent to `
` in HTML. 33 | 34 | Classes are placed after IDs and are optional 35 | 36 | ## Attributes 37 | 38 | Tag attributes are placed inside square brackets and use the standard style. For example `meta[charset="UTF-8" someOtherAttribute]` would be the same as `` in HTML. 39 | 40 | ### Unique attributes 41 | 42 | Any attribute can be a _unique attribute_. This means that if duplicates of the attribute are found, then the last one is used. If a duplicated attribute is not unique, then the values are placed together with a space separating them. For example, `lang` is a unique attribute so `html[lang="en" lang="de"]` would turn into ``; but `class` is not a unique attribute so `p.lead[class="bold"]` or `p[class="bold" class="lead"]` would both become `

`. 43 | 44 | In the future, you'll be able to specify if you want to add or remove unique attributes in a project in the project config file. The default unique attributes are `lang` and `id`. 45 | 46 | Attributes are placed after classes and are optional. 47 | 48 | ## Implicit tags 49 | 50 | One of the large strengths of HBML is it's implicit tags. As the name suggests, these are implied tags and allow your code to look cleaner and be more readable. 51 | 52 | Implicit tags are either `div`s or `span`s depending on the parent element type and default to `div`s. To give you an idea of how to use implicit tags, all of the following result in an implicit tag as the root tag: 53 | - ` > "This text is under an implicit tag"`\ 54 | Nothing before a curly bracket or arrow will create an implicit tag 55 | - `#tagID { "An implicit tag with an ID" }`\ 56 | Tag types are optional, but that doesn't mean you can't put an ID, classes, or attributes 57 | - `.bold[href='some_page.html']` 58 | 59 | ## Strings 60 | 61 | Strings in HBML can use one of three delimiters `'`, `"`, or \`. If you use the `'` or `"` delimiter, newlines won't be included in the resulting string, but using \` will include newlines. 62 | 63 | Strings are also put through a substitution process to turn characters that might cause problems in HTML into their HTML codes such as `<` being replaced with &lt 64 | -------------------------------------------------------------------------------- /src/parser/parser.js: -------------------------------------------------------------------------------- 1 | import {DEFAULT_MACROS} from "../token.js"; 2 | import {Error} from "../error.js"; 3 | import {next, remaining, st, stn, update_src} from "./util.js"; 4 | import {convertReservedChar, parse_inner, parseAttrs, parseComment, parseStr, parseTag} from "./main.js"; 5 | import {handleImport, import_parse, handleInsert} from "./at-commands.js"; 6 | import {get_macro, parse_macro_def} from "./macros.js"; 7 | 8 | /** 9 | * # Parser Class 10 | * This class handles tokenisation from HBML to an AST which can then be turned into HTML. 11 | */ 12 | export class Parser { 13 | /** Dynamic source string. Is altered during parsing 14 | * @type {string} */ 15 | src 16 | /** Source file path. Used for error handling 17 | * @type {string} */ 18 | path 19 | /** Current line in input 20 | * @type {number} */ 21 | ln 22 | /** Current column in input 23 | * @type {number} */ 24 | col 25 | /** Current index character in the {@link this.src input string} 26 | * @type {number} */ 27 | index 28 | /** Boolean that's only `true` if the parser is being used for building HTML 29 | * @type {boolean} */ 30 | isBuild 31 | /** Macro array of all currently in-scope macros 32 | * @type {Array>} */ 33 | macros 34 | 35 | constructor(src, path, build = true) { 36 | this.src = src 37 | this.path = path 38 | this.ln = 1 39 | this.col = 1 40 | this.index = 0 41 | this.isBuild = build 42 | this.macros = [Object.assign({}, DEFAULT_MACROS)] 43 | } 44 | 45 | /** Alias for `new Parser`. Required to avoid circular imports */ 46 | new(src, path, build = true) { return new Parser(src, path, build) } 47 | 48 | // util functions 49 | next = () => { return next(this) } 50 | remaining = () => { return remaining(this) } 51 | st = () => { return st(this) } 52 | stn = () => { return stn(this) } 53 | update_src = () => { return update_src(this) } 54 | 55 | // parser functions 56 | /** 57 | * ## Parser function 58 | * Returns an AST of {@link Token tokens} and strings or an error 59 | * @return {{ok: (Array | null), err: (Error | null)}} 60 | */ 61 | parse = () => { 62 | let tokens = [] 63 | 64 | while (this.remaining()) { 65 | let previous = this.src 66 | const {ok, err} = this.parse_inner("div", true, false) 67 | if (err) return {ok: null, err: new Error(err, this.path, this.ln, this.col)} 68 | if (previous === this.src) return { 69 | ok: null, 70 | err: new Error("Unable to parse remaining text", this.path, this.ln, this.col) 71 | } 72 | if (ok !== null) tokens = [...tokens, ...ok] 73 | } 74 | 75 | return {ok: tokens, err: null} 76 | } 77 | parse_inner = (default_tag, str_replace, under_macro_def) => { return parse_inner(this, default_tag, str_replace, under_macro_def)} 78 | import_parse = (prefix) => { return import_parse(this, prefix) } 79 | handleImport = () => { return handleImport(this) } 80 | handleInsert = () => { return handleInsert(this) } 81 | convertReservedChar = (input) => { return convertReservedChar(this, input) } 82 | parseComment = () => { return parseComment(this) } 83 | parseStr = (delim, convert) => { return parseStr(this, delim, convert) } 84 | parseAttrs = (unique_replace, unique_position, initial) => { return parseAttrs(this, unique_replace, unique_position, initial) } 85 | parseTag = (default_tag) => { return parseTag(this, default_tag) } 86 | 87 | // macros 88 | get_macro = (name) => { return get_macro(this, name) } 89 | parse_macro_def = () => { return parse_macro_def(this) } 90 | } 91 | 92 | /** 93 | * Alias for tokenising and then stringifying input text 94 | * @param src {string} Input string 95 | * @param path {string} Input file path. User for error info 96 | * @return {{ok: (string | null), err: (Object | null )}} 97 | */ 98 | export const fullStringify = function (src, path) { 99 | const {ok, err} = new Parser(src, path, true).parse() 100 | if (err) { 101 | return {ok: null, err: err} 102 | } 103 | return {ok: ok.map((t) => t.toString()).join(""), err: null} 104 | } 105 | -------------------------------------------------------------------------------- /usage.md: -------------------------------------------------------------------------------- 1 | # CLI Usage 2 | 3 | ## Install 4 | 5 | ```shell 6 | npm install -g hbml 7 | ``` 8 | 9 | ## Commands 10 | 11 | ```shell 12 | hbml [command] 13 | ``` 14 | 15 | All commands including `hbml` can have the `-h` or `--help` flags passed to print help information. 16 | 17 | The `hbml` command also accepts the `-V` or `--version` flag which will result in version information being printed. It also accepts the `help` command as an alias for `hbml -h`. 18 | 19 | ### Build 20 | 21 | ```shell 22 | hbml build {project}|([source]... [-o path] [-s=skip_opt]...) 23 | ``` 24 | 25 | This takes in paths to directories or files and build the hbml files into html files. If a directory if given as part of the specified source paths, every hbml file under that directory is built. If no source files are given, the current working directory is used. The `-o` flag is used to specify file output path and defaults to the current working directory. 26 | 27 | If `project` is specified after `build`, it looks for the `hbml.json` [config file](#config) in the current working directory and uses the arguments from that. 28 | 29 | The `-s` flag is for skipping errors. The available values for this are: 30 | 31 | | Value | Description | 32 | |-------------|----------------------------------------------------------------| 33 | | `not_found` | Will skip over file not found errors with a warning | 34 | | `write` | Will skip over errors in writing output html with a warning | 35 | | `parse` | Will skip over errors in parsing HBML into HTML with a warning | 36 | 37 | The final output of any file is `$PWD//` if the given file is relative, otherwise it will write to `/.html` 38 | 39 | ### Lint 40 | 41 | ```shell 42 | hbml lint {project}|([source]... [-c file] [-o path] [-p] [-s=skip_opt]...) 43 | ``` 44 | 45 | Lints all specified files. Source files are specified the same way as for the [`build`](#build) command with the same assumptions if none are given. 46 | 47 | If `project` is specified after `lint`, it looks for the `hbml.json` [config file](#config) in the current working directory and uses the arguments from that. 48 | 49 | The `-c` flag is for specifying a config file to use (see the [config section](#config)). If none is given, it will look for a `hbml.json` file in the current directory for config. If this is not found, it will assume the default configuration specified in the [config section](#config). 50 | 51 | The `-o` flag is to specify a custom output instead of overwriting the existing files. The `-p` flag is equivalent to `-o linted` but takes lower precedent i.e. if both `-o` and `-p` are specified where `-o` has a usable value, the given value is used insteadof `linted`. Specifying both flags will result in a warning being emitted. 52 | 53 | The `-s` flag is the same for `hbml build` 54 | 55 | ## Config 56 | 57 | Place a `hbml.json` file in your project root directory with the following format. None of the values are required. The comments represent the required type if the value is given, and the value in brackets represents the default value 58 | 59 | ```js 60 | { 61 | "lint": { 62 | "src": ["list", "of", "paths"], // string[] (["."]) 63 | "output": "output prefix", // string (".") 64 | "config": { 65 | "indent": { 66 | "character": "space or tab", // " " or "\t" ("\t") 67 | "count": "number of characters per indent" // number (1) 68 | }, 69 | "pre_tag_space": "spaces before bracket or >", // number (1) 70 | "post_tag_space": "spaces after >", // number (1) 71 | "inline_same_line": "make > elements all on the same line", // boolean (true) 72 | "keep_implicit": "keeps implicit 'div's and 'span's", // boolean (true) 73 | "void_inline": "make void elements inline" // boolean (true) 74 | } 75 | }, 76 | "build": { 77 | "src": ["list", "of", "paths"], // string[] (["."]) 78 | "output": "output prefix", // string (".") 79 | "allow": { 80 | "not_found": "ignore file not found errors", // boolean (false) 81 | "write": "ignore file write errors", // boolean (false) 82 | "parse": "ignore file parsing errors" // boolean (false) 83 | } 84 | } 85 | } 86 | ``` 87 | -------------------------------------------------------------------------------- /docs/src/macros/3.conditionals.md: -------------------------------------------------------------------------------- 1 | # Conditionals 2 | 3 | In the previous section, we gave an example of a simple list macro. However, that would only work for three child elements. In this section, we'll introduce the `:consume` and `:consume-all` macros that make macros much more powerful. 4 | 5 | The `:consume` macro checks how many child elements are required for the section under it, and will only return that if there are the right number of children left. 6 | 7 | For example, using our list example again, we might want to make sure that for each `:child`, there actually is one. So we could have 8 | 9 | ```hbml 10 | --list > ul { 11 | :consume > li > :child 12 | :consume > li > :child 13 | :consume > li > :child 14 | } 15 | ``` 16 | 17 | This will mean that if we only gave two elements, the list would only have two elements in it now because the last `:consume` would see that it required one child, and that none were left, so it wouldn't expand into anything. 18 | 19 | But, we can do better. What if we gave four elements. The list would still only have three elements, and we'd have one element left un-used. This is where `:consume-all` comes in. `:consume-all` acts just like `:consume` but will repeat until there aren't enough child elements left. 20 | 21 | This means we can make our list macro much better 22 | 23 | ```hbml 24 | --better-list > ul > :consume-all > li > :child 25 | ``` 26 | 27 | ## The `:unwrap` macro 28 | 29 | The last macro is the `:unwrap` macro. This macro takes some elements, and "unwraps" them. Comments and strings are left alone, but elements have their children returned. 30 | 31 | ```hbml 32 | :unwrap { 33 | { "Text" } 34 | "More text" 35 | .class { p > "A paragraph" } 36 | } 37 | // Equivalent 38 | "Text" 39 | "More text" 40 | p > "A paragraph" 41 | ``` 42 | 43 | ## Examples 44 | 45 | ### A two columned table 46 | 47 | When writing documentation, you may find yourself making a lot of tables with two columns in them. So, you might define a macro similar to this one 48 | 49 | ```hbml 50 | --2table > table { 51 | thead { tr { td > :child td > :child } } 52 | tbody > :consume-all { 53 | tr { td > :child td > :child } 54 | } 55 | } 56 | ``` 57 | 58 | If we then consider equivalents, we see what happens if `:consume-all` doesn't use every element 59 | 60 | ```hbml 61 | // macro 62 | :2table { "Heading 1" "Heading 2" "Row 1" "Value 1" } 63 | // equivalent 64 | table { 65 | thead { tr { td > "Heading 1" td > "Heading 2" } } 66 | tbody > tr { td > "Row 1" td > "Value 1" } 67 | } 68 | ``` 69 | 70 | When we put the right number of elements under the macro call, it works as expected. But, if we add one more element under our macro 71 | 72 | ```hbml 73 | // macro 74 | :2table { "Heading 1" "Heading 2" "Row 1" "Value 1", "Row 2" } 75 | // equivalent 76 | table { 77 | thead { tr { td > "Heading 1" td > "Heading 2" } } 78 | tbody > tr { td > "Row 1" td > "Value 1" } 79 | } 80 | ``` 81 | 82 | We see that the new value is ignored because the `:consume-all` section in the macro required two elements but only got 1. If we added an extra value, the macro would add in a new row to the table 83 | 84 | 85 | ### An interesting table macro 86 | 87 | If we wanted to make our table macro nice and generic, we might make something like this 88 | 89 | ```hbml 90 | --table-row > tr > :consume-all > td > :child 91 | --table > table > :consume-all > :table-row > :unwrap > :child 92 | ``` 93 | 94 | This might look a little daunting, but we can work through it; especially if we specify what kind of inputs the macro expects. 95 | 96 | Let's start with `:table-row`. This macro expects to be given a load of elements, each of which will be put into a `td` rag and all of them under a `tr` tag. 97 | 98 | Now, `:table`. This expects to be given several elements each of which with child elements (see the example usage below). For each given element, it'll expand them (returning the elements children), and pass that to `:table-row` to generate a table row. This is then repeated for each given element, and then all of it gets wrapped up in a `table`. 99 | 100 | ```hbml 101 | :table { 102 | { "Heading 1" "Heading 2" } 103 | { "Row 1" "Value 1" } 104 | { "Row 2" "Value 2" } 105 | } 106 | // Equivalent 107 | table { 108 | tr { td > "Heading 1" td > "Heading 2" } 109 | tr { td > "Row 1" td > "Value 1" } 110 | tr { td > "Row 2" td > "Value 2" } 111 | } 112 | ``` 113 | -------------------------------------------------------------------------------- /highlight/mode-hbml.js: -------------------------------------------------------------------------------- 1 | define("ace/mode/hbml_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/text_highlight_rules"],function(e,t,n){"use strict";var r=e("../lib/oop"),i=e("./text_highlight_rules").TextHighlightRules,s=function(e){this.$rules={start:[{token:["keyword.other","text","support.constant"],regex:"(@import)( +)([^ \n]+)"},{token:["text","meta.tag.tag-name"],regex:"--([a-zA-Z0-9-_])+"},{token:"meta.tag.tag-name",regex:":?[a-zA-Z0-9-_]+"},{token:["text","meta.tag.tag-name"],regex:"(#)([a-zA-Z0-9-_]+)"},{token:["text","meta.tag.tag-name"],regex:"(\\.)([a-zA-Z0-9-_]+)"},{token:"text",regex:"\\[",next:"attr"},{token:"comment.start.hbml",regex:"\\/\\*",next:"comment_inline"},{token:"comment.hbml",regex:"//",next:"comment_full"},{include:"string"},{defaultToken:"text"}],comment_inline:[{token:"comment.end",regex:"\\*\\/",next:"start"},{defaultToken:"comment.block"}],comment_full:[{token:"comment",regex:"^",next:"start"},{defaultToken:"comment.line"}],attr:[{token:["entity.other.attribute-name","text","string"],regex:"([a-zA-Z0-9-_]+)(\\s*=\\s*)('[^']+')"},{token:["entity.other.attribute-name","text","string"],regex:'([a-zA-Z0-9-_]+)(\\s*=\\s*)("[^"]+")'},{token:["entity.other.attribute-name","text","string"],regex:"([a-zA-Z0-9-_]+)(\\s*=\\s*)(`[^`]+`)"},{token:["entity.other.attribute-name","text","string"],regex:"([a-zA-Z0-9-_]+)(\\s*=\\s*)([^\\s\\]]+)"},{token:"text",regex:"\\]",next:"start"},{token:"entity.other.attribute-name",regex:"[a-zA-Z0-9-_]+"},{defaultToken:"text"}],string:[{token:"string",regex:"'",push:[{token:"string",regex:"'",next:"pop"},{defaultToken:"string"}]},{token:"string",regex:'"',push:[{token:"string",regex:'"',next:"pop"},{defaultToken:"string"}]},{token:"string",regex:"`",push:[{token:"string",regex:"`",next:"pop"},{defaultToken:"string"}]}]},this.normalizeRules()};r.inherits(s,i),t.HBMLHighlightRules=s}),define("ace/mode/folding/cstyle",["require","exports","module","ace/lib/oop","ace/range","ace/mode/folding/fold_mode"],function(e,t,n){"use strict";var r=e("../../lib/oop"),i=e("../../range").Range,s=e("./fold_mode").FoldMode,o=t.FoldMode=function(e){e&&(this.foldingStartMarker=new RegExp(this.foldingStartMarker.source.replace(/\|[^|]*?$/,"|"+e.start)),this.foldingStopMarker=new RegExp(this.foldingStopMarker.source.replace(/\|[^|]*?$/,"|"+e.end)))};r.inherits(o,s),function(){this.foldingStartMarker=/([\{\[\(])[^\}\]\)]*$|^\s*(\/\*)/,this.foldingStopMarker=/^[^\[\{\(]*([\}\]\)])|^[\s\*]*(\*\/)/,this.singleLineBlockCommentRe=/^\s*(\/\*).*\*\/\s*$/,this.tripleStarBlockCommentRe=/^\s*(\/\*\*\*).*\*\/\s*$/,this.startRegionRe=/^\s*(\/\*|\/\/)#?region\b/,this._getFoldWidgetBase=this.getFoldWidget,this.getFoldWidget=function(e,t,n){var r=e.getLine(n);if(this.singleLineBlockCommentRe.test(r)&&!this.startRegionRe.test(r)&&!this.tripleStarBlockCommentRe.test(r))return"";var i=this._getFoldWidgetBase(e,t,n);return!i&&this.startRegionRe.test(r)?"start":i},this.getFoldWidgetRange=function(e,t,n,r){var i=e.getLine(n);if(this.startRegionRe.test(i))return this.getCommentRegionBlock(e,i,n);var s=i.match(this.foldingStartMarker);if(s){var o=s.index;if(s[1])return this.openingBracketBlock(e,s[1],n,o);var u=e.getCommentFoldRange(n,o+s[0].length,1);return u&&!u.isMultiLine()&&(r?u=this.getSectionRange(e,n):t!="all"&&(u=null)),u}if(t==="markbegin")return;var s=i.match(this.foldingStopMarker);if(s){var o=s.index+s[0].length;return s[1]?this.closingBracketBlock(e,s[1],n,o):e.getCommentFoldRange(n,o,-1)}},this.getSectionRange=function(e,t){var n=e.getLine(t),r=n.search(/\S/),s=t,o=n.length;t+=1;var u=t,a=e.getLength();while(++tf)break;var l=this.getFoldWidgetRange(e,"all",t);if(l){if(l.start.row<=s)break;if(l.isMultiLine())t=l.end.row;else if(r==f)break}u=t}return new i(s,o,u,e.getLine(u).length)},this.getCommentRegionBlock=function(e,t,n){var r=t.search(/\s*$/),s=e.getLength(),o=n,u=/^\s*(?:\/\*|\/\/|--)#?(end)?region\b/,a=1;while(++no)return new i(o,r,l,t.length)}}.call(o.prototype)}),define("ace/mode/hbml",["require","exports","module","ace/mode/hbml_highlight_rules","ace/mode/folding/cstyle","ace/mode/text","ace/lib/oop"],function(e,t,n){"use strict";function u(){this.HighlightRules=r,this.foldingRules=new i}var r=e("./hbml_highlight_rules").HBMLHighlightRules,i=e("./folding/cstyle").FoldMode,s=e("./text").Mode,o=e("../lib/oop");o.inherits(u,s),function(){this.lineCommentStart="//",this.blockComment={start:"/*",end:"*/"},this.$quotes={'"':'"',"'":'"',"`":"`"},this.$id="ace/mode/hbml"}.call(u.prototype),t.Mode=u}); (function() { 2 | window.require(["ace/mode/hbml"], function(m) { 3 | if (typeof module == "object" && typeof exports == "object" && module) { 4 | module.exports = m; 5 | } 6 | }); 7 | })(); 8 | -------------------------------------------------------------------------------- /src/commands/reverse.js: -------------------------------------------------------------------------------- 1 | import {parser as HTMLparser} from 'posthtml-parser' 2 | import fs from "fs"; 3 | import {DEFAULT_ALLOW, VOID_ELEMENTS, CONFIG_DEFAULTS} from "../constants.js"; 4 | import {Token} from "../token.js"; 5 | import chalk from "chalk"; 6 | import {expand_paths, log_ep_err} from "./fs_prelude.js"; 7 | import npath from "path"; 8 | 9 | export const reverse_runner = (args) => { 10 | if (args["h"] !== undefined || args["help"] !== undefined) { 11 | reverse_help() 12 | } 13 | if (args["o"] !== undefined && typeof args["o"] !== "string") { 14 | console.log(chalk.red(`Too many output prefixes specified! Expected at most 1! See 'hbml build -h' for help`)) 15 | process.exit(1) 16 | } 17 | const paths_res = expand_paths( 18 | args["_"] ? args["_"] : [], 19 | args["o"] ? args["o"] : "", ".html", ".hbml") 20 | if (paths_res.err.length > 0) log_ep_err(paths_res.err, DEFAULT_ALLOW) 21 | const paths = paths_res.ok 22 | delete args["_"] 23 | delete args["o"] 24 | if (args["v"] !== undefined && typeof args["v"] !== "boolean") { 25 | console.log(chalk.red(`Verbose flag does not take any values! See 'hbml build -h' for help`)) 26 | process.exit(1) 27 | } 28 | const verbose = args["v"] ? args["v"] : false 29 | delete args["v"] 30 | if (Object.keys(args).length !== 0) { 31 | console.log(chalk.red(`Unexpected arguments ${Object.keys(args).join(", ")}! See 'hbml build -h' for help`)) 32 | process.exit(1) 33 | } 34 | paths.forEach((p) => { 35 | if (verbose) process.stdout.write(`[ ] ${p.read}\r`) 36 | const {ok, err} = reverse(fs.readFileSync(p.read).toString()) 37 | if (err !== null) { 38 | console.log(chalk.red(verbose ? `[✖] ${p.read} error: ${err}` : `Error in ${p.read}: ${err}`)) 39 | } else { 40 | if (!fs.existsSync(npath.dirname(p.write))) fs.mkdirSync(npath.dirname(p.write), {recursive: true}) 41 | fs.writeFileSync(p.write, ok) 42 | if (verbose) console.log(chalk.green(`[✓] ${p.read}\r`)) 43 | } 44 | }) 45 | } 46 | 47 | const reverse_help = () => { 48 | console.log(`Usage: hbml reverse [files]... 49 | 50 | Converts HTML files into HBML 51 | 52 | ${chalk.bold(chalk.underline('Arguments:'))} 53 | [files] HTML files to turn into HBML. These can be files or directories which will be recursively traversed 54 | 55 | ${chalk.bold(chalk.underline('Options:'))} 56 | -o 57 | Output path prefix. Prefixes relative paths only 58 | -v 59 | Verbose output. Prints the name of the current file being converted followed by the conversion result 60 | -h,--help 61 | Shows this message`); 62 | process.exit() 63 | } 64 | 65 | /** 66 | * HTML to HBML converter. Takes a path to the file and parses the HTML then converts it to HBML. Write the output to 67 | * a specified output path 68 | * @param src {string} HTML to convert into HBML 69 | * @return {{ok: (string|null), err: (null|string)}} 70 | */ 71 | export const reverse = (src) => { 72 | if (src.trim() === "") return {ok: "", err: null} 73 | let tokens = HTMLparser(src) 74 | if (tokens.length === 0) return {ok: null, err: `Unable to parse input`} 75 | const convert = (t) => { 76 | if (typeof t === "string") { 77 | if (//.test(t)) return new Token("c t", {}, {value: //.exec(t)[1]}, []) 78 | return t 79 | } 80 | return new Token( 81 | t.tag, t.attrs ? t.attrs : {}, {void: VOID_ELEMENTS.includes(t.tag)}, 82 | t.content ? t.content.filter((t) => typeof t === "string" ? t.trim() !== "" : true).map((c) => convert(c)) : [] 83 | ) 84 | } 85 | tokens = tokens.filter((t) => typeof t === "string" ? t.trim() !== "" : true).map((t) => convert(t)) 86 | const doctype_index = tokens.findIndex((t) => typeof t === "string" && //i.test(t)) 87 | if (doctype_index >= 0) { 88 | let n = 1 89 | let found = false 90 | while (doctype_index + n < tokens.length) { 91 | if (typeof tokens[doctype_index + n] === "object") { 92 | if (tokens[doctype_index + n].type === "html") { 93 | found = true 94 | break 95 | } else if (tokens[doctype_index + n].type === "c t") n++ 96 | else break 97 | } else break 98 | } 99 | if (found) { 100 | let new_attrs = Object.assign({}, tokens[doctype_index + n].attributes) 101 | if (new_attrs["lang"] === "en") delete new_attrs["lang"] 102 | const clone = (t) => { 103 | if (typeof t === "string") return t 104 | return new Token( 105 | t.type, Object.assign({}, t.attributes), Object.assign({}, t.additional), 106 | t.children.map((t) => clone(t)) 107 | ) 108 | } 109 | tokens = [ 110 | ...tokens.slice(0, doctype_index), ...tokens.slice(doctype_index + 1, doctype_index + n), 111 | new Token( 112 | ":root", new_attrs, Object.assign({}, tokens[doctype_index + n].additional), 113 | tokens[doctype_index + n].children.map((t) => clone(t)) 114 | ), ...tokens.slice(doctype_index + n + 1) 115 | ] 116 | } 117 | } 118 | const lint_opts = {...CONFIG_DEFAULTS, 'lint.config.element_preference': "arrow", 'lint.config.remove_empty': true} 119 | return {ok: tokens.map((t) => typeof t === "object" ? t.lint(0, false, lint_opts) : t).join(""), err: null} 120 | } 121 | -------------------------------------------------------------------------------- /src/parser/at-commands.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Commands starting with `@` 3 | */ 4 | 5 | import npath from "path" 6 | import fs from "fs" 7 | import {Token, Macro, DEFAULT_MACROS} from "../token.js" 8 | import {BUILTIN_MACROS} from "../constants.js" 9 | 10 | /** 11 | * Parse and handle any `@import` statements 12 | * @param self {Parser} Parser instance 13 | */ 14 | export const handleImport = (self) => { 15 | self.index += 7 16 | self.st() 17 | let path = "" 18 | while (self.remaining()) { 19 | if (" \t\n".includes(self.next())) break 20 | path += self.next() 21 | self.index++ 22 | self.col++ 23 | } 24 | let prefix = "" 25 | if (self.remaining() && self.next() !== "\n") { 26 | self.st() 27 | while (self.remaining()) { 28 | if (".#[{>]}'\"` \t\n".includes(self.next())) break 29 | prefix += self.next() 30 | self.index++ 31 | self.col++ 32 | } 33 | prefix += ":" 34 | } 35 | let src 36 | // find the file 37 | if (path.startsWith("http")) { 38 | let req = new XMLHttpRequest() 39 | req.open("GET", path, false) 40 | req.send(null) 41 | if (req.status !== 200) return {ok: null, err: `Unable to access ${path} (response ${req.status} '${req.responseText}')`} 42 | src = req.responseText 43 | } else { 44 | path = npath.join(npath.isAbsolute(path) ? "" : process.cwd(), path) 45 | switch (npath.extname(path)) { 46 | case "": path += ".hbml"; break 47 | case ".hbml": break 48 | default: 49 | return { 50 | ok: null, 51 | err: `Cannot import non-HBML file ${path}!` + 52 | fs.existsSync(path) ? "" : "File also doesn't exist!" 53 | } 54 | } 55 | if (!fs.existsSync(path)) return {ok: null, err: `Imported file ${path} does not exist`} 56 | src = fs.readFileSync(path).toString() 57 | } 58 | const {ok, err} = self.new(src, path, true).import_parse(prefix) 59 | if (err !== null) return {ok: null, err: `Error importing file ${path} (${err.toString()})`} 60 | self.update_src() 61 | for (const imported_macro in ok) { 62 | if (self.macros[self.macros.length - 1][imported_macro] !== undefined){ 63 | return {ok: null, err: "Cannot redefine macros through imports. Try using a namespace instead"} 64 | } 65 | self.macros[self.macros.length - 1][imported_macro] = ok[imported_macro] 66 | } 67 | return {ok: null, err: null} 68 | } 69 | 70 | /** 71 | * Parser function that returns namespaced macros to be imported 72 | * @param self {Parser} Parser instance 73 | * @param prefix {string} Namespace prefix 74 | * @return {{ok: (Object | null), err: (Error | null)}} 75 | */ 76 | export const import_parse = (self, prefix) => { 77 | const {_, err} = self.parse() 78 | if (err !== null) return {ok: null, err: err} 79 | let out = Object.assign({}, self.macros[0]) 80 | // delete the returned default macros 81 | // might need to rework this to check if the defaults were re-defined 82 | Object.keys(DEFAULT_MACROS).forEach((k) => delete out[k]) 83 | if (prefix === "") return {ok: out, err: null} 84 | // traverse all macros and add namespaces to them 85 | const updateMacroCall = (t) => { 86 | if (typeof t === "string") return t 87 | if (t.type[0] === ":" && !BUILTIN_MACROS.includes(t.type)) t.type = `:${prefix}${t.type.slice(1)}` 88 | return new Token(t.type, t.attributes, t.additional, t.children.map((c) => updateMacroCall(c))) 89 | } 90 | let prefixed_out = {} 91 | for (const outKey in out) { 92 | prefixed_out[`${prefix}${outKey}`] = new Macro(out[outKey].rep.map((t) => updateMacroCall(t)), out[outKey].void, `${prefix}${outKey}`, {file: self.path, col: self.col, line: self.ln}) 93 | } 94 | return {ok: prefixed_out, err: null} 95 | } 96 | 97 | /** 98 | * Parse and handle any `@insert` statements 99 | * @param self {Parser} Parser instance 100 | * @return {{ok: Array|null, err: null|string}} 101 | */ 102 | export const handleInsert = (self) => { 103 | self.index += 7 104 | self.st() 105 | let path = "" 106 | while (self.remaining()) { 107 | if (" \t\n".includes(self.next())) break 108 | path += self.next() 109 | self.index++ 110 | self.col++ 111 | } 112 | self.update_src() 113 | let src 114 | // find the file 115 | if (path.startsWith("http")) { 116 | let req = new XMLHttpRequest() 117 | req.open("GET", path, false) 118 | req.send(null) 119 | if (req.status !== 200) return {ok: null, err: `Unable to access ${path} (response ${req.status} '${req.responseText}')`} 120 | src = req.responseText 121 | } else { 122 | path = npath.join(npath.isAbsolute(path) ? "" : process.cwd(), path) 123 | if (!fs.existsSync(path)) return {ok: null, err: `Inserted file ${path} does not exist`} 124 | switch (npath.extname(path)) { 125 | case ".hbml": 126 | src = fs.readFileSync(path).toString() 127 | break 128 | default: 129 | src = '`' + fs.readFileSync(path).toString().replace('`', '\\`') + '`' 130 | } 131 | } 132 | const inner_parser = self.new(src, path, true) 133 | const {ok, err} = inner_parser.parse() 134 | if (err !== null) return {ok: null, err: `Error importing file ${path} (${err.toString()})`} 135 | const macros = Object.assign({}, inner_parser.macros[0]) 136 | Object.keys(DEFAULT_MACROS).forEach((k) => delete macros[k]) 137 | self.macros[self.macros.length - 1] = {...self.macros[self.macros.length - 1], ...macros} 138 | return {ok: ok, err: null} 139 | } 140 | -------------------------------------------------------------------------------- /src/commands/build.js: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import fs from "fs" 3 | import npath from "path" 4 | import {fullStringify} from "../parser/parser.js"; 5 | import {getConfig} from "../config_parse.js"; 6 | import {expand_paths, log_ep_err} from "./fs_prelude.js"; 7 | import {DEFAULT_ALLOW} from "../constants.js"; 8 | 9 | /** 10 | * Build function runner 11 | * 12 | * Takes in arguments and runs the appropriate command 13 | * @param args {Object} command line arguments after build command 14 | * @param project{boolean} is this a project command 15 | */ 16 | export const build_runner = (args, project) => { 17 | // help flags 18 | if (args["h"] !== undefined || args["help"] !== undefined) { 19 | build_help() 20 | } 21 | let files 22 | let out 23 | let allow = Object.assign({}, DEFAULT_ALLOW) 24 | if (project) { 25 | const conf_res = getConfig() 26 | if (conf_res.err) { 27 | console.log(chalk.red(`Error parsing config file (${conf_res.err}`)) 28 | process.exit(1) 29 | } 30 | files = conf_res.ok["build.src"] 31 | out = conf_res.ok["build.output"] 32 | Object.keys(allow).forEach((k) => allow[k] = conf_res.ok[`build.allow.${k}`]) 33 | } else { 34 | // get files to build 35 | files = args["_"]; 36 | if (!files) { 37 | files = ["."] 38 | } 39 | delete args["_"] 40 | // get output path 41 | out = args["o"] 42 | if (out === undefined) { 43 | out = "." 44 | } else if (Array.isArray(out)) { 45 | console.log(chalk.red("Too many arguments given for output flag (-o)! Expected at most 1!")) 46 | process.exit(1) 47 | } 48 | delete args["o"] 49 | let skip_arr = args["s"]; 50 | if (skip_arr === undefined) { 51 | skip_arr = [] 52 | } 53 | skip_arr.forEach((s) => { 54 | switch (s) { 55 | case "write": 56 | allow.write = true 57 | break 58 | case "not_found": 59 | allow.not_found = true 60 | break 61 | case "parse": 62 | allow.parse = true 63 | break 64 | default: 65 | console.log(chalk.red(`Unknown allow value ${s}! See 'hbml build -h' for help`)) 66 | process.exit(1) 67 | } 68 | }) 69 | delete args["s"] 70 | } 71 | // check no unexpected args were given 72 | if (Object.keys(args).length !== 0) { 73 | console.log(chalk.red(`Unexpected arguments ${Object.keys(args).join(", ")}! See 'hbml build -h' for help`)) 74 | process.exit(1) 75 | } 76 | build_internal(files, out, allow) 77 | } 78 | 79 | /** 80 | * Help function for build command 81 | * 82 | * Prints help info for the build command then ends the process 83 | */ 84 | const build_help = () => { 85 | console.log(`Usage: hbml build {project}|([source]... [options]) 86 | 87 | Builds HBML files into HTML files 88 | 89 | If project is provided, uses argumets from hbml.json in the cwd 90 | 91 | ${chalk.bold(chalk.underline('Arguments:'))} 92 | [source] HBML file to build into HTML, or directory to traverse to find all HBML files to build to HTML 93 | 94 | ${chalk.bold(chalk.underline('Options:'))} 95 | -o 96 | Output directory for HTML files. Defaults to current working directory 97 | -a= 98 | Allow option for errors. Allowable values are: 99 | - 'write' for allowing write errors 100 | - 'not_found' for allowing file not found errors 101 | - 'parse' for allowing HBML parsing errors 102 | -h,--help 103 | Shows this message`); 104 | process.exit() 105 | } 106 | 107 | /** 108 | * Internal function to run the builder on HBML files 109 | * @param paths {string[]} Given paths to traverse/build 110 | * @param output {string} Output path prefix 111 | * @param allow {Object} Allow arguments 112 | */ 113 | const build_internal = (paths, output, allow) => { 114 | console.log("Building HTML files...") 115 | const path_res = expand_paths(paths, output) 116 | if (path_res.err.length > 0) log_ep_err(path_res.err, allow) 117 | for (let i = 0; i < path_res.ok.length; i++) { 118 | if (!parse_file(path_res.ok[i], allow)) break 119 | } 120 | console.log(`Finished building HBML files`) 121 | } 122 | 123 | /** 124 | * File writer function 125 | * 126 | * Takes path object and builds it into a HTML file 127 | * @param path {{read: string, write: string}} Path object 128 | * @param allow {Object} Allow arguments 129 | * @return {boolean} 130 | */ 131 | const parse_file = (path, allow) => { 132 | path.write = `${path.write.slice(0, path.write.length - 5)}.html` 133 | const {ok, err} = fullStringify(fs.readFileSync(path.read).toString(), path.read) 134 | if (err) { 135 | if (allow.parse) { 136 | console.log(chalk.yellow(`Unable to parse file ${path.write} ${err.ln}:${err.col}(${err.desc})! Skipping over file`)) 137 | } else { 138 | console.log(chalk.red(`Unable to parse file ${path.write} ${err.ln}:${err.col}(${err.desc})! Stopping!\nTo skip over parsing errors, pass the -s=parse flag`)) 139 | process.exit(1) 140 | } 141 | return false 142 | } 143 | if (!fs.existsSync(npath.dirname(path.write))) { 144 | fs.mkdirSync(npath.dirname(path.write), {recursive: true}) 145 | } 146 | try { 147 | fs.writeFileSync(path.write, ok) 148 | } catch (e) { 149 | if (allow.write) { 150 | console.log(chalk.yellow(`Unable to write file ${path.write} (${e})! Skipping over file`)) 151 | } else { 152 | console.log(chalk.red(`Unable to write file ${path.write} (${e})! Stopping!\nTo skip over write errors, pass the -s=write flag`)) 153 | return false 154 | } 155 | } 156 | return true 157 | } 158 | -------------------------------------------------------------------------------- /src/commands/lint.js: -------------------------------------------------------------------------------- 1 | import {CONFIG_DEFAULTS} from "../constants.js"; 2 | import {getConfig} from "../config_parse.js"; 3 | import chalk from "chalk"; 4 | import npath from "path"; 5 | import fs from "fs"; 6 | import {Parser} from "../parser/parser.js"; 7 | import {expand_paths} from "./fs_prelude.js"; 8 | 9 | export const lint_runner = (args, project) => { 10 | // help flags 11 | if (args["h"] !== undefined || args["help"] !== undefined) { 12 | lint_help() 13 | } 14 | let files 15 | let out 16 | let allow = {write: false, not_found: false, parse: false} 17 | let conf = CONFIG_DEFAULTS 18 | if (project) { 19 | const conf_res = getConfig() 20 | if (conf_res.err) { 21 | console.log(chalk.red(`Error parsing config file (${conf_res.err})`)) 22 | process.exit(1) 23 | } 24 | files = conf_res.ok["lint.src"] 25 | out = conf_res.ok["lint.output"] 26 | Object.keys(allow).forEach((k) => allow[k] = conf_res.ok[`build.allow.${k}`]) 27 | conf = conf_res.ok 28 | } else { 29 | // get files to build 30 | files = args["_"]; 31 | if (files === undefined) { 32 | files = ["."] 33 | } 34 | delete args["_"] 35 | // get output path 36 | out = args["o"] 37 | if (out === undefined) { 38 | out = args["p"] ? "linted" : "." 39 | delete args["p"] 40 | } else if (Array.isArray(out)) { 41 | console.log(chalk.red("Too many arguments given for output flag (-o)! Expected at most 1!")) 42 | process.exit(1) 43 | } 44 | if (args["p"]) { 45 | console.log(chalk.yellow("-p and -o flags found! Ignoring -p")) 46 | delete args["p"] 47 | } 48 | delete args["o"] 49 | let skip_arr = args["s"]; 50 | if (skip_arr === undefined) { 51 | skip_arr = [] 52 | } 53 | skip_arr.forEach((s) => { 54 | switch (s) { 55 | case "write": 56 | allow.write = true 57 | break 58 | case "not_found": 59 | allow.not_found = true 60 | break 61 | case "parse": 62 | allow.parse = true 63 | break 64 | default: 65 | console.log(chalk.red(`Unknown allow value ${s}!\nSee 'hbml lint -h' for help`)) 66 | process.exit(1) 67 | } 68 | }) 69 | delete args["s"] 70 | const expected_keys = Object.keys(CONFIG_DEFAULTS) 71 | let given_keys = Object.keys(args).filter((k) => !expected_keys.includes(`lint.config.${k}`)) 72 | if (Object.keys(given_keys).length !== 0) { 73 | console.log(chalk.red(`Unknown argument${Object.keys(given_keys) ? "s":""}: ${given_keys.map((k) => `--${k}`).join(" ")}!\nCheck the config file docs for help`)) 74 | process.exit(1) 75 | } 76 | Object.entries(args).forEach(([k, v]) => { 77 | if (typeof conf[`lint.config.${k}`] === typeof v) conf[`lint.config.${k}`] = v 78 | else { 79 | console.log(chalk.red(`Incorrect type for argument --${k}! Expected ${typeof conf[`lint.config.${k}`]} found ${typeof v}!\nCheck the config file docs for help`)) 80 | process.exit(1) 81 | } 82 | }) 83 | } 84 | // check no unexpected args were given 85 | if (Object.keys(args).length !== 0) { 86 | console.log(chalk.red(`Unexpected arguments ${Object.keys(args).join(", ")}! See 'hbml lint -h' for help`)) 87 | process.exit(1) 88 | } 89 | lint_internal(files, out, allow, conf) 90 | } 91 | /** 92 | * Prints help for the lint command then exits 93 | */ 94 | const lint_help = () => { 95 | console.log(`Usage: hbml lint {project}|([source]... [options]) 96 | 97 | Builds HBML files into HTML files 98 | 99 | If project is provided, uses argumets from hbml.json in the cwd 100 | 101 | ${chalk.bold(chalk.underline('Arguments:'))} 102 | [source] HBML file to build into HTML, or directory to traverse to find all HBML files to build to HTML 103 | 104 | ${chalk.bold(chalk.underline('Options:'))} 105 | -o 106 | Output directory for HTML files. Defaults to current working directory 107 | -p 108 | Alias for -o "linted". If passed with -o, will raise a warning and be ignored 109 | -a= 110 | Allow option for errors. Allowable values are: 111 | - 'write' for allowing write errors 112 | - 'not_found' for allowing file not found errors 113 | - 'parse' for allowing HBML parsing errors 114 | -h,--help 115 | Shows this message`); 116 | process.exit() 117 | } 118 | 119 | /** 120 | * Internal function to run the linter on HBML files 121 | * @param paths {string[]} Given paths to traverse/build 122 | * @param output {string} Output path prefix 123 | * @param allow {Object} Allow arguments 124 | * @param lint_opts{Object} Linting options 125 | */ 126 | const lint_internal = (paths, output, allow, lint_opts) => { 127 | console.log("Linting HTML files...") 128 | const {ok, err} = expand_paths(paths, output, ".hbml", ".hbml") 129 | if (err.length > 0) { 130 | if (allow.not_found) { 131 | console.log(chalk.yellow(`Error${err.length === 0 ? "s" : ""} expanding given paths:`)) 132 | err.forEach((t) => { 133 | console.log(chalk.yellow(`\t${t.path}: ${t.type}`)) 134 | }) 135 | } else { 136 | console.log(chalk.red(`Stopping due to error${err.length === 0 ? "s" : ""} expanding given paths(To skip over missing files, pass the -s=not_found flag):`)) 137 | err.forEach((t) => { 138 | console.log(chalk.red(`\t${t.path}: ${t.type}`)) 139 | }) 140 | return 141 | } 142 | } 143 | for (let i = 0; i < ok.length; i++) { 144 | if (!lint_file(ok[i], allow, lint_opts)) break 145 | } 146 | console.log(`Finished linting HBML files`) 147 | } 148 | 149 | /** 150 | * Lint an individual file 151 | * @param path{Object} Path to file 152 | * @param allow{Object} Allow options 153 | * @param opts{Object} Lint options 154 | * @return {boolean} 155 | */ 156 | const lint_file = (path, allow, opts) => { 157 | const res = new Parser(fs.readFileSync(path.read).toString(), path.read, false).parse() 158 | if (res.err) { 159 | if (allow.parse) { 160 | console.log(chalk.yellow(`Unable to parse file ${path.read} ${res.err.ln}:${res.err.col}(${res.err.desc})! Skipping over file`)) 161 | } else { 162 | console.log(chalk.red(`Unable to parse file ${path.read} ${res.err.ln}:${res.err.col}(${res.err.desc})! Stopping!\nTo skip over incorrectly formatted files, pass the -s=parse flag`)) 163 | return false 164 | } 165 | return true 166 | } 167 | const out = res.ok.map((t) => typeof t === "object" ? t.lint(0, false, opts) : t).join("\n") 168 | if (!fs.existsSync(npath.dirname(path.write))) { 169 | fs.mkdirSync(npath.dirname(path.write), {recursive: true}) 170 | } 171 | try { fs.writeFileSync(path.write, out) } 172 | catch (e) { 173 | if (allow.write) { 174 | console.log(chalk.yellow(`Unable to write file ${path.write} (${e})! Skipping over file`)) 175 | } else { 176 | console.log(chalk.red(`Unable to write file ${path.write} (${e})! Stopping!\nTo skip over write errors, pass the -s=write flag`)) 177 | return false 178 | } 179 | } 180 | return true 181 | } 182 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HBML: Hyper Braced Markup Language 2 | 3 | This is a Javascript module that contains a toy parser I made for fun. 4 | 5 | I wasn't intending on putting this on GitHub since it's definitely not efficient or well written, but some people were 6 | curious and so no harm no foul. 7 | 8 | But just keep in mind that this is very much a toy, and invalid syntax will just cause the parser to spit out broken 9 | HTML. 10 | 11 | [Try version of parser on my website](https://aydenh.ca/hbml/) 12 | 13 | ## Premise 14 | 15 | Make HTML a little more concise and use syntax that is more similar to CSS and Javascript. 16 | 17 | ## Syntax 18 | 19 | The root element of every HBML document is `:root` which becomes `html[lang="en"]` when converted to html 20 | 21 | The syntax for defining an element is similar to how you would write a CSS selector. 22 | 23 | 1. Enter the name of the element. 24 | - The name may be omitted in favour of only defining ids, classes, and attributes 25 | - This will cause the element to default to be a div (or span when a child of an inline element). 26 | 27 | 2. Ids and classes can be appended to the end of the element name with '#*id*' and '.*class*' respectively. 28 | - As many classes and ids can be appended to an element as needed. 29 | 30 | 3. Attributes can be added inside square brackets [] appended to the element name. 31 | 32 | - The last set of attributes appended to an element will be the only one that takes effect. 33 | - This behaviour allows removing or overwriting the language property of `:root`, `:root[]` to 34 | remove, `:root[lang="fr"]` to overwrite. 35 | 36 | 4. After the element a pair of braces {} can be added to define the contents of the element, these are not required if 37 | the element is empty or is void. 38 | 39 | 5. Inside the braces child elements may be added, text nodes* may be added ("Hello, World!"), and multiline comments may 40 | be added (/* comment */). 41 | 42 | *Text nodes can be defined with "double quotes", 'single quotes', or \`back ticks\` 43 | 44 | Example empty section element with an id of "wrapper" 45 | 46 | ```hbml 47 | section#wrapper 48 | ``` 49 | 50 | Example empty div element with a class of "holder" 51 | 52 | ```hbml 53 | div.holder 54 | ``` 55 | 56 | Example p element with inline style 57 | 58 | ```hbml 59 | p[style="color: red;"] {"Hello, World!"} 60 | ``` 61 | 62 | Example h1 element using single child syntax followed by multi child syntax 63 | 64 | ```hbml 65 | h1 > "Single child syntax only allows one text node or child element" 66 | 67 | h1 { 68 | "With multi child syntax you " 69 | "can have multiple text nodes concatenated" 70 | } 71 | ``` 72 | 73 | Example implicit div element with multiple classes 74 | 75 | ```hbml 76 | .class-one.class-two.class-three { 77 | "Text inside an implicit div" 78 | } 79 | ``` 80 | 81 | Example of implicit div nesting using single child syntax 82 | 83 | ```hbml 84 | .layer-1 > .layer-2 > .layer-3 85 | ``` 86 | 87 | ## HTML vs HBML examples 88 | 89 | ### Classes, Ids, and Attributes 90 | 91 | HTML 92 | 93 | ```html 94 |

Paragraph Text

95 | 96 | ``` 97 | 98 | HBML 99 | 100 | ```hbml 101 | p#myParagraph.paraClass1.paraClass2 { 102 | "Paragraph Text" 103 | } 104 | input[type="text"] 105 | ``` 106 | 107 | ### Default divs and spans 108 | 109 | HTML 110 | 111 | ```html 112 | 113 |
Some text
114 | 115 | This part is automatically a span because of the parent 116 | 117 | ``` 118 | 119 | HBML 120 | 121 | ```hbml 122 | /* I made divs (or spans inside default inline elements) the default if nothing is specified */ 123 | {"Some text"} 124 | span { 125 | {"This part is automatically a span because of the parent"} 126 | } 127 | ``` 128 | 129 | ### Basic page example 130 | 131 | HTML 132 | 133 | ```html 134 | 135 | 136 | 137 | 138 | 139 | Title of webpage 140 | 145 | 146 | 147 |

Heading 1

148 |
149 |

A paragraph inside a div

150 |
151 | Clickable link 152 | 153 | 154 | ``` 155 | 156 | HBML 157 | 158 | ```hbml 159 | :root { 160 | head { 161 | meta[charset="UTF-8"] 162 | meta[name="viewport" content="width=device-width, initial-scale=1"] 163 | title { "Title of webpage" } 164 | style { " 165 | body { 166 | font-family: sans-serif; 167 | } 168 | " } 169 | } 170 | body { 171 | h1 { "Heading 1" } 172 | 173 | div { 174 | p { "A paragraph inside a div" } 175 | } 176 | 177 | a[href="./"] { "Clickable link" } 178 | } 179 | } 180 | ``` 181 | 182 | ### More complex example 183 | 184 | HTML 185 | 186 | ```html 187 | 188 | 189 | 190 | 191 | 192 | Title of webpage 193 | 202 | 203 | 204 | 205 |
206 |

Heading 1

207 |

Heading 2

208 |

Heading 3

209 |

Heading 4

210 |
Heading 5
211 |
Heading 6
212 |
213 | 214 |
215 |

A paragraph inside a div

216 |

This also works for a single piece of text

217 |
218 |
219 |
220 |
Text directly inside the .layer-3 div
221 |
Some text inside an implicit div
222 |
Other text inside an explicit div
223 |
Text inside the .layer-3 div after the child elements 224 |
225 |
226 |
227 | 228 |

Text with a highlighted part right here followed by more text

229 | 230 |
Double quote string
Single quote string
Back tick string
231 | 232 | 233 | ``` 234 | 235 | HBML 236 | 237 | ```hbml 238 | :root { 239 | head { 240 | meta[charset="UTF-8"] 241 | meta[name="viewport" content="width=device-width, initial-scale=1"] 242 | title > "Title of webpage" 243 | style > " 244 | body { 245 | font-family: 'Roboto', sans-serif; 246 | } 247 | 248 | .highlight { 249 | background-color: yellow; 250 | } 251 | " 252 | } 253 | body { 254 | 255 | /* Various headings */ 256 | 257 | section { 258 | h1 > "Heading 1" 259 | h2 > "Heading 2" 260 | h3 > "Heading 3" 261 | h4 > "Heading 4" 262 | h5 > "Heading 5" 263 | h6 > "Heading 6" 264 | } 265 | 266 | 267 | /* Divs and classes with multiple children */ 268 | 269 | div { 270 | p { "A paragraph inside a div" } 271 | p > "This also works for a single piece of text" 272 | } 273 | 274 | .layer-1 > .layer-2 > .layer-3 { 275 | "Text directly inside the .layer-3 div" 276 | 277 | br 278 | > "Some text inside an implicit div" 279 | div > "Other text inside an explicit div" 280 | br 281 | 282 | "Text inside the .layer-3 div after the child elements" 283 | } 284 | 285 | /* Combination of inlining and multiple children */ 286 | 287 | p {"Text with a highlighted part " span.highlight>"right here" " followed by more text"} 288 | 289 | /* Testing different strings */ 290 | div { 291 | "Double quote string" br 'Single quote string' br `Back tick string` 292 | } 293 | } 294 | } 295 | ``` -------------------------------------------------------------------------------- /test/parser.tests.mjs: -------------------------------------------------------------------------------- 1 | import {strictEqual as equal} from "assert" 2 | import {fullStringify} from "../src/parser/parser.js"; 3 | 4 | const p = (src) => { 5 | const {ok, err} = fullStringify(src, "test env") 6 | return err ? err : ok 7 | } 8 | 9 | describe("Testing HBML parser", () => { 10 | describe("Strings", () => { 11 | it("Double quotes", () => { 12 | equal(p(`"Test ""string"`), "Test string") 13 | equal(p(`" Test string "`), " Test string ") 14 | equal(p(`"Test\n string"`), "Test string") 15 | }) 16 | it("Single quotes", () => { 17 | equal(p(`'Test ''string'`), "Test string") 18 | equal(p(`' Test string '`), " Test string ") 19 | equal(p(`'Test\n string'`), "Test string") 20 | }) 21 | it("Backticks", () => { 22 | equal(p(`\`Test \`\`string\``), "Test string") 23 | equal(p(`\` Test string \``), " Test string ") 24 | equal(p(`\`Test\n string\``), "Test\n string") 25 | }) 26 | it("Other", () => { 27 | equal(p(`""''\`\``), "") 28 | equal(p(`"\\\\ backslash can appear inside string literal, even before an escape \\\\\\", but <> can't"`), "\\ backslash can appear inside string literal, even before an escape \\\", but <> can't") 29 | equal(p(`"All "'working '\`together\``), "All working together") 30 | equal(p(`hr"Let's"br'break'br"this"br\`up!\``), "
Let's
break
this
up!") 31 | equal(p(`"Double \nquotes a"'nd single\n quotes ignore newlines'\`\nbut backticks keep them\``), "Double quotes and single quotes ignore newlines\nbut backticks keep them") 32 | }) 33 | }) 34 | describe("Comments", () => { 35 | it("Single line comments",() => { 36 | equal(p("//"), "") 37 | equal(p("//\n"), "") 38 | equal(p("//test comment"), "") 39 | equal(p("// test comment"), "") 40 | equal(p("// test comment\n"), "") 41 | }) 42 | it("Multiline comments",() => { 43 | equal(p("/**/"), "") 44 | equal(p("/* test comment */"), "") 45 | equal(p("/* test" + 46 | "comment */"), "") 48 | }) 49 | }) 50 | describe("Elements", () => { 51 | it("Blocks", () => { 52 | equal(p("div {}"), "
") 53 | equal(p("div>p"), "

") 54 | equal(p("div> p"), "

") 55 | equal(p("div >p"), "

") 56 | equal(p("div > p"), "

") 57 | equal(p("div{}"), "
") 58 | equal(p("div { }"), "
") 59 | equal(p("div {\n}"), "
") 60 | equal(p("div {'Test'}"), "
Test
") 61 | equal(p("div { 'Test' }"), "
Test
") 62 | equal(p("div {div}"), "
") 63 | equal(p("div { div }"), "
") 64 | equal(p("div {div > 'Test'}"), "
Test
") 65 | equal(p(">>i"), "
") 66 | equal(p("div{}div{ }div{\n}"), "
") 67 | }) 68 | it("Block Implicits (div)", () => { 69 | equal(p("{}"), "
") 70 | equal(p(">"), "
") 71 | equal(p("> p"), "

") 72 | }) 73 | it("Inline Implicits (span)", () => { 74 | equal(p("span > {}"), "") 75 | equal(p("i {{'Test'}p}"), "Test

") 76 | }) 77 | it("IDs and Classes", () => { 78 | equal(p("div#my-id"), `
`) 79 | equal(p(".class"), `
`) 80 | equal(p(".class.class2.class3"), `
`) 81 | equal(p("div#my-id.class.class2.class3"), `
`) 82 | }) 83 | it("Attributes", () => { 84 | equal(p("span[style=color:red;] > 'Text'"), `Text`) 85 | equal(p("#my-id.class.class2[data-attribute]"), `
`) 86 | equal(p("link[href=https://cool-website.com/]\n" + 87 | " link[rel=stylesheet href=./local-styles/style.css]\n" + 88 | " meta[charset=\"UTF-8\"]\n" + 89 | " meta[\n" + 90 | " name=`viewport`\n" + 91 | " content='width=device-width, initial-scale=1'\n" + 92 | " ]"), 93 | `` 94 | ) 95 | equal(p('input#input-id[type=checkbox name="checkboxName" checked]\n' + 96 | ' [data-attribute=Test\\ value1]\n' + 97 | ' [data-attribute="Test value2"]\n' + 98 | ' [data-attribute=\'Test value3\']\n' + 99 | ' [data-attribute=\`Test value4\`]\n' + 100 | ' [data-first-attribute="First attr" data-second-attribute=\'Second attr\']> "Test"'), 101 | `
Test
`) 102 | }) 103 | 104 | 105 | it("Other", () => { 106 | equal(p("h1>h2>h3>\n'Layered'"), "

Layered

") 107 | equal(p(".l1 > span.l2 > .l3 > 'Layered'"), `
Layered
`) 108 | equal(p("p> {'Text'}"), "

Text

") 109 | equal(p("p >'Text'"), "

Text

") 110 | equal(p("section"), "
") 111 | }) 112 | }) 113 | describe("@insert", () => { 114 | describe('HBML file', () => { 115 | it('In root', () => { 116 | equal(p('@insert test/insert_test.hbml :test'), '
Other words
Hello, world!') 117 | }) 118 | it('Shadowing', () => { 119 | equal(p('--test > "Blank" { @insert test/insert_test.hbml :test } :test'), '
Other words
Hello, world!
Blank') 120 | }) 121 | it('Redefinition', () => { 122 | equal(p('--test > "Blank" :test @insert test/insert_test.hbml :test'), 'Blank
Other words
Hello, world!') 123 | }) 124 | }) 125 | it("Text file", () => { 126 | equal(p('@insert test/raw_text.txt'), 'Voluptates fugit ut temporibus odit sint.') 127 | equal(p('> @insert test/raw_text.txt'), '
Voluptates fugit ut temporibus odit sint.
') 128 | }) 129 | }) 130 | describe('Full Pages', () => { 131 | it("Small", () => { 132 | equal(p(`:root[lang=en] { 133 | head { 134 | title > "Page title" 135 | } 136 | 137 | body { 138 | h1 > "Heading Level 1" 139 | section { 140 | p > "Paragraph 1" 141 | p > "Paragraph 2" 142 | } 143 | } 144 | }`), 145 | `Page title

Heading Level 1

Paragraph 1

Paragraph 2

` 146 | ) 147 | }) 148 | it("Large", () => { 149 | equal(p(`:root[lang="en-CA"] { 150 | head { 151 | meta[charset="UTF-8"] 152 | meta[ 153 | name="viewport" 154 | content="width=device-width, initial-scale=1" 155 | ] 156 | 157 | title > "Title of webpage" 158 | style > " 159 | body { 160 | font-family: 'Roboto', sans-serif; 161 | } 162 | 163 | .highlight { 164 | background-color: yellow; 165 | } 166 | " 167 | } 168 | body { 169 | 170 | /* Various inline headings */ 171 | 172 | section { 173 | h1 > "Heading 1" 174 | h2 > "Heading 2" 175 | h3 > "Heading 3" 176 | h4 > "Heading 4" 177 | h5 > "Heading 5" 178 | h6 > "Heading 6" 179 | } 180 | 181 | 182 | /* Divs and classes with multiple children */ 183 | 184 | div { 185 | p { "A paragraph inside a div" } 186 | p > "This also works for a single piece of text" 187 | } 188 | 189 | .layer-1 > .layer-2 > .layer-3 { 190 | "Text directly inside the .layer-3 div" 191 | 192 | br 193 | > "Some text inside an implicit div" 194 | div > "Other text inside an explicit div" 195 | br 196 | 197 | "Text inside the .layer-3 div after the child elements" 198 | } 199 | 200 | a[href="./"] > "Clickable link" 201 | 202 | /* Combination of inlining and multiple children */ 203 | 204 | p {"Text with a highlighted part " span.highlight>"right here" " followed by more text"} 205 | 206 | /* Testing different strings */ 207 | div { 208 | "Double quote string" br 'Single quote string' br \`Back tick string\` 209 | } 210 | /* some strange but technically allowable syntax */ 211 | a[at1="1" 212 | at2="2"] 213 | } 214 | }`), 215 | `Title of webpage

Heading 1

Heading 2

Heading 3

Heading 4

Heading 5
Heading 6

A paragraph inside a div

This also works for a single piece of text

Text directly inside the .layer-3 div
Some text inside an implicit div
Other text inside an explicit div

Text inside the .layer-3 div after the child elements
Clickable link

Text with a highlighted part right here followed by more text

Double quote string
Single quote string
Back tick string
` 216 | ) 217 | }) 218 | }); 219 | }) -------------------------------------------------------------------------------- /src/parser/main.js: -------------------------------------------------------------------------------- 1 | import {Token} from "../token.js"; 2 | import {BUILTIN_MACROS, INLINE_ELEMENTS, LITERAL_DELIMITERS, UNIQUE_ATTRS, VOID_ELEMENTS} from "../constants.js"; 3 | import chalk from "chalk"; 4 | 5 | /** 6 | * Main parser function. Called by {@link Parser.parse} 7 | * @param self {Parser} Parser instance 8 | * @param default_tag {string} Default tag to use when no other is given 9 | * @param str_replace {boolean} Pass any string through the {@link convertReservedChar} function 10 | * @param under_macro_def {boolean} Is the parser parsing under a macro definition 11 | * @return {{ok: (Array | null), err: (string | null)}} 12 | */ 13 | export const parse_inner = (self, default_tag, str_replace, under_macro_def) => { 14 | // move over blank characters 15 | self.stn() 16 | if (!self.remaining() || self.next() === "}") return {ok: null, err: null} 17 | // check for string 18 | if (LITERAL_DELIMITERS.includes(self.next())) { 19 | let res = self.parseStr(self.next(), str_replace) 20 | if (res.err) return {ok: null, err: res.err} 21 | return {ok: [res.ok], err: null} 22 | } 23 | //check for macro def 24 | if (self.next() === "-" && self.src[self.index + 1] === "-") { 25 | self.index += 2 26 | return self.parse_macro_def() 27 | } 28 | // check for comment 29 | if (self.next() === "/") { 30 | self.src = self.src.slice(self.index) 31 | let res = self.parseComment() 32 | if (res.err) return {ok: null, err: res.err} 33 | return {ok: [res.ok], err: null} 34 | } 35 | // check for @ commands 36 | if (self.src.startsWith("@import", self.index)) return self.handleImport() 37 | if (self.src.startsWith("@insert", self.index)) return self.handleInsert() 38 | // get tag 39 | let tag_res = self.parseTag(default_tag) 40 | if (tag_res.err) return {ok: null, err: tag_res.err} 41 | let {type, attrs, additional} = tag_res.ok 42 | 43 | // check if the tag is a macro 44 | let macro = undefined 45 | if (type[0] === ":" && !BUILTIN_MACROS.includes(type)) { 46 | // try to get macro 47 | if (type === ":") return {ok: null, err: "Macro cannot have an empty name"} 48 | if (!under_macro_def) { 49 | const {ok, err} = self.get_macro(type.slice(1)) 50 | if (ok === null && self.isBuild) return {ok: null, err: err} 51 | macro = ok 52 | if (ok.void) { 53 | self.update_src() 54 | return self.isBuild ? {ok: ok.expand([], attrs, self).ok, err: null} : {ok: [new Token(type, attrs, additional, [])], err: null} 55 | } 56 | } 57 | } 58 | 59 | if (additional["void"]) { 60 | self.update_src() 61 | return {ok: [new Token(type, attrs, additional, [])], err: null} 62 | } 63 | 64 | // match the inner section 65 | let children = [] 66 | default_tag = INLINE_ELEMENTS.includes(type) ? "span" : "div" 67 | str_replace = type !== "style" 68 | // skip spaces and tabs 69 | self.st() 70 | if (!self.remaining()){ 71 | self.update_src() 72 | if (macro === undefined) return {ok: [new Token(type, attrs, additional, [])], err: null} 73 | return typeof macro === "object" ? macro.expand([], attrs, self) : {ok: macro([]), err: null} 74 | } 75 | // check if element has one inner or block inner 76 | if (self.next() === ">") { 77 | additional["inline"] = true 78 | self.index++ 79 | self.col++ 80 | self.update_src() 81 | self.macros.push({}) 82 | let res = self.parse_inner(default_tag, str_replace, under_macro_def) 83 | self.macros.pop() 84 | if (res.err) return {ok: null, err: res.err} 85 | if (res.ok !== null) children = [...children, ...res.ok] 86 | } else if (self.next() === "{") { 87 | self.macros.push({}) 88 | self.index++ 89 | self.col++ 90 | self.update_src() 91 | while (self.remaining() && self.next() !== "}") { 92 | let res = self.parse_inner(default_tag, str_replace, under_macro_def) 93 | if (res.err) return {ok: null, err: res.err, rem: ""} 94 | if (res.ok !== null) children = [...children, ...res.ok] 95 | self.stn() 96 | } 97 | if (!self.remaining()) return {ok: null, err: "Unclosed block! Expected closing '}' found EOF!"} 98 | self.macros.pop() 99 | self.index++ 100 | self.col++ 101 | } 102 | self.update_src() 103 | 104 | if (macro !== undefined && self.isBuild) { 105 | if (typeof macro === "object") return macro.expand(children, attrs, self) 106 | return {ok: macro(children), err: null} 107 | } 108 | 109 | return {ok: [new Token(type, attrs, additional, children)], err: null} 110 | } 111 | 112 | /** 113 | * Converts reserved HTML characters into their respective HTML codes 114 | * @param self {Parser} Parser instance 115 | * @param input {string} Input string to convert 116 | * @return {string} Converted string 117 | */ 118 | export const convertReservedChar = (self, input) => { 119 | const reserved = { 120 | "<": "<", 121 | ">": ">", 122 | } 123 | for (const property in reserved) input = input.replaceAll(property, reserved[property]) 124 | return input 125 | } 126 | 127 | /** 128 | * Parse a comment 129 | * @param self {Parser} Parser instance 130 | * @return {{ok: (Token | null), err: (string | null)}} 131 | */ 132 | export const parseComment = (self) => { 133 | let out = "" 134 | const multiline = self.src[1] === "*" 135 | 136 | if (!(multiline || self.src[1] === "/")) return {ok: null, err: "Expected '*' or '/' after /"} 137 | self.index = 2 138 | 139 | while (true) { 140 | if (!self.remaining()) { 141 | if (!multiline) break 142 | return {ok: null, err: "Expected end of comment! Found EOF"} 143 | } 144 | const next = self.next() 145 | 146 | self.index++ 147 | self.col++ 148 | if (multiline && next === "*" && self.next() === "/") { 149 | self.index++ 150 | break 151 | } else if (next === "\n") { 152 | if (!multiline) break 153 | out += next 154 | self.col = 0 155 | self.ln++ 156 | } else out += next 157 | } 158 | self.update_src() 159 | 160 | return {ok: new Token("c t", {}, {value: out}, []), err: null} 161 | } 162 | 163 | /** 164 | * Parse a string until required closing delimiters are found 165 | * @param self {Parser} Parser instance 166 | * @param delim {string} String delimiter 167 | * @param convert {boolean} Pass the output through {@link Parser.convertReservedChar} 168 | * @return {{ok: (string | null), err: (null | string)}} 169 | */ 170 | export const parseStr = (self, delim, convert) => { 171 | self.index++ 172 | self.col++ 173 | let out = "" 174 | 175 | let escape = false 176 | 177 | while (self.remaining()) { 178 | if (self.next() === "\\" && !escape) escape = true 179 | else if (delim.includes(self.next()) && !escape) { 180 | if (self.next() === "]") self.index-- 181 | break 182 | } else if (self.next() === "\n") { 183 | self.col = 0 184 | self.ln++ 185 | if (delim === "`") out += "\n" 186 | } else { 187 | escape = false 188 | out += self.next() 189 | } 190 | self.index++ 191 | self.col++ 192 | } 193 | if (!self.remaining()) return {ok: null, err: "Unclosed string"} 194 | self.index++ 195 | self.col++ 196 | self.update_src() 197 | 198 | return { 199 | ok: convert ? self.convertReservedChar(out) : out, 200 | err: null, 201 | } 202 | } 203 | 204 | /** 205 | * Parse attributes from inside square brackets. Called after finding an opening square bracket 206 | * @param self {Parser} Parser instance 207 | * @param unique_replace {number} Allow unique attribute replacements (0 -> no message, 1 -> warning, 2 -> error) 208 | * @param unique_position {boolean} `false` if keep first occurrence of a uniquer attribute, `true` for keep the last occurrence 209 | * @param initial {Object} Initial attributes 210 | * @return {{ok: (Object | null), err: (null | string)}} 211 | */ 212 | export const parseAttrs = (self, unique_replace, unique_position, initial) => { 213 | let attrsObj = initial 214 | 215 | const insertValue = (key, value) => { 216 | if (attrsObj[key] === undefined) attrsObj[key] = value 217 | else { 218 | if (UNIQUE_ATTRS.includes(key)) { 219 | switch (unique_replace) { 220 | case 0: 221 | attrsObj[key] = value 222 | break 223 | case 1: 224 | console.log(chalk.yellow(`Duplicate unique value found (${attrsObj[key]} and ${value}) ${self.file} ${self.line}:${self.col}`)); 225 | attrsObj[key] = value 226 | break 227 | default: 228 | return {ok: null, err: `Duplicate unique value found (${attrsObj[key]} and ${value})`, rem: ""} 229 | } 230 | } else { 231 | switch (`${typeof attrsObj[key]} ${typeof value}`) { 232 | case "string string": 233 | attrsObj[key] += ` ${value}` 234 | break 235 | case "boolean string": 236 | attrsObj[key] = value 237 | } 238 | } 239 | } 240 | } 241 | 242 | while (self.remaining() && self.next() !== "]") { 243 | // skip stn 244 | self.stn() 245 | // parse key 246 | let key = "" 247 | while (self.remaining()) { 248 | if (" \t\n='\"`]".includes(self.next())) break 249 | key += self.next() 250 | self.index++ 251 | self.col++ 252 | } 253 | if (self.next() === "]") { 254 | if (key) { 255 | const res = insertValue(key, true) 256 | if (res) return res 257 | } 258 | self.index++ 259 | self.col++ 260 | self.update_src() 261 | return {ok: attrsObj, err: null} 262 | } 263 | if (!key) return {ok: null, err: "Empty attribute key!"} 264 | if (self.next() === "=") { 265 | // parse attribute value 266 | if (LITERAL_DELIMITERS.includes(self.src[self.index + 1])) self.index++ 267 | const delim = LITERAL_DELIMITERS.includes(self.next()) ? self.next() : " \t\n]" 268 | self.update_src() 269 | const res = self.parseStr(delim, false) 270 | if (res.err) return {ok: null, err: res.err, rem: ""} 271 | self.index-- 272 | res.ok.replaceAll("\"", """).split(/ +/g).forEach((v) => { 273 | const res = insertValue(key, v) 274 | if (res) return res 275 | }) 276 | } else { 277 | const res = insertValue(key, true) 278 | if (res) return res 279 | } 280 | self.index++ 281 | } 282 | 283 | if (!self.remaining()) return {ok: null, err: "Unclosed attribute brackets", rem: ""} 284 | self.index++ 285 | self.col++ 286 | self.update_src() 287 | 288 | return {ok: attrsObj, err: null} 289 | } 290 | 291 | /** 292 | * Parse a tag name, classes, and id; then return the type, attributes, and additional objects 293 | * @param self {Parser} Parser instance 294 | * @param default_tag {string} default tag if the tag is implicit 295 | * @return {{ok: ({type: string, attrs: Object, additional: Object} | null), err: (null | string)}} 296 | */ 297 | export const parseTag = (self, default_tag) => { 298 | let type; 299 | let implicit; 300 | if (!">#.{[>}".includes(self.next())) { 301 | implicit = false 302 | type = "" 303 | while (self.remaining() && !"#. \t\n\"'`/>{[}".includes(self.next())) { 304 | type += self.next() 305 | self.index++ 306 | self.col++ 307 | } 308 | } else { 309 | implicit = true 310 | type = default_tag 311 | } 312 | const isVoid = VOID_ELEMENTS.includes(type) 313 | if (!self.remaining()) { 314 | return {ok: {type: type, attrs: {}, additional: {void: isVoid, implicit: implicit}}, err: null} 315 | } 316 | let attrs = {} 317 | // check for id 318 | if (self.next() === "#") { 319 | let id = "" 320 | self.index++ 321 | self.col++ 322 | while (self.remaining() && !">{.[ \t\n".includes(self.next())) { 323 | id += self.next() 324 | self.index++ 325 | self.col++ 326 | } 327 | attrs["id"] = id 328 | } 329 | if (!self.remaining()) { 330 | return {ok: {type: type, attrs: attrs, additional: {void: isVoid, implicit: implicit}}, err: null} 331 | } 332 | // check for classes 333 | if (self.next() === ".") { 334 | let class_ = "" 335 | while (self.remaining() && !">{[ \t\n".includes(self.next())) { 336 | class_ += self.next() 337 | self.index++ 338 | self.col++ 339 | } 340 | attrs["class"] = class_.replaceAll(".", " ").slice(1) 341 | } 342 | if (!self.remaining()) return { 343 | ok: {type: type, attrs: attrs, additional: {void: isVoid, implicit: implicit}}, 344 | err: null 345 | } 346 | 347 | // check for attributes 348 | if (self.next() === "[") { 349 | self.index++ 350 | self.update_src() 351 | const attr_res = self.parseAttrs(1, true, attrs) 352 | if (attr_res.err) return {ok: null, err: attr_res.err} 353 | attrs = attr_res.ok 354 | self.index = 0 355 | } 356 | return {ok: {type: type, attrs: attrs, additional: {void: isVoid, implicit: implicit}}, err: null} 357 | } 358 | -------------------------------------------------------------------------------- /src/token.js: -------------------------------------------------------------------------------- 1 | import {UNIQUE_ATTRS, BUILTIN_MACROS} from "./constants.js"; 2 | 3 | /** 4 | * Token class 5 | * 6 | * Contains 7 | */ 8 | export class Token { 9 | /** Token type (tag) 10 | * @type {string} */ 11 | type 12 | /** Token attributes object. Includes ID and classes 13 | * @type {Object} */ 14 | attributes 15 | /** Additional token information such as voidness 16 | * @type {Object} */ 17 | additional 18 | /** token child tokens 19 | * @type {(Token|string)[]} */ 20 | children 21 | /** 22 | * Token class 23 | * 24 | * Holds the type, attributes, and children of a given token 25 | * @param type {string} 26 | * @param attributes {Object} 27 | * @param additional {Object} 28 | * @param children {(Token|string)[]} 29 | */ 30 | constructor(type, attributes, additional = {}, children) { 31 | this.type = type 32 | this.attributes = attributes 33 | this.additional = additional 34 | this.children = children 35 | } 36 | 37 | /** 38 | * Stringify the token and child elements into HTML 39 | * @return {string} 40 | */ 41 | toString() { 42 | switch (this.type) { 43 | case "c t": 44 | return `` 45 | case "!DOCTYPE": 46 | return '' 47 | default: 48 | const attrs = Object.keys(this.attributes).length === 0 ? "" : " " + Object.entries(this.attributes) 49 | .reduce((acc, [k, v]) => v === true ? `${acc} ${k}` : `${acc} ${k}="${v}"`, "").slice(1) 50 | if (this.additional["void"]) return `<${this.type}${attrs}${this.type === "!DOCTYPE" ? "" : "/"}>` 51 | return `<${this.type}${attrs}>${ 52 | this.children.reduce((acc, tok) => `${acc}${tok.toString()}`, "") 53 | }` 54 | } 55 | } 56 | 57 | /** 58 | * Lint an element. kill me 59 | * @param ident{number} Indent depth 60 | * @param inline{boolean} true if the element is on the same line as its parent, false otherwise 61 | * @param opts{Object} Lint options 62 | * @return {string} 63 | */ 64 | lint(ident, inline, opts) { 65 | // additional modifying options 66 | switch (opts['lint.config.element_preference']) { 67 | case "arrow": 68 | if (this.children.length === 1) this.additional["inline"] = true 69 | break 70 | case "bracket": 71 | this.additional["inline"] = false 72 | break 73 | } 74 | if (opts['lint.config.remove_empty']) { 75 | if (this.children.length === 0) this.additional["void"] = true 76 | } 77 | 78 | // indentation prefix before the line 79 | let ident_str = opts['lint.config.indent.character'].repeat(inline ? 0 : ident * opts['lint.config.indent.count']) 80 | // comments are handled like strings but are caught here 81 | if (this.type === "c t") return `${ident_str}/* ${this.additional["value"].trim()} */\n` 82 | // get tag in accordance with config values 83 | const reduces_attrs = Object.entries(this.attributes).filter(([k, _]) => k !== "id" && k !== "class") 84 | const modified_attrs = Object.keys(reduces_attrs).length > 0 ? reduces_attrs.reduce((acc, [k, v]) => v === true ? `${acc} ${k}` : `${acc} ${k}="${v.replaceAll('"', '\\"')}"`, "").slice(1) : "" 85 | const full_tag_str = `${ 86 | (this.additional["implicit"] && !opts['lint.config.replace_implicit']) ? "" : this.type}${ 87 | this.attributes["id"] ? `#${this.attributes["id"]}` : ""}${ 88 | this.attributes["class"] ? `.${this.attributes["class"].replaceAll(" ", ".")}` : ""}${ 89 | modified_attrs ? `[${modified_attrs}]` : ""}` 90 | // if it's a void tag just return the tag 91 | if (this.additional["void"]) return `${ident_str}${full_tag_str}\n` 92 | // otherwise set up the return variable 93 | let out = `${ident_str}${full_tag_str}${" ".repeat(full_tag_str ? opts['lint.config.pre_tag_space'] : 0)}` + 94 | `${this.additional["inline"] ? `>${" ".repeat(opts['lint.config.inline_same_line'] ? opts['lint.config.post_tag_space'] : 0)}` : "{"}` 95 | if (this.additional["inline"] && opts['lint.config.inline_same_line']) { 96 | // inline elements have one child so we just append to the output string 97 | const child = this.children.pop() 98 | out += typeof child === "string" ? `"${child}"` : child.lint(ident, true, opts) 99 | } else { 100 | if (this.children.length !== 0) out += "\n" 101 | // reset the indent string. if the element is inline, the original will be an empty string so we update it to be properly indented 102 | ident_str = opts['lint.config.indent.character'].repeat(ident * opts['lint.config.indent.count']) 103 | out += this.children.reduce((acc, child) => { 104 | return acc + (typeof child === "string" ? `${ident_str}${opts['lint.config.indent.character']}"${child}"\n` : child.lint(ident + 1, false, opts)) 105 | }, "") 106 | if (!this.additional["inline"]) out += `${this.children.length !== 0 ? ident_str : " ".repeat(opts['lint.config.post_tag_space'])}}` 107 | } 108 | return out + (inline ? "" : "\n") 109 | } 110 | 111 | /** 112 | * Recursively counts the number of `:child` child elements under an element. Used for macros 113 | */ 114 | count_child() { 115 | let child_count = 0 116 | let isVoid = true 117 | let children = false 118 | this.children = this.children.map((child) => { 119 | if (typeof child !== "string") { 120 | if (child.type === ":child" || child.type === ":unwrap-child") { 121 | child_count++ 122 | isVoid = false 123 | } else if (child.type === ":children") { 124 | this.additional["children"] = true 125 | isVoid = false 126 | } else { 127 | const res = child.count_child() 128 | child = res.tok 129 | if (!children) children = child.additional["children"] 130 | if (isVoid) isVoid = res.isVoid 131 | if (![":consume", ":consume-all"].includes(child.type)) child_count += child.additional["child count"] 132 | } 133 | } 134 | return child 135 | }) 136 | this.additional["child count"] = child_count 137 | this.additional["children"] = children 138 | return {tok: this, isVoid: isVoid} 139 | } 140 | 141 | /** 142 | * Replace built-in macros (`:child` etc.) with the correct elements 143 | * @param elements {(Token | string)[]} 144 | * @return {(Token | string)[]} 145 | */ 146 | replace(elements) { 147 | switch (this.type) { 148 | case ":child": 149 | const child = elements.pop() 150 | return [child ? child : ""] 151 | case ":unwrap-child": 152 | return elements.at(-1)?.children ? elements.pop().children : [""] 153 | case ":children": 154 | const j = elements.length 155 | let out = [] 156 | for (let i = 0; i < j; i++) out.push(elements.pop()) 157 | return out 158 | case ":consume": 159 | if (elements.length >= this.additional["child count"]) { 160 | return this.children.reduce((acc, tok) => { 161 | if (typeof tok === "string") return {expand: [...acc.expand, tok], rem: acc.rem} 162 | const res = tok.replace(elements) 163 | return [...acc, ...res] 164 | }, []) 165 | } else return [] 166 | case ":consume-all": 167 | if (elements.length >= this.additional["child count"]) { 168 | let holdover = [] 169 | while (elements.length >= this.additional["child count"]) { 170 | holdover = this.children.reduce((acc, tok) => { 171 | if (typeof tok === "string") return [...acc, tok] 172 | const res = tok.replace(elements) 173 | return [...acc, ...res] 174 | }, holdover) 175 | } 176 | return holdover 177 | } else return [] 178 | default: 179 | const inner = this.children.reduce((acc, tok) => { 180 | if (typeof tok === "string") return [...acc, tok] 181 | const res = tok.replace(elements) 182 | return [...acc, ...res] 183 | }, []) 184 | return [new Token(this.type, this.attributes, this.additional, inner)] 185 | } 186 | } 187 | 188 | /** 189 | * Expands a macro with error handling 190 | * @param p {Parser} Parser instance 191 | * @return {{ok: (Token|string)[]|null, err: null|string}} Cloned token 192 | */ 193 | expand(p) { 194 | // Get replaced children 195 | let new_children = [] 196 | for (let i = 0; i < this.children.length; i++) { 197 | if (typeof this.children[i] === "string") new_children.push(this.children[i]) 198 | else { 199 | const res = this.children[i].expand(p) 200 | if (res.err) return {ok: null, err: res.err} 201 | new_children = [...new_children, ...res.ok] 202 | } 203 | } 204 | // Return a cloned token or expanded macro 205 | if (this.type[0] === ":" && !BUILTIN_MACROS.includes(this.type)) { 206 | const {ok, err} = p.get_macro(this.type.slice(1)) 207 | if (err) return {ok: null, err: err} 208 | if (typeof ok === "function") return {ok: ok(new_children), err: null} 209 | if (ok.void) { 210 | const res = ok.get_rep(p) 211 | if (res.err) return {ok: null, err: err} 212 | return {ok: [...res.ok, ...new_children], err: null} 213 | } else return ok.expand(new_children, this.attributes, p) 214 | } else return { 215 | ok: [new Token( 216 | this.type, 217 | Object.assign({}, this.attributes), 218 | Object.assign({}, this.additional), 219 | new_children 220 | )], 221 | err: null 222 | } 223 | } 224 | } 225 | 226 | /** 227 | * Macro class 228 | * 229 | * Representation of macros as per their definition 230 | */ 231 | export class Macro { 232 | /** 233 | * @param rep {(Token|string)[]} Replacement object array 234 | * @param isVoid {boolean} Does the macro accept child elements 235 | * @param name {string} Macro name 236 | * @param def {{file: string, line: number, col: number}} 237 | * @return {Macro} 238 | */ 239 | constructor(rep, isVoid, name, def) { 240 | this.rep = rep 241 | this.void = isVoid 242 | this.name = name 243 | this.def = def 244 | return this 245 | } 246 | 247 | /** 248 | * Replace elements with a macro expansion. Requires the macro inputs to be nested objects not a token stream 249 | * @param elements {(Token | string)[]} 250 | * @param attrs {Object} Attributes on the macro call (ID and classes) 251 | * @param parser {Parser} Parser class instance calling macro expansion 252 | * @return {{ok: (Token | string)[] | null, err: (string | null)}} 253 | */ 254 | expand(elements, attrs, parser) { 255 | // Reverse elements so `.pop()` return the next element, not next last 256 | elements.reverse() 257 | // Replace all the built-in macros (`:child` etc.) with the correct element(s) 258 | let child_replaced = [] 259 | for (let i = 0; i < this.rep.length; i++) { 260 | if (typeof this.rep[i] === "string") child_replaced.push(this.rep[i]) 261 | else child_replaced = [...child_replaced, ...this.rep[i].replace(elements)] 262 | } 263 | // Expand the macros or clone tokens for the macr definition 264 | let rep = [] 265 | for (let i = 0; i < child_replaced.length; i++) { 266 | if (typeof child_replaced[i] === "string") rep.push(child_replaced[i]) 267 | else { 268 | const res = child_replaced[i].expand(parser) 269 | if (res.err) return {ok: null, err: `${res.err}\n\tunder macro call :${this.name}(${this.def.file} ${this.def.line}:${this.def.col})`} 270 | else rep = [...rep, ...res.ok] 271 | } 272 | } 273 | // Apply attributes to the root element(s) of the expanded macro 274 | if (rep.length === 0) delete attrs["id"] 275 | if (rep.length === 1) { 276 | if (typeof rep[0] !== "string" && attrs["id"] !== undefined) { 277 | rep[0].attributes["id"] = attrs["id"] 278 | } 279 | delete attrs["id"] 280 | } 281 | if (attrs["id"] !== undefined) return {ok: null, err: "ID cannot be applied to a macro with multiple root elements"} 282 | rep.map((t) => { 283 | if (typeof t === "string") return t 284 | for (const attr in attrs) { 285 | if (UNIQUE_ATTRS.includes(attr)) t.attributes[attr] = attrs[attr] 286 | else { 287 | // check if attr already exists 288 | if (t.attributes[attr] !== undefined) { 289 | if (typeof t.attributes[attr] === "string") t.attributes[attr] += " " + attrs[attr] 290 | else t.attributes[attr] = attrs[attr] 291 | } else { 292 | t.attributes[attr] = attrs[attr] 293 | } 294 | } 295 | } 296 | return t 297 | }) 298 | return {ok: rep, err: null} 299 | } 300 | 301 | /** 302 | * Gets a clone of a macro replacement where the replacement is expanded properly 303 | * @param p {Parser} Parser instance 304 | * @return {{err: null, ok: *[]}} 305 | */ 306 | get_rep = (p) => { 307 | let rep = [] 308 | this.rep.map((t) => { 309 | if (typeof t === "string") rep.push(t) 310 | else { 311 | const {ok, err} = t.expand(p) 312 | if (err) return {ok: null, err: err} 313 | rep = [...rep, ...ok] 314 | } 315 | }) 316 | return {ok: rep, err: null} 317 | } 318 | } 319 | 320 | /** 321 | * Default macros. These also show the two types of macro. Firstly, the `:root` macro is a class macro meaning it could 322 | * be defined by the user. These are the standard macros. The `:unwrap` macro is a function macro and can only be 323 | * defined internally. These macros are just functions that take one argument, macro call child elements of type 324 | * `Array`, and return the same type. The child elements passed are already expanded. Function macros 325 | * cannot return errors. 326 | * @type {{root: Macro, unwrap: (function(Array): Array)}} 327 | */ 328 | export const DEFAULT_MACROS = { 329 | "root": new Macro([ 330 | new Token("!DOCTYPE", {html: true}, {}, []), 331 | new Token("html", {lang: "en"}, {"child count": 0, "children": true}, [new Token(":children", {}, {}, [])]) 332 | ], false, "root", {file: "Built-in", col: 0, line: 0}), 333 | "unwrap": (c) => { 334 | let out = [] 335 | c.forEach((t) => { 336 | if (typeof t === "string") out.push(t) 337 | else { 338 | if (t.type === "c t") out.push(t) 339 | else out = [...out, ...t.children] 340 | } 341 | }) 342 | return out 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /test/macros.tests.mjs: -------------------------------------------------------------------------------- 1 | import {strictEqual as equal, notStrictEqual as nequal} from "assert" 2 | import {fullStringify} from "../src/parser/parser.js"; 3 | 4 | const p = (src) => { 5 | const {ok, err} = fullStringify(src, "test env") 6 | return err ? err : ok 7 | } 8 | 9 | const f = (src) => fullStringify(src, "test env").err 10 | 11 | describe("Macros", () => { 12 | describe("Built-in macros", () => { 13 | describe(":child", () => { 14 | it("Correct elements", () => { 15 | equal(p("--test > :child :test > 'test'"), "test") 16 | equal(p("--test { :child :child } :test { 'test' 'macro' }"), "testmacro") 17 | }) 18 | it("Uses too many elements", () => { 19 | equal(p("--test > :child :test"), "") 20 | equal(p("--test { :child :child } :test { 'test' }"), "test") 21 | }) 22 | it("Uses too few elements", () => { 23 | equal(p("--test > :child :test { 'test' 'macro' }"), "test") 24 | equal(p("--test { :child :child } :test { 'test' 'macro' 'inner' }"), "testmacro") 25 | }) 26 | }) 27 | describe(":children", () => { 28 | it("Existing children", () => { 29 | equal(p("--test > :children :test"), "") 30 | equal(p("--test > :children :test > 'test'"), "test") 31 | equal(p("--test > :children :test { 'test' 'macro' }"), "testmacro") 32 | }) 33 | it("With :child after", () => { 34 | equal(p("--test { :children :child } :test"), "") 35 | equal(p("--test { :children :child } :test > 'test'"), "test") 36 | equal(p("--test { :children :child } :test { 'test' 'macro' }"), "testmacro") 37 | }) 38 | it("With :child before", () => { 39 | equal(p("--test { :child :children } :test"), "") 40 | equal(p("--test { :child :children } :test > 'test'"), "test") 41 | equal(p("--test { :child :children } :test { 'test' 'macro' }"), "testmacro") 42 | }) 43 | }) 44 | describe(":consume", () => { 45 | it('with required elements', () => { 46 | equal(p("--test > :consume > :child :test > 'test'"), "test") 47 | equal(p("--test > :consume { :child :child } :test { 'test' 'macro' }"), "testmacro") 48 | }) 49 | it('without required elements', () => { 50 | equal(p("--test > :consume > :child :test"), "") 51 | equal(p("--test > :consume { :child :child } :test > 'test'"), "") 52 | }) 53 | it('get elements after', () => { 54 | equal(p("--test { :consume > :child :child } :test { 'test' 'macro' }"), "testmacro") 55 | equal(p("--test { :consume { :child :child } :child } :test { 'test' 'macro' 'again' }"), "testmacroagain") 56 | }) 57 | it("3 element list with :consume", () => { 58 | equal( 59 | p(`--list { ul { 60 | :consume > li > :child 61 | :consume > li > :child 62 | :consume > li > :child 63 | }} 64 | :list {"Text 1" "Text 2" "Text 3"} 65 | :list {"Text 1" "Text 2"} 66 | `), 67 | `
  • Text 1
  • Text 2
  • Text 3
  • Text 1
  • Text 2
` 68 | ) 69 | }) 70 | }) 71 | describe(":consume-all", () => { 72 | it('with required elements once', () => { 73 | equal(p("--test > :consume-all > :child :test > 'test'"), "test") 74 | equal(p("--test > :consume-all { :child :child } :test { 'test' 'macro' }"), "testmacro") 75 | }) 76 | it('with required elements more than once', () => { 77 | equal(p("--test > :consume-all > :child :test { 'test' 'macro' }"), "testmacro") 78 | equal(p("--test > :consume-all { :child :child } :test { 'test' 'macro' 'test2' 'macro2' }"), "testmacrotest2macro2") 79 | }) 80 | it('without required elements', () => { 81 | equal(p("--test > :consume-all > :child :test"), "") 82 | equal(p("--test > :consume-all { :child :child } :test > 'test'"), "") 83 | }) 84 | it('get elements after', () => { 85 | equal(p("--test { :consume-all > :child :child } :test { 'test' 'macro' }"), "testmacro") 86 | equal(p("--test { :consume-all { :child :child } :child } :test { 'test' 'macro' 'again' }"), "testmacroagain") 87 | }) 88 | }) 89 | describe(":root", () => { 90 | it('Default', () => { 91 | equal(p(":root"), ``) 92 | equal(p(":root > 'Text'"), `Text`) 93 | }) 94 | it("Altered lang attribute", () => { 95 | equal(p(`:root[lang=en-CA]`), ``) 96 | equal(p(`:root[lang="en-CA"]`), ``) 97 | }) 98 | }) 99 | it(":unwrap", () => { 100 | equal(p(":unwrap"), ``) 101 | equal(p(`:unwrap { 102 | "Text 1" 103 | {{"Text 2"}{p}} 104 | // A comment 105 | {} 106 | }`), 107 | `Text 1
Text 2

` 108 | ) 109 | }) 110 | }) 111 | it("Nested :consume and :consume-all", () => { 112 | equal(p("--test > :consume { :child :consume > :child } :test { 't1' 't2' } :test > 't1'"), "t1t2t1") 113 | }) 114 | it("Nested macro calls", () => { 115 | equal(p("--m1{'words ':children}--m2{'more words ':m1{:child 'final words'}}:m2>'middle '"), "more words words middle final words") 116 | }) 117 | it("Simple replacements", () => { 118 | equal(p("--macro-string > 'Hello, world!' :macro-string"), "Hello, world!") 119 | equal(p("--macro-multi-string { 'Hello,'' world!' } :macro-multi-string"), "Hello, world!") 120 | equal(p("--macro-multi-string > { 'Hello,'' world!' } :macro-multi-string"), "
Hello, world!
") 121 | equal(p("--macro-multi-string {{ 'Hello,'' world!'}} :macro-multi-string"), "
Hello, world!
") 122 | equal(p("--nested-divs { {} {{}} } :nested-divs"), "
") 123 | equal(p("--nested-divs > { {} {{}} } :nested-divs"), "
") 124 | 125 | equal(p(`--head-base { 126 | script[src=script1.js] 127 | link[rel='stylesheet' href='style.css'] 128 | } :head-base.class`), 129 | ``) 130 | }) 131 | describe("Attributes", () => { 132 | it('Single root elements', () => { 133 | equal(p(`--test > p > "test" :test.class1`), `

test

`) 134 | equal(p(`--test > "test" :test.class1`), `test`) 135 | equal(p(`--test > p > "test" :test[attr=value]`), `

test

`) 136 | equal(p(`--test > "test" :test[attr=value]`), `test`) 137 | equal(p(`--test > p > "test" :test#id`), `

test

`) 138 | equal(p(`--test > "test" :test#id`), `test`) 139 | }) 140 | it('Multiple root elements', () => { 141 | equal(p(`--test { p > "test" h1 > "other" } :test.class1`), `

test

other

`) 142 | equal(p(`--test { p > "test" h1 > "other" } :test[attr=value]`), `

test

other

`) 143 | nequal(f(`--test { p > "test" h1 > "other" } :test#id`), "") 144 | }) 145 | }) 146 | it("Void macro with implicit div after", () => { 147 | equal(p("--test > 'test' :test > 'other'"), `test
other
`) 148 | }) 149 | describe("Shadowing macros", () => { 150 | it("Correctly shadowing macros", () => { 151 | equal(p(`--test > "outer" { --test > "inner" :test } :test`), "
inner
outer") 152 | equal(p(`--test { "outer " :child } { --test { "inner " :child } :test > "macro" } :test > "parent macro"`), "
inner macro
outer parent macro") 153 | }) 154 | it("Attempted shadowing with change of voidness", () => { 155 | nequal(f(`--test > "outer" { --test > :child :test > "fail" } :test`), "") 156 | nequal(f(`--test > :child { --test > "fail" :test > "fail" } :test`), "") 157 | nequal(f(`--test { "outer " :child } { --test > "inner :test } :test > "parent macro"`), "") 158 | nequal(f(`--test > "test" { --test { "fail" :consume > :child } :test } :test`), "") 159 | }) 160 | }) 161 | describe("Other things", () => { 162 | it('Calling a macro from under another macro call with :children', () => { 163 | equal(p(`--replace { "t1" "t2" "t3" } 164 | --divify > :consume-all >> :child 165 | :divify > :replace 166 | --pass > :divify > :children 167 | :pass { "t1" "t2" "t3" } 168 | :pass > :replace`), 169 | `
t1
t2
t3
t1
t2
t3
t1
t2
t3
` 170 | ) 171 | 172 | equal(p(` 173 | --paragraph-me { 174 | :consume-all > p > :child 175 | } 176 | 177 | 178 | --call-pm { 179 | h1 > :child 180 | :paragraph-me > :children 181 | } 182 | 183 | :call-pm { 184 | "Heading" 185 | "Para 1" 186 | "Para 2" 187 | "Para 3" 188 | } 189 | `), 190 | `

Heading

Para 1

Para 2

Para 3

` 191 | ) 192 | }) 193 | it('Splitting single macro passed into multiple children', () => { 194 | equal(p(` 195 | --macro-to-be-passed { 196 | "Child 1 string" 197 | {"Child 2 inside implicit div"} 198 | } 199 | 200 | --macro-to-pass-to { 201 | { 202 | h2 > "First child" 203 | :child 204 | } 205 | { 206 | h2 > "Second child" 207 | :child 208 | } 209 | { 210 | h3 > "Another child" 211 | :child 212 | } 213 | } 214 | 215 | :macro-to-pass-to { 216 | :macro-to-be-passed 217 | "Additional child" 218 | } 219 | `), 220 | `

First child

Child 1 string

Second child

Child 2 inside implicit div

Another child

Additional child
` 221 | ) 222 | }) 223 | it('Calling a macro from under another macro with children from above macro', () => { 224 | equal(p(` 225 | --macro > { 226 | span > "Stuff in a span" 227 | :children 228 | } 229 | 230 | --another-macro { 231 | h1 > "Heading inside macro" 232 | 233 | :macro.macro-div { 234 | {:child} 235 | "defined in definition" 236 | :child 237 | {span > "Some other text" } 238 | } 239 | :children 240 | } 241 | :another-macro { 242 | "defined outside definition 1" 243 | "defined outside definition 2" 244 | > "Extra 1" 245 | p > "Extra 2" 246 | } 247 | `), 248 | `

Heading inside macro

Stuff in a span
defined outside definition 1
defined in definitiondefined outside definition 2
Some other text
Extra 1

Extra 2

` 249 | ) 250 | }) 251 | 252 | it('Macros with classes applied to route', () => { 253 | equal(p("--macroWith > .test1.test2\n--macroWithout > {}\n:macroWith\n:macroWith.test3.test4\n:macroWithout.test3.test4"), `
`) 254 | }) 255 | }) 256 | describe("Sample macros", () => { 257 | it("3 element list", () => { 258 | equal( 259 | p(` 260 | --list { 261 | ul { 262 | li > :child 263 | li > :child 264 | li > :child 265 | } 266 | } 267 | :list {"Text 1" "Text 2" "Text 3"} 268 | :list {"Text 1" "Text 2"} 269 | `), 270 | `
  • Text 1
  • Text 2
  • Text 3
  • Text 1
  • Text 2
` 271 | ) 272 | }) 273 | it("Better list", () => { 274 | equal( 275 | p("--better-list { ul > :consume-all > li > :child } :better-list :better-list > 'elem1' :better-list { 'elem1' 'elem2' 'elem3' }"), 276 | `
    • elem1
    • elem1
    • elem2
    • elem3
    ` 277 | ) 278 | }) 279 | it("Section macro", () => { 280 | equal( 281 | p(` 282 | --section-macro > section { 283 | h2 > :child 284 | div > :children 285 | } 286 | :section-macro {"Heading 1" p > "Paragraph 1" p > "Paragraph 2"} 287 | :section-macro#section-id {"Heading 1" p > "Paragraph 1" p > "Paragraph 2" p > "Paragraph 3"} 288 | `), 289 | `

    Heading 1

    Paragraph 1

    Paragraph 2

    Heading 1

    Paragraph 1

    Paragraph 2

    Paragraph 3

    ` 290 | ) 291 | }) 292 | it("Section with heading and inner list", () => { 293 | equal(p(`--list { 294 | h2 > :child 295 | ul { 296 | :consume-all > li > :child 297 | } 298 | } 299 | :list {"Heading" "Text 1"} 300 | :list {"Heading" "Text 1" "Text 2" "Text 3"}`), 301 | `

    Heading

    • Text 1

    Heading

    • Text 1
    • Text 2
    • Text 3
    ` 302 | ) 303 | }) 304 | it("2 column table", () => { 305 | equal( 306 | p(` 307 | --table > table { 308 | thead { tr { th > :child th > :child } } 309 | tbody > :consume-all { 310 | tr { td > :child td > :child } 311 | } 312 | } 313 | :table#test-id { 314 | "Column 1" "Column 2" 315 | "Row 1" "Value 1" 316 | "Row 2" "Value 2" 317 | "Row 3" 318 | } 319 | `), 320 | `
    Column 1Column 2
    Row 1Value 1
    Row 2Value 2
    `) 321 | }) 322 | }) 323 | describe("Importing macros", () => { 324 | it("Simple imports", () => { 325 | equal(p(`@import ./test/importable_macros 326 | :external-macro`), `Hello, world!`) 327 | equal(p(`@import ./test/importable_macros.hbml 328 | :external-macro2`), `

    Hello, world!

    `) 329 | }) 330 | it("Namespaced imports", () => { 331 | equal(p(`@import ./test/importable_macros.hbml namespace 332 | :namespace:external-macro`), `Hello, world!`) 333 | equal(p(`@import ./test/importable_macros.hbml namespace 334 | :namespace:external-macro3 > "test"`), `testHello, world!`) 335 | }) 336 | }) 337 | }) 338 | -------------------------------------------------------------------------------- /bundle/hbml_browser.min.js: -------------------------------------------------------------------------------- 1 | /*! hbml 0.1.0 2023-01-02 19:49 */ 2 | const LITERAL_DELIMITERS="\"'`",VOID_ELEMENTS=["area","base","br","col","embed","hr","img","input","link","meta","param","source","track","wbr","!DOCTYPE",":child",":children"],INLINE_ELEMENTS=["abbr","acronym","audio","b","bdi","bdo","big","br","button","canvas","cite","code","data","datalist","del","dfn","em","embed","i","iframe","img","input","ins","kbd","label","map","mark","meter","noscript","object","output","picture","progress","q","ruby","s","samp","script","select","slot","small","span","strong","sub","sup","svg","template","textarea","time","u","tt","var","video","wbr"],UNIQUE_ATTRS=["lang","id"],BUILTIN_MACROS=[":child",":children",":consume",":consume-all"],DEFAULT_ALLOW={write:!1,not_found:!1,parse:!1},CONFIG_DEFAULTS={"lint.src":["/"],"lint.output":"/","lint.allow.not_found":!1,"lint.allow.write":!1,"lint.allow.parse":!1,"lint.config.indent.character":"\t","lint.config.indent.count":1,"lint.config.pre_tag_space":1,"lint.config.post_tag_space":1,"lint.config.inline_same_line":!0,"lint.config.keep_implicit":!0,"lint.config.void_inline":!0,"lint.config.element_preference":"preserve","lint.config.remove_empty":!1,"build.src":["/"],"build.output":"html","build.allow.not_found":!1,"build.allow.write":!1,"build.allow.parse":!1};class Token{type;attributes;additional;children;constructor(e,r,t={},n){this.type=e,this.attributes=r,this.additional=t,this.children=n}toString(){switch(this.type){case"c t":return``;case"!DOCTYPE":return"";default:var e=0===Object.keys(this.attributes).length?"":" "+Object.entries(this.attributes).reduce((e,[r,t])=>!0===t?e+" "+r:e+` ${r}="${t}"`,"").slice(1);return this.additional.void?`<${this.type}${e}${"!DOCTYPE"===this.type?"":"/"}>`:`<${this.type}${e}>${this.children.reduce((e,r)=>""+e+r.toString(),"")}`}}lint(t,e,n){switch(n["lint.config.element_preference"]){case"arrow":1===this.children.length&&(this.additional.inline=!0);break;case"bracket":this.additional.inline=!1}n["lint.config.remove_empty"]&&0===this.children.length&&(this.additional.void=!0);let i=n["lint.config.indent.character"].repeat(e?0:t*n["lint.config.indent.count"]);if("c t"===this.type)return`${i}/* ${this.additional.value.trim()} */\n`;var r=Object.entries(this.attributes).filter(([e])=>"id"!==e&&"class"!==e),r=0!0===t?e+" "+r:`${e} ${r}="${t.replaceAll('"','\\"')}"`,"").slice(1):"",r=""+(this.additional.implicit&&!n["lint.config.replace_implicit"]?"":this.type)+(this.attributes.id?"#"+this.attributes.id:"")+(this.attributes.class?"."+this.attributes.class.replaceAll(" ","."):"")+(r?`[${r}]`:"");if(this.additional.void)return i+r+` 3 | `;let l=i+r+" ".repeat(r?n["lint.config.pre_tag_space"]:0)+(this.additional.inline?">"+" ".repeat(n["lint.config.inline_same_line"]?n["lint.config.post_tag_space"]:0):"{");return this.additional.inline&&n["lint.config.inline_same_line"]?(r=this.children.pop(),l+="string"==typeof r?`"${r}"`:r.lint(t,!0,n)):(0!==this.children.length&&(l+="\n"),i=n["lint.config.indent.character"].repeat(t*n["lint.config.indent.count"]),l+=this.children.reduce((e,r)=>e+("string"==typeof r?""+i+n["lint.config.indent.character"]+`"${r}" 4 | `:r.lint(t+1,!1,n)),""),this.additional.inline||(l+=`${0!==this.children.length?i:" ".repeat(n["lint.config.post_tag_space"])}}`)),l+(e?"":"\n")}count_child(){let t=0,n=!0,i=!1;return this.children=this.children.map(e=>{var r;return"string"!=typeof e&&(":child"===e.type||":unwrap-child"===e.type?(t++,n=!1):":children"===e.type?(this.additional.children=!0,n=!1):(e=(r=e.count_child()).tok,i=i||e.additional.children,n=n&&r.isVoid,[":consume",":consume-all"].includes(e.type)||(t+=e.additional["child count"]))),e}),this.additional["child count"]=t,this.additional.children=i,{tok:this,isVoid:n}}replace(t){switch(this.type){case":child":var e=t.pop();return[e||""];case":unwrap-child":return t.at(-1)?.children?t.pop().children:[""];case":children":var r=t.length,n=[];for(let e=0;e=this.additional["child count"]?this.children.reduce((e,r)=>{return"string"==typeof r?{expand:[...e.expand,r],rem:e.rem}:(r=r.replace(t),[...e,...r])},[]):[];case":consume-all":if(t.length>=this.additional["child count"]){let e=[];for(;t.length>=this.additional["child count"];)e=this.children.reduce((e,r)=>{return"string"==typeof r?[...e,r]:(r=r.replace(t),[...e,...r])},e);return e}return[];default:e=this.children.reduce((e,r)=>{return"string"==typeof r?[...e,r]:(r=r.replace(t),[...e,...r])},[]);return[new Token(this.type,this.attributes,this.additional,e)]}}expand(r){let t=[];for(let e=0;e{if("string"!=typeof e)for(const r in t)!UNIQUE_ATTRS.includes(r)&&void 0!==e.attributes[r]&&"string"==typeof e.attributes[r]?e.attributes[r]+=" "+t[r]:e.attributes[r]=t[r];return e}),{ok:l,err:null})}get_rep=t=>{let n=[];return this.rep.map(e=>{if("string"==typeof e)n.push(e);else{var{ok:e,err:r}=e.expand(t);if(r)return{ok:null,err:r};n=[...n,...e]}}),{ok:n,err:null}}}const DEFAULT_MACROS={root:new Macro([new Token("!DOCTYPE",{html:!0},{},[]),new Token("html",{lang:"en"},{"child count":0,children:!0},[new Token(":children",{},{},[])])],!1,"root",{file:"Built-in",col:0,line:0}),unwrap:e=>{let r=[];return e.forEach(e=>{"string"==typeof e||"c t"===e.type?r.push(e):r=[...r,...e.children]}),r}};class Error{constructor(e,r,t,n){this.desc=e,this.file=r,this.ln=t,this.col=n}toString(){return`${this.desc} ${this.file} ${this.ln}:`+this.col}}const convert=r=>{if("#text"===r.nodeName)return""===r.data.trim()?null:r.data;if("#comment"===r.nodeName)return new Token("c t",{},{value:r.nodeValue},[]);let t=[];r.childNodes.forEach(e=>{e=convert(e);null!==e&&t.push(e)});var n={};for(let e=0;e{let r=[];return(new DOMParser).parseFromString(e,"text/html").body.childNodes.forEach(e=>{e=convert(e);null!==e&&r.push(e)}),r.map(e=>"object"==typeof e?e.lint(0,!1,lint_opts):e).join("")},full=e=>{let r=[];e=(new DOMParser).parseFromString(e,"text/html").head.parentElement;return e.childNodes.forEach(e=>{e=convert(e);null!==e&&r.push(e)}),new Token(":root","en"!==e.lang?{lang:e.lang}:{},{},r).lint(0,!1,lint_opts)};const handleImport=e=>{e.index+=7,e.st();let r="";for(;e.remaining()&&!" \t\n".includes(e.next());)r+=e.next(),e.index++,e.col++;let t="";if(e.remaining()&&"\n"!==e.next()){for(e.st();e.remaining()&&!".#[{>]}'\"` \t\n".includes(e.next());)t+=e.next(),e.index++,e.col++;t+=":"}let n;if(r.startsWith("http")){var i=new XMLHttpRequest;if(i.open("GET",r,!1),i.send(null),200!==i.status)return{ok:null,err:`Unable to access ${r} (response ${i.status} '${i.responseText}')`};n=i.responseText}else{switch(r=npath.join(npath.isAbsolute(r)?"":process.cwd(),r),npath.extname(r)){case"":r+=".hbml";break;case".hbml":break;default:return{ok:null,err:`Cannot import non-HBML file ${r}!`+fs.existsSync(r)?"":"File also doesn't exist!"}}if(!fs.existsSync(r))return{ok:null,err:`Imported file ${r} does not exist`};n=fs.readFileSync(r).toString()}var{ok:l,err:i}=e.new(n,r,!0).import_parse(t);if(null!==i)return{ok:null,err:`Error importing file ${r} (${i.toString()})`};e.update_src();for(const s in l){if(void 0!==e.macros[e.macros.length-1][s])return{ok:null,err:"Cannot redefine macros through imports. Try using a namespace instead"};e.macros[e.macros.length-1][s]=l[s]}return{ok:null,err:null}},import_parse=(e,r)=>{var t=e.parse()["err"];if(null!==t)return{ok:null,err:t};let n=Object.assign({},e.macros[0]);if(Object.keys(DEFAULT_MACROS).forEach(e=>delete n[e]),""===r)return{ok:n,err:null};const i=e=>"string"==typeof e?e:(":"!==e.type[0]||BUILTIN_MACROS.includes(e.type)||(e.type=":"+r+e.type.slice(1)),new Token(e.type,e.attributes,e.additional,e.children.map(e=>i(e))));var l={};for(const s in n)l[""+r+s]=new Macro(n[s].rep.map(e=>i(e)),n[s].void,""+r+s,{file:e.path,col:e.col,line:e.ln});return{ok:l,err:null}},handleInsert=e=>{e.index+=7,e.st();let r="";for(;e.remaining()&&!" \t\n".includes(e.next());)r+=e.next(),e.index++,e.col++;e.update_src();let t;if(r.startsWith("http")){var n=new XMLHttpRequest;if(n.open("GET",r,!1),n.send(null),200!==n.status)return{ok:null,err:`Unable to access ${r} (response ${n.status} '${n.responseText}')`};t=n.responseText}else{if(r=npath.join(npath.isAbsolute(r)?"":process.cwd(),r),!fs.existsSync(r))return{ok:null,err:`Inserted file ${r} does not exist`};t=".hbml"===npath.extname(r)?fs.readFileSync(r).toString():"`"+fs.readFileSync(r).toString().replace("`","\\`")+"`"}var n=e.new(t,r,!0),{ok:i,err:l}=n.parse();if(null!==l)return{ok:null,err:`Error importing file ${r} (${l.toString()})`};const s=Object.assign({},n.macros[0]);return Object.keys(DEFAULT_MACROS).forEach(e=>delete s[e]),e.macros[e.macros.length-1]={...e.macros[e.macros.length-1],...s},{ok:i,err:null}};const get_macro=(e,r)=>{let t=void 0,n=e.macros.length;for(;0{var r,t,{ok:n,err:i}=e.parse_inner("div",!1,!0);return i?{ok:null,err:i}:(r=(i=n[0].count_child()).tok,(t=e.get_macro(r.type)).ok&&t.ok.void!==i.isVoid?{ok:null,err:"Macro redefinitions must preserve voidness"}:(e.macros[e.macros.length-1][r.type]=new Macro(r.children,i.isVoid,r.type,{file:e.path,col:e.col,line:e.ln}),e.update_src(),e.isBuild?{ok:null,err:null}:{ok:[new Token("--"+n[0].type,n[0].attributes,{...n[0].additional,void:0===r.children.length},r.children)],err:null}))};const parse_inner=(e,r,t,n)=>{if(e.stn(),!e.remaining()||"}"===e.next())return{ok:null,err:null};if(LITERAL_DELIMITERS.includes(e.next()))return(i=e.parseStr(e.next(),t)).err?{ok:null,err:i.err}:{ok:[i.ok],err:null};if("-"===e.next()&&"-"===e.src[e.index+1])return e.index+=2,e.parse_macro_def();if("/"===e.next())return e.src=e.src.slice(e.index),(i=e.parseComment()).err?{ok:null,err:i.err}:{ok:[i.ok],err:null};if(e.src.startsWith("@import",e.index))return e.handleImport();if(e.src.startsWith("@insert",e.index))return e.handleInsert();var i=e.parseTag(r);if(i.err)return{ok:null,err:i.err};var{type:i,attrs:l,additional:s}=i.ok;let o=void 0;if(":"===i[0]&&!BUILTIN_MACROS.includes(i)){if(":"===i)return{ok:null,err:"Macro cannot have an empty name"};if(!n){var{ok:a,err:c}=e.get_macro(i.slice(1));if(null===a&&e.isBuild)return{ok:null,err:c};if((o=a).void)return e.update_src(),e.isBuild?{ok:a.expand([],l,e).ok,err:null}:{ok:[new Token(i,l,s,[])],err:null}}}if(s.void)return e.update_src(),{ok:[new Token(i,l,s,[])],err:null};let d=[];if(r=INLINE_ELEMENTS.includes(i)?"span":"div",t="style"!==i,e.st(),!e.remaining())return e.update_src(),void 0===o?{ok:[new Token(i,l,s,[])],err:null}:"object"==typeof o?o.expand([],l,e):{ok:o([]),err:null};if(">"===e.next()){s.inline=!0,e.index++,e.col++,e.update_src(),e.macros.push({});c=e.parse_inner(r,t,n);if(e.macros.pop(),c.err)return{ok:null,err:c.err};null!==c.ok&&(d=[...d,...c.ok])}else if("{"===e.next()){for(e.macros.push({}),e.index++,e.col++,e.update_src();e.remaining()&&"}"!==e.next();){var u=e.parse_inner(r,t,n);if(u.err)return{ok:null,err:u.err,rem:""};null!==u.ok&&(d=[...d,...u.ok]),e.stn()}if(!e.remaining())return{ok:null,err:"Unclosed block! Expected closing '}' found EOF!"};e.macros.pop(),e.index++,e.col++}return e.update_src(),void 0!==o&&e.isBuild?"object"==typeof o?o.expand(d,l,e):{ok:o(d),err:null}:{ok:[new Token(i,l,s,d)],err:null}},convertReservedChar=(e,r)=>{var t={"<":"<",">":">"};for(const n in t)r=r.replaceAll(n,t[n]);return r},parseComment=e=>{let r="";var t="*"===e.src[1];if(!t&&"/"!==e.src[1])return{ok:null,err:"Expected '*' or '/' after /"};for(e.index=2;;){if(!e.remaining()){if(t)return{ok:null,err:"Expected end of comment! Found EOF"};break}var n=e.next();if(e.index++,e.col++,t&&"*"===n&&"/"===e.next()){e.index++;break}if("\n"===n){if(!t)break;r+=n,e.col=0,e.ln++}else r+=n}return e.update_src(),{ok:new Token("c t",{},{value:r},[]),err:null}},parseStr=(e,r,t)=>{e.index++,e.col++;let n="",i=!1;for(;e.remaining();){if("\\"!==e.next()||i){if(r.includes(e.next())&&!i){"]"===e.next()&&e.index--;break}"\n"===e.next()?(e.col=0,e.ln++,"`"===r&&(n+="\n")):(i=!1,n+=e.next())}else i=!0;e.index++,e.col++}return e.remaining()?(e.index++,e.col++,e.update_src(),{ok:t?e.convertReservedChar(n):n,err:null}):{ok:null,err:"Unclosed string"}},parseAttrs=(t,n,e,r)=>{let i=r;const l=(e,r)=>{if(void 0===i[e])i[e]=r;else if(UNIQUE_ATTRS.includes(e))switch(n){case 0:i[e]=r;break;case 1:console.log(chalk.yellow(`Duplicate unique value found (${i[e]} and ${r}) ${t.file} ${t.line}:`+t.col)),i[e]=r;break;default:return{ok:null,err:`Duplicate unique value found (${i[e]} and ${r})`,rem:""}}else switch(typeof i[e]+" "+typeof r){case"string string":i[e]+=" "+r;break;case"boolean string":i[e]=r}};for(;t.remaining()&&"]"!==t.next();){t.stn();let r="";for(;t.remaining()&&!" \t\n='\"`]".includes(t.next());)r+=t.next(),t.index++,t.col++;if("]"===t.next()){if(r){var s=l(r,!0);if(s)return s}return t.index++,t.col++,t.update_src(),{ok:i,err:null}}if(!r)return{ok:null,err:"Empty attribute key!"};if("="===t.next()){LITERAL_DELIMITERS.includes(t.src[t.index+1])&&t.index++;var s=LITERAL_DELIMITERS.includes(t.next())?t.next():" \t\n]",o=(t.update_src(),t.parseStr(s,!1));if(o.err)return{ok:null,err:o.err,rem:""};t.index--,o.ok.replaceAll('"',""").split(/ +/g).forEach(e=>{e=l(r,e);if(e)return e})}else{o=l(r,!0);if(o)return o}t.index++}return t.remaining()?(t.index++,t.col++,t.update_src(),{ok:i,err:null}):{ok:null,err:"Unclosed attribute brackets",rem:""}},parseTag=(r,e)=>{let t,n;if(">#.{[>}".includes(r.next()))n=!0,t=e;else for(n=!1,t="";r.remaining()&&!"#. \t\n\"'`/>{[}".includes(r.next());)t+=r.next(),r.index++,r.col++;e=VOID_ELEMENTS.includes(t);if(!r.remaining())return{ok:{type:t,attrs:{},additional:{void:e,implicit:n}},err:null};let i={};if("#"===r.next()){let e="";for(r.index++,r.col++;r.remaining()&&!">{.[ \t\n".includes(r.next());)e+=r.next(),r.index++,r.col++;i.id=e}if(r.remaining()){if("."===r.next()){let e="";for(;r.remaining()&&!">{[ \t\n".includes(r.next());)e+=r.next(),r.index++,r.col++;i.class=e.replaceAll("."," ").slice(1)}if(r.remaining()&&"["===r.next()){r.index++,r.update_src();var l=r.parseAttrs(1,!0,i);if(l.err)return{ok:null,err:l.err};i=l.ok,r.index=0}}return{ok:{type:t,attrs:i,additional:{void:e,implicit:n}},err:null}};class Parser{src;path;ln;col;index;isBuild;macros;constructor(e,r,t=!0){this.src=e,this.path=r,this.ln=1,this.col=1,this.index=0,this.isBuild=t,this.macros=[Object.assign({},DEFAULT_MACROS)]}new(e,r,t=!0){return new Parser(e,r,t)}next=()=>next(this);remaining=()=>remaining(this);st=()=>st(this);stn=()=>stn(this);update_src=()=>update_src(this);parse=()=>{let e=[];for(;this.remaining();){var r=this.src,{ok:t,err:n}=this.parse_inner("div",!0,!1);if(n)return{ok:null,err:new Error(n,this.path,this.ln,this.col)};if(r===this.src)return{ok:null,err:new Error("Unable to parse remaining text",this.path,this.ln,this.col)};null!==t&&(e=[...e,...t])}return{ok:e,err:null}};parse_inner=(e,r,t)=>parse_inner(this,e,r,t);import_parse=e=>import_parse(this,e);handleImport=()=>handleImport(this);handleInsert=()=>handleInsert(this);convertReservedChar=e=>convertReservedChar(this,e);parseComment=()=>parseComment(this);parseStr=(e,r)=>parseStr(this,e,r);parseAttrs=(e,r,t)=>parseAttrs(this,e,r,t);parseTag=e=>parseTag(this,e);get_macro=e=>get_macro(this,e);parse_macro_def=()=>parse_macro_def(this)}const fullStringify=function(e,r){var{ok:e,err:r}=new Parser(e,r,!0).parse();return r?{ok:null,err:r}:{ok:e.map(e=>e.toString()).join(""),err:null}},next=e=>e.src[e.index],remaining=e=>e.index{for(;e.remaining();){if(" "!==e.next()&&"\t"!==e.next()){e.update_src();break}e.col++,e.index++}e.remaining()||e.update_src()},stn=e=>{for(;e.remaining();)if(" "===e.next()||"\t"===e.next())e.col++,e.index++;else{if("\n"!==e.next()){e.update_src();break}e.col=1,e.index++,e.ln++}e.remaining()||e.update_src()},update_src=e=>{e.src=e.src.slice(e.index),e.index=0};export{fullStringify,snippet,full}; -------------------------------------------------------------------------------- /docs/theme/theme.tmtheme: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | author 6 | Brittany Chiang 7 | colorSpaceName 8 | sRGB 9 | name 10 | ayu 11 | semanticClass 12 | halcyon 13 | settings 14 | 15 | 16 | settings 17 | 18 | background 19 | #1d2433 20 | foreground 21 | #a2aabc 22 | caret 23 | #FFCC66 24 | findHighlight 25 | #8695b7 26 | findHighlightForeground 27 | #d7dce2 28 | guide 29 | #2f3b54 30 | activeGuide 31 | #2f3b54 32 | stackGuide 33 | #2f3b54 34 | gutter 35 | #1d2433 36 | gutterForeground 37 | #8695b755 38 | inactiveBackground 39 | #1d2433 40 | inactiveSelection 41 | #2f3b54 42 | invisibles 43 | #6679a4 44 | lineHighlight 45 | #2f3b54 46 | popupCss 47 | 48 | html, body { 49 | background-color: #1d2433; 50 | font-size: 12px; 51 | color: #a2aabc; 52 | padding: 0; 53 | } 54 | body { 55 | padding: 5px; 56 | } 57 | div { 58 | padding-bottom: -3px; 59 | } 60 | b, strong { 61 | font-weight: normal; 62 | } 63 | a { 64 | color: rgba(92, 207, 230, .7); 65 | line-height: 16px; 66 | } 67 | .type { 68 | color: #ef6b73; 69 | } 70 | .name { 71 | color: #ffd580; 72 | } 73 | .param { 74 | color: #FFD580; 75 | } 76 | .current { 77 | text-decoration: underline; 78 | } 79 | 80 | selection 81 | #2f3b54 82 | selectionBorder 83 | #a2aabc35 84 | shadow 85 | #00000010 86 | 87 | 88 | 89 | name 90 | Comments 91 | scope 92 | comment, punctuation.definition.comment 93 | settings 94 | 95 | fontStyle 96 | italic 97 | foreground 98 | #8695b799 99 | 100 | 101 | 102 | name 103 | Variable 104 | scope 105 | variable 106 | settings 107 | 108 | foreground 109 | #a2aabc 110 | 111 | 112 | 113 | name 114 | Keyword 115 | scope 116 | keyword, keyword.operator 117 | settings 118 | 119 | foreground 120 | #FFAE57 121 | 122 | 123 | 124 | name 125 | Storage 126 | scope 127 | storage.type, storage.modifier 128 | settings 129 | 130 | foreground 131 | #c3a6ff 132 | 133 | 134 | 135 | name 136 | Operator, Misc 137 | scope 138 | constant.other.color, meta.tag, punctuation.separator.inheritance.php, punctuation.section.embedded, keyword.other.substitution 139 | settings 140 | 141 | foreground 142 | #5ccfe6 143 | 144 | 145 | 146 | name 147 | Tag 148 | scope 149 | entity.name.tag, meta.tag.sgml 150 | settings 151 | 152 | foreground 153 | #5ccfe6 154 | 155 | 156 | 157 | name 158 | Git Gutter Deleted 159 | scope 160 | markup.deleted.git_gutter 161 | settings 162 | 163 | foreground 164 | #ef6b73 165 | 166 | 167 | 168 | name 169 | Function, Special Method, Block Level 170 | scope 171 | entity.name, entity.name.class, entity.other.inherited-class, variable.function, support.function, keyword.other.special-method, meta.block-level 172 | settings 173 | 174 | foreground 175 | #FFD580 176 | 177 | 178 | 179 | name 180 | Other Variable, String Link 181 | scope 182 | support.other.variable, string.other.link 183 | settings 184 | 185 | foreground 186 | #ef6b73 187 | 188 | 189 | 190 | name 191 | Number, Constant, Function Argument, Tag Attribute, Embedded 192 | scope 193 | constant.numeric, constant.language, constant.character, keyword.other.unit 194 | settings 195 | 196 | foreground 197 | #c3a6ff 198 | 199 | 200 | 201 | name 202 | Number, Constant, Function Argument, Tag Attribute, Embedded 203 | scope 204 | support.constant, meta.jsx.js, punctuation.section, string.unquoted.label 205 | settings 206 | 207 | foreground 208 | #a2aabc 209 | 210 | 211 | 212 | name 213 | String, Symbols, Inherited Class, Markup Heading 214 | scope 215 | string, keyword.other.template, constant.other.symbol, constant.other.key, entity.other.inherited-class, markup.heading, markup.inserted.git_gutter, meta.group.braces.curly 216 | settings 217 | 218 | fontStyle 219 | normal 220 | foreground 221 | #bae67e 222 | 223 | 224 | 225 | name 226 | Class, Support 227 | scope 228 | entity.name.type.class, support.type, support.class, support.orther.namespace.use.php, meta.use.php, support.other.namespace.php, markup.changed.git_gutter 229 | settings 230 | 231 | foreground 232 | #5ccfe6 233 | 234 | 235 | 236 | name 237 | Sub-methods 238 | scope 239 | entity.name.module.js, variable.import.parameter.js, variable.other.class.js 240 | settings 241 | 242 | foreground 243 | #5ccfe6 244 | 245 | 246 | 247 | name 248 | Language methods 249 | scope 250 | variable.language 251 | settings 252 | 253 | fontStyle 254 | italic 255 | foreground 256 | #5ccfe6 257 | 258 | 259 | 260 | name 261 | Invalid 262 | scope 263 | invalid, invalid.illegal 264 | settings 265 | 266 | foreground 267 | #ef6b73 268 | 269 | 270 | 271 | name 272 | Deprecated 273 | scope 274 | invalid.deprecated 275 | settings 276 | 277 | background 278 | #FFAE57 279 | foreground 280 | #d7dce2 281 | 282 | 283 | 284 | name 285 | Html punctuations tags 286 | scope 287 | punctuation.definition.tag.end, punctuation.definition.tag.begin, punctuation.definition.tag, meta.group.braces.curly.js, meta.property-value, meta.jsx.js 288 | settings 289 | 290 | foreground 291 | #a2aabc 292 | 293 | 294 | 295 | name 296 | Attributes 297 | scope 298 | entity.other.attribute-name, meta.attribute-with-value.style, constant.other.color.rgb-value, meta.at-rule.media, support.constant.mathematical-symbols, 299 | punctuation.separator.key-value 300 | settings 301 | 302 | foreground 303 | #FFAE57 304 | 305 | 306 | 307 | name 308 | Inserted 309 | scope 310 | markup.inserted 311 | settings 312 | 313 | foreground 314 | #bae67e 315 | 316 | 317 | 318 | name 319 | Deleted 320 | scope 321 | markup.deleted 322 | settings 323 | 324 | foreground 325 | #5ccfe6 326 | 327 | 328 | 329 | name 330 | Changed 331 | scope 332 | markup.changed 333 | settings 334 | 335 | foreground 336 | #FFAE57 337 | 338 | 339 | 340 | name 341 | Regular Expressions and Escape Characters 342 | scope 343 | string.regexp, constant.character.escape 344 | settings 345 | 346 | foreground 347 | #95E6CB 348 | 349 | 350 | 351 | name 352 | URL 353 | scope 354 | *url*, *link*, *uri* 355 | settings 356 | 357 | fontStyle 358 | underline 359 | 360 | 361 | 362 | name 363 | Search Results Nums 364 | scope 365 | constant.numeric.line-number.find-in-files - match 366 | settings 367 | 368 | foreground 369 | #8695b7 370 | 371 | 372 | 373 | name 374 | Search Results Lines 375 | scope 376 | entity.name.filename.find-in-files 377 | settings 378 | 379 | foreground 380 | #bae67e 381 | 382 | 383 | 384 | name 385 | Decorators 386 | scope 387 | tag.decorator.js entity.name.tag.js, tag.decorator.js punctuation.definition.tag.js 388 | settings 389 | 390 | fontStyle 391 | italic 392 | foreground 393 | #ffd580 394 | 395 | 396 | 397 | name 398 | ES7 Bind Operator 399 | scope 400 | constant.other.object.key 401 | settings 402 | 403 | foreground 404 | #5ccfe6 405 | 406 | 407 | 408 | name 409 | entity.name.method 410 | scope 411 | entity.name.method 412 | settings 413 | 414 | fontStyle 415 | italic 416 | foreground 417 | #ffd580 418 | 419 | 420 | 421 | name 422 | meta.method.js 423 | scope 424 | entity.name.function, variable.function.constructor 425 | settings 426 | 427 | foreground 428 | #ffd580 429 | 430 | 431 | 432 | name 433 | Markup - Italic 434 | scope 435 | markup.italic 436 | settings 437 | 438 | fontStyle 439 | italic 440 | foreground 441 | #ef6b73 442 | 443 | 444 | 445 | name 446 | Markup - Bold 447 | scope 448 | markup.bold 449 | settings 450 | 451 | fontStyle 452 | bold 453 | foreground 454 | #ef6b73 455 | 456 | 457 | 458 | name 459 | Markup - Underline 460 | scope 461 | markup.underline 462 | settings 463 | 464 | fontStyle 465 | underline 466 | foreground 467 | #c3a6ff 468 | 469 | 470 | 471 | name 472 | Markup - Strike 473 | scope 474 | markup.strike 475 | settings 476 | 477 | fontStyle 478 | strike 479 | foreground 480 | #ffd580 481 | 482 | 483 | 484 | name 485 | Markup - Quote 486 | scope 487 | markup.quote 488 | settings 489 | 490 | fontStyle 491 | italic 492 | foreground 493 | #80D4FF 494 | 495 | 496 | 497 | name 498 | Markup - Raw Block 499 | scope 500 | markup.raw.block 501 | settings 502 | 503 | foreground 504 | #FFAE57 505 | 506 | 507 | 508 | name 509 | Markup - Table 510 | scope 511 | markup.table 512 | settings 513 | 514 | background 515 | #1d2433aa 516 | foreground 517 | #5ccfe6 518 | 519 | 520 | 521 | name 522 | Markdown - Plain 523 | scope 524 | text.html.markdown, punctuation.definition.list_item.markdown 525 | settings 526 | 527 | foreground 528 | #a2aabc 529 | 530 | 531 | 532 | name 533 | Markdown - Markup Raw Inline 534 | scope 535 | text.html.markdown markup.raw.inline 536 | settings 537 | 538 | foreground 539 | #5ccfe6 540 | 541 | 542 | 543 | name 544 | Markdown - Line Break 545 | scope 546 | text.html.markdown meta.dummy.line-break 547 | settings 548 | 549 | foreground 550 | #8695b7 551 | 552 | 553 | 554 | name 555 | Markdown - Heading 556 | scope 557 | markdown.heading, markup.heading | markup.heading entity.name, markup.heading.markdown punctuation.definition.heading.markdown 558 | settings 559 | 560 | foreground 561 | #bae67e 562 | 563 | 564 | 565 | name 566 | Markdown - Blockquote 567 | scope 568 | markup.quote, punctuation.definition.blockquote.markdown 569 | settings 570 | 571 | fontStyle 572 | italic 573 | foreground 574 | #80D4FF 575 | 576 | 577 | 578 | name 579 | Markdown - Link 580 | scope 581 | string.other.link.title.markdown 582 | settings 583 | 584 | fontStyle 585 | underline 586 | foreground 587 | #ffd580 588 | 589 | 590 | 591 | name 592 | Markdown - Raw Block Fenced 593 | scope 594 | markup.raw.block.fenced.markdown 595 | settings 596 | 597 | background 598 | #d7dce210 599 | foreground 600 | #a2aabc 601 | 602 | 603 | 604 | name 605 | Markdown - Fenced Bode Block 606 | scope 607 | punctuation.definition.fenced.markdown, variable.language.fenced.markdown 608 | settings 609 | 610 | background 611 | #d7dce210 612 | foreground 613 | #8695b7 614 | 615 | 616 | 617 | name 618 | Markdown - Fenced Language 619 | scope 620 | variable.language.fenced.markdown 621 | settings 622 | 623 | fontStyle 624 | 625 | foreground 626 | #8695b7 627 | 628 | 629 | 630 | name 631 | Markdown - Separator 632 | scope 633 | meta.separator 634 | settings 635 | 636 | background 637 | #d7dce210 638 | fontStyle 639 | bold 640 | foreground 641 | #8695b7 642 | 643 | 644 | 645 | name 646 | JSON Key - Level 0 647 | scope 648 | source.json meta.structure.dictionary.json string.quoted.double.json - meta.structure.dictionary.json meta.structure.dictionary.value.json string.quoted.double.json, source.json meta.structure.dictionary.json punctuation.definition.string - meta.structure.dictionary.json meta.structure.dictionary.value.json punctuation.definition.string 649 | settings 650 | 651 | foreground 652 | #5ccfe6 653 | 654 | 655 | 656 | name 657 | JSON Key - Level 1 658 | scope 659 | source.json meta meta.structure.dictionary.json string.quoted.double.json - meta meta.structure.dictionary.json meta.structure.dictionary.value.json string.quoted.double.json, source.json meta meta.structure.dictionary.json punctuation.definition.string - meta meta.structure.dictionary.json meta.structure.dictionary.value.json punctuation.definition.string 660 | settings 661 | 662 | foreground 663 | #5ccfe6 664 | 665 | 666 | 667 | name 668 | JSON Key - Level 2 669 | scope 670 | source.json meta meta meta meta.structure.dictionary.json string.quoted.double.json - meta meta meta meta.structure.dictionary.json meta.structure.dictionary.value.json string.quoted.double.json, source.json meta meta meta meta.structure.dictionary.json punctuation.definition.string - meta meta meta meta.structure.dictionary.json meta.structure.dictionary.value.json punctuation.definition.string 671 | settings 672 | 673 | foreground 674 | #ffae57 675 | 676 | 677 | 678 | name 679 | JSON Key - Level 3 680 | scope 681 | source.json meta meta meta meta meta meta.structure.dictionary.json string.quoted.double.json - meta meta meta meta meta meta.structure.dictionary.json meta.structure.dictionary.value.json string.quoted.double.json, source.json meta meta meta meta meta meta.structure.dictionary.json punctuation.definition.string - meta meta meta meta meta meta.structure.dictionary.json meta.structure.dictionary.value.json punctuation.definition.string 682 | settings 683 | 684 | foreground 685 | #5ccfe6 686 | 687 | 688 | 689 | name 690 | JSON Key - Level 4 691 | scope 692 | source.json meta meta meta meta meta meta meta meta.structure.dictionary.json string.quoted.double.json - meta meta meta meta meta meta meta meta.structure.dictionary.json meta.structure.dictionary.value.json string.quoted.double.json, source.json meta meta meta meta meta meta meta meta.structure.dictionary.json punctuation.definition.string - meta meta meta meta meta meta meta meta.structure.dictionary.json meta.structure.dictionary.value.json punctuation.definition.string 693 | settings 694 | 695 | foreground 696 | #ffae57 697 | 698 | 699 | 700 | name 701 | JSON Key - Level 5 702 | scope 703 | source.json meta meta meta meta meta meta meta meta meta meta.structure.dictionary.json string.quoted.double.json - meta meta meta meta meta meta meta meta meta meta.structure.dictionary.json meta.structure.dictionary.value.json string.quoted.double.json, source.json meta meta meta meta meta meta meta meta meta meta.structure.dictionary.json punctuation.definition.string - meta meta meta meta meta meta meta meta meta meta.structure.dictionary.json meta.structure.dictionary.value.json punctuation.definition.string 704 | settings 705 | 706 | foreground 707 | #5ccfe6 708 | 709 | 710 | 711 | name 712 | JSON Key - Level 6 713 | scope 714 | source.json meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary.json string.quoted.double.json - meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary.json meta.structure.dictionary.value.json string.quoted.double.json, source.json meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary.json punctuation.definition.string - meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary.json meta.structure.dictionary.value.json punctuation.definition.string 715 | settings 716 | 717 | foreground 718 | #ffae57 719 | 720 | 721 | 722 | name 723 | JSON Key - Level 7 724 | scope 725 | source.json meta meta meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary.json string.quoted.double.json - meta meta meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary.json meta.structure.dictionary.value.json string.quoted.double.json, source.json meta meta meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary.json punctuation.definition.string - meta meta meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary.json meta.structure.dictionary.value.json punctuation.definition.string 726 | settings 727 | 728 | foreground 729 | #5ccfe6 730 | 731 | 732 | 733 | name 734 | JSON Key - Level 8 735 | scope 736 | source.json meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary.json string.quoted.double.json - meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary.json meta.structure.dictionary.value.json string.quoted.double.json, source.json meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary.json punctuation.definition.string - meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary.json meta.structure.dictionary.value.json punctuation.definition.string 737 | settings 738 | 739 | foreground 740 | #ffae57 741 | 742 | 743 | 744 | name 745 | AceJump Label - Blue 746 | scope 747 | acejump.label.blue 748 | settings 749 | 750 | background 751 | #5ccfe6 752 | foreground 753 | #d7dce2 754 | 755 | 756 | 757 | name 758 | AceJump Label - Green 759 | scope 760 | acejump.label.green 761 | settings 762 | 763 | background 764 | #bae67e 765 | foreground 766 | #d7dce2 767 | 768 | 769 | 770 | name 771 | AceJump Label - Orange 772 | scope 773 | acejump.label.orange 774 | settings 775 | 776 | background 777 | #FFAE57 778 | foreground 779 | #d7dce2 780 | 781 | 782 | 783 | name 784 | AceJump Label - Purple 785 | scope 786 | acejump.label.purple 787 | settings 788 | 789 | background 790 | #ef6b73 791 | foreground 792 | #d7dce2 793 | 794 | 795 | 796 | name 797 | SublimeLinter Warning 798 | scope 799 | sublimelinter.mark.warning 800 | settings 801 | 802 | foreground 803 | #5ccfe6 804 | 805 | 806 | 807 | name 808 | SublimeLinter Gutter Mark 809 | scope 810 | sublimelinter.gutter-mark 811 | settings 812 | 813 | foreground 814 | #d7dce2 815 | 816 | 817 | 818 | name 819 | SublimeLinter Error 820 | scope 821 | sublimelinter.mark.error 822 | settings 823 | 824 | foreground 825 | #ef6b73 826 | 827 | 828 | 829 | name 830 | GitGutter Ignored 831 | scope 832 | markup.ignored.git_gutter 833 | settings 834 | 835 | foreground 836 | #8695b7 837 | 838 | 839 | 840 | name 841 | GitGutter Untracked 842 | scope 843 | markup.untracked.git_gutter 844 | settings 845 | 846 | foreground 847 | #8695b7 848 | 849 | 850 | 851 | name 852 | GutterColor 853 | scope 854 | gutter_color 855 | settings 856 | 857 | foreground 858 | #d7dce2 859 | 860 | 861 | 862 | uuid 863 | 0e709986-46a0-40a0-b3bf-c8dfe525c455 864 | 865 | 866 | --------------------------------------------------------------------------------