diff --git a/editor/README.md b/editor/README.md deleted file mode 100644 index 600d266..0000000 --- a/editor/README.md +++ /dev/null @@ -1,97 +0,0 @@ -# Editor Tooling - -Editor support for Deserve, including syntax highlighting for Deserve View Engine (DVE) templates. - -## Table of Contents - -- [DVE (Deserve View Engine)](#dve-deserve-view-engine) -- [Example: Use DVE in Deserve](#example-use-dve-in-deserve) - - [Project Structure](#project-structure) - - [1) Add Templates](#1-add-templates) - - [2) Configure Router](#2-configure-router) - - [3) Render in a Route](#3-render-in-a-route) -- [Syntax Highlighting (Cursor / VS Code / Trae)](#syntax-highlighting-cursor--vs-code--trae) - -## DVE (Deserve View Engine) - -DVE is Deserve's built-in view engine for rendering `.dve` templates. - -## Example: Use DVE in Deserve - -### Project Structure - -``` -. -├── main.ts -├── routes/ -│ └── index.ts -└── views/ - ├── index.dve - └── partials/ - └── header.dve -``` - -### 1) Add Templates - -Create `views/index.dve`: - -```txt -{{> partials/header.dve}} -Hello {{ user?.name ?? 'Guest' }}. -``` - -Create `views/partials/header.dve`: - -```txt -

Welcome

