Repository: nuejs/nue Branch: master Commit: eb1dead53227 Files: 321 Total size: 803.8 KB Directory structure: gitextract_7ybgpagi/ ├── .editorconfig ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug-report.md │ │ ├── config.yml │ │ └── feature-request.md │ └── workflows/ │ ├── build-template-zips.yml │ ├── links.yaml.disabled │ ├── push-examples.yaml.disabled │ ├── push-site.yaml.disabled │ ├── repo/ │ │ └── build-page/ │ │ └── action.yaml │ └── test.yaml ├── .gitignore ├── .prettierrc.yaml ├── CONTRIBUTING.md ├── LICENSE ├── bunfig.toml ├── package.json └── packages/ ├── nuedom/ │ ├── Makefile │ ├── README.md │ ├── bin/ │ │ ├── minify.js │ │ └── serve.js │ ├── package.json │ ├── src/ │ │ ├── compiler/ │ │ │ ├── ast.js │ │ │ ├── attributes.js │ │ │ ├── compiler.js │ │ │ ├── context.js │ │ │ ├── document.js │ │ │ ├── html5.js │ │ │ └── tokenizer.js │ │ ├── dom/ │ │ │ ├── diff.js │ │ │ ├── fakedom.js │ │ │ ├── node.js │ │ │ └── render.js │ │ ├── index.js │ │ ├── nue-jit.js │ │ └── nue.js │ └── test/ │ ├── ast.test.js │ ├── attributes.test.js │ ├── compiler.test.js │ ├── component.test.js │ ├── context.test.js │ ├── document.test.js │ ├── domdiff.test.js │ ├── event.test.js │ ├── event.util.js │ ├── fakedom.test.js │ ├── index.html │ ├── loop.test.js │ ├── render.test.js │ └── tokenizer.test.js ├── nueglow/ │ ├── Makefile │ ├── README.md │ ├── css/ │ │ ├── build.js │ │ ├── light.css │ │ ├── markers.css │ │ └── syntax.css │ ├── index.js │ ├── package.json │ └── test/ │ ├── generate.js │ ├── glow-test.css │ └── glow.test.js ├── nuekit/ │ ├── Makefile │ ├── README.md │ ├── client/ │ │ ├── error.js │ │ ├── hmr.js │ │ ├── mount.js │ │ └── transitions.js │ ├── package.json │ ├── src/ │ │ ├── asset.js │ │ ├── cli.js │ │ ├── cmd/ │ │ │ ├── build.js │ │ │ ├── create.js │ │ │ ├── preview.js │ │ │ └── serve.js │ │ ├── collections.js │ │ ├── conf.js │ │ ├── deps.js │ │ ├── file.js │ │ ├── render/ │ │ │ ├── feed.js │ │ │ ├── head.js │ │ │ ├── page.js │ │ │ └── svg.js │ │ ├── server/ │ │ │ ├── README.md │ │ │ ├── index.js │ │ │ ├── model.js │ │ │ ├── proxy.js │ │ │ └── worker.js │ │ ├── site.js │ │ ├── system.js │ │ └── tools/ │ │ ├── README.md │ │ ├── css.js │ │ ├── fswalk.js │ │ ├── fswatch.js │ │ └── server.js │ └── test/ │ ├── cli.test.js │ ├── cmd/ │ │ ├── build.test.js │ │ ├── create.test.js │ │ └── serve.test.js │ ├── collections.test.js │ ├── conf.test.js │ ├── css.test.js │ ├── deps.test.js │ ├── file.test.js │ ├── fswalk.test.js │ ├── fswatch.test.js │ ├── render/ │ │ ├── asset-render.test.js │ │ ├── feed.test.js │ │ ├── head.test.js │ │ ├── md.test.js │ │ ├── page.test.js │ │ └── svg.test.js │ ├── server/ │ │ ├── model.test.js │ │ ├── proxy.test.js │ │ └── worker.test.js │ ├── server.test.js │ ├── site.test.js │ ├── system.test.js │ └── test-utils.js ├── nuemark/ │ ├── .npmignore │ ├── Makefile │ ├── README.md │ ├── index.js │ ├── package.json │ ├── src/ │ │ ├── parse-blocks.js │ │ ├── parse-document.js │ │ ├── parse-inline.js │ │ ├── parse-tag.js │ │ ├── render-blocks.js │ │ ├── render-inline.js │ │ └── render-tag.js │ └── test/ │ ├── block.test.js │ ├── document.test.js │ ├── inline.test.js │ └── tag.test.js ├── nueserver/ │ ├── Makefile │ ├── README.md │ ├── nueserver.js │ ├── package.json │ └── test/ │ ├── route.test.js │ └── server.test.js ├── nuestate/ │ ├── Makefile │ ├── README.md │ ├── package.json │ ├── src/ │ │ └── state.js │ └── test/ │ ├── browser.test.js │ ├── index.html │ └── state.test.js ├── nueyaml/ │ ├── Makefile │ ├── README.md │ ├── nueyaml.js │ ├── nueyaml.test.js │ └── package.json ├── templates/ │ ├── blog/ │ │ ├── index.css │ │ ├── index.html │ │ ├── layout.html │ │ ├── posts/ │ │ │ ├── components.html │ │ │ ├── css-is-awesome.md │ │ │ ├── design-engineering.md │ │ │ ├── design-systems.md │ │ │ └── extreme-performance.md │ │ └── site.yaml │ ├── full/ │ │ ├── .gitignore │ │ ├── 404.md │ │ ├── @shared/ │ │ │ ├── design/ │ │ │ │ ├── README.md │ │ │ │ ├── base.css │ │ │ │ ├── button.css │ │ │ │ ├── components.css │ │ │ │ ├── content.css │ │ │ │ ├── dialog.css │ │ │ │ ├── document.css │ │ │ │ ├── figure.css │ │ │ │ ├── form.css │ │ │ │ ├── layout.css │ │ │ │ ├── modifier.css │ │ │ │ ├── syntax.css │ │ │ │ └── table.css │ │ │ ├── lib/ │ │ │ │ └── crud.js │ │ │ ├── server/ │ │ │ │ ├── data/ │ │ │ │ │ ├── leads.json │ │ │ │ │ └── users.json │ │ │ │ └── index.js │ │ │ └── ui/ │ │ │ ├── components.html │ │ │ ├── footer.html │ │ │ ├── header.html │ │ │ └── isomorphic.html │ │ ├── Makefile │ │ ├── admin/ │ │ │ ├── app.yaml │ │ │ ├── index.html │ │ │ └── ui/ │ │ │ ├── lead.html │ │ │ ├── leads.html │ │ │ └── shared.html │ │ ├── blog/ │ │ │ ├── components.html │ │ │ ├── css-beats-js.md │ │ │ ├── css-is-awesome.md │ │ │ ├── design-engineering.md │ │ │ ├── design-systems.md │ │ │ ├── extreme-performance.md │ │ │ ├── index.md │ │ │ └── web-standards.md │ │ ├── contact/ │ │ │ ├── contact.html │ │ │ ├── index.md │ │ │ └── thanks.md │ │ ├── docs/ │ │ │ ├── accessible-design.md │ │ │ ├── app.yaml │ │ │ ├── color-theory.md │ │ │ ├── css-architecture-patterns.md │ │ │ ├── design-systems-that-scale.md │ │ │ ├── index.md │ │ │ ├── layout-with-css.md │ │ │ ├── layout.html │ │ │ ├── performance-optimization.md │ │ │ ├── progressive-enhancement.md │ │ │ ├── semantic-html.md │ │ │ ├── testing-web-standards.md │ │ │ └── web-typo-fundamentals.md │ │ ├── index.md │ │ ├── login/ │ │ │ └── index.html │ │ ├── package.json │ │ └── site.yaml │ ├── minimal/ │ │ ├── index.css │ │ └── index.html │ └── spa/ │ ├── css/ │ │ ├── base.css │ │ └── components.css │ ├── index.html │ ├── server/ │ │ ├── data/ │ │ │ └── users.json │ │ └── index.js │ ├── site.yaml │ └── ui/ │ ├── entry.html │ └── table.html └── www/ ├── 404.md ├── @shared/ │ ├── .gitignore │ ├── data/ │ │ ├── authors.yaml │ │ ├── features.yaml │ │ ├── topics.js │ │ ├── topics.test.js │ │ └── topics.yaml │ ├── lib/ │ │ ├── assembly/ │ │ │ ├── assembly.css │ │ │ └── assembly.html │ │ ├── console/ │ │ │ ├── console.css │ │ │ ├── console.html │ │ │ └── console.js │ │ ├── hero/ │ │ │ ├── cta-buttons.html │ │ │ └── hero.css │ │ ├── stack/ │ │ │ ├── stack.css │ │ │ └── stack.html │ │ ├── syntax.css │ │ └── video/ │ │ ├── controller.js │ │ ├── player.html │ │ └── video.css │ └── ui/ │ ├── global-layout.css │ ├── global-layout.html │ ├── grid.css │ ├── icons.html │ └── join-list.html ├── Makefile ├── blog/ │ ├── 2.0/ │ │ ├── app.yaml │ │ └── index.md │ ├── app.yaml │ ├── backstory/ │ │ └── index.md │ ├── index.md │ ├── large-scale-apps/ │ │ └── index.md │ ├── perfect-web-framework/ │ │ ├── index.md │ │ └── perfect.css │ ├── rethinking-reactivity/ │ │ └── index.md │ ├── standards-first-react-alternative/ │ │ ├── complex-table.md │ │ ├── index.md │ │ └── simple-table.md │ ├── standards-first-web-framework/ │ │ └── index.md │ ├── tailwind-misinformation-engine/ │ │ └── index.md │ ├── tailwind-vs-semantic-css/ │ │ └── index.md │ └── ui/ │ ├── blog.css │ └── blog.html ├── docs/ │ ├── app.yaml │ ├── build-system.md │ ├── cli.md │ ├── configuration.md │ ├── contributing.md │ ├── css-development.md │ ├── design-engineering.md │ ├── design-systems.md │ ├── examples/ │ │ └── nue-counter.html │ ├── getting-started.md │ ├── html-file-types.md │ ├── html-syntax.md │ ├── index.md │ ├── interactive-components.md │ ├── js-enhancements.md │ ├── layout-system.md │ ├── migration.md │ ├── minimalism.md │ ├── nuedom.md │ ├── nueglow.md │ ├── nuekit.md │ ├── nuemark-syntax.md │ ├── nuemark.md │ ├── nueserver.md │ ├── nuestate.md │ ├── nueyaml.md │ ├── page-dependencies.md │ ├── project-structure.md │ ├── roadmap.md │ ├── separation-of-concerns.md │ ├── server-api.md │ ├── single-page-apps.md │ ├── spa-development.md │ ├── state-api.md │ ├── svg-development.md │ ├── syntax-highlighting.md │ ├── template-data.md │ ├── ui/ │ │ ├── docs.css │ │ └── docs.html │ ├── universal-data-model.md │ ├── website-development.md │ ├── why-nue-old.md │ ├── why-nue.md │ └── yaml-syntax.md ├── home/ │ ├── app.yaml │ ├── commands.txt │ └── home.css ├── index.md ├── package.json ├── site.yaml └── test.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true charset = utf-8 end_of_line = lf indent_size = 2 tab_width = 2 indent_style = space insert_final_newline = true trim_trailing_whitespace = true ================================================ FILE: .github/ISSUE_TEMPLATE/bug-report.md ================================================ --- name: 🐞 Bug Report about: Create a bug report to help us improve Nue type: bug --- ### Describe the Bug ### Environment ### Minimal Reproduction ### Logs & Additional Context ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: true contact_links: - name: 💬 Discussions url: https://github.com/nuejs/nue/discussions about: Use discussions if you have an idea for improvement or want to ask a question ================================================ FILE: .github/ISSUE_TEMPLATE/feature-request.md ================================================ --- name: 💡 Feature Request or Improvement about: Suggest an idea or improvement labels: new feature, improvement type: feature --- ### Is your feature request or improvement related to a problem? ### Solution you'd like ### Alternatives you've considered ### Additional context ================================================ FILE: .github/workflows/build-template-zips.yml ================================================ name: Build template zips on: push: branches: [ master, 2.0 ] paths: - 'packages/templates/**' permissions: contents: write jobs: build-templates: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Create template zips run: | cd packages/templates zip -r minimal.zip minimal/ zip -r blog.zip blog/ zip -r spa.zip spa/ zip -r full.zip full/ - name: Commit zips run: | git config user.name github-actions git config user.email github-actions@github.com git add packages/templates/*.zip git diff --quiet && git diff --staged --quiet || git commit -m "Update template zips" git push ================================================ FILE: .github/workflows/links.yaml.disabled ================================================ name: Check Links on: push: branches: - master paths: - '**/README.md' - 'packages/nuejs.org/**' pull_request: paths: - '**/README.md' - 'packages/nuejs.org/**' workflow_dispatch: jobs: readme: if: ${{ github.repository_owner == 'nuejs' || github.event_name == 'workflow_dispatch' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Check README links uses: lycheeverse/lychee-action@v2 with: args: > --no-progress --include-fragments --exclude-path "node_modules" --exclude "localhost" --base "." -- "**/README.md" token: ${{ secrets.GITHUB_TOKEN }} site: if: ${{ github.repository_owner == 'nuejs' || github.event_name == 'workflow_dispatch' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Build docs uses: ./.github/workflows/repo/build-page with: root: packages/nuejs.org # keep server running in background - name: Run server run: nue -pr packages/nuejs.org -P 8080 & # find generated HTML files, convert paths to urls # (might be superfluous in the future with some fixes on lychee) - name: Find files to check id: files run: | links=$(find packages/nuejs.org/.dist/prod -name "*.html" -printf "http://localhost:8080/%P ") echo "links=$links" >> $GITHUB_OUTPUT - name: Check site links uses: lycheeverse/lychee-action@v2 with: args: > --no-progress --include-fragments --accept "403, 429, 503, 999" --remap "http://localhost:8080/@ https://nuejs.org/@" --remap "http://localhost:8080/todomvc https://nuejs.org/todomvc" --remap "http://localhost:8080/glow-demo https://nuejs.org/glow-demo" --remap "http://localhost:8080/docs/nuejs/examples https://nuejs.org/docs/nuejs/examples" --remap "http://localhost:8080/docs/tutorials https://nuejs.org/docs/tutorials" --remap "http://localhost:8080/hyper/demo/ https://nuejs.org/hyper/demo/" --exclude "http://localhost:8080/docs/how-it-works.html" --exclude "http://localhost:8080/img/mpa-build.mp4" --exclude "http://localhost:8080/404.html" --exclude "https?://x.com/" -- ${{ steps.files.outputs.links }} token: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/push-examples.yaml.disabled ================================================ name: Push examples to web server on: push: branches: - master paths: - packages/examples/** workflow_dispatch: jobs: examples: if: ${{ github.repository_owner == 'nuejs' || github.event_name == 'workflow_dispatch' }} strategy: fail-fast: false matrix: dir: # dirs in "examples" to run build & push process for - simple-blog - simple-mpa env: dir: 'packages/examples/${{ matrix.dir }}/' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: # get entries to check for change in dir fetch-depth: 2 - name: Check dir changes run: git diff --name-only HEAD^ HEAD | grep -q "^${{ env.dir }}" if: ${{ github.event_name != 'workflow_dispatch' }} # subsitutes non-alphanumeric characters with _ - name: Build secret variable names run: | mdir="${{ matrix.dir }}" name="${mdir//[^[:alnum:]]/_}" echo "$name" echo "storage_name=STORAGE_NAME_$name" >> $GITHUB_ENV echo "storage_pass=STORAGE_PASS_$name" >> $GITHUB_ENV echo "pull_id=PULL_ID_$name" >> $GITHUB_ENV # creates archive of dir, removes img dirs, moves content to subdir "source" - name: Create test archive run: tar -C "${{ env.dir }}" --exclude "*/img" --transform "s/^\.\//source\//" -cf "${{ env.dir }}test.tar.gz" . if: ${{ matrix.dir == 'simple-blog' }} # creates archive of dir, moves content to subdir "source" - name: Create source archive run: git archive --prefix "source/" -o "${{ env.dir }}source.tar.gz" "HEAD:${{ env.dir }}" # Copy source archive, to allow old source path on web server. Step should get removed after new release and a bit of time - name: Duplicate source archive run: cp "${{ env.dir }}source.tar.gz" "${{ env.dir }}${{ matrix.dir }}.tar.gz" - name: Nue build uses: ./.github/workflows/repo/build-page with: root: ${{ env.dir }} - name: BunnyCDN storage deployer uses: ayeressian/bunnycdn-storage-deploy@v2.2.5 with: source: '${{ env.dir }}.dist/prod' storageZoneName: '${{ secrets[env.storage_name] }}' storagePassword: '${{ secrets[env.storage_pass] }}' pullZoneId: '${{ secrets[env.pull_id] }}' accessKey: '${{ secrets.BUNNY_API_KEY }}' upload: 'true' remove: 'false' purgePullZone: 'true' ================================================ FILE: .github/workflows/push-site.yaml.disabled ================================================ name: Push site to web server on: push: branches: - master paths: - packages/nuejs.org/** workflow_dispatch: jobs: site: if: ${{ github.repository_owner == 'nuejs' || github.event_name == 'workflow_dispatch' }} env: dir: 'packages/nuejs.org/' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Nue build uses: ./.github/workflows/repo/build-page with: root: ${{ env.dir }} - name: BunnyCDN storage deployer uses: ayeressian/bunnycdn-storage-deploy@v2.2.5 with: source: '${{ env.dir }}.dist/prod' storageZoneName: '${{ secrets.STORAGE_NAME_site }}' storagePassword: '${{ secrets.STORAGE_PASS_site }}' pullZoneId: '${{ secrets.PULL_ID_site }}' accessKey: '${{ secrets.BUNNY_API_KEY }}' upload: 'true' remove: 'false' purgePullZone: 'true' ================================================ FILE: .github/workflows/repo/build-page/action.yaml ================================================ name: Repository nue page build description: | Internal composite action to install dependencies, register the `nue` command, and build a Nue project. inputs: root: description: | The root directory of the Nue project to build. This is passed to the `--root` option of the `nue build` command. default: '.' required: false runs: using: composite steps: - uses: oven-sh/setup-bun@v2 - name: Install and link nue shell: bash run: | bun install cd packages/nuekit/ bun link - name: Build Nue Project shell: bash run: nue build -pr "${{ inputs.root }}" ================================================ FILE: .github/workflows/test.yaml ================================================ name: Test on: push: branches: - master paths-ignore: - .github/** - packages/templates/** - packages/www/** pull_request: paths-ignore: - .github/** - packages/templates/** - packages/www/** workflow_dispatch: jobs: test: if: ${{ github.repository_owner == 'nuejs' || github.event_name == 'workflow_dispatch' }} strategy: matrix: os: ['ubuntu-22.04', 'macos-latest', 'windows-latest'] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 - name: Install and test with Bun run: | bun -v bun install bun test --coverage ================================================ FILE: .gitignore ================================================ node_modules coverage .dist .nue dist _test test_dir performance.js .DS_Store .idea .vscode .env ================================================ FILE: .prettierrc.yaml ================================================ printWidth: 100 tabWidth: 2 useTabs: false semi: false singleQuote: true quoteProps: 'as-needed' trailingComma: 'es5' bracketSpacing: true arrowParens: 'avoid' requirePragma: false insertPragma: false proseWrap: 'preserve' htmlWhitespaceSensitivity: 'css' endOfLine: 'lf' embeddedLanguageFormatting: 'auto' singleAttributePerLine: false ================================================ FILE: CONTRIBUTING.md ================================================ ## Details here https://nuejs.org/docs/contributing ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2025-present, Tero Piirainen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: bunfig.toml ================================================ [test] coveragePathIgnorePatterns = ["**/test/**", "**/test_dir/**"] ================================================ FILE: package.json ================================================ { "name": "nue-monorepo", "private": true, "workspaces": [ "packages/*" ] } ================================================ FILE: packages/nuedom/Makefile ================================================ run-tests: bun test demo: @ bun bin/serve minify: @ bun bin/minify.js coverage: bun test --coverage ================================================ FILE: packages/nuedom/README.md ================================================ # Nuedom: HTML first UI assembly Nuedom (or just "Nue") is a markup language that extends HTML with just enough syntax to build websites, apps and SVG images. It's a different development model based on document structure rather than programmatic composition. ## UI assembly Nue differs from React "composition" in both syntax and architecture: **HTML over JavaScript** - In Nue, your UI is a document tree with data bindings and event listeners. In React, it's JavaScript functions returning objects. This difference changes the way you think about application structure. **Standards first** - ``, `
`, ``, form validation, scroll-snap, container queries. Modern HTML is interactive. Nue adds the missing pieces for dynamic behavior. **DOM based** - Nue's AST maps directly to DOM operations. No virtual DOM, no reconciliation. A button is a `

