├── parser └── .gitkeep ├── bindings ├── python │ └── tree_sitter_glimmer │ │ ├── py.typed │ │ ├── __init__.pyi │ │ ├── __init__.py │ │ └── binding.c ├── go │ ├── go.mod │ ├── binding.go │ └── binding_test.go ├── node │ ├── index.js │ ├── index.d.ts │ └── binding.cc ├── c │ ├── tree-sitter-glimmer.pc.in │ └── tree-sitter-glimmer.h ├── swift │ └── TreeSitterGlimmer │ │ └── glimmer.h └── rust │ ├── build.rs │ └── lib.rs ├── .prettierignore ├── queries └── glimmer │ ├── folds.scm │ ├── locals.scm │ ├── injections.scm │ ├── indents.scm │ └── highlights.scm ├── test ├── corpus │ ├── text.txt │ ├── primitives.txt │ ├── glimmer_components.txt │ ├── element_content.txt │ ├── handlebars.txt │ ├── comments.txt │ ├── block_statement.txt │ ├── element.txt │ └── element_attributes.txt └── highlight │ └── highlights.hbs ├── .github ├── dependabot.yml └── workflows │ ├── regenerate.yml │ ├── linting.yml │ ├── publish.yml │ ├── ci.yml │ └── plan-release.yml ├── .gitattributes ├── .gitignore ├── binding.gyp ├── Cargo.toml ├── .editorconfig ├── pyproject.toml ├── README.md ├── docs └── config.json ├── src ├── tree_sitter │ ├── alloc.h │ ├── parser.h │ └── array.h ├── scanner.c ├── node-types.json └── grammar.json ├── scripts ├── ci-install.sh └── format-queries.lua ├── CHANGELOG.md ├── RELEASE.md ├── Package.swift ├── setup.py ├── package.json ├── .release-plan.json ├── Makefile └── grammar.js /parser/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bindings/python/tree_sitter_glimmer/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bindings/python/tree_sitter_glimmer/__init__.pyi: -------------------------------------------------------------------------------- 1 | def language() -> int: ... 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Highlight test fixtures 2 | test/highlight/ 3 | 4 | *.yml 5 | *.yaml 6 | *.md 7 | *.json 8 | -------------------------------------------------------------------------------- /queries/glimmer/folds.scm: -------------------------------------------------------------------------------- 1 | [ 2 | (element_node 3 | (element_node_start)) 4 | (block_statement) 5 | ] @fold 6 | -------------------------------------------------------------------------------- /test/corpus/text.txt: -------------------------------------------------------------------------------- 1 | ================== 2 | Text 3 | ================== 4 | Foo 5 | --- 6 | 7 | (template 8 | (text_node)) 9 | -------------------------------------------------------------------------------- /bindings/python/tree_sitter_glimmer/__init__.py: -------------------------------------------------------------------------------- 1 | "Glimmer grammar for tree-sitter" 2 | 3 | from ._binding import language 4 | 5 | __all__ = ["language"] 6 | -------------------------------------------------------------------------------- /bindings/go/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tree-sitter/tree-sitter-glimmer 2 | 3 | go 1.22 4 | 5 | require github.com/smacker/go-tree-sitter v0.0.0-20230720070738-0d0a9f78d8f8 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "10:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /queries/glimmer/locals.scm: -------------------------------------------------------------------------------- 1 | [ 2 | (element_node) 3 | (block_statement) 4 | ] @local.scope 5 | 6 | (identifier) @local.reference 7 | 8 | (block_params 9 | (identifier) @local.definition.var) 10 | -------------------------------------------------------------------------------- /bindings/node/index.js: -------------------------------------------------------------------------------- 1 | const root = require("path").join(__dirname, "..", ".."); 2 | 3 | module.exports = require("node-gyp-build")(root); 4 | 5 | try { 6 | module.exports.nodeTypeInfo = require("../../src/node-types.json"); 7 | } catch (_) {} 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | 3 | src/*.json linguist-generated 4 | src/parser.c linguist-generated 5 | src/tree_sitter/* linguist-generated 6 | 7 | bindings/** linguist-generated 8 | binding.gyp linguist-generated 9 | setup.py linguist-generated 10 | Makefile linguist-generated 11 | Package.swift linguist-generated 12 | -------------------------------------------------------------------------------- /bindings/c/tree-sitter-glimmer.pc.in: -------------------------------------------------------------------------------- 1 | prefix=@PREFIX@ 2 | libdir=@LIBDIR@ 3 | includedir=@INCLUDEDIR@ 4 | 5 | Name: tree-sitter-glimmer 6 | Description: Glimmer grammar for tree-sitter 7 | URL: @URL@ 8 | Version: @VERSION@ 9 | Requires: @REQUIRES@ 10 | Libs: -L${libdir} @ADDITIONAL_LIBS@ -ltree-sitter-glimmer 11 | Cflags: -I${includedir} 12 | -------------------------------------------------------------------------------- /bindings/c/tree-sitter-glimmer.h: -------------------------------------------------------------------------------- 1 | #ifndef TREE_SITTER_GLIMMER_H_ 2 | #define TREE_SITTER_GLIMMER_H_ 3 | 4 | typedef struct TSLanguage TSLanguage; 5 | 6 | #ifdef __cplusplus 7 | extern "C" { 8 | #endif 9 | 10 | const TSLanguage *tree_sitter_glimmer(void); 11 | 12 | #ifdef __cplusplus 13 | } 14 | #endif 15 | 16 | #endif // TREE_SITTER_GLIMMER_H_ 17 | -------------------------------------------------------------------------------- /bindings/swift/TreeSitterGlimmer/glimmer.h: -------------------------------------------------------------------------------- 1 | #ifndef TREE_SITTER_GLIMMER_H_ 2 | #define TREE_SITTER_GLIMMER_H_ 3 | 4 | typedef struct TSLanguage TSLanguage; 5 | 6 | #ifdef __cplusplus 7 | extern "C" { 8 | #endif 9 | 10 | const TSLanguage *tree_sitter_glimmer(void); 11 | 12 | #ifdef __cplusplus 13 | } 14 | #endif 15 | 16 | #endif // TREE_SITTER_GLIMMER_H_ 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Local Building 2 | parser/*.so 3 | 4 | # Dependencies 5 | node_modules 6 | Cargo.lock 7 | 8 | # Build Output 9 | build 10 | target/ 11 | 12 | # Generates Docs Output 13 | docs/*.html 14 | 15 | # Yarn 3 (w/o Zero-Installs) 16 | .pnp.* 17 | .yarn/* 18 | !.yarn/patches 19 | !.yarn/plugins 20 | !.yarn/releases 21 | !.yarn/sdks 22 | !.yarn/versions 23 | -------------------------------------------------------------------------------- /bindings/go/binding.go: -------------------------------------------------------------------------------- 1 | package tree_sitter_glimmer 2 | 3 | // #cgo CFLAGS: -std=c11 -fPIC 4 | // #include "../../src/parser.c" 5 | // // NOTE: if your language has an external scanner, add it here. 6 | import "C" 7 | 8 | import "unsafe" 9 | 10 | // Get the tree-sitter Language for this grammar. 11 | func Language() unsafe.Pointer { 12 | return unsafe.Pointer(C.tree_sitter_glimmer()) 13 | } 14 | -------------------------------------------------------------------------------- /bindings/go/binding_test.go: -------------------------------------------------------------------------------- 1 | package tree_sitter_glimmer_test 2 | 3 | import ( 4 | "testing" 5 | 6 | tree_sitter "github.com/smacker/go-tree-sitter" 7 | "github.com/tree-sitter/tree-sitter-glimmer" 8 | ) 9 | 10 | func TestCanLoadGrammar(t *testing.T) { 11 | language := tree_sitter.NewLanguage(tree_sitter_glimmer.Language()) 12 | if language == nil { 13 | t.Errorf("Error loading Glimmer grammar") 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/regenerate.yml: -------------------------------------------------------------------------------- 1 | name: Regenerate parser 2 | 3 | on: 4 | pull_request: 5 | 6 | permissions: 7 | contents: write 8 | pull-requests: write 9 | 10 | concurrency: 11 | group: ${{github.workflow}}-${{github.ref}} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | regenerate: 16 | uses: tree-sitter/workflows/.github/workflows/regenerate.yml@main 17 | if: >- 18 | !github.event.repository.is_template && 19 | github.actor == 'dependabot[bot]' 20 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | { 4 | "target_name": "tree_sitter_glimmer_binding", 5 | "dependencies": [ 6 | "=0.22.6" 21 | 22 | [build-dependencies] 23 | cc = "1.0.87" 24 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | 9 | [*.{json,toml,yml,gyp}] 10 | indent_style = space 11 | indent_size = 2 12 | 13 | [*.js] 14 | indent_style = space 15 | indent_size = 2 16 | 17 | [*.rs] 18 | indent_style = space 19 | indent_size = 4 20 | 21 | [*.{c,cc,h}] 22 | indent_style = space 23 | indent_size = 4 24 | 25 | [*.{py,pyi}] 26 | indent_style = space 27 | indent_size = 4 28 | 29 | [*.swift] 30 | indent_style = space 31 | indent_size = 4 32 | 33 | [*.go] 34 | indent_style = tab 35 | indent_size = 8 36 | 37 | [Makefile] 38 | indent_style = tab 39 | indent_size = 8 40 | -------------------------------------------------------------------------------- /bindings/node/binding.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | typedef struct TSLanguage TSLanguage; 4 | 5 | extern "C" TSLanguage *tree_sitter_glimmer(); 6 | 7 | // "tree-sitter", "language" hashed with BLAKE2 8 | const napi_type_tag LANGUAGE_TYPE_TAG = { 9 | 0x8AF2E5212AD58ABF, 0xD5006CAD83ABBA16 10 | }; 11 | 12 | Napi::Object Init(Napi::Env env, Napi::Object exports) { 13 | exports["name"] = Napi::String::New(env, "glimmer"); 14 | auto language = Napi::External::New(env, tree_sitter_glimmer()); 15 | language.TypeTag(&LANGUAGE_TYPE_TAG); 16 | exports["language"] = language; 17 | return exports; 18 | } 19 | 20 | NODE_API_MODULE(tree_sitter_glimmer_binding, Init) 21 | -------------------------------------------------------------------------------- /queries/glimmer/injections.scm: -------------------------------------------------------------------------------- 1 | ; comments 2 | ((comment_statement) @injection.content 3 | (#set! injection.language "comment")) 4 | 5 | ; 33 | 34 | 38 | 39 | 40 | {{!-- Primitives --}} 41 | 42 | {{true}} 43 | {{! <- boolean }} 44 | {{false}} 45 | {{! <- boolean }} 46 | {{"a string"}} 47 | {{! <- string }} 48 | {{42}} 49 | {{! <- number }} 50 | 51 | 52 | 53 | {{!-- Path Expression --}} 54 | 55 | {{foo.bar}} 56 | {{! <- @tag.delimiter }} 57 | {{! ^ @variable }} 58 | {{! ^ @tag.delimiter }} 59 | {{! ^ @variable }} 60 | 61 | {{this.bar}} 62 | {{! ^ @variable.builtin }} 63 | 64 | 65 | 66 | {{!-- Glimmer Component API --}} 67 | 68 | 69 | {{! ^ constructor }} 70 | {{! ^ property }} 71 | {{! ^ keyword }} 72 | {{! ^ operator }} 73 | {{! ^ operator }} 74 | {{whatever}} 75 | 76 | 77 | 78 | {{! ^ constructor }} 79 | {{! ^ constructor }} 80 | {{! ^ constructor }} 81 | 82 | 83 | <:header> 84 | {{! <- tag }} 85 | 86 | {{! ^ tag }} 87 | 88 | 89 | 90 | {{!-- Helper Invocation --}} 91 | 92 | {{some-helper (sub-expression this.property)}} 93 | {{! ^ function }} 94 | {{! ^ @tag.delimiter }} 95 | {{! ^ function }} 96 | {{! ^ variable.builtin }} 97 | {{! ^ variable }} 98 | 99 | 100 | 101 | {{!-- Keywords --}} 102 | 103 | {{yield whatever}} 104 | {{! ^ keyword }} 105 | 106 | {{yield}} 107 | {{! ^ keyword}} 108 | 109 | {{outlet}} 110 | {{! ^ keyword }} 111 | 112 | 113 | {{!-- Block Statements --}} 114 | 115 | {{#if condition}} 116 | {{! <- @tag.delimiter }} 117 | {{! ^ conditional }} 118 | {{! ^ variable }} 119 | {{else}} 120 | {{! ^ conditional }} 121 | {{/if}} 122 | {{! <- @tag.delimiter }} 123 | {{! ^ conditional }} 124 | 125 | {{#each array key="@index" as |item|}} 126 | {{! <- @tag.delimiter }} 127 | {{! ^ conditional }} 128 | {{! ^ variable }} 129 | {{! ^ property }} 130 | {{! ^ string }} 131 | {{! ^ keyword }} 132 | {{! ^ operator }} 133 | {{! ^ variable }} 134 | {{! ^ operator }} 135 | {{/each}} 136 | {{! <- @tag.delimiter }} 137 | {{! ^ conditional }} 138 | 139 | {{#let (or "first" "second") as |result|}} 140 | {{! ^ conditional }} 141 | {{/let}} 142 | {{! ^ conditional }} 143 | 144 | {{#with (or "first" "second") as |result|}} 145 | {{! ^ conditional }} 146 | {{/with}} 147 | {{! ^ conditional }} 148 | 149 | {{#each-in object as |result|}} 150 | {{! ^ conditional }} 151 | {{/each-in}} 152 | {{! ^ conditional }} 153 | 154 | 155 | 156 | {{!-- Comments --}} 157 | 158 | {{! testing }} 159 | {{! <- comment }} 160 | 161 | {{! <- comment }} 162 | {{!-- testing --}} 163 | {{! <- comment }} 164 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION := 0.0.1 2 | 3 | LANGUAGE_NAME := tree-sitter-glimmer 4 | 5 | # repository 6 | SRC_DIR := src 7 | 8 | PARSER_REPO_URL := $(shell git -C $(SRC_DIR) remote get-url origin 2>/dev/null) 9 | 10 | ifeq ($(PARSER_URL),) 11 | PARSER_URL := $(subst .git,,$(PARSER_REPO_URL)) 12 | ifeq ($(shell echo $(PARSER_URL) | grep '^[a-z][-+.0-9a-z]*://'),) 13 | PARSER_URL := $(subst :,/,$(PARSER_URL)) 14 | PARSER_URL := $(subst git@,https://,$(PARSER_URL)) 15 | endif 16 | endif 17 | 18 | TS ?= tree-sitter 19 | 20 | # ABI versioning 21 | SONAME_MAJOR := $(word 1,$(subst ., ,$(VERSION))) 22 | SONAME_MINOR := $(word 2,$(subst ., ,$(VERSION))) 23 | 24 | # install directory layout 25 | PREFIX ?= /usr/local 26 | INCLUDEDIR ?= $(PREFIX)/include 27 | LIBDIR ?= $(PREFIX)/lib 28 | PCLIBDIR ?= $(LIBDIR)/pkgconfig 29 | 30 | # object files 31 | OBJS := $(patsubst %.c,%.o,$(wildcard $(SRC_DIR)/*.c)) 32 | 33 | # flags 34 | ARFLAGS := rcs 35 | override CFLAGS += -I$(SRC_DIR) -std=c11 -fPIC 36 | 37 | # OS-specific bits 38 | ifeq ($(OS),Windows_NT) 39 | $(error "Windows is not supported") 40 | else ifeq ($(shell uname),Darwin) 41 | SOEXT = dylib 42 | SOEXTVER_MAJOR = $(SONAME_MAJOR).dylib 43 | SOEXTVER = $(SONAME_MAJOR).$(SONAME_MINOR).dylib 44 | LINKSHARED := $(LINKSHARED)-dynamiclib -Wl, 45 | ifneq ($(ADDITIONAL_LIBS),) 46 | LINKSHARED := $(LINKSHARED)$(ADDITIONAL_LIBS), 47 | endif 48 | LINKSHARED := $(LINKSHARED)-install_name,$(LIBDIR)/lib$(LANGUAGE_NAME).$(SONAME_MAJOR).dylib,-rpath,@executable_path/../Frameworks 49 | else 50 | SOEXT = so 51 | SOEXTVER_MAJOR = so.$(SONAME_MAJOR) 52 | SOEXTVER = so.$(SONAME_MAJOR).$(SONAME_MINOR) 53 | LINKSHARED := $(LINKSHARED)-shared -Wl, 54 | ifneq ($(ADDITIONAL_LIBS),) 55 | LINKSHARED := $(LINKSHARED)$(ADDITIONAL_LIBS) 56 | endif 57 | LINKSHARED := $(LINKSHARED)-soname,lib$(LANGUAGE_NAME).so.$(SONAME_MAJOR) 58 | endif 59 | ifneq ($(filter $(shell uname),FreeBSD NetBSD DragonFly),) 60 | PCLIBDIR := $(PREFIX)/libdata/pkgconfig 61 | endif 62 | 63 | all: lib$(LANGUAGE_NAME).a lib$(LANGUAGE_NAME).$(SOEXT) $(LANGUAGE_NAME).pc 64 | 65 | lib$(LANGUAGE_NAME).a: $(OBJS) 66 | $(AR) $(ARFLAGS) $@ $^ 67 | 68 | lib$(LANGUAGE_NAME).$(SOEXT): $(OBJS) 69 | $(CC) $(LDFLAGS) $(LINKSHARED) $^ $(LDLIBS) -o $@ 70 | ifneq ($(STRIP),) 71 | $(STRIP) $@ 72 | endif 73 | 74 | $(LANGUAGE_NAME).pc: bindings/c/$(LANGUAGE_NAME).pc.in 75 | sed -e 's|@URL@|$(PARSER_URL)|' \ 76 | -e 's|@VERSION@|$(VERSION)|' \ 77 | -e 's|@LIBDIR@|$(LIBDIR)|' \ 78 | -e 's|@INCLUDEDIR@|$(INCLUDEDIR)|' \ 79 | -e 's|@REQUIRES@|$(REQUIRES)|' \ 80 | -e 's|@ADDITIONAL_LIBS@|$(ADDITIONAL_LIBS)|' \ 81 | -e 's|=$(PREFIX)|=$${prefix}|' \ 82 | -e 's|@PREFIX@|$(PREFIX)|' $< > $@ 83 | 84 | $(SRC_DIR)/parser.c: grammar.js 85 | $(TS) generate --no-bindings 86 | 87 | install: all 88 | install -Dm644 bindings/c/$(LANGUAGE_NAME).h '$(DESTDIR)$(INCLUDEDIR)'/tree_sitter/$(LANGUAGE_NAME).h 89 | install -Dm644 $(LANGUAGE_NAME).pc '$(DESTDIR)$(PCLIBDIR)'/$(LANGUAGE_NAME).pc 90 | install -Dm755 lib$(LANGUAGE_NAME).a '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).a 91 | install -m755 lib$(LANGUAGE_NAME).$(SOEXT) '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).$(SOEXTVER) 92 | ln -sf lib$(LANGUAGE_NAME).$(SOEXTVER) '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).$(SOEXTVER_MAJOR) 93 | ln -sf lib$(LANGUAGE_NAME).$(SOEXTVER_MAJOR) '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).$(SOEXT) 94 | 95 | uninstall: 96 | $(RM) '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).a \ 97 | '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).$(SOEXTVER) \ 98 | '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).$(SOEXTVER_MAJOR) \ 99 | '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).$(SOEXT) \ 100 | '$(DESTDIR)$(INCLUDEDIR)'/tree_sitter/$(LANGUAGE_NAME).h \ 101 | '$(DESTDIR)$(PCLIBDIR)'/$(LANGUAGE_NAME).pc 102 | 103 | clean: 104 | $(RM) $(OBJS) $(LANGUAGE_NAME).pc lib$(LANGUAGE_NAME).a lib$(LANGUAGE_NAME).$(SOEXT) 105 | 106 | test: 107 | $(TS) test 108 | 109 | .PHONY: all install uninstall clean test 110 | -------------------------------------------------------------------------------- /test/corpus/element_attributes.txt: -------------------------------------------------------------------------------- 1 | ================== 2 | String Value 3 | ================== 4 |
5 | --- 6 | 7 | (template 8 | (element_node 9 | (element_node_start 10 | (tag_name) 11 | (attribute_node 12 | (attribute_name) 13 | (concat_statement)) 14 | (attribute_node 15 | (attribute_name) 16 | (concat_statement))) 17 | (element_node_end 18 | (tag_name)))) 19 | 20 | ================== 21 | Number Value 22 | ================== 23 |
24 | --- 25 | 26 | (template 27 | (element_node 28 | (element_node_start 29 | (tag_name) 30 | (attribute_node 31 | (attribute_name) 32 | (number_literal))) 33 | (element_node_end 34 | (tag_name)))) 35 | 36 | ================== 37 | Handlebars Value 38 | ================== 39 |
40 | --- 41 | 42 | (template 43 | (element_node 44 | (element_node_start 45 | (tag_name) 46 | (attribute_node 47 | (attribute_name) 48 | (mustache_statement 49 | (identifier)))) 50 | (element_node_end 51 | (tag_name)))) 52 | 53 | ================== 54 | No Value 55 | ================== 56 |
57 | --- 58 | 59 | (template 60 | (element_node 61 | (element_node_start 62 | (tag_name) 63 | (attribute_node 64 | (attribute_name))) 65 | (element_node_end 66 | (tag_name)))) 67 | 68 | ================== 69 | Concat Statement Value 70 | ================== 71 |
72 |
73 |
74 |
75 |
76 | --- 77 | 78 | (template 79 | (element_node 80 | (element_node_start 81 | (tag_name) 82 | (attribute_node 83 | (attribute_name) 84 | (concat_statement 85 | (mustache_statement 86 | (identifier))))) 87 | (element_node_end 88 | (tag_name))) 89 | (text_node) 90 | (element_node 91 | (element_node_start 92 | (tag_name) 93 | (attribute_node 94 | (attribute_name) 95 | (concat_statement 96 | (mustache_statement 97 | (identifier))))) 98 | (element_node_end 99 | (tag_name))) 100 | (text_node) 101 | (element_node 102 | (element_node_start 103 | (tag_name) 104 | (attribute_node 105 | (attribute_name) 106 | (concat_statement 107 | (mustache_statement 108 | (identifier))))) 109 | (element_node_end 110 | (tag_name))) 111 | (text_node) 112 | (element_node 113 | (element_node_start 114 | (tag_name) 115 | (attribute_node 116 | (attribute_name) 117 | (concat_statement 118 | (mustache_statement 119 | (identifier)) 120 | (mustache_statement 121 | (identifier))))) 122 | (element_node_end 123 | (tag_name))) 124 | (text_node) 125 | (element_node 126 | (element_node_start 127 | (tag_name) 128 | (attribute_node 129 | (attribute_name) 130 | (concat_statement 131 | (mustache_statement 132 | (string_literal))))) 133 | (element_node_end 134 | (tag_name)))) 135 | 136 | ================== 137 | On a Void Element 138 | ================== 139 |
140 | --- 141 | 142 | (template 143 | (element_node 144 | (element_node_void 145 | (tag_name) 146 | (attribute_node 147 | (attribute_name) 148 | (concat_statement))))) 149 | 150 | ================== 151 | Splattributes 152 | ================== 153 |
154 | --- 155 | 156 | (template 157 | (element_node 158 | (element_node_start 159 | (tag_name) 160 | (attribute_node 161 | (attribute_name))) 162 | (element_node_end 163 | (tag_name)))) 164 | 165 | -------------------------------------------------------------------------------- /src/scanner.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | enum TokenType { 6 | COMMENT 7 | }; 8 | 9 | void *tree_sitter_glimmer_external_scanner_create() { return NULL; } 10 | void tree_sitter_glimmer_external_scanner_destroy(void *p) {} 11 | void tree_sitter_glimmer_external_scanner_reset(void *p) {} 12 | unsigned tree_sitter_glimmer_external_scanner_serialize(void *p, char *buffer) { return 0; } 13 | void tree_sitter_glimmer_external_scanner_deserialize(void *p, const char *b, unsigned n) {} 14 | 15 | static void advance(TSLexer *lexer) { lexer->advance(lexer, false); } 16 | 17 | static bool scan_html_comment(TSLexer *lexer) { 18 | // Ensure the next characters are `!--` 19 | if (lexer->lookahead != '!') return false; 20 | advance(lexer); 21 | if (lexer->lookahead != '-') return false; 22 | advance(lexer); 23 | if (lexer->lookahead != '-') return false; 24 | advance(lexer); 25 | 26 | // Consume characters until we read `-->` 27 | unsigned dashes = 0; 28 | while (lexer->lookahead) { 29 | switch (lexer->lookahead) { 30 | case '-': 31 | ++dashes; 32 | break; 33 | case '>': 34 | if (dashes >= 2) { 35 | lexer->result_symbol = COMMENT; 36 | advance(lexer); 37 | lexer->mark_end(lexer); 38 | return true; 39 | } 40 | default: 41 | dashes = 0; 42 | } 43 | advance(lexer); 44 | } 45 | 46 | return false; 47 | } 48 | 49 | static bool scan_multi_line_handlebars_comment(TSLexer *lexer) { 50 | // Ensure the next character is `-` 51 | if (lexer->lookahead != '-') return false; 52 | 53 | // Consume characters until we read `--}}` 54 | unsigned dashes = 0; 55 | unsigned brackets = 0; 56 | while (lexer->lookahead) { 57 | switch (lexer->lookahead) { 58 | case '-': 59 | ++dashes; 60 | break; 61 | case '}': 62 | ++brackets; 63 | if (dashes >= 2 && brackets == 2) { 64 | lexer->result_symbol = COMMENT; 65 | advance(lexer); 66 | lexer->mark_end(lexer); 67 | return true; 68 | } else { 69 | break; 70 | } 71 | default: 72 | dashes = 0; 73 | brackets = 0; 74 | } 75 | advance(lexer); 76 | } 77 | 78 | return false; 79 | } 80 | 81 | static bool scan_single_line_handlebars_comment(TSLexer *lexer) { 82 | // Consume characters until we read `}}` 83 | unsigned brackets = 0; 84 | while (lexer->lookahead) { 85 | switch (lexer->lookahead) { 86 | case '}': 87 | ++brackets; 88 | if (brackets == 2) { 89 | lexer->result_symbol = COMMENT; 90 | advance(lexer); 91 | lexer->mark_end(lexer); 92 | return true; 93 | } else { 94 | break; 95 | } 96 | default: 97 | brackets = 0; 98 | } 99 | advance(lexer); 100 | } 101 | 102 | return false; 103 | } 104 | 105 | 106 | static bool scan_handlebars_comment(TSLexer *lexer) { 107 | // Ensure the next characters are `{!` 108 | if (lexer->lookahead != '{') return false; 109 | advance(lexer); 110 | if (lexer->lookahead != '!') return false; 111 | advance(lexer); 112 | 113 | // Handle either multi-line or single-line comment 114 | switch (lexer->lookahead) { 115 | // Multi-Line Comment 116 | case '-': 117 | advance(lexer); 118 | 119 | return scan_multi_line_handlebars_comment(lexer); 120 | 121 | // Single-Line Comment 122 | default: 123 | advance(lexer); 124 | 125 | return scan_single_line_handlebars_comment(lexer); 126 | } 127 | 128 | return false; 129 | } 130 | 131 | bool tree_sitter_glimmer_external_scanner_scan(void *payload, TSLexer *lexer, 132 | const bool *valid_symbols) { 133 | // Eat whitespace 134 | while (iswspace(lexer->lookahead)) { 135 | lexer->advance(lexer, true); 136 | } 137 | 138 | if (valid_symbols[COMMENT]) { 139 | switch (lexer->lookahead) { 140 | case '<': 141 | lexer->mark_end(lexer); 142 | advance(lexer); 143 | 144 | return scan_html_comment(lexer); 145 | 146 | case '{': 147 | lexer->mark_end(lexer); 148 | advance(lexer); 149 | 150 | return scan_handlebars_comment(lexer); 151 | 152 | default: 153 | return false; 154 | 155 | } 156 | } 157 | 158 | return false; 159 | } 160 | -------------------------------------------------------------------------------- /grammar.js: -------------------------------------------------------------------------------- 1 | module.exports = grammar({ 2 | name: "glimmer", 3 | 4 | externals: ($) => [$.comment], 5 | 6 | rules: { 7 | // Entire file 8 | template: ($) => repeat($._declaration), 9 | 10 | // Each individual "thing" in the file 11 | _declaration: ($) => 12 | choice( 13 | alias($.comment, $.comment_statement), 14 | $.mustache_statement, 15 | $.block_statement, 16 | $.element_node, 17 | $.text_node, 18 | ), 19 | 20 | // 21 | // Text 22 | // 23 | 24 | // Match anything that doesn't start with 25 | // - An open/close HTML delimiter (<, >) 26 | // - An open/close Mustache delimiter ({, }) 27 | text_node: () => token(/[^<>{}]+/), 28 | 29 | // 30 | // Primitives 31 | // 32 | 33 | string_literal: ($) => 34 | choice($._single_quote_string_literal, $._double_quote_string_literal), 35 | // https://github.com/tree-sitter/tree-sitter-javascript/blob/37af80d372ae9e2f5adc2c6321d5a34294dc348b/grammar.js#L826 36 | _single_quote_string_literal: () => seq("'", /[^'\\]*/, "'"), 37 | // https://github.com/tree-sitter/tree-sitter-javascript/blob/37af80d372ae9e2f5adc2c6321d5a34294dc348b/grammar.js#L818 38 | _double_quote_string_literal: () => seq('"', /[^"\\]*/, '"'), 39 | 40 | number_literal: () => /[0-9]+/, 41 | 42 | boolean_literal: () => choice("true", "false"), 43 | 44 | // 45 | // HTML Elements 46 | // 47 | 48 | // Match a sequence of letters, plus 49 | // - @ (for arguments, so they can only appear first) 50 | // - Hyphens (for web components) 51 | // - Period (for contextual Glimmer components) 52 | // - Colon (for component namespacing and named blocks) 53 | tag_name: () => /(@?[a-zA-Z0-9]|-|:|\.)+/, 54 | 55 | // "Normal" elements with separate opening and closing tags 56 | element_node_start: ($) => 57 | seq( 58 | "<", 59 | $.tag_name, 60 | repeat( 61 | choice( 62 | $.attribute_node, 63 | $.mustache_statement, 64 | alias($.comment, $.comment_statement), 65 | ), 66 | ), 67 | optional($.block_params), 68 | ">", 69 | ), 70 | element_node_end: ($) => seq(""), 71 | 72 | // "Void" elements are self-closing 73 | element_node_void: ($) => 74 | seq( 75 | "<", 76 | $.tag_name, 77 | repeat( 78 | choice( 79 | $.attribute_node, 80 | $.mustache_statement, 81 | alias($.comment, $.comment_statement), 82 | ), 83 | ), 84 | "/>", 85 | ), 86 | 87 | // An "Element" is either a "normal" or "void" element 88 | element_node: ($) => 89 | choice( 90 | seq($.element_node_start, repeat($._declaration), $.element_node_end), 91 | $.element_node_void, 92 | ), 93 | 94 | attribute_name: () => /[^<>"'/={}()\s\.,!?|]+/, 95 | 96 | _splattributes: () => "...attributes", 97 | 98 | attribute_node: ($) => 99 | choice( 100 | seq( 101 | $.attribute_name, 102 | optional( 103 | seq( 104 | "=", 105 | choice( 106 | $.concat_statement, 107 | $.number_literal, 108 | $.mustache_statement, 109 | ), 110 | ), 111 | ), 112 | ), 113 | alias($._splattributes, $.attribute_name), 114 | ), 115 | 116 | // Special attribute-value strings that can embed a mustache statement 117 | concat_statement: ($) => 118 | choice( 119 | $._single_quote_concat_statement, 120 | $._double_quote_concat_statement, 121 | ), 122 | 123 | _single_quote_concat_statement: ($) => 124 | seq( 125 | "'", 126 | repeat( 127 | choice( 128 | $._mustache_safe_single_quote_string_literal_content, 129 | $.mustache_statement, 130 | ), 131 | ), 132 | "'", 133 | ), 134 | _double_quote_concat_statement: ($) => 135 | seq( 136 | '"', 137 | repeat( 138 | choice( 139 | $._mustache_safe_double_quote_string_literal_content, 140 | $.mustache_statement, 141 | ), 142 | ), 143 | '"', 144 | ), 145 | 146 | _mustache_safe_string_literal: ($) => 147 | choice( 148 | $._mustache_safe_single_quote_string_literal, 149 | $._mustache_safe_double_quote_string_literal, 150 | ), 151 | _mustache_safe_single_quote_string_literal_content: () => /[^'\\{]+/, 152 | _mustache_safe_single_quote_string_literal: ($) => 153 | seq("'", $._mustache_safe_single_quote_string_literal_content, "'"), 154 | _mustache_safe_double_quote_string_literal_content: () => /[^"\\{]+/, 155 | _mustache_safe_double_quote_string_literal: ($) => 156 | seq('"', $._mustache_safe_double_quote_string_literal_content, '"'), 157 | 158 | block_params: ($) => seq("as", "|", repeat($.identifier), "|"), 159 | 160 | identifier: () => /[^<>"'/={}()\s\.,!|]+/, 161 | path_expression: ($) => seq($.identifier, repeat1(seq(".", $.identifier))), 162 | 163 | // Represents anything that can be a "value"; things like 164 | // - Strings 165 | // - Numbers 166 | // - Variables 167 | // - Handlebars sub-expressions 168 | _expression: ($) => 169 | choice( 170 | prec(1, $.number_literal), 171 | $.string_literal, 172 | $.boolean_literal, 173 | $.sub_expression, 174 | $.path_expression, 175 | $.identifier, 176 | ), 177 | 178 | hash_pair: ($) => 179 | seq(field("key", $.identifier), "=", field("value", $._expression)), 180 | 181 | mustache_statement: ($) => 182 | seq( 183 | choice("{{", "{{~"), 184 | choice($._expression, $.helper_invocation), 185 | choice("}}", "~}}"), 186 | ), 187 | 188 | sub_expression: ($) => 189 | seq("(", choice($._expression, $.helper_invocation), ")"), 190 | 191 | // There *must* be either: 192 | // - 1 or more positional argument and 0 or more hash pairs 193 | // - 0 or more positional arguments and 1 or more hash pairs 194 | _arguments: ($) => 195 | choice( 196 | seq(repeat1(field("argument", $._expression)), repeat($.hash_pair)), 197 | seq(repeat(field("argument", $._expression)), repeat1($.hash_pair)), 198 | ), 199 | 200 | helper_invocation: ($) => 201 | seq( 202 | field("helper", choice($.identifier, $.path_expression)), 203 | $._arguments, 204 | ), 205 | 206 | // 207 | // Block Expression 208 | // 209 | 210 | block_statement_start: ($) => 211 | seq( 212 | choice("{{#", "{{~#"), 213 | field("path", $.identifier), 214 | $._arguments, 215 | optional($.block_params), 216 | choice("}}", "~}}"), 217 | ), 218 | 219 | block_statement_end: ($) => 220 | seq( 221 | choice("{{/", "{{~/"), 222 | field("path", $.identifier), 223 | choice("}}", "~}}"), 224 | ), 225 | 226 | block_statement: ($) => 227 | seq( 228 | $.block_statement_start, 229 | field("program", repeat($._declaration)), 230 | $.block_statement_end, 231 | ), 232 | }, 233 | }); 234 | -------------------------------------------------------------------------------- /src/tree_sitter/parser.h: -------------------------------------------------------------------------------- 1 | #ifndef TREE_SITTER_PARSER_H_ 2 | #define TREE_SITTER_PARSER_H_ 3 | 4 | #ifdef __cplusplus 5 | extern "C" { 6 | #endif 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | #define ts_builtin_sym_error ((TSSymbol)-1) 13 | #define ts_builtin_sym_end 0 14 | #define TREE_SITTER_SERIALIZATION_BUFFER_SIZE 1024 15 | 16 | #ifndef TREE_SITTER_API_H_ 17 | typedef uint16_t TSStateId; 18 | typedef uint16_t TSSymbol; 19 | typedef uint16_t TSFieldId; 20 | typedef struct TSLanguage TSLanguage; 21 | #endif 22 | 23 | typedef struct { 24 | TSFieldId field_id; 25 | uint8_t child_index; 26 | bool inherited; 27 | } TSFieldMapEntry; 28 | 29 | typedef struct { 30 | uint16_t index; 31 | uint16_t length; 32 | } TSFieldMapSlice; 33 | 34 | typedef struct { 35 | bool visible; 36 | bool named; 37 | bool supertype; 38 | } TSSymbolMetadata; 39 | 40 | typedef struct TSLexer TSLexer; 41 | 42 | struct TSLexer { 43 | int32_t lookahead; 44 | TSSymbol result_symbol; 45 | void (*advance)(TSLexer *, bool); 46 | void (*mark_end)(TSLexer *); 47 | uint32_t (*get_column)(TSLexer *); 48 | bool (*is_at_included_range_start)(const TSLexer *); 49 | bool (*eof)(const TSLexer *); 50 | }; 51 | 52 | typedef enum { 53 | TSParseActionTypeShift, 54 | TSParseActionTypeReduce, 55 | TSParseActionTypeAccept, 56 | TSParseActionTypeRecover, 57 | } TSParseActionType; 58 | 59 | typedef union { 60 | struct { 61 | uint8_t type; 62 | TSStateId state; 63 | bool extra; 64 | bool repetition; 65 | } shift; 66 | struct { 67 | uint8_t type; 68 | uint8_t child_count; 69 | TSSymbol symbol; 70 | int16_t dynamic_precedence; 71 | uint16_t production_id; 72 | } reduce; 73 | uint8_t type; 74 | } TSParseAction; 75 | 76 | typedef struct { 77 | uint16_t lex_state; 78 | uint16_t external_lex_state; 79 | } TSLexMode; 80 | 81 | typedef union { 82 | TSParseAction action; 83 | struct { 84 | uint8_t count; 85 | bool reusable; 86 | } entry; 87 | } TSParseActionEntry; 88 | 89 | typedef struct { 90 | int32_t start; 91 | int32_t end; 92 | } TSCharacterRange; 93 | 94 | struct TSLanguage { 95 | uint32_t version; 96 | uint32_t symbol_count; 97 | uint32_t alias_count; 98 | uint32_t token_count; 99 | uint32_t external_token_count; 100 | uint32_t state_count; 101 | uint32_t large_state_count; 102 | uint32_t production_id_count; 103 | uint32_t field_count; 104 | uint16_t max_alias_sequence_length; 105 | const uint16_t *parse_table; 106 | const uint16_t *small_parse_table; 107 | const uint32_t *small_parse_table_map; 108 | const TSParseActionEntry *parse_actions; 109 | const char * const *symbol_names; 110 | const char * const *field_names; 111 | const TSFieldMapSlice *field_map_slices; 112 | const TSFieldMapEntry *field_map_entries; 113 | const TSSymbolMetadata *symbol_metadata; 114 | const TSSymbol *public_symbol_map; 115 | const uint16_t *alias_map; 116 | const TSSymbol *alias_sequences; 117 | const TSLexMode *lex_modes; 118 | bool (*lex_fn)(TSLexer *, TSStateId); 119 | bool (*keyword_lex_fn)(TSLexer *, TSStateId); 120 | TSSymbol keyword_capture_token; 121 | struct { 122 | const bool *states; 123 | const TSSymbol *symbol_map; 124 | void *(*create)(void); 125 | void (*destroy)(void *); 126 | bool (*scan)(void *, TSLexer *, const bool *symbol_whitelist); 127 | unsigned (*serialize)(void *, char *); 128 | void (*deserialize)(void *, const char *, unsigned); 129 | } external_scanner; 130 | const TSStateId *primary_state_ids; 131 | }; 132 | 133 | static inline bool set_contains(TSCharacterRange *ranges, uint32_t len, int32_t lookahead) { 134 | uint32_t index = 0; 135 | uint32_t size = len - index; 136 | while (size > 1) { 137 | uint32_t half_size = size / 2; 138 | uint32_t mid_index = index + half_size; 139 | TSCharacterRange *range = &ranges[mid_index]; 140 | if (lookahead >= range->start && lookahead <= range->end) { 141 | return true; 142 | } else if (lookahead > range->end) { 143 | index = mid_index; 144 | } 145 | size -= half_size; 146 | } 147 | TSCharacterRange *range = &ranges[index]; 148 | return (lookahead >= range->start && lookahead <= range->end); 149 | } 150 | 151 | /* 152 | * Lexer Macros 153 | */ 154 | 155 | #ifdef _MSC_VER 156 | #define UNUSED __pragma(warning(suppress : 4101)) 157 | #else 158 | #define UNUSED __attribute__((unused)) 159 | #endif 160 | 161 | #define START_LEXER() \ 162 | bool result = false; \ 163 | bool skip = false; \ 164 | UNUSED \ 165 | bool eof = false; \ 166 | int32_t lookahead; \ 167 | goto start; \ 168 | next_state: \ 169 | lexer->advance(lexer, skip); \ 170 | start: \ 171 | skip = false; \ 172 | lookahead = lexer->lookahead; 173 | 174 | #define ADVANCE(state_value) \ 175 | { \ 176 | state = state_value; \ 177 | goto next_state; \ 178 | } 179 | 180 | #define ADVANCE_MAP(...) \ 181 | { \ 182 | static const uint16_t map[] = { __VA_ARGS__ }; \ 183 | for (uint32_t i = 0; i < sizeof(map) / sizeof(map[0]); i += 2) { \ 184 | if (map[i] == lookahead) { \ 185 | state = map[i + 1]; \ 186 | goto next_state; \ 187 | } \ 188 | } \ 189 | } 190 | 191 | #define SKIP(state_value) \ 192 | { \ 193 | skip = true; \ 194 | state = state_value; \ 195 | goto next_state; \ 196 | } 197 | 198 | #define ACCEPT_TOKEN(symbol_value) \ 199 | result = true; \ 200 | lexer->result_symbol = symbol_value; \ 201 | lexer->mark_end(lexer); 202 | 203 | #define END_STATE() return result; 204 | 205 | /* 206 | * Parse Table Macros 207 | */ 208 | 209 | #define SMALL_STATE(id) ((id) - LARGE_STATE_COUNT) 210 | 211 | #define STATE(id) id 212 | 213 | #define ACTIONS(id) id 214 | 215 | #define SHIFT(state_value) \ 216 | {{ \ 217 | .shift = { \ 218 | .type = TSParseActionTypeShift, \ 219 | .state = (state_value) \ 220 | } \ 221 | }} 222 | 223 | #define SHIFT_REPEAT(state_value) \ 224 | {{ \ 225 | .shift = { \ 226 | .type = TSParseActionTypeShift, \ 227 | .state = (state_value), \ 228 | .repetition = true \ 229 | } \ 230 | }} 231 | 232 | #define SHIFT_EXTRA() \ 233 | {{ \ 234 | .shift = { \ 235 | .type = TSParseActionTypeShift, \ 236 | .extra = true \ 237 | } \ 238 | }} 239 | 240 | #define REDUCE(symbol_name, children, precedence, prod_id) \ 241 | {{ \ 242 | .reduce = { \ 243 | .type = TSParseActionTypeReduce, \ 244 | .symbol = symbol_name, \ 245 | .child_count = children, \ 246 | .dynamic_precedence = precedence, \ 247 | .production_id = prod_id \ 248 | }, \ 249 | }} 250 | 251 | #define RECOVER() \ 252 | {{ \ 253 | .type = TSParseActionTypeRecover \ 254 | }} 255 | 256 | #define ACCEPT_INPUT() \ 257 | {{ \ 258 | .type = TSParseActionTypeAccept \ 259 | }} 260 | 261 | #ifdef __cplusplus 262 | } 263 | #endif 264 | 265 | #endif // TREE_SITTER_PARSER_H_ 266 | -------------------------------------------------------------------------------- /src/tree_sitter/array.h: -------------------------------------------------------------------------------- 1 | #ifndef TREE_SITTER_ARRAY_H_ 2 | #define TREE_SITTER_ARRAY_H_ 3 | 4 | #ifdef __cplusplus 5 | extern "C" { 6 | #endif 7 | 8 | #include "./alloc.h" 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #ifdef _MSC_VER 17 | #pragma warning(disable : 4101) 18 | #elif defined(__GNUC__) || defined(__clang__) 19 | #pragma GCC diagnostic push 20 | #pragma GCC diagnostic ignored "-Wunused-variable" 21 | #endif 22 | 23 | #define Array(T) \ 24 | struct { \ 25 | T *contents; \ 26 | uint32_t size; \ 27 | uint32_t capacity; \ 28 | } 29 | 30 | /// Initialize an array. 31 | #define array_init(self) \ 32 | ((self)->size = 0, (self)->capacity = 0, (self)->contents = NULL) 33 | 34 | /// Create an empty array. 35 | #define array_new() \ 36 | { NULL, 0, 0 } 37 | 38 | /// Get a pointer to the element at a given `index` in the array. 39 | #define array_get(self, _index) \ 40 | (assert((uint32_t)(_index) < (self)->size), &(self)->contents[_index]) 41 | 42 | /// Get a pointer to the first element in the array. 43 | #define array_front(self) array_get(self, 0) 44 | 45 | /// Get a pointer to the last element in the array. 46 | #define array_back(self) array_get(self, (self)->size - 1) 47 | 48 | /// Clear the array, setting its size to zero. Note that this does not free any 49 | /// memory allocated for the array's contents. 50 | #define array_clear(self) ((self)->size = 0) 51 | 52 | /// Reserve `new_capacity` elements of space in the array. If `new_capacity` is 53 | /// less than the array's current capacity, this function has no effect. 54 | #define array_reserve(self, new_capacity) \ 55 | _array__reserve((Array *)(self), array_elem_size(self), new_capacity) 56 | 57 | /// Free any memory allocated for this array. Note that this does not free any 58 | /// memory allocated for the array's contents. 59 | #define array_delete(self) _array__delete((Array *)(self)) 60 | 61 | /// Push a new `element` onto the end of the array. 62 | #define array_push(self, element) \ 63 | (_array__grow((Array *)(self), 1, array_elem_size(self)), \ 64 | (self)->contents[(self)->size++] = (element)) 65 | 66 | /// Increase the array's size by `count` elements. 67 | /// New elements are zero-initialized. 68 | #define array_grow_by(self, count) \ 69 | do { \ 70 | if ((count) == 0) break; \ 71 | _array__grow((Array *)(self), count, array_elem_size(self)); \ 72 | memset((self)->contents + (self)->size, 0, (count) * array_elem_size(self)); \ 73 | (self)->size += (count); \ 74 | } while (0) 75 | 76 | /// Append all elements from one array to the end of another. 77 | #define array_push_all(self, other) \ 78 | array_extend((self), (other)->size, (other)->contents) 79 | 80 | /// Append `count` elements to the end of the array, reading their values from the 81 | /// `contents` pointer. 82 | #define array_extend(self, count, contents) \ 83 | _array__splice( \ 84 | (Array *)(self), array_elem_size(self), (self)->size, \ 85 | 0, count, contents \ 86 | ) 87 | 88 | /// Remove `old_count` elements from the array starting at the given `index`. At 89 | /// the same index, insert `new_count` new elements, reading their values from the 90 | /// `new_contents` pointer. 91 | #define array_splice(self, _index, old_count, new_count, new_contents) \ 92 | _array__splice( \ 93 | (Array *)(self), array_elem_size(self), _index, \ 94 | old_count, new_count, new_contents \ 95 | ) 96 | 97 | /// Insert one `element` into the array at the given `index`. 98 | #define array_insert(self, _index, element) \ 99 | _array__splice((Array *)(self), array_elem_size(self), _index, 0, 1, &(element)) 100 | 101 | /// Remove one element from the array at the given `index`. 102 | #define array_erase(self, _index) \ 103 | _array__erase((Array *)(self), array_elem_size(self), _index) 104 | 105 | /// Pop the last element off the array, returning the element by value. 106 | #define array_pop(self) ((self)->contents[--(self)->size]) 107 | 108 | /// Assign the contents of one array to another, reallocating if necessary. 109 | #define array_assign(self, other) \ 110 | _array__assign((Array *)(self), (const Array *)(other), array_elem_size(self)) 111 | 112 | /// Swap one array with another 113 | #define array_swap(self, other) \ 114 | _array__swap((Array *)(self), (Array *)(other)) 115 | 116 | /// Get the size of the array contents 117 | #define array_elem_size(self) (sizeof *(self)->contents) 118 | 119 | /// Search a sorted array for a given `needle` value, using the given `compare` 120 | /// callback to determine the order. 121 | /// 122 | /// If an existing element is found to be equal to `needle`, then the `index` 123 | /// out-parameter is set to the existing value's index, and the `exists` 124 | /// out-parameter is set to true. Otherwise, `index` is set to an index where 125 | /// `needle` should be inserted in order to preserve the sorting, and `exists` 126 | /// is set to false. 127 | #define array_search_sorted_with(self, compare, needle, _index, _exists) \ 128 | _array__search_sorted(self, 0, compare, , needle, _index, _exists) 129 | 130 | /// Search a sorted array for a given `needle` value, using integer comparisons 131 | /// of a given struct field (specified with a leading dot) to determine the order. 132 | /// 133 | /// See also `array_search_sorted_with`. 134 | #define array_search_sorted_by(self, field, needle, _index, _exists) \ 135 | _array__search_sorted(self, 0, _compare_int, field, needle, _index, _exists) 136 | 137 | /// Insert a given `value` into a sorted array, using the given `compare` 138 | /// callback to determine the order. 139 | #define array_insert_sorted_with(self, compare, value) \ 140 | do { \ 141 | unsigned _index, _exists; \ 142 | array_search_sorted_with(self, compare, &(value), &_index, &_exists); \ 143 | if (!_exists) array_insert(self, _index, value); \ 144 | } while (0) 145 | 146 | /// Insert a given `value` into a sorted array, using integer comparisons of 147 | /// a given struct field (specified with a leading dot) to determine the order. 148 | /// 149 | /// See also `array_search_sorted_by`. 150 | #define array_insert_sorted_by(self, field, value) \ 151 | do { \ 152 | unsigned _index, _exists; \ 153 | array_search_sorted_by(self, field, (value) field, &_index, &_exists); \ 154 | if (!_exists) array_insert(self, _index, value); \ 155 | } while (0) 156 | 157 | // Private 158 | 159 | typedef Array(void) Array; 160 | 161 | /// This is not what you're looking for, see `array_delete`. 162 | static inline void _array__delete(Array *self) { 163 | if (self->contents) { 164 | ts_free(self->contents); 165 | self->contents = NULL; 166 | self->size = 0; 167 | self->capacity = 0; 168 | } 169 | } 170 | 171 | /// This is not what you're looking for, see `array_erase`. 172 | static inline void _array__erase(Array *self, size_t element_size, 173 | uint32_t index) { 174 | assert(index < self->size); 175 | char *contents = (char *)self->contents; 176 | memmove(contents + index * element_size, contents + (index + 1) * element_size, 177 | (self->size - index - 1) * element_size); 178 | self->size--; 179 | } 180 | 181 | /// This is not what you're looking for, see `array_reserve`. 182 | static inline void _array__reserve(Array *self, size_t element_size, uint32_t new_capacity) { 183 | if (new_capacity > self->capacity) { 184 | if (self->contents) { 185 | self->contents = ts_realloc(self->contents, new_capacity * element_size); 186 | } else { 187 | self->contents = ts_malloc(new_capacity * element_size); 188 | } 189 | self->capacity = new_capacity; 190 | } 191 | } 192 | 193 | /// This is not what you're looking for, see `array_assign`. 194 | static inline void _array__assign(Array *self, const Array *other, size_t element_size) { 195 | _array__reserve(self, element_size, other->size); 196 | self->size = other->size; 197 | memcpy(self->contents, other->contents, self->size * element_size); 198 | } 199 | 200 | /// This is not what you're looking for, see `array_swap`. 201 | static inline void _array__swap(Array *self, Array *other) { 202 | Array swap = *other; 203 | *other = *self; 204 | *self = swap; 205 | } 206 | 207 | /// This is not what you're looking for, see `array_push` or `array_grow_by`. 208 | static inline void _array__grow(Array *self, uint32_t count, size_t element_size) { 209 | uint32_t new_size = self->size + count; 210 | if (new_size > self->capacity) { 211 | uint32_t new_capacity = self->capacity * 2; 212 | if (new_capacity < 8) new_capacity = 8; 213 | if (new_capacity < new_size) new_capacity = new_size; 214 | _array__reserve(self, element_size, new_capacity); 215 | } 216 | } 217 | 218 | /// This is not what you're looking for, see `array_splice`. 219 | static inline void _array__splice(Array *self, size_t element_size, 220 | uint32_t index, uint32_t old_count, 221 | uint32_t new_count, const void *elements) { 222 | uint32_t new_size = self->size + new_count - old_count; 223 | uint32_t old_end = index + old_count; 224 | uint32_t new_end = index + new_count; 225 | assert(old_end <= self->size); 226 | 227 | _array__reserve(self, element_size, new_size); 228 | 229 | char *contents = (char *)self->contents; 230 | if (self->size > old_end) { 231 | memmove( 232 | contents + new_end * element_size, 233 | contents + old_end * element_size, 234 | (self->size - old_end) * element_size 235 | ); 236 | } 237 | if (new_count > 0) { 238 | if (elements) { 239 | memcpy( 240 | (contents + index * element_size), 241 | elements, 242 | new_count * element_size 243 | ); 244 | } else { 245 | memset( 246 | (contents + index * element_size), 247 | 0, 248 | new_count * element_size 249 | ); 250 | } 251 | } 252 | self->size += new_count - old_count; 253 | } 254 | 255 | /// A binary search routine, based on Rust's `std::slice::binary_search_by`. 256 | /// This is not what you're looking for, see `array_search_sorted_with` or `array_search_sorted_by`. 257 | #define _array__search_sorted(self, start, compare, suffix, needle, _index, _exists) \ 258 | do { \ 259 | *(_index) = start; \ 260 | *(_exists) = false; \ 261 | uint32_t size = (self)->size - *(_index); \ 262 | if (size == 0) break; \ 263 | int comparison; \ 264 | while (size > 1) { \ 265 | uint32_t half_size = size / 2; \ 266 | uint32_t mid_index = *(_index) + half_size; \ 267 | comparison = compare(&((self)->contents[mid_index] suffix), (needle)); \ 268 | if (comparison <= 0) *(_index) = mid_index; \ 269 | size -= half_size; \ 270 | } \ 271 | comparison = compare(&((self)->contents[*(_index)] suffix), (needle)); \ 272 | if (comparison == 0) *(_exists) = true; \ 273 | else if (comparison < 0) *(_index) += 1; \ 274 | } while (0) 275 | 276 | /// Helper macro for the `_sorted_by` routines below. This takes the left (existing) 277 | /// parameter by reference in order to work with the generic sorting function above. 278 | #define _compare_int(a, b) ((int)*(a) - (int)(b)) 279 | 280 | #ifdef _MSC_VER 281 | #pragma warning(default : 4101) 282 | #elif defined(__GNUC__) || defined(__clang__) 283 | #pragma GCC diagnostic pop 284 | #endif 285 | 286 | #ifdef __cplusplus 287 | } 288 | #endif 289 | 290 | #endif // TREE_SITTER_ARRAY_H_ 291 | -------------------------------------------------------------------------------- /scripts/format-queries.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S nvim -l 2 | 3 | local ts = vim.treesitter 4 | local get_node_text = ts.get_node_text 5 | 6 | ---@type string[] 7 | local files 8 | 9 | local arg = _G.arg[1] or "." 10 | if arg:match ".*%.scm$" then 11 | files = { arg } 12 | else 13 | files = vim.fn.split(vim.fn.glob(arg .. "/**/*.scm")) 14 | end 15 | 16 | ts.query.add_predicate("kind-eq?", function(match, _, _, pred) 17 | local node = match[pred[2]] 18 | if not node then 19 | return true 20 | end 21 | 22 | local types = { unpack(pred, 3) } 23 | return vim.tbl_contains(types, node:type()) 24 | end, true) 25 | 26 | ts.query.add_predicate("is-start-of-line?", function(match, _, _, pred) 27 | local node = match[pred[2]] 28 | if not node then 29 | return true 30 | end 31 | local start_row, start_col = node:start() 32 | return vim.fn.indent(start_row + 1) == start_col 33 | end) 34 | 35 | --- Control the indent here. Change to \t if uses tab instead 36 | local indent_str = " " 37 | local textwidth = 100 38 | 39 | -- Query to control the formatter 40 | local format_queries = [[ 41 | ;;query 42 | ;; Ignore next node with `; format-ignore` 43 | ( 44 | (comment) @_pattern 45 | . 46 | (_) @format.ignore 47 | (#lua-match? @_pattern "^;+%s*format%-ignore")) 48 | 49 | ;; {{{ 50 | ;; Add newlines to top level nodes 51 | ;; {{{ 52 | ;; Preserve inline comments 53 | (program 54 | . (_) 55 | (comment) @format.prepend-newline 56 | (#is-start-of-line? @format.prepend-newline)) 57 | (program 58 | . (_) 59 | (comment) @_comment 60 | . 61 | (comment) @format.prepend-newline 62 | (#not-is-start-of-line? @_comment) 63 | (#is-start-of-line? @format.prepend-newline)) 64 | ; Extra newline for modelines 65 | (program 66 | (comment) @_modeline 67 | . 68 | (_) @format.prepend-newline 69 | (#is-start-of-line? @_modeline) 70 | (#contains? @_modeline "^;+%s*inherits:")) 71 | (program 72 | (comment) @_modeline 73 | . 74 | (_) @format.prepend-newline 75 | (#is-start-of-line? @_modeline) 76 | (#contains? @_modeline "^;+%s*extends%s*$")) 77 | ;; }}} 78 | ;; Making sure all top-level patterns are separated 79 | (program 80 | (_) @format.append-newline) 81 | (program 82 | (_) @format.cancel-append .) 83 | (program 84 | . (_) 85 | [ 86 | (list) 87 | (grouping) 88 | (named_node) 89 | (anonymous_node) 90 | (field_definition) 91 | ] @format.prepend-newline) 92 | 93 | (program 94 | (comment) @_comment 95 | . 96 | [ 97 | (list) 98 | (grouping) 99 | (named_node) 100 | (anonymous_node) 101 | (field_definition) 102 | (comment) 103 | ] @format.cancel-prepend 104 | (#is-start-of-line? @_comment) 105 | (#not-lua-match? @_comment "^;+%s*inherits:") 106 | (#not-lua-match? @_comment "^;+%s*extends%s*$")) 107 | ;; }}} 108 | 109 | ;; delims 110 | [ 111 | ":" 112 | "." 113 | ] @format.append-space 114 | ( 115 | "." @format.prepend-space @format.cancel-append 116 | . 117 | ")") 118 | 119 | ;; List handler 120 | ;; Only starts indent if 2 or more elements 121 | (list 122 | "[" @format.indent.begin 123 | "]" @format.indent.dedent) 124 | ;; Otherwise, remove brackets 125 | (list 126 | "[" @format.remove @format.cancel-append 127 | . 128 | (_) @format.cancel-append 129 | . 130 | "]" @format.remove) 131 | ;; [ ... ] @capture1 @capture2 132 | ;; Append newlines for nodes inside the list 133 | (list 134 | (_) @format.append-newline 135 | (#not-kind-eq? @format.append-newline "capture" "quantifier")) 136 | 137 | ;; (_), "_" and _ handler 138 | ;; Start indents if it's one of these patterns 139 | (named_node 140 | [ 141 | "_" 142 | name: (identifier) 143 | ] @format.indent.begin 144 | . 145 | [ 146 | (list) ; (foo [...]) 147 | (grouping) ; (foo ((foo))) 148 | (negated_field) ; (foo !field) 149 | (field_definition) ; (foo field: (...)) 150 | (named_node) ; (foo (bar)) 151 | (predicate) ; (named_node (#set!)) 152 | (anonymous_node) 153 | "." 154 | ]) 155 | ;; Honoring comment's position within a node 156 | (named_node 157 | [ 158 | "_" 159 | name: (identifier) 160 | ] @format.indent.begin 161 | . 162 | (comment) @_comment 163 | (#is-start-of-line? @_comment)) 164 | (named_node 165 | [ 166 | "_" 167 | name: (identifier) 168 | ] @format.indent.begin @format.cancel-append 169 | . 170 | "."? @format.prepend-newline 171 | . 172 | (comment) @format.prepend-space 173 | (#not-is-start-of-line? @format.prepend-space)) 174 | 175 | ;; Add newlines for other nodes, in case the top node is indented 176 | (named_node 177 | [ 178 | (list) 179 | (grouping) 180 | (negated_field) 181 | (field_definition) 182 | (named_node) 183 | (predicate) 184 | (anonymous_node) 185 | "." 186 | ] @format.append-newline) 187 | 188 | ;; Collapse closing parentheses 189 | (named_node 190 | [ 191 | "_" 192 | name: (identifier) 193 | (_) 194 | ] @format.cancel-append 195 | . 196 | ")" 197 | (#not-kind-eq? @format.cancel-append "comment")) 198 | 199 | ;; All captures should be separated with a space 200 | (capture) @format.prepend-space 201 | 202 | ;; Workaround to just use the string's content 203 | (anonymous_node (string) @format.keep) 204 | 205 | ; ( (_) ) handler 206 | (grouping 207 | "(" 208 | . 209 | [ 210 | (named_node) ; ((foo)) 211 | (list) ; ([foo] (...)) 212 | (anonymous_node) ; ("foo") 213 | (grouping . (_)) 214 | ] @format.indent.begin 215 | . 216 | (_)) 217 | (grouping 218 | "(" 219 | . 220 | (grouping) @format.indent.begin 221 | (predicate)) 222 | (grouping 223 | "(" 224 | [ 225 | (anonymous_node) 226 | (named_node) 227 | (list) 228 | (predicate) 229 | (grouping . (_)) 230 | (field_definition) 231 | "." 232 | ] @format.append-newline 233 | (_) .) 234 | ;; Collapsing closing parens 235 | (grouping 236 | (_) @format.cancel-append . ")" 237 | (#not-kind-eq? @format.cancel-append "comment")) 238 | (grouping 239 | (capture) @format.prepend-space) 240 | ;; Remove unnecessary parens 241 | (grouping 242 | "(" @format.remove 243 | . 244 | (_) 245 | . 246 | ")" @format.remove .) 247 | (grouping 248 | "(" @format.remove 249 | . 250 | [ 251 | (anonymous_node 252 | name: (string) .) 253 | (named_node 254 | [ 255 | "_" 256 | name: (identifier) 257 | ] .) 258 | ] 259 | . 260 | ")" @format.remove 261 | . 262 | (capture)) 263 | 264 | ; Separate this query to avoid capture duplication 265 | (predicate 266 | "(" @format.indent.begin @format.cancel-append) 267 | (predicate 268 | (parameters 269 | (comment) @format.prepend-newline 270 | . 271 | (_) @format.cancel-prepend) 272 | (#is-start-of-line? @format.prepend-newline)) 273 | (predicate 274 | (parameters 275 | (_) @format.prepend-space) 276 | (#set! conditional-newline)) 277 | (predicate 278 | (parameters 279 | . 280 | (capture) 281 | . (_) @format.prepend-space) 282 | (#set! lookahead-newline) 283 | (#set! conditional-newline)) 284 | ;; Workaround to keep the string's content 285 | (string) @format.keep 286 | 287 | ;; Comment related handlers 288 | (comment) @format.append-newline 289 | ;; comment styling. Feel free to change in the future 290 | ((comment) @format.replace 291 | (#gsub! @format.replace "^;+(%s*.-)%s*$" ";%1")) 292 | ;; Preserve end of line comments 293 | ( 294 | [ 295 | "." 296 | ":" 297 | (list) 298 | (grouping) 299 | (named_node) 300 | (anonymous_node) 301 | (negated_field) 302 | ] @format.cancel-append 303 | . 304 | (quantifier)? 305 | . 306 | "."? @format.prepend-newline ; Make sure anchor are not eol but start of newline 307 | . 308 | (comment) @format.prepend-space 309 | (#not-is-start-of-line? @format.prepend-space)) 310 | ]] 311 | 312 | ---@param lines string[] 313 | ---@param lines_to_append string[] 314 | local function append_lines(lines, lines_to_append) 315 | for i = 1, #lines_to_append, 1 do 316 | lines[#lines] = lines[#lines] .. lines_to_append[i] 317 | if i ~= #lines_to_append then 318 | lines[#lines + 1] = "" 319 | end 320 | end 321 | end 322 | 323 | ---@param bufnr integer 324 | ---@param node TSNode 325 | ---@param lines string[] 326 | ---@param q table 327 | ---@param level integer 328 | local function iter(bufnr, node, lines, q, level) 329 | --- Sometimes 2 queries apply append twice. This is to prevent the case from happening 330 | local apply_newline = false 331 | for child, _ in node:iter_children() do 332 | local id = child:id() 333 | repeat 334 | if apply_newline then 335 | apply_newline = false 336 | lines[#lines + 1] = string.rep(indent_str, level) 337 | end 338 | if q["format.ignore"][id] then 339 | local text = vim.split(get_node_text(child, bufnr):gsub("\r\n?", "\n"), "\n", { trimempty = true }) 340 | append_lines(lines, text) 341 | break 342 | elseif q["format.remove"][id] then 343 | break 344 | end 345 | if not q["format.cancel-prepend"][id] then 346 | if q["format.prepend-newline"][id] then 347 | lines[#lines + 1] = string.rep(indent_str, level) 348 | elseif q["format.prepend-space"][id] then 349 | if not q["format.prepend-space"][id]["conditional-newline"] then 350 | lines[#lines] = lines[#lines] .. " " 351 | elseif child:byte_length() + 1 + #lines[#lines] > textwidth then 352 | lines[#lines + 1] = string.rep(indent_str, level) 353 | else 354 | -- Do a rough guess of the actual byte length. If it's larger than `columns` then add a newline first 355 | -- column - byte_end + byte_start 356 | local _, _, byte_start = child:start() 357 | local _, _, byte_end = node:end_() 358 | if 359 | q["format.prepend-space"][id]["lookahead-newline"] 360 | and textwidth - (byte_end - byte_start) - #lines[#lines] < 0 361 | then 362 | lines[#lines + 1] = string.rep(indent_str, level) 363 | else 364 | lines[#lines] = lines[#lines] .. " " 365 | end 366 | end 367 | end 368 | end 369 | if q["format.replace"][id] then 370 | append_lines(lines, vim.split(q["format.replace"][id].text, "\n", { trimempty = true })) 371 | elseif child:named_child_count() == 0 or q["format.keep"][id] then 372 | append_lines( 373 | lines, 374 | vim.split(string.gsub(get_node_text(child, bufnr), "\r\n?", "\n"), "\n+", { trimempty = true }) 375 | ) 376 | else 377 | iter(bufnr, child, lines, q, level) 378 | end 379 | if q["format.indent.begin"][id] then 380 | level = level + 1 381 | apply_newline = true 382 | break 383 | end 384 | if q["format.indent.dedent"][id] then 385 | if string.match(lines[#lines], "^%s*" .. get_node_text(child, bufnr)) then 386 | lines[#lines] = string.sub(lines[#lines], 1 + #string.rep(indent_str, 1)) 387 | end 388 | end 389 | if q["format.indent.end"][id] then 390 | level = math.max(level - 1, 0) 391 | if string.match(lines[#lines], "^%s*" .. get_node_text(child, bufnr)) then 392 | lines[#lines] = string.sub(lines[#lines], 1 + #string.rep(indent_str, 1)) 393 | end 394 | break 395 | end 396 | until true 397 | repeat 398 | if q["format.cancel-append"][id] then 399 | apply_newline = false 400 | end 401 | if not q["format.cancel-append"][id] then 402 | if q["format.append-newline"][id] then 403 | apply_newline = true 404 | elseif q["format.append-space"][id] then 405 | lines[#lines] = lines[#lines] .. " " 406 | end 407 | end 408 | until true 409 | end 410 | end 411 | 412 | ---@param bufnr integer 413 | ---@param queries string 414 | local function format(bufnr, queries) 415 | local lines = { "" } 416 | -- stylua: ignore 417 | local map = { 418 | ['format.ignore'] = {}, -- Ignore the node and its children 419 | ['format.indent.begin'] = {}, -- +1 shiftwidth for all nodes after this 420 | ['format.indent.end'] = {}, -- -1 shiftwidth for all nodes after this 421 | ['format.indent.dedent'] = {}, -- -1 shiftwidth for this line only 422 | ['format.prepend-space'] = {}, -- Prepend a space before inserting the node 423 | ['format.prepend-newline'] = {}, -- Prepend a \n before inserting the node 424 | ['format.append-space'] = {}, -- Append a space after inserting the node 425 | ['format.append-newline'] = {}, -- Append a newline after inserting the node 426 | ['format.cancel-append'] = {}, -- Cancel any `@format.append-*` applied to the node 427 | ['format.cancel-prepend'] = {}, -- Cancel any `@format.prepend-*` applied to the node 428 | ['format.keep'] = {}, -- String content is not exposed as a syntax node. This is a workaround for it 429 | ['format.replace'] = {}, -- Dedicated capture used to store results of `(#gsub!)` 430 | ['format.remove'] = {}, -- Do not add the syntax node to the result, i.e. brackets [], parens () 431 | } 432 | local root = ts.get_parser(bufnr, "query"):parse(true)[1]:root() 433 | local query = ts.query.parse("query", queries) 434 | for id, node, metadata in query:iter_captures(root, bufnr) do 435 | if query.captures[id]:sub(1, 1) ~= "_" then 436 | map[query.captures[id]][node:id()] = metadata and (metadata[id] and metadata[id] or metadata) or {} 437 | end 438 | end 439 | 440 | iter(bufnr, root, lines, map, 0) 441 | vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) 442 | end 443 | 444 | for _, file in ipairs(files) do 445 | local buf = vim.fn.bufadd(file) 446 | vim.fn.bufload(file) 447 | vim.api.nvim_set_current_buf(buf) 448 | format(buf, format_queries) 449 | end 450 | 451 | vim.cmd "silent wa!" 452 | -------------------------------------------------------------------------------- /src/node-types.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "attribute_node", 4 | "named": true, 5 | "fields": {}, 6 | "children": { 7 | "multiple": true, 8 | "required": true, 9 | "types": [ 10 | { 11 | "type": "attribute_name", 12 | "named": true 13 | }, 14 | { 15 | "type": "concat_statement", 16 | "named": true 17 | }, 18 | { 19 | "type": "mustache_statement", 20 | "named": true 21 | }, 22 | { 23 | "type": "number_literal", 24 | "named": true 25 | } 26 | ] 27 | } 28 | }, 29 | { 30 | "type": "block_params", 31 | "named": true, 32 | "fields": {}, 33 | "children": { 34 | "multiple": true, 35 | "required": false, 36 | "types": [ 37 | { 38 | "type": "identifier", 39 | "named": true 40 | } 41 | ] 42 | } 43 | }, 44 | { 45 | "type": "block_statement", 46 | "named": true, 47 | "fields": { 48 | "program": { 49 | "multiple": true, 50 | "required": false, 51 | "types": [ 52 | { 53 | "type": "block_statement", 54 | "named": true 55 | }, 56 | { 57 | "type": "comment_statement", 58 | "named": true 59 | }, 60 | { 61 | "type": "element_node", 62 | "named": true 63 | }, 64 | { 65 | "type": "mustache_statement", 66 | "named": true 67 | }, 68 | { 69 | "type": "text_node", 70 | "named": true 71 | } 72 | ] 73 | } 74 | }, 75 | "children": { 76 | "multiple": true, 77 | "required": true, 78 | "types": [ 79 | { 80 | "type": "block_statement_end", 81 | "named": true 82 | }, 83 | { 84 | "type": "block_statement_start", 85 | "named": true 86 | } 87 | ] 88 | } 89 | }, 90 | { 91 | "type": "block_statement_end", 92 | "named": true, 93 | "fields": { 94 | "path": { 95 | "multiple": false, 96 | "required": true, 97 | "types": [ 98 | { 99 | "type": "identifier", 100 | "named": true 101 | } 102 | ] 103 | } 104 | } 105 | }, 106 | { 107 | "type": "block_statement_start", 108 | "named": true, 109 | "fields": { 110 | "argument": { 111 | "multiple": true, 112 | "required": false, 113 | "types": [ 114 | { 115 | "type": "boolean_literal", 116 | "named": true 117 | }, 118 | { 119 | "type": "identifier", 120 | "named": true 121 | }, 122 | { 123 | "type": "number_literal", 124 | "named": true 125 | }, 126 | { 127 | "type": "path_expression", 128 | "named": true 129 | }, 130 | { 131 | "type": "string_literal", 132 | "named": true 133 | }, 134 | { 135 | "type": "sub_expression", 136 | "named": true 137 | } 138 | ] 139 | }, 140 | "path": { 141 | "multiple": false, 142 | "required": true, 143 | "types": [ 144 | { 145 | "type": "identifier", 146 | "named": true 147 | } 148 | ] 149 | } 150 | }, 151 | "children": { 152 | "multiple": true, 153 | "required": false, 154 | "types": [ 155 | { 156 | "type": "block_params", 157 | "named": true 158 | }, 159 | { 160 | "type": "hash_pair", 161 | "named": true 162 | } 163 | ] 164 | } 165 | }, 166 | { 167 | "type": "boolean_literal", 168 | "named": true, 169 | "fields": {} 170 | }, 171 | { 172 | "type": "concat_statement", 173 | "named": true, 174 | "fields": {}, 175 | "children": { 176 | "multiple": true, 177 | "required": false, 178 | "types": [ 179 | { 180 | "type": "mustache_statement", 181 | "named": true 182 | } 183 | ] 184 | } 185 | }, 186 | { 187 | "type": "element_node", 188 | "named": true, 189 | "fields": {}, 190 | "children": { 191 | "multiple": true, 192 | "required": true, 193 | "types": [ 194 | { 195 | "type": "block_statement", 196 | "named": true 197 | }, 198 | { 199 | "type": "comment_statement", 200 | "named": true 201 | }, 202 | { 203 | "type": "element_node", 204 | "named": true 205 | }, 206 | { 207 | "type": "element_node_end", 208 | "named": true 209 | }, 210 | { 211 | "type": "element_node_start", 212 | "named": true 213 | }, 214 | { 215 | "type": "element_node_void", 216 | "named": true 217 | }, 218 | { 219 | "type": "mustache_statement", 220 | "named": true 221 | }, 222 | { 223 | "type": "text_node", 224 | "named": true 225 | } 226 | ] 227 | } 228 | }, 229 | { 230 | "type": "element_node_end", 231 | "named": true, 232 | "fields": {}, 233 | "children": { 234 | "multiple": false, 235 | "required": true, 236 | "types": [ 237 | { 238 | "type": "tag_name", 239 | "named": true 240 | } 241 | ] 242 | } 243 | }, 244 | { 245 | "type": "element_node_start", 246 | "named": true, 247 | "fields": {}, 248 | "children": { 249 | "multiple": true, 250 | "required": true, 251 | "types": [ 252 | { 253 | "type": "attribute_node", 254 | "named": true 255 | }, 256 | { 257 | "type": "block_params", 258 | "named": true 259 | }, 260 | { 261 | "type": "comment_statement", 262 | "named": true 263 | }, 264 | { 265 | "type": "mustache_statement", 266 | "named": true 267 | }, 268 | { 269 | "type": "tag_name", 270 | "named": true 271 | } 272 | ] 273 | } 274 | }, 275 | { 276 | "type": "element_node_void", 277 | "named": true, 278 | "fields": {}, 279 | "children": { 280 | "multiple": true, 281 | "required": true, 282 | "types": [ 283 | { 284 | "type": "attribute_node", 285 | "named": true 286 | }, 287 | { 288 | "type": "comment_statement", 289 | "named": true 290 | }, 291 | { 292 | "type": "mustache_statement", 293 | "named": true 294 | }, 295 | { 296 | "type": "tag_name", 297 | "named": true 298 | } 299 | ] 300 | } 301 | }, 302 | { 303 | "type": "hash_pair", 304 | "named": true, 305 | "fields": { 306 | "key": { 307 | "multiple": false, 308 | "required": true, 309 | "types": [ 310 | { 311 | "type": "identifier", 312 | "named": true 313 | } 314 | ] 315 | }, 316 | "value": { 317 | "multiple": false, 318 | "required": true, 319 | "types": [ 320 | { 321 | "type": "boolean_literal", 322 | "named": true 323 | }, 324 | { 325 | "type": "identifier", 326 | "named": true 327 | }, 328 | { 329 | "type": "number_literal", 330 | "named": true 331 | }, 332 | { 333 | "type": "path_expression", 334 | "named": true 335 | }, 336 | { 337 | "type": "string_literal", 338 | "named": true 339 | }, 340 | { 341 | "type": "sub_expression", 342 | "named": true 343 | } 344 | ] 345 | } 346 | } 347 | }, 348 | { 349 | "type": "helper_invocation", 350 | "named": true, 351 | "fields": { 352 | "argument": { 353 | "multiple": true, 354 | "required": false, 355 | "types": [ 356 | { 357 | "type": "boolean_literal", 358 | "named": true 359 | }, 360 | { 361 | "type": "identifier", 362 | "named": true 363 | }, 364 | { 365 | "type": "number_literal", 366 | "named": true 367 | }, 368 | { 369 | "type": "path_expression", 370 | "named": true 371 | }, 372 | { 373 | "type": "string_literal", 374 | "named": true 375 | }, 376 | { 377 | "type": "sub_expression", 378 | "named": true 379 | } 380 | ] 381 | }, 382 | "helper": { 383 | "multiple": false, 384 | "required": true, 385 | "types": [ 386 | { 387 | "type": "identifier", 388 | "named": true 389 | }, 390 | { 391 | "type": "path_expression", 392 | "named": true 393 | } 394 | ] 395 | } 396 | }, 397 | "children": { 398 | "multiple": true, 399 | "required": false, 400 | "types": [ 401 | { 402 | "type": "hash_pair", 403 | "named": true 404 | } 405 | ] 406 | } 407 | }, 408 | { 409 | "type": "mustache_statement", 410 | "named": true, 411 | "fields": {}, 412 | "children": { 413 | "multiple": false, 414 | "required": true, 415 | "types": [ 416 | { 417 | "type": "boolean_literal", 418 | "named": true 419 | }, 420 | { 421 | "type": "helper_invocation", 422 | "named": true 423 | }, 424 | { 425 | "type": "identifier", 426 | "named": true 427 | }, 428 | { 429 | "type": "number_literal", 430 | "named": true 431 | }, 432 | { 433 | "type": "path_expression", 434 | "named": true 435 | }, 436 | { 437 | "type": "string_literal", 438 | "named": true 439 | }, 440 | { 441 | "type": "sub_expression", 442 | "named": true 443 | } 444 | ] 445 | } 446 | }, 447 | { 448 | "type": "path_expression", 449 | "named": true, 450 | "fields": {}, 451 | "children": { 452 | "multiple": true, 453 | "required": true, 454 | "types": [ 455 | { 456 | "type": "identifier", 457 | "named": true 458 | } 459 | ] 460 | } 461 | }, 462 | { 463 | "type": "string_literal", 464 | "named": true, 465 | "fields": {} 466 | }, 467 | { 468 | "type": "sub_expression", 469 | "named": true, 470 | "fields": {}, 471 | "children": { 472 | "multiple": false, 473 | "required": true, 474 | "types": [ 475 | { 476 | "type": "boolean_literal", 477 | "named": true 478 | }, 479 | { 480 | "type": "helper_invocation", 481 | "named": true 482 | }, 483 | { 484 | "type": "identifier", 485 | "named": true 486 | }, 487 | { 488 | "type": "number_literal", 489 | "named": true 490 | }, 491 | { 492 | "type": "path_expression", 493 | "named": true 494 | }, 495 | { 496 | "type": "string_literal", 497 | "named": true 498 | }, 499 | { 500 | "type": "sub_expression", 501 | "named": true 502 | } 503 | ] 504 | } 505 | }, 506 | { 507 | "type": "template", 508 | "named": true, 509 | "fields": {}, 510 | "children": { 511 | "multiple": true, 512 | "required": false, 513 | "types": [ 514 | { 515 | "type": "block_statement", 516 | "named": true 517 | }, 518 | { 519 | "type": "comment_statement", 520 | "named": true 521 | }, 522 | { 523 | "type": "element_node", 524 | "named": true 525 | }, 526 | { 527 | "type": "mustache_statement", 528 | "named": true 529 | }, 530 | { 531 | "type": "text_node", 532 | "named": true 533 | } 534 | ] 535 | } 536 | }, 537 | { 538 | "type": "\"", 539 | "named": false 540 | }, 541 | { 542 | "type": "'", 543 | "named": false 544 | }, 545 | { 546 | "type": "(", 547 | "named": false 548 | }, 549 | { 550 | "type": ")", 551 | "named": false 552 | }, 553 | { 554 | "type": ".", 555 | "named": false 556 | }, 557 | { 558 | "type": "/>", 559 | "named": false 560 | }, 561 | { 562 | "type": "<", 563 | "named": false 564 | }, 565 | { 566 | "type": "", 575 | "named": false 576 | }, 577 | { 578 | "type": "as", 579 | "named": false 580 | }, 581 | { 582 | "type": "attribute_name", 583 | "named": true 584 | }, 585 | { 586 | "type": "comment_statement", 587 | "named": true 588 | }, 589 | { 590 | "type": "false", 591 | "named": false 592 | }, 593 | { 594 | "type": "identifier", 595 | "named": true 596 | }, 597 | { 598 | "type": "number_literal", 599 | "named": true 600 | }, 601 | { 602 | "type": "tag_name", 603 | "named": true 604 | }, 605 | { 606 | "type": "text_node", 607 | "named": true 608 | }, 609 | { 610 | "type": "true", 611 | "named": false 612 | }, 613 | { 614 | "type": "{{", 615 | "named": false 616 | }, 617 | { 618 | "type": "{{#", 619 | "named": false 620 | }, 621 | { 622 | "type": "{{/", 623 | "named": false 624 | }, 625 | { 626 | "type": "{{~", 627 | "named": false 628 | }, 629 | { 630 | "type": "{{~#", 631 | "named": false 632 | }, 633 | { 634 | "type": "{{~/", 635 | "named": false 636 | }, 637 | { 638 | "type": "|", 639 | "named": false 640 | }, 641 | { 642 | "type": "}}", 643 | "named": false 644 | }, 645 | { 646 | "type": "~}}", 647 | "named": false 648 | } 649 | ] -------------------------------------------------------------------------------- /src/grammar.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "glimmer", 3 | "rules": { 4 | "template": { 5 | "type": "REPEAT", 6 | "content": { 7 | "type": "SYMBOL", 8 | "name": "_declaration" 9 | } 10 | }, 11 | "_declaration": { 12 | "type": "CHOICE", 13 | "members": [ 14 | { 15 | "type": "ALIAS", 16 | "content": { 17 | "type": "SYMBOL", 18 | "name": "comment" 19 | }, 20 | "named": true, 21 | "value": "comment_statement" 22 | }, 23 | { 24 | "type": "SYMBOL", 25 | "name": "mustache_statement" 26 | }, 27 | { 28 | "type": "SYMBOL", 29 | "name": "block_statement" 30 | }, 31 | { 32 | "type": "SYMBOL", 33 | "name": "element_node" 34 | }, 35 | { 36 | "type": "SYMBOL", 37 | "name": "text_node" 38 | } 39 | ] 40 | }, 41 | "text_node": { 42 | "type": "TOKEN", 43 | "content": { 44 | "type": "PATTERN", 45 | "value": "[^<>{}]+" 46 | } 47 | }, 48 | "string_literal": { 49 | "type": "CHOICE", 50 | "members": [ 51 | { 52 | "type": "SYMBOL", 53 | "name": "_single_quote_string_literal" 54 | }, 55 | { 56 | "type": "SYMBOL", 57 | "name": "_double_quote_string_literal" 58 | } 59 | ] 60 | }, 61 | "_single_quote_string_literal": { 62 | "type": "SEQ", 63 | "members": [ 64 | { 65 | "type": "STRING", 66 | "value": "'" 67 | }, 68 | { 69 | "type": "PATTERN", 70 | "value": "[^'\\\\]*" 71 | }, 72 | { 73 | "type": "STRING", 74 | "value": "'" 75 | } 76 | ] 77 | }, 78 | "_double_quote_string_literal": { 79 | "type": "SEQ", 80 | "members": [ 81 | { 82 | "type": "STRING", 83 | "value": "\"" 84 | }, 85 | { 86 | "type": "PATTERN", 87 | "value": "[^\"\\\\]*" 88 | }, 89 | { 90 | "type": "STRING", 91 | "value": "\"" 92 | } 93 | ] 94 | }, 95 | "number_literal": { 96 | "type": "PATTERN", 97 | "value": "[0-9]+" 98 | }, 99 | "boolean_literal": { 100 | "type": "CHOICE", 101 | "members": [ 102 | { 103 | "type": "STRING", 104 | "value": "true" 105 | }, 106 | { 107 | "type": "STRING", 108 | "value": "false" 109 | } 110 | ] 111 | }, 112 | "tag_name": { 113 | "type": "PATTERN", 114 | "value": "(@?[a-zA-Z0-9]|-|:|\\.)+" 115 | }, 116 | "element_node_start": { 117 | "type": "SEQ", 118 | "members": [ 119 | { 120 | "type": "STRING", 121 | "value": "<" 122 | }, 123 | { 124 | "type": "SYMBOL", 125 | "name": "tag_name" 126 | }, 127 | { 128 | "type": "REPEAT", 129 | "content": { 130 | "type": "CHOICE", 131 | "members": [ 132 | { 133 | "type": "SYMBOL", 134 | "name": "attribute_node" 135 | }, 136 | { 137 | "type": "SYMBOL", 138 | "name": "mustache_statement" 139 | }, 140 | { 141 | "type": "ALIAS", 142 | "content": { 143 | "type": "SYMBOL", 144 | "name": "comment" 145 | }, 146 | "named": true, 147 | "value": "comment_statement" 148 | } 149 | ] 150 | } 151 | }, 152 | { 153 | "type": "CHOICE", 154 | "members": [ 155 | { 156 | "type": "SYMBOL", 157 | "name": "block_params" 158 | }, 159 | { 160 | "type": "BLANK" 161 | } 162 | ] 163 | }, 164 | { 165 | "type": "STRING", 166 | "value": ">" 167 | } 168 | ] 169 | }, 170 | "element_node_end": { 171 | "type": "SEQ", 172 | "members": [ 173 | { 174 | "type": "STRING", 175 | "value": "" 184 | } 185 | ] 186 | }, 187 | "element_node_void": { 188 | "type": "SEQ", 189 | "members": [ 190 | { 191 | "type": "STRING", 192 | "value": "<" 193 | }, 194 | { 195 | "type": "SYMBOL", 196 | "name": "tag_name" 197 | }, 198 | { 199 | "type": "REPEAT", 200 | "content": { 201 | "type": "CHOICE", 202 | "members": [ 203 | { 204 | "type": "SYMBOL", 205 | "name": "attribute_node" 206 | }, 207 | { 208 | "type": "SYMBOL", 209 | "name": "mustache_statement" 210 | }, 211 | { 212 | "type": "ALIAS", 213 | "content": { 214 | "type": "SYMBOL", 215 | "name": "comment" 216 | }, 217 | "named": true, 218 | "value": "comment_statement" 219 | } 220 | ] 221 | } 222 | }, 223 | { 224 | "type": "STRING", 225 | "value": "/>" 226 | } 227 | ] 228 | }, 229 | "element_node": { 230 | "type": "CHOICE", 231 | "members": [ 232 | { 233 | "type": "SEQ", 234 | "members": [ 235 | { 236 | "type": "SYMBOL", 237 | "name": "element_node_start" 238 | }, 239 | { 240 | "type": "REPEAT", 241 | "content": { 242 | "type": "SYMBOL", 243 | "name": "_declaration" 244 | } 245 | }, 246 | { 247 | "type": "SYMBOL", 248 | "name": "element_node_end" 249 | } 250 | ] 251 | }, 252 | { 253 | "type": "SYMBOL", 254 | "name": "element_node_void" 255 | } 256 | ] 257 | }, 258 | "attribute_name": { 259 | "type": "PATTERN", 260 | "value": "[^<>\"'/={}()\\s\\.,!?|]+" 261 | }, 262 | "_splattributes": { 263 | "type": "STRING", 264 | "value": "...attributes" 265 | }, 266 | "attribute_node": { 267 | "type": "CHOICE", 268 | "members": [ 269 | { 270 | "type": "SEQ", 271 | "members": [ 272 | { 273 | "type": "SYMBOL", 274 | "name": "attribute_name" 275 | }, 276 | { 277 | "type": "CHOICE", 278 | "members": [ 279 | { 280 | "type": "SEQ", 281 | "members": [ 282 | { 283 | "type": "STRING", 284 | "value": "=" 285 | }, 286 | { 287 | "type": "CHOICE", 288 | "members": [ 289 | { 290 | "type": "SYMBOL", 291 | "name": "concat_statement" 292 | }, 293 | { 294 | "type": "SYMBOL", 295 | "name": "number_literal" 296 | }, 297 | { 298 | "type": "SYMBOL", 299 | "name": "mustache_statement" 300 | } 301 | ] 302 | } 303 | ] 304 | }, 305 | { 306 | "type": "BLANK" 307 | } 308 | ] 309 | } 310 | ] 311 | }, 312 | { 313 | "type": "ALIAS", 314 | "content": { 315 | "type": "SYMBOL", 316 | "name": "_splattributes" 317 | }, 318 | "named": true, 319 | "value": "attribute_name" 320 | } 321 | ] 322 | }, 323 | "concat_statement": { 324 | "type": "CHOICE", 325 | "members": [ 326 | { 327 | "type": "SYMBOL", 328 | "name": "_single_quote_concat_statement" 329 | }, 330 | { 331 | "type": "SYMBOL", 332 | "name": "_double_quote_concat_statement" 333 | } 334 | ] 335 | }, 336 | "_single_quote_concat_statement": { 337 | "type": "SEQ", 338 | "members": [ 339 | { 340 | "type": "STRING", 341 | "value": "'" 342 | }, 343 | { 344 | "type": "REPEAT", 345 | "content": { 346 | "type": "CHOICE", 347 | "members": [ 348 | { 349 | "type": "SYMBOL", 350 | "name": "_mustache_safe_single_quote_string_literal_content" 351 | }, 352 | { 353 | "type": "SYMBOL", 354 | "name": "mustache_statement" 355 | } 356 | ] 357 | } 358 | }, 359 | { 360 | "type": "STRING", 361 | "value": "'" 362 | } 363 | ] 364 | }, 365 | "_double_quote_concat_statement": { 366 | "type": "SEQ", 367 | "members": [ 368 | { 369 | "type": "STRING", 370 | "value": "\"" 371 | }, 372 | { 373 | "type": "REPEAT", 374 | "content": { 375 | "type": "CHOICE", 376 | "members": [ 377 | { 378 | "type": "SYMBOL", 379 | "name": "_mustache_safe_double_quote_string_literal_content" 380 | }, 381 | { 382 | "type": "SYMBOL", 383 | "name": "mustache_statement" 384 | } 385 | ] 386 | } 387 | }, 388 | { 389 | "type": "STRING", 390 | "value": "\"" 391 | } 392 | ] 393 | }, 394 | "_mustache_safe_string_literal": { 395 | "type": "CHOICE", 396 | "members": [ 397 | { 398 | "type": "SYMBOL", 399 | "name": "_mustache_safe_single_quote_string_literal" 400 | }, 401 | { 402 | "type": "SYMBOL", 403 | "name": "_mustache_safe_double_quote_string_literal" 404 | } 405 | ] 406 | }, 407 | "_mustache_safe_single_quote_string_literal_content": { 408 | "type": "PATTERN", 409 | "value": "[^'\\\\{]+" 410 | }, 411 | "_mustache_safe_single_quote_string_literal": { 412 | "type": "SEQ", 413 | "members": [ 414 | { 415 | "type": "STRING", 416 | "value": "'" 417 | }, 418 | { 419 | "type": "SYMBOL", 420 | "name": "_mustache_safe_single_quote_string_literal_content" 421 | }, 422 | { 423 | "type": "STRING", 424 | "value": "'" 425 | } 426 | ] 427 | }, 428 | "_mustache_safe_double_quote_string_literal_content": { 429 | "type": "PATTERN", 430 | "value": "[^\"\\\\{]+" 431 | }, 432 | "_mustache_safe_double_quote_string_literal": { 433 | "type": "SEQ", 434 | "members": [ 435 | { 436 | "type": "STRING", 437 | "value": "\"" 438 | }, 439 | { 440 | "type": "SYMBOL", 441 | "name": "_mustache_safe_double_quote_string_literal_content" 442 | }, 443 | { 444 | "type": "STRING", 445 | "value": "\"" 446 | } 447 | ] 448 | }, 449 | "block_params": { 450 | "type": "SEQ", 451 | "members": [ 452 | { 453 | "type": "STRING", 454 | "value": "as" 455 | }, 456 | { 457 | "type": "STRING", 458 | "value": "|" 459 | }, 460 | { 461 | "type": "REPEAT", 462 | "content": { 463 | "type": "SYMBOL", 464 | "name": "identifier" 465 | } 466 | }, 467 | { 468 | "type": "STRING", 469 | "value": "|" 470 | } 471 | ] 472 | }, 473 | "identifier": { 474 | "type": "PATTERN", 475 | "value": "[^<>\"'/={}()\\s\\.,!|]+" 476 | }, 477 | "path_expression": { 478 | "type": "SEQ", 479 | "members": [ 480 | { 481 | "type": "SYMBOL", 482 | "name": "identifier" 483 | }, 484 | { 485 | "type": "REPEAT1", 486 | "content": { 487 | "type": "SEQ", 488 | "members": [ 489 | { 490 | "type": "STRING", 491 | "value": "." 492 | }, 493 | { 494 | "type": "SYMBOL", 495 | "name": "identifier" 496 | } 497 | ] 498 | } 499 | } 500 | ] 501 | }, 502 | "_expression": { 503 | "type": "CHOICE", 504 | "members": [ 505 | { 506 | "type": "PREC", 507 | "value": 1, 508 | "content": { 509 | "type": "SYMBOL", 510 | "name": "number_literal" 511 | } 512 | }, 513 | { 514 | "type": "SYMBOL", 515 | "name": "string_literal" 516 | }, 517 | { 518 | "type": "SYMBOL", 519 | "name": "boolean_literal" 520 | }, 521 | { 522 | "type": "SYMBOL", 523 | "name": "sub_expression" 524 | }, 525 | { 526 | "type": "SYMBOL", 527 | "name": "path_expression" 528 | }, 529 | { 530 | "type": "SYMBOL", 531 | "name": "identifier" 532 | } 533 | ] 534 | }, 535 | "hash_pair": { 536 | "type": "SEQ", 537 | "members": [ 538 | { 539 | "type": "FIELD", 540 | "name": "key", 541 | "content": { 542 | "type": "SYMBOL", 543 | "name": "identifier" 544 | } 545 | }, 546 | { 547 | "type": "STRING", 548 | "value": "=" 549 | }, 550 | { 551 | "type": "FIELD", 552 | "name": "value", 553 | "content": { 554 | "type": "SYMBOL", 555 | "name": "_expression" 556 | } 557 | } 558 | ] 559 | }, 560 | "mustache_statement": { 561 | "type": "SEQ", 562 | "members": [ 563 | { 564 | "type": "CHOICE", 565 | "members": [ 566 | { 567 | "type": "STRING", 568 | "value": "{{" 569 | }, 570 | { 571 | "type": "STRING", 572 | "value": "{{~" 573 | } 574 | ] 575 | }, 576 | { 577 | "type": "CHOICE", 578 | "members": [ 579 | { 580 | "type": "SYMBOL", 581 | "name": "_expression" 582 | }, 583 | { 584 | "type": "SYMBOL", 585 | "name": "helper_invocation" 586 | } 587 | ] 588 | }, 589 | { 590 | "type": "CHOICE", 591 | "members": [ 592 | { 593 | "type": "STRING", 594 | "value": "}}" 595 | }, 596 | { 597 | "type": "STRING", 598 | "value": "~}}" 599 | } 600 | ] 601 | } 602 | ] 603 | }, 604 | "sub_expression": { 605 | "type": "SEQ", 606 | "members": [ 607 | { 608 | "type": "STRING", 609 | "value": "(" 610 | }, 611 | { 612 | "type": "CHOICE", 613 | "members": [ 614 | { 615 | "type": "SYMBOL", 616 | "name": "_expression" 617 | }, 618 | { 619 | "type": "SYMBOL", 620 | "name": "helper_invocation" 621 | } 622 | ] 623 | }, 624 | { 625 | "type": "STRING", 626 | "value": ")" 627 | } 628 | ] 629 | }, 630 | "_arguments": { 631 | "type": "CHOICE", 632 | "members": [ 633 | { 634 | "type": "SEQ", 635 | "members": [ 636 | { 637 | "type": "REPEAT1", 638 | "content": { 639 | "type": "FIELD", 640 | "name": "argument", 641 | "content": { 642 | "type": "SYMBOL", 643 | "name": "_expression" 644 | } 645 | } 646 | }, 647 | { 648 | "type": "REPEAT", 649 | "content": { 650 | "type": "SYMBOL", 651 | "name": "hash_pair" 652 | } 653 | } 654 | ] 655 | }, 656 | { 657 | "type": "SEQ", 658 | "members": [ 659 | { 660 | "type": "REPEAT", 661 | "content": { 662 | "type": "FIELD", 663 | "name": "argument", 664 | "content": { 665 | "type": "SYMBOL", 666 | "name": "_expression" 667 | } 668 | } 669 | }, 670 | { 671 | "type": "REPEAT1", 672 | "content": { 673 | "type": "SYMBOL", 674 | "name": "hash_pair" 675 | } 676 | } 677 | ] 678 | } 679 | ] 680 | }, 681 | "helper_invocation": { 682 | "type": "SEQ", 683 | "members": [ 684 | { 685 | "type": "FIELD", 686 | "name": "helper", 687 | "content": { 688 | "type": "CHOICE", 689 | "members": [ 690 | { 691 | "type": "SYMBOL", 692 | "name": "identifier" 693 | }, 694 | { 695 | "type": "SYMBOL", 696 | "name": "path_expression" 697 | } 698 | ] 699 | } 700 | }, 701 | { 702 | "type": "SYMBOL", 703 | "name": "_arguments" 704 | } 705 | ] 706 | }, 707 | "block_statement_start": { 708 | "type": "SEQ", 709 | "members": [ 710 | { 711 | "type": "CHOICE", 712 | "members": [ 713 | { 714 | "type": "STRING", 715 | "value": "{{#" 716 | }, 717 | { 718 | "type": "STRING", 719 | "value": "{{~#" 720 | } 721 | ] 722 | }, 723 | { 724 | "type": "FIELD", 725 | "name": "path", 726 | "content": { 727 | "type": "SYMBOL", 728 | "name": "identifier" 729 | } 730 | }, 731 | { 732 | "type": "SYMBOL", 733 | "name": "_arguments" 734 | }, 735 | { 736 | "type": "CHOICE", 737 | "members": [ 738 | { 739 | "type": "SYMBOL", 740 | "name": "block_params" 741 | }, 742 | { 743 | "type": "BLANK" 744 | } 745 | ] 746 | }, 747 | { 748 | "type": "CHOICE", 749 | "members": [ 750 | { 751 | "type": "STRING", 752 | "value": "}}" 753 | }, 754 | { 755 | "type": "STRING", 756 | "value": "~}}" 757 | } 758 | ] 759 | } 760 | ] 761 | }, 762 | "block_statement_end": { 763 | "type": "SEQ", 764 | "members": [ 765 | { 766 | "type": "CHOICE", 767 | "members": [ 768 | { 769 | "type": "STRING", 770 | "value": "{{/" 771 | }, 772 | { 773 | "type": "STRING", 774 | "value": "{{~/" 775 | } 776 | ] 777 | }, 778 | { 779 | "type": "FIELD", 780 | "name": "path", 781 | "content": { 782 | "type": "SYMBOL", 783 | "name": "identifier" 784 | } 785 | }, 786 | { 787 | "type": "CHOICE", 788 | "members": [ 789 | { 790 | "type": "STRING", 791 | "value": "}}" 792 | }, 793 | { 794 | "type": "STRING", 795 | "value": "~}}" 796 | } 797 | ] 798 | } 799 | ] 800 | }, 801 | "block_statement": { 802 | "type": "SEQ", 803 | "members": [ 804 | { 805 | "type": "SYMBOL", 806 | "name": "block_statement_start" 807 | }, 808 | { 809 | "type": "FIELD", 810 | "name": "program", 811 | "content": { 812 | "type": "REPEAT", 813 | "content": { 814 | "type": "SYMBOL", 815 | "name": "_declaration" 816 | } 817 | } 818 | }, 819 | { 820 | "type": "SYMBOL", 821 | "name": "block_statement_end" 822 | } 823 | ] 824 | } 825 | }, 826 | "extras": [ 827 | { 828 | "type": "PATTERN", 829 | "value": "\\s" 830 | } 831 | ], 832 | "conflicts": [], 833 | "precedences": [], 834 | "externals": [ 835 | { 836 | "type": "SYMBOL", 837 | "name": "comment" 838 | } 839 | ], 840 | "inline": [], 841 | "supertypes": [] 842 | } 843 | --------------------------------------------------------------------------------