-``` - -### 2) Configure Router - -Enable DVE by setting `viewsDir` when the router is created. - -```ts -import { Router } from '@neabyte/deserve' - -const router = new Router({ - routesDir: './routes', - viewsDir: './views' -}) - -await router.serve(8000) -``` - -### 3) Render in a Route - -Create `routes/index.ts`: - -```ts -import type { Context } from '@neabyte/deserve' - -export async function GET(ctx: Context) { - return await ctx.render('index', { user: { name: 'Nea' } }) -} -``` - -Run the server and open `http://localhost:8000` to see the rendered page. - -## Syntax Highlighting (Cursor / VS Code / Trae) - -Deserve ships a local DVE extension package at `editor/dve/dve-language-0.1.0.vsix`. - -Install it with an editor CLI: - -```bash -# Trae -trae --install-extension ./dve/dve-language-0.1.0.vsix --force - -# VS Code -code --install-extension ./dve/dve-language-0.1.0.vsix --force - -# Cursor -cursor --install-extension ./dve/dve-language-0.1.0.vsix --force -``` - -After installing, reload the editor window and open any `.dve` file, where HTML stays the base syntax with the embedded DVE tags highlighted on top. - -- **DVE syntax reference**: See [`editor/dve/README.md`](dve/README.md) diff --git a/editor/dve/README.md b/editor/dve/README.md deleted file mode 100644 index 9b0bd66..0000000 --- a/editor/dve/README.md +++ /dev/null @@ -1,250 +0,0 @@ -# DVE Grammar - -A short, friendly tour of the Deserve `.dve` template syntax that reads from top to bottom, so the first open of a `.dve` file feels easy instead of intimidating. - -- **Editor tooling overview**: See [`editor/README.md`](../README.md) - -## Table of Contents - -- [Install Local VSIX](#install-local-vsix) -- [Start Here](#start-here) -- [Variables](#variables) -- [Raw Output (Unescaped)](#raw-output-unescaped) -- [Include](#include) -- [If / Else](#if--else) -- [Each](#each) -- [Each Metadata](#each-metadata) -- [Expressions](#expressions) -- [Operator Reference](#operator-reference) -- [Snippets](#snippets) -- [Advanced Examples](#advanced-examples) -- [What DVE Does Not Do](#what-dve-does-not-do) -- [Editor Scope Mapping](#editor-scope-mapping) - -## Install Local VSIX - -This folder ships a prebuilt VSIX package, so nothing needs to be built first: - -```txt -dve-language-0.1.0.vsix -``` - -Install it from this directory with an editor CLI: - -```bash -# VS Code -code --install-extension ./dve-language-0.1.0.vsix --force - -# Cursor -cursor --install-extension ./dve-language-0.1.0.vsix --force - -# Trae -trae --install-extension ./dve-language-0.1.0.vsix --force -``` - -Reload the editor after installing, and since DVE builds on HTML syntax the `.dve` files keep full HTML highlighting with the template tags layered on top. - -## Start Here - -The whole language comes down to two tags, and once those land the rest is just small variations. - -A `{{ ... }}` tag **shows a value** while a `{{#... }} ... {{/... }}` tag wraps a **block** like an if or a loop, and everything further down builds on those two shapes. - -```txt -Hello {{ user?.name ?? 'Guest' }}. -{{#if user?.isAdmin}}ADMIN{{else}}USER{{/if}} -``` - -## Variables - -A value wrapped in double braces is printed onto the page, and DVE escapes HTML by default so user input can never sneak in markup or open an injection hole. - -```txt -Hello {{ name }}. -``` - -## Raw Output (Unescaped) - -Triple braces print the value as-is with no escaping, which is meant only for HTML that is already known to be safe. - -```txt -{{{ trustedHtml }}} -``` - -## Include - -A repeated piece of markup can live in its own file and get pulled in with an include, where the path is resolved relative to the configured `viewsDir`. - -```txt -{{> partials/header.dve}} -``` - -## If / Else - -An `#if` block renders its body only when the condition is truthy and an optional `else` covers the other case, and every `#if` must be closed with a matching `/if` or DVE reports the block as unclosed. - -```txt -{{#if ok}}YES{{else}}NO{{/if}} -``` - -## Each - -An `#each` block walks an array and `as` names the current item, and leaving the name out falls back to `item`. - -```txt -{{#each items as item}}{{ item }},{{/each}} -``` - -## Each Metadata - -Inside an `#each` block four helpers are available for free, so the loop position never has to be tracked by hand: - -- `@index` — current position, starting at 0 -- `@first` — true on the first item -- `@last` — true on the last item -- `@length` — total number of items - -```txt -{{#each items as item}}({{ @index }}/{{ @length }} {{#if @first}}F{{else}}-{{/if}}{{#if @last}}L{{else}}-{{/if}}={{ item }});{{/each}} -``` - -## Expressions - -Any `{{ ... }}` tag accepts a small JavaScript-like expression, so a value can be read, given a fallback, compared, or run through a little math all in one place. - -```txt -Hello {{ user?.name ?? 'Guest' }}. -Total {{ price * quantity }} -{{#if age >= 18}}Adult{{else}}Minor{{/if}} -``` - -A few behaviours worth knowing: - -- A dotted path like `user.profile.name` reads nested values, and missing data along the way resolves to `undefined` -- Both `.` and `?.` return `undefined` when the object is missing, so a deep lookup never throws -- Strings use `"double"` or `'single'` quotes and understand the `\n`, `\t`, `\r` escapes -- Numbers can be decimals or exponents like `2.5` or `1e3` - -## Operator Reference - -Everything DVE understands, lowest precedence at the top down to highest at the bottom, and anything not on this list is rejected by the parser on purpose. - -| Group | Operators | Example | -| -------------- | --------------------------------------------------- | -------------------------- | -| Ternary | `? :` | `{{ ok ? 'yes' : 'no' }}` | -| Nullish | `??` | `{{ name ?? 'Guest' }}` | -| Logical OR | `\|\|` | `{{ a \|\| b }}` | -| Logical AND | `&&` | `{{ a && b }}` | -| Equality | `===` `!==` `==` `!=` | `{{ role === 'admin' }}` | -| Relational | `>` `<` `>=` `<=` | `{{ age >= 18 }}` | -| Additive | `+` `-` | `{{ a + b }}` | -| Multiplicative | `*` `/` `%` | `{{ total % 2 }}` | -| Unary | `!` `+` `-` | `{{ !done }}` | -| Member | `.` `?.` | `{{ user?.profile.name }}` | -| Grouping | `( )` | `{{ (a + b) * c }}` | -| Literals | numbers, strings, `true` `false` `null` `undefined` | `{{ 1 + 2 * 3 }}` | - -## Snippets - -Type a prefix and press Tab to drop in the syntax that is easiest to forget. - -| Prefix | Inserts | -| ---------- | ------------------------------------- | -| `dve` | `{{ value }}` | -| `dveraw` | `{{{ html }}}` | -| `dveinc` | `{{> partials/header.dve }}` | -| `dveif` | `{{#if}} ... {{else}} ... {{/if}}` | -| `dveifn` | multi-line `{{#if}}` block | -| `dveeach` | `{{#each items as item}}` block | -| `dveeachm` | `#each` block with `@index`/`@length` | -| `dvetern` | `{{ cond ? yes : no }}` | -| `dvedef` | `{{ value ?? 'fallback' }}` | -| `dveopt` | `{{ user?.name ?? 'Guest' }}` | -| `dvecmt` | `` | - -## Advanced Examples - -### Layout + Partial Composition - -`views/layout.dve`: - -```txt - - - {{> partials/header.dve}} -
- {{{ bodyHtml }}} -
- {{> partials/footer.dve}} - - -``` - -`views/partials/header.dve`: - -```txt -
-