Message sent!

``` Check [Nue website](https://nuejs.org/docs/nuedom) for details. ================================================ FILE: packages/nuedom/bin/minify.js ================================================ import { version } from '../package.json' with { type: 'json' } const year = new Date().getFullYear() const banner = `/** * Nue v${version} - Copyright (c) ${year} Tero Piirainen and contributors * * Licensed under the MIT License */` for (const name of ['nue', 'nue-jit']) { try { const result = await Bun.build({ entrypoints: [`src/${name}.js`], naming: `[dir]/${name}.js`, target: 'browser', outdir: 'dist', minify: true, banner }) if (result.success) console.info(`Minified dist/${name}.js`) else { console.error('Build failed:') result.logs.forEach(log => console.error(log)) process.exit(1) } } catch (err) { console.error(`Bundling error: ${err.message}`) console.info(err) process.exit(1) } } ================================================ FILE: packages/nuedom/bin/serve.js ================================================ const server = Bun.serve({ port: 4400, routes: { '/favicon.ico': async () => { return new Response('', { headers: { 'Content-Type': 'text/plain' } }) }, }, fetch(req) { let path = new URL(req.url).pathname if (path.endsWith('/')) path += 'index.html' return new Response(Bun.file('.' + path)) } }) console.log('\nDemo ready:\n') console.log(`✅ Browser: ${server.url}test/\n`) console.log(`✅ Editor: test/index.html\n`) ================================================ FILE: packages/nuedom/package.json ================================================ { "name": "nuedom", "version": "0.1.0", "description": "HTML first UI assembly", "homepage": "https://nuejs.org/docs/nuedom", "license": "MIT", "type": "module", "main": "src/index.js", "browser": "src/nue-jit.js", "repository": { "url": "https://github.com/nuejs/nue", "directory": "packages/nuedom", "type": "git" }, "engines": { "bun": ">= 1.2", "node": ">= 20.0.0" }, "scripts": { "minify": "bun bin/minify.js" }, "files": [ "src" ] } ================================================ FILE: packages/nuedom/src/compiler/ast.js ================================================ import { HTML5_TAGS, SVG_TAGS } from './html5.js' import { parseAttributes } from './attributes.js' import { addContext } from './context.js' // former tag.js / parseTag export function createAST(block, imports) { const { tag, children, text, meta } = block if (text) return parseText(text, imports) const { tagName, attr } = parseOpeningTag(tag) if (tagName == 'slot') return { slot: true } const specs = attr ? parseAttributes(attr, imports) : {} const comp = { tag: tagName.trim(), ...specs } if (meta) comp.meta = meta const i = SVG_TAGS.findIndex(el => el.toLowerCase() == tagName.toLowerCase()) if (i >= 0) { comp.svg = true const correct = SVG_TAGS[i] if (correct != tagName) { comp.tag = correct console.warn(`Fixed SVG case: ${tagName} -> ${correct}`) } } else if (tagName.includes('-') || !HTML5_TAGS.includes(tagName)) { comp.is_custom = true } const mount = comp.attr?.find(a => a.name == 'mount') if (mount) { comp.attr.splice(comp.attr.indexOf(mount), 1) comp.is_custom = true comp.mount = mount } if (children.length) { const ret = parseChildren(children, imports) if (ret.script) comp.script = convertGetters(convertFunctions(ret.script)) if (ret.children.length) comp.children = ret.children } return comp } function parseOpeningTag(tag) { tag = tag.replace('/>', '>') const i = tag.indexOf(' ') if (i == -1) return { tagName: tag.slice(1, -1) } return { tagName: tag.slice(1, i), attr: tag.slice(i + 1, -1).trim() } } function parseChildren(arr, imports) { const scriptEl = arr.find(el => el.tag == '', pos) if (scriptEnd == -1) throw new SyntaxError(`Missing `) tokens.push(template.slice(contentStart, scriptEnd)) // Add script content as one token tokens.push('') return scriptEnd + 9 // Move past } function addExpr(template, pos, tokens) { const start = template.slice(pos, pos + 3) let braces = 1 if (start.startsWith('{{{')) braces = 3 else if (start.startsWith('{{')) braces = 2 const closing = '}'.repeat(braces) const i = template.indexOf(closing, pos + braces) if (i == -1) throw new SyntaxError(`Unclosed expression at ${pos}`) const end = i + braces let token = template.slice(pos, end) // normalize {{{ foo }}} to {{ foo }} if (braces == 3) token = '{{' + token.slice(3, -3) + '}}' tokens.push(token) return end } function addText(template, pos, tokens) { let i = pos while (i < template.length) { if (template[i] == '<' || (i + 1 < template.length && template[i] == '{')) break i++ } tokens.push(template.slice(pos, i)) return i } function throwSyntaxError(pos, snippet) { throw new SyntaxError(`Unclosed tag at position ${pos}: "${snippet}"`) } ================================================ FILE: packages/nuedom/src/dom/diff.js ================================================ // Nue • (c) 2025 Tero Piirainen & contributors, MIT Licensed /* Universal DOM diff optimized for simplicity and performance: 1. No VDOM overhead Diffs DOM nodes directly, skipping virtual tree creation 2. Targeted updates Only changes attributes and children that differ 3. Keyed diffs Uses keys to reuse nodes like React, but without component lifecycle baggage 4. Positional fallback Handles non-keyed cases with simple, linear iteration 5. Minimal Memory Brutally simple: just two Array.from() calls, no deep cloning or state tracking 6. Raw JavaScript Low-level control over output (no extra transpilile step) */ export function domdiff(prev, next) { const parent = prev.parentNode if (prev == next) return prev if (!prev && next) return parent?.appendChild(next) if (!next && prev) return parent?.removeChild(prev) if (prev.nodeType == 3 && next.nodeType == 3) { prev.textContent = next.textContent return prev } if (prev.nodeType != next.nodeType || prev.tagName != next.tagName) { return parent.replaceChild(next, prev) } updateAttributes(prev, next) const kids = Array.from(next.childNodes) kids[0]?.getAttribute?.('key') ? diffChildrenByKey(prev, kids) : diffChildren(prev, kids) return prev } function updateAttributes(prev, next) { for (let attr of next.attributes) { if (prev.getAttribute(attr.name) != attr.value) prev.setAttribute(attr.name, attr.value) } for (let attr of prev.attributes) { if (!next.hasAttribute(attr.name)) prev.removeAttribute(attr.name) } } function diffChildren(prev, kids) { const prevKids = Array.from(prev.childNodes) const len = Math.max(prevKids.length, kids.length) for (let i = 0; i < len; i++) { const prevKid = prevKids[i] const kid = kids[i] if (!prevKid) prev.appendChild(kid) else if (!kid) prev.removeChild(prevKids[i]) else domdiff(prevKid, kid, prev) } } function diffChildrenByKey(prev, kids) { const prevKids = Array.from(prev.childNodes) const keyMap = {} for (let kid of prevKids) keyMap[kid.getAttribute('key')] = kid while (prev.firstChild) prev.removeChild(prev.firstChild) for (let kid of kids) { const key = kid.getAttribute('key') const prevKid = keyMap[key] prev.appendChild(prevKid ? domdiff(prevKid, kid, prev) : kid) } } ================================================ FILE: packages/nuedom/src/dom/fakedom.js ================================================ // TODO: use html5.js const caseSensitive = ['foreignObject', 'clipPath', 'linearGradient', 'radialGradient', 'textPath', 'animateMotion', 'animateTransform'] const voidTags = ['img', 'br', 'hr', 'input', 'meta', 'link', 'area', 'base', 'col', 'embed', 'source', 'track', 'wbr'] function createNode(nodeType, tagName = null) { return { nodeType, // 1=element, 3=text, 11=fragment tagName: tagName?.toUpperCase(), innerHTML: '', children: [], childNodes: [], attributes: new AttributeMap(), classList: createClassList(), parentNode: null } } class AttributeMap extends Map { *[Symbol.iterator]() { for (const [name, value] of super[Symbol.iterator]()) { yield { name, value } } } } function createClassList() { const classes = new Set() return { classes, add(...names) { names.forEach(name => { if (name) classes.add(name) }) }, get length() { return classes.size }, toString() { return Array.from(classes).join(' ') } } } function serialize(node) { if (node.nodeType == 3) return node._textContent || node.textContent // fragments if (node.nodeType == 11) return node.children.map(serialize).join('') const lowerTag = node.tagName.toLowerCase() const tag = caseSensitive.find(name => name.toLowerCase() == lowerTag) || lowerTag const attrs = [] // add classList if it has classes and no class attribute already set if (node.classList && node.classList.length > 0 && !node.attributes.has('class')) { attrs.push(`class="${node.classList.toString()}"`) } for (const attr of node.attributes) { attrs.push(`${attr.name}="${attr.value}"`) } const attrStr = attrs.length ? ' ' + attrs.join(' ') : '' const children = node.children.map(serialize).join('') // self-closing tags if (voidTags.includes(tag)) return `<${tag}${attrStr}>` return `<${tag}${attrStr}>${children}` } function createElement(tag, nodeType = 1) { const base = createNode(nodeType, tag) const fns = {} const element = { ...base, // methods appendChild(child) { if (!child) return child child.parentNode = this this.children.push(child) this.childNodes.push(child) return child }, removeChild(child) { const index = this.children.indexOf(child) if (index > -1) { this.children.splice(index, 1) this.childNodes.splice(index, 1) child.parentNode = null } return child }, replaceChild(newChild, oldChild) { const index = this.children.indexOf(oldChild) if (index > -1) { this.children[index] = newChild this.childNodes[index] = newChild newChild.parentNode = this oldChild.parentNode = null } return oldChild }, setAttribute(name, value) { this.attributes.set(name, String(value)) if (name == 'class') { this.classList.classes.clear() this.classList.add(value) } }, getAttribute(name) { return this.attributes.get(name) || null }, hasAttribute(name) { return this.attributes.has(name) }, removeAttribute(name) { this.attributes.delete(name) if (name == 'class') { this.classList.classes.clear() } }, replaceWith(...nodes) { if (!this.parentNode) return const parent = this.parentNode const index = parent.children.indexOf(this) if (index > -1) { parent.children.splice(index, 1, ...nodes) parent.childNodes.splice(index, 1, ...nodes) nodes.forEach(node => node.parentNode = parent) this.parentNode = null } }, // only element selectors needed (on test suite) querySelector(query) { if (this.tagName == query.toUpperCase()) return this for (let child of this.children) { const el = child.querySelector?.(query) if (el) return el } }, // event handling addEventListener(name, fn) { fns[name] = fn }, dispatchEvent(e) { const fn = fns[e.type] fn?.({ target: this, ...e }) }, // getters get firstChild() { return this.childNodes[0] }, get innerHTML() { return this.children.map(serialize).join('') }, get outerHTML() { return serialize(this) }, // Add this to your createElement function in the element object: get textContent() { let text = '' for (let child of this.childNodes) { text += child.textContent || '' } return text }, set innerHTML(html) { this.children.length = 0 this.childNodes.length = 0 // TODO: html parsing instead of textContent if (html) { const textNode = createNode(3) textNode.textContent = html this.appendChild(textNode) } } } return element } export function createDocument() { return { createElement, createElementNS(ns, tag) { return createElement(tag) // simplified }, createTextNode(text) { const node = createNode(3) node.textContent = text ?? '' return node }, createDocumentFragment() { return createElement(null, 11) }, body: createElement('body') } } ================================================ FILE: packages/nuedom/src/dom/node.js ================================================ // Nue • (c) 2025 Tero Piirainen & contributors, MIT Licensed import { domdiff } from './diff.js' const is_browser = typeof window == 'object' export function createNode(ast, data={}, opts={}, parent) { const { script } = ast let root function update(values) { if (values) Object.assign(self, values) if (fire('onupdate') !== false) { const next = render(ast, self).firstChild // Domino: cannot be swapped (TODO: check with fakedom) if (is_browser || parent) domdiff(root, next, root.parentNode) else domdiff(root.firstChild, next, root) fire('updated') } } // Object.assign(self, getAttrData(ast, self)) const self = { ...data, ...opts.globals, ...getAttrData(ast, data), update, parent } if (script) { try { if (typeof script == 'string') new Function(script).call(self) else script.call(self) } catch (e) { console.error('` : '' return `<${tag} nue="${tag}">${js}` } ================================================ FILE: packages/nuedom/src/dom/render.js ================================================ import { createDocument } from './fakedom.js' import { parseNue } from '../compiler/document.js' import { createNode } from './node.js' function renderAST(ast, opts={}) { const { root } = mountAST(ast, opts) return root.innerHTML } export function renderNue(template, opts={}) { if (typeof template != 'string') return renderAST(template, opts) const { lib } = parseNue(template) const { deps=[] } = opts opts.deps = [...lib.slice(1), ...deps] return renderAST(lib[0], opts) } // exported for testing purposes only export function mountAST(ast, opts) { const dep = opts.deps?.find(c => ast.tag == (c.is || c.tag)) if (ast.is_custom && (!dep && !ast.mount || ast == dep)) { ast = Object.assign({}, ast) delete ast.is_custom; ast.tag = 'div' } global.document = createDocument() const node = createNode(ast, opts.data, opts) node.mount(document.body) return node } ================================================ FILE: packages/nuedom/src/index.js ================================================ // Server-side API export { renderNue } from './dom/render.js' export { parseNue } from './compiler/document.js' export { compileNue } from './compiler/compiler.js' export { createDocument } from './dom/fakedom.js' ================================================ FILE: packages/nuedom/src/nue-jit.js ================================================ // The browser API with compiler import { parseNue } from './compiler/document.js' import { createNode } from './dom/node.js' const template = document.querySelector('template')?.innerHTML if (template) { const { root, lib } = parseNue(template) const app = createNode(root, {}, { deps: lib }) const wrap = document.createElement('div') document.body.appendChild(wrap) app.mount(wrap) } ================================================ FILE: packages/nuedom/src/nue.js ================================================ // The browser API import { createNode } from './dom/node.js' export { domdiff } from './dom/diff.js' export function mount(ast, opts) { if (ast.is_custom) ast = { ...ast, is_custom: false, tag: 'div' } const node = createNode(ast, opts.data, opts) node.mount(opts.root) return node } ================================================ FILE: packages/nuedom/test/ast.test.js ================================================ import { createAST, convertFunctions, convertGetters } from '../src/compiler/ast.js' import { tokenize } from '../src/compiler/tokenizer.js' import { parseBlocks } from '../src/compiler/document.js' function testTag(template, expected) { const [ block ] = parseBlocks(tokenize(template)) const tag = createAST(block) if (expected === true) return tag expect(tag).toEqual(expected) } test('closed tag', () => { testTag('', { tag: 'foo', is_custom: true }) }) test('SVG tag', () => { testTag('', { tag: 'path', svg: true }) }) test('is attrib', () => { testTag('

Hello

', { tag: 'p', is: 'hey', children: [{ text: 'Hello' }] }) }) test('nested element', () => { testTag('

Hi

', { tag: "foo", is_custom: true, children: [{ tag: "p", children: [{ text: "Hi" }]}] }) }) test('nested tag', () => { testTag('

', { tag: 'p', children: [{ tag: 'bar', attr: [{ name: 'count', fn: '2', is_data: true } ], is_custom: true, }], }) }) test('element with expression', () => { testTag('

{ val } {{ val }}

', { children: [ {fn: '_.val', }, { fn: '_.val', html: true }], tag: "p", }) }) test('nested script', () => { const template = ` ` testTag(template, { tag: "counter", script: "this.count = 1", is_custom: true, }) }) test('client script', () => { const ast = testTag(` `, true) expect(ast.children.length).toBe(2) }) test('conditional', () => { const template = `

Hello

Lol Mid Meh Test
` const kids = testTag(template, true).children expect(kids.length).toBe(3) expect(kids[1].some.length).toBe(3) expect(kids[2].some.length).toBe(1) }) test('slot', () => { const template = `

Hello

` const kids = testTag(template, true).children expect(kids[1]).toEqual({ slot: true }) }) test('newlines', () => { const template = ` ` const tag = testTag(template, true) expect(tag.tag).toBe('textarea') expect(tag.attr.length).toBe(2) expect(tag.children[0].text).toInclude('\n') }) test('skip style tags', () => { testTag('
', { tag: 'div' }) }) test('convert function', () => { expect(convertFunctions(`format(str) {}`)).toEqual('this.format = function(str) {}') expect(convertFunctions(`async format(str) {}`)).toEqual('this.format = async function(str) {}') expect(convertFunctions(`if(true) {}`)).toEqual('if(true) {}') expect(convertFunctions(`for(true) {}`)).toEqual('for(true) {}') expect(convertFunctions(`for (true) {}`)).toEqual('for (true) {}') }) test('simple getter', () => { expect(convertGetters('get foo() { }')).toEqual("Object.defineProperty(this, 'foo', { get() { } })") }) test('multiline getter', () => { const js = convertGetters(` get status() { const { start, length } = view return start + length } `) expect(js).toInclude('{ get() {') expect(js).toInclude('return start + length} })') }) ================================================ FILE: packages/nuedom/test/attributes.test.js ================================================ import { tokenizeAttr, parseAttributes, parseClassHelper, parseExpression, parseFor, parseForArgs, } from '../src/compiler/attributes.js' test('tokenize attr', () => { const attr = 'class=test disabled :onclick="log($event)"' expect(tokenizeAttr(attr)).toEqual(['class=test', 'disabled', ':onclick=log($event)']) }) test('tokenize class helper', () => { const attr = 'class="{ active: active(item), foo: true } bar"' expect(tokenizeAttr(attr)).toEqual(["class={ active: active(item), foo: true } bar"]) }) test('tokenize complex', () => { const attr = `:onclick="this.fire('bomb' == state) || console.log(a || $event)" disabled goto="10"` const attrs = tokenizeAttr(attr) expect(attrs[0]).toBe(":onclick=this.fire('bomb' == state) || console.log(a || $event)") expect(attrs.length).toBe(3) }) // parse expression test('plain value', () => { expect(parseExpression('plain value')).toBeUndefined() }) test('expression', () => { expect(parseExpression('{ 1 + 1 }')).toBe('(1 + 1)') }) test('interpolation', () => { expect(parseExpression("foo { cute('hey') } baz")).toBe(`"foo " + (_.cute('hey')) + " baz"`) }) test('class helper', () => { expect(parseClassHelper("tabular: true, is-active: false")).toBe('_.$concat({tabular: true, "is-active": false})') }) test('class helper plain value', () => { expect(parseClassHelper("hypa, move: 1")).toBe('_.$concat({hypa: _.hypa, move: 1})') }) test('complex class helper', () => { const expr = parseExpression('prefix [ hey: bar, boo: baz() ]', 'class') expect(expr).toBe('"prefix " + _.$concat({hey: _.bar, boo: _.baz()})') }) test('complex event argument', () => { expect(parseAttributes(':oninput="seek(index, $event)"').handlers[0]).toEqual({ h_fn: '_.seek(_.index, $e)', name: 'oninput', }) }) // for arguments test('for args', () => { expect(parseForArgs('item, $i')).toMatchObject({ keys: ['item'], index: '$i' }) }) test('for arg parenthesis', () => { expect(parseForArgs('(item, i)')).toMatchObject({ keys: ['item'], index: 'i' }) }) test('[k, v]', () => { expect(parseForArgs('[k, v]')).toEqual({ keys: ['k', 'v'], is_entries: true }) }) test('([k, v])', () => { expect(parseForArgs('([k, v])')).toEqual({ keys: ['k', 'v'], is_entries: true }) }) test('[lang, text], i', () => { expect(parseForArgs('[lang, text], i')).toEqual({ keys: ['lang', 'text'], index: 'i', is_entries: true }) }) test('({ lang, text }, i)', () => { expect(parseForArgs('({ lang, text }, i)')).toMatchObject({ keys: ['lang', 'text'], index: 'i' }) }) test('({k, v})', () => { expect(parseForArgs('({k, v})')).toEqual({ keys: ['k', 'v'], is_entries: false }) }) test('simple for clause', () => { expect(parseFor('item, i in items')).toMatchObject({ fn: '_.items', keys: ['item'], index: 'i' }) }) test('complex for clause', () => { expect(parseFor('({ foo, bar }, $index) in Object.entries(items)')).toMatchObject({ fn: 'Object.entries(_.items)', keys: ['foo', 'bar'], index: '$index' }) }) test('basic attrs', () => { const [a, b] = parseAttributes('class=test disabled').attr expect(a).toEqual({ name: "class", val: "test" }) expect(b).toEqual({ name: "disabled", bool: true, val: true }) }) test('equals character', () => { const [ attr ] = parseAttributes('href="?id=100"').attr expect(attr.name).toBe('href') expect(attr.val).toBe('?id=100') }) test('for attribute', () => { expect(parseAttributes(':each="item, i of items"').for).toMatchObject({ fn: '_.items', keys: [ 'item' ], index: 'i', }) }) test('if attribute', () => { expect(parseAttributes(':if="item"')).toEqual({ if: "_.item" }) }) test('skip style attribute', () => { const { attr } = parseAttributes('width=100 style="color: blue"') expect(attr.length).toBe(1) }) ================================================ FILE: packages/nuedom/test/compiler.test.js ================================================ import { compileFn } from '../src/compiler/compiler.js' import { compileNue } from '..' test('empty template', () => { const js = compileNue('') expect(js).toContain('export const lib = []') }) test('compileNue', () => { const template = '

Hello

' const js = compileNue(template) expect(js).toStartWith('export const lib = [') }) test('multiple components', () => { const template = ` \${ fn(1) } \${ fn(2) } ` const js = compileNue(template) expect(js).toContain("tag: 'comp1'") expect(js).toContain("tag: 'comp2'") }) test('function', () => { const js = compileNue('${ fn(2) }') expect(js).toInclude('fn: _=>(_.fn(2))') }) test('quoted function', () => { const js = compileNue(`\${ 'foo' + "bar" }`) expect(js).toInclude(`=>('foo' + "bar")`) }) test('script blocks', () => { const template = ` ` const js = compileNue(template) expect(js).toContain('import { format }') expect(js).toContain('pretty() { }') expect(js).toContain('export const lib =') }) test(' `) expect(js).toInclude('script: function()') expect(js).toInclude('_=>_.val') }) test('compileFn', () => { expect(compileFn('_.foo')).toBe('_=>_.foo') expect(compileFn('_.foo + 1')).toBe('_=>(_.foo + 1)') expect(compileFn('_.foo', true)).toBe('(_,$e)=>_.foo($e)') expect(compileFn('i++; log(1)', true)).toBe('(_,$e)=>{i++; log(1)}') expect(compileFn('log(1)', true)).toBe('(_,$e)=>log(1)') }) ================================================ FILE: packages/nuedom/test/component.test.js ================================================ import { renderNue } from '../src/dom/render.js' test('self-closing', () => { expect(renderNue('')).toBe('') }) test('render', () => { expect(renderNue('Hello')).toBe('
Hello
') }) test('child', () => { const html = renderNue(' Hello') expect(html).toBe('
Hello
') }) test('', () => { const html = renderNue(' Hello') expect(html).toBe('
Hello
') }) test(':is=hey', () => { const html = renderNue(' Hello') expect(html).toBe('Hello') }) test('', () => { const html = renderNue('Hey') expect(html).toBe('
Hey
') }) test('', () => { const html = renderNue('Hey') expect(html).toBe('Hey') }) test('params', () => { const html = renderNue(` { text }`) expect(html).toBe('
bar
') }) test('child data', () => { const template = `
{ hey[0] } { value } ` expect(renderNue(template)).toInclude('
hey 100
') }) test('script', () => { const template = ` { text } ` const html = renderNue(template) expect(html).toBe('
Hello
') }) test('if-else', () => { const template = `
Fail

Else

` const html = renderNue(template) expect(html).toBe('

Else

') }) test('parent & child params', () => { const template = ` ` const html = renderNue(template) expect(html).toBe('
') }) test('class merging', () => { const template = ` ` const html = renderNue(template) expect(html).toBe('
') }) test(':bind', () => { const template = `

{title}

{ desc }

` const data = { title: 'Hello', desc: 'World' } const html = renderNue(template, { data: { data } }) expect(html).toBe('

Hello

World

') }) test(':bind this', () => { const html = renderNue(`

{ title }

{ desc }

`) expect(html).toInclude('

Hello

World

') }) test('slot', () => { const template = `

{ desc }

yo { desc }

{ hey }

` const html = renderNue(template, { data: { title: 'Hello', desc: 'World' } }) expect(html).toBe('

Hello

World

yoWorld
') }) test('mount attribute', () => { const template = ` Hello ` const html = renderNue(template, { data: { type: 'item', id: 1 } }) expect(html).toBe('Hello') }) test('