{{ title ?? 'Untitled' }}

- {{#if user?.name}}

Hello {{ user.name }}.

{{else}}

Hello Guest.

{{/if}} -
-``` - -### Lists With Conditional Blocks - -```txt -{{#if items?.length ?? 0}} - -{{else}} -

No items.

-{{/if}} -``` - -### Nested Each (Matrix-Style) - -```txt - - {{#each rows as row}} - - {{#each row as cell}} - - {{/each}} - - {{/each}} -
{{ cell }}
-``` - -## What DVE Does Not Do - -DVE stays small on purpose so a template can never run arbitrary code, and these limits are the safety boundary rather than missing features: - -- No function or method calls -- No array indexing like `items[0]` -- No assignment or variable declarations -- No regular expressions or arbitrary JavaScript - -Two guardrails also stop runaway templates, where include nesting is capped at 64 levels deep and a single `#each` is capped at 100,000 iterations, and crossing either one raises a clear error instead of hanging. - -Anything that needs real logic belongs in the route handler, where the finished value gets computed and then passed into the template. - -## Editor Scope Mapping - -| Syntax | Scope | -| ---------------------------------- | ------------------------------------------------------- | -| `{{` `}}` `{{{` `}}}` | `meta.tag.output.dve` / `meta.tag.raw.dve` | -| `#if` `#each` `else` `/if` `/each` | `keyword.control.dve` | -| `>` (include) | `keyword.control.include.dve` | -| `as` (in #each) | `keyword.operator.as.dve` | -| Include path | `string.unquoted.path.dve` | -| `true` `false` `null` `undefined` | `constant.language.dve` | -| Numbers (incl. `1e3`) | `constant.numeric.dve` | -| `"..."` `'...'` | `string.quoted.double.dve` / `string.quoted.single.dve` | -| Operators `?.` `===` `??` etc. | `keyword.operator.dve` | -| Identifiers / variables | `variable.other.dve` | -| Item name in #each | `variable.parameter.dve` | diff --git a/editor/dve/dve-language-0.1.0.vsix b/editor/dve/dve-language-0.1.0.vsix deleted file mode 100644 index a729a0d..0000000 Binary files a/editor/dve/dve-language-0.1.0.vsix and /dev/null differ diff --git a/editor/dve/language-configuration.json b/editor/dve/language-configuration.json deleted file mode 100644 index ae56a0e..0000000 --- a/editor/dve/language-configuration.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "comments": {}, - "brackets": [ - ["{{", "}}"], - ["{{{", "}}}"] - ], - "autoClosingPairs": [ - { "open": "{{", "close": "}}" }, - { "open": "{{{", "close": "}}}" }, - { "open": "\"", "close": "\"", "notIn": ["string"] }, - { "open": "'", "close": "'", "notIn": ["string"] } - ], - "surroundingPairs": [ - ["{{", "}}"], - ["{{{", "}}}"], - ["\"", "\""], - ["'", "'"] - ] -} diff --git a/editor/dve/package.json b/editor/dve/package.json deleted file mode 100644 index 1bcaa23..0000000 --- a/editor/dve/package.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "name": "dve-language", - "displayName": "DVE Template Language", - "description": "Syntax highlighting for Deserve (.dve) templates", - "version": "0.1.0", - "publisher": "neabyte", - "engines": { - "vscode": "^1.74.0" - }, - "categories": [ - "Programming Languages" - ], - "contributes": { - "languages": [ - { - "id": "dve", - "aliases": [ - "DVE", - "dve" - ], - "extensions": [ - ".dve" - ], - "configuration": "./language-configuration.json" - } - ], - "snippets": [ - { - "language": "dve", - "path": "./snippets/dve.code-snippets" - } - ], - "grammars": [ - { - "language": "dve", - "scopeName": "text.html.dve", - "path": "./syntaxes/dve.tmLanguage.json", - "embeddedLanguages": { - "meta.embedded.line.dve": "dve", - "meta.embedded.block.dve": "dve" - } - } - ] - } -} diff --git a/editor/dve/snippets/dve.code-snippets b/editor/dve/snippets/dve.code-snippets deleted file mode 100644 index 44899f3..0000000 --- a/editor/dve/snippets/dve.code-snippets +++ /dev/null @@ -1,61 +0,0 @@ -{ - "DVE: Tag": { - "prefix": "dve", - "body": ["{{ ${1:value} }}"], - "description": "Insert DVE tag: {{ value }}" - }, - "DVE: Raw Tag": { - "prefix": "dveraw", - "body": ["{{{ ${1:html} }}}"], - "description": "Insert DVE raw tag: {{{ html }}}" - }, - "DVE: Include": { - "prefix": "dveinc", - "body": ["{{> ${1:partials/header.dve} }}"], - "description": "Insert DVE include: {{> path }}" - }, - "DVE: If / Else": { - "prefix": "dveif", - "body": ["{{#if ${1:condition}}}${2:then}{{else}}${3:else}{{/if}}"], - "description": "Insert DVE if/else block" - }, - "DVE: If": { - "prefix": "dveifn", - "body": ["{{#if ${1:condition}}}", " ${2:then}", "{{/if}}"], - "description": "Insert DVE if block (multi-line)" - }, - "DVE: Each": { - "prefix": "dveeach", - "body": ["{{#each ${1:items} as ${2:item}}}", " ${3:{{ item }}}", "{{/each}}"], - "description": "Insert DVE each block" - }, - "DVE: Each (With Meta)": { - "prefix": "dveeachm", - "body": [ - "{{#each ${1:items} as ${2:item}}}", - " ({{ @index }}/{{ @length }}) ${3:{{ item }}}", - "{{/each}}" - ], - "description": "Insert DVE each block with @index/@length" - }, - "DVE: Ternary": { - "prefix": "dvetern", - "body": ["{{ ${1:condition} ? ${2:yes} : ${3:no} }}"], - "description": "Insert DVE ternary: {{ cond ? yes : no }}" - }, - "DVE: Default (Nullish)": { - "prefix": "dvedef", - "body": ["{{ ${1:value} ?? ${2:'fallback'} }}"], - "description": "Insert DVE nullish default: {{ value ?? 'fallback' }}" - }, - "DVE: Optional Chain": { - "prefix": "dveopt", - "body": ["{{ ${1:user}?.${2:name} ?? ${3:'Guest'} }}"], - "description": "Insert DVE optional chain: {{ user?.name ?? 'Guest' }}" - }, - "DVE: Comment (HTML)": { - "prefix": "dvecmt", - "body": [""], - "description": "Insert HTML comment inside template" - } -} diff --git a/editor/dve/syntaxes/dve.tmLanguage.json b/editor/dve/syntaxes/dve.tmLanguage.json deleted file mode 100644 index ac7e8db..0000000 --- a/editor/dve/syntaxes/dve.tmLanguage.json +++ /dev/null @@ -1,141 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", - "name": "DVE", - "scopeName": "text.html.dve", - "fileTypes": ["dve"], - "patterns": [{ "include": "#dve-tags" }, { "include": "text.html.basic" }], - "repository": { - "dve-tags": { - "patterns": [ - { "include": "#dve-raw-tag" }, - { "include": "#dve-include-tag" }, - { "include": "#dve-block-tag" }, - { "include": "#dve-output-tag" } - ] - }, - "dve-raw-tag": { - "begin": "\\{\\{\\{", - "beginCaptures": { - "0": { "name": "punctuation.section.embedded.begin.dve" } - }, - "end": "\\}\\}\\}", - "endCaptures": { - "0": { "name": "punctuation.section.embedded.end.dve" } - }, - "name": "meta.embedded.line.dve meta.tag.raw.dve", - "contentName": "meta.expression.dve", - "patterns": [{ "include": "#expression" }] - }, - "dve-include-tag": { - "begin": "\\{\\{\\s*(>)", - "beginCaptures": { - "0": { "name": "punctuation.section.embedded.begin.dve" }, - "1": { "name": "keyword.control.include.dve" } - }, - "end": "\\}\\}", - "endCaptures": { - "0": { "name": "punctuation.section.embedded.end.dve" } - }, - "name": "meta.embedded.line.dve meta.tag.include.dve", - "patterns": [{ "include": "#path" }] - }, - "dve-block-tag": { - "begin": "\\{\\{\\s*(#if|#each|else|/if|/each)\\b", - "beginCaptures": { - "0": { "name": "punctuation.section.embedded.begin.dve" }, - "1": { "name": "keyword.control.dve" } - }, - "end": "\\}\\}", - "endCaptures": { - "0": { "name": "punctuation.section.embedded.end.dve" } - }, - "name": "meta.embedded.block.dve meta.tag.block.dve", - "patterns": [ - { - "match": "\\b(as)\\s+([a-zA-Z_$][a-zA-Z0-9_$]*)", - "captures": { - "1": { "name": "keyword.operator.as.dve" }, - "2": { "name": "variable.parameter.dve" } - } - }, - { "include": "#expression" } - ] - }, - "dve-output-tag": { - "begin": "\\{\\{", - "beginCaptures": { - "0": { "name": "punctuation.section.embedded.begin.dve" } - }, - "end": "\\}\\}", - "endCaptures": { - "0": { "name": "punctuation.section.embedded.end.dve" } - }, - "name": "meta.embedded.line.dve meta.tag.output.dve", - "contentName": "meta.expression.dve", - "patterns": [{ "include": "#expression" }] - }, - "expression": { - "patterns": [ - { "include": "#string-double" }, - { "include": "#string-single" }, - { "include": "#number" }, - { "include": "#literal" }, - { "include": "#operator" }, - { "include": "#identifier" } - ] - }, - "path": { - "match": "[@a-zA-Z0-9_$./\\\\-]+\\.dve|[@a-zA-Z_$][a-zA-Z0-9_$.]*", - "name": "string.unquoted.path.dve" - }, - "string-double": { - "begin": "\"", - "end": "\"", - "name": "string.quoted.double.dve", - "patterns": [ - { - "match": "\\\\.", - "name": "constant.character.escape.dve" - } - ] - }, - "string-single": { - "begin": "'", - "end": "'", - "name": "string.quoted.single.dve", - "patterns": [ - { - "match": "\\\\.", - "name": "constant.character.escape.dve" - } - ] - }, - "number": { - "match": "\\b[0-9]+(\\.[0-9]+)?([eE][+-]?[0-9]+)?\\b", - "name": "constant.numeric.dve" - }, - "literal": { - "match": "\\b(true|false|null|undefined)\\b", - "name": "constant.language.dve" - }, - "operator": { - "match": "\\?\\.|===|!==|==|!=|&&|\\|\\||\\?\\?|>=|<=|[?.!:()+\\-*/%<>]", - "name": "keyword.operator.dve" - }, - "identifier": { - "match": "\\b([a-zA-Z_$@][a-zA-Z0-9_$]*)\\b", - "name": "variable.other.dve" - } - }, - "injections": { - "L:text.html.dve - (meta.embedded.line.dve | meta.embedded.block.dve)": { - "patterns": [{ "include": "#dve-tags" }] - }, - "L:text.html.basic": { - "patterns": [{ "include": "#dve-tags" }] - }, - "L:string.quoted.double.html, L:string.quoted.single.html": { - "patterns": [{ "include": "#dve-tags" }] - } - } -}