Showing preview only (881K chars total). Download the full file or copy to clipboard to get everything.
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
---
<!-- Please search to see if an issue or discussion already exists for the bug you encountered -->
### Describe the Bug
<!-- Describe current and expected behavior -->
### Environment
<!--
Example:
- OS: Windows / Mac / Linux
- Nuekit version & JS runtime: `nue --version`
-->
### Minimal Reproduction
<!-- Please provide a minimal reproduction (e.g. GitHub repo, code snippets, ...) or simple steps to reproduce the bug -->
### Logs & Additional Context
<!-- Add additional information like logs or other related information -->
================================================
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?
<!-- Describe what the problem is -->
### Solution you'd like
<!-- A clear and concise description of what you want to happen -->
### Alternatives you've considered
<!-- Alternative solutions or features you have thought of -->
### Additional context
<!-- Add any other context or screenshots about the feature request or improvement here -->
================================================
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** - `<dialog>`, `<details>`, `<popover>`, 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 `<button>`, not a "`<Button>`" component with 50KB of dependencies and hundreds of megabytes of runtime.
### How it looks
```html
<!doctype html>
<!-- form with native validation -->
<form :onsubmit="submit">
<input type="email" name="email" required>
<textarea name="message" minlength="10" required></textarea>
<button>Send</button>
<script>
async submit(e) {
await fetch('/api/contact', {
body: new FormData(e.target),
method: 'POST'
})
success.showPopover()
}
</script>
</form>
<!-- native dialog for success -->
<dialog id="success" popover>
<h2>Message sent!</h2>
<button popovertarget="success">Close</button>
</dialog>
```
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 == '<script>')
const children = arr.filter(el => el != scriptEl).map(el => createAST(el, imports))
const script = scriptEl ? scriptEl.children[0]?.text.trim() : ''
return { children: mergeConditionals(children), script }
}
function mergeConditionals(arr) {
return arr.reduce((result, current) => {
const is_if = current.if || current['else-if'] || current.else
const last = result[result.length - 1]
if (is_if) {
if (current.if) result.push({ some: [current] })
else if (last?.some) last.some.push(current)
else result.push({ some: [current] })
} else {
result.push(current)
}
return result
}, [])
}
function parseText(text, imports) {
if (text[0] == '{' && text.endsWith('}')) {
const is_html = text[1] == '{'
const am = is_html ? 2 : 1
const expr = { fn: addContext(text.slice(am, -am), imports).trim() }
if (is_html) expr.html = is_html
return expr
}
return { text }
}
// foo() {} --> this.foo = function() { }
export function convertFunctions(script) {
return script.replace(
/^( *)(async\s+)?(\w+)\s*\(([^)]*)\)\s*{/gm, (_, indent, asy, name, args) => {
if (_.includes('function') || ['for', 'if'].includes(name)) return _
return `${indent}this.${name} = ${asy ? 'async ' : ''}function(${args.trim()}) {`
}
)
}
export function convertGetters(script) {
return script
// Handle multiline getters first
.replace(
/^( *)get (\w+)\(\)\s*\{([\s\S]*?)\n\1\}/gm,
(_, indent, name, body) => {
return `${indent}Object.defineProperty(this, '${name}', { get() {${body}} })`
}
)
// Then handle single-line getters
.replace(
/^( *)get (\w+)\(\)\s*\{([^}]*)\}/gm,
(_, indent, name, body) => {
return `${indent}Object.defineProperty(this, '${name}', { get() {${body}} })`
}
)
}
================================================
FILE: packages/nuedom/src/compiler/attributes.js
================================================
import { BOOLEAN, EVENTS, STRICT_ATTRS } from './html5.js'
import { addContext } from './context.js'
export function tokenizeAttr(attrStr) {
return attrStr.match(/[^\s=]+(="[^"]*"|=[^\s]*|)/g)?.map(token =>
token.replace(/="([^"]*)"/, '=$1')
) || []
}
export function parseAttributes(attrs, imports) {
const tokens = tokenizeAttr(attrs)
const result = {}
function push(key, val) {
const arr = result[key] = result[key] || []
arr.push(val)
}
for (const token of tokens) {
let { name, value } = parseNameVal(token)
const is_colon = name[0] == ':'
const base = is_colon ? name.slice(1) : name
if (name == ':each') {
result.for = parseFor(value, imports)
} else if (name == ':is') {
result.is = value
} else if (name == 'style') {
// ignore
} else if ([':if', ':else-if', ':else'].includes(name)) {
result[base] = base == 'else' ? true : addContext(value, imports)
// event handler
} else if (name.slice(0, 3) == ':on' && EVENTS.includes(name.slice(3))) {
if (!value) value = name.slice(1)
if (!/\W/.test(value)) value += '($e)'
push('handlers', { name: base, h_fn: addContext(value, imports) })
} else {
const attr = { name: base }
// :href --> :href="href"
if (is_colon && !value) value = base
const fn = is_colon && !value.includes('{') ? addContext(value, imports) : parseExpression(value, name, imports)
if (fn) attr.fn = fn
else attr.val = value
if (name.startsWith('--')) {
attr.name = name.slice(2)
attr.is_var = true
} else if (BOOLEAN.includes(base)) {
attr.bool = true
if (!fn && !value) attr.val = true
} else if (is_colon) {
attr.is_data = true
} else {
const correct = STRICT_ATTRS.find(el => el.toLowerCase() == name.toLowerCase())
if (correct && name != correct) {
attr.name = correct
console.warn(`Fixed SVG case: ${name} -> ${correct}`)
}
}
push('attr', attr)
}
}
return result
}
function parseNameVal(attr) {
const i = attr.indexOf('=')
if (i == -1) return { name: attr, value: '' }
const name = attr.slice(0, i)
const value = attr.slice(i + 1)
return { name, value }
}
export function parseFor(str, imports) {
const [args, _, expr] = str.split(/\s+(in|of)\s+/)
const for_args = parseForArgs(args.trim())
const fn = addContext(expr, imports)
return { fn, ...for_args }
}
export function parseForArgs(args) {
// Clean parentheses first, which fixes the failing test
const cleaned = args.replace(/^\(|\)$/g, '');
const hasIndex = /[}\]],/.exec(cleaned) || (!'{['.includes(cleaned[0]) && cleaned.includes(','));
const keys = cleaned.replace(/[(){}[\]]/g, '').split(',').map(part => part.trim());
const is_entries = cleaned.includes('[');
return hasIndex ? { keys: keys.slice(0, -1), index: keys.pop(), is_entries } : { keys, is_entries };
}
export function parseExpression(str, attr_name, imports) {
const parts = str.split(/(\{[^{}]*\}|\[[^\[\]]*\])/)
if (parts.length < 2) return
return parts.map(part => {
if (part[0] == '[' && attr_name == 'class') {
return parseClassHelper(part.slice(1, -1).trim(), imports)
} else if (part[0] == '{') {
return `(${ addContext(part.slice(1, -1).trim(), imports) })`
}
return part ? `"${part}"` : ''
}).filter(part => part !== '').join(' + ')
}
export function parseClassHelper(val, imports) {
const pairs = val.split(',').map(pair => pair.trim())
const obj = pairs.map(pair => {
let [name, expr] = pair.split(':').map(s => s.trim())
if (!expr) expr = name
if (name.includes('-')) name = `"${ name }"`
return `${ name }: ${ addContext(expr, imports) }`
})
return `_.$concat({${obj.join(', ')}})`
}
================================================
FILE: packages/nuedom/src/compiler/compiler.js
================================================
/* Compiles template string to JavaScript object (AST) */
import { inspect } from 'node:util'
import { parseNue } from './document.js'
export function compileNue(template) {
const doc = typeof template == 'string' ? parseNue(template) : template
return compileDoc(doc)
}
function compileDoc(doc) {
const { script, lib } = doc
const js = []
if (script) js.push(script)
const lib_str = compileJS(inspect(lib, { compact: true, depth: Infinity }))
js.push(`export const lib = ${ lib_str }`)
return js.join('\n')
}
const RE_FN = /(script|h_fn|fn):\s*(['"`])([^\2]*?)\2/g
export function compileJS(js) {
return js.replace(RE_FN, function(_, key, __, expr) {
return key == 'script' ? `${key}: function() { ${expr.trim().replaceAll('\\n', '\n')} \n\t\t}`
: `${key}: ${compileFn(expr, key[0] == 'h')}`
})
}
// compiler.test.js: _.foo + 1 --> _=>(_.foo + 1)
export function compileFn(str, is_event) {
str = str.trim()
const word = str.startsWith('_.') ? str.slice(2) : str
const is_simple = !/\W/.test(word)
if (is_event) {
return '(_,$e)=>' + (is_simple ? `${str}($e)`
: str.includes(';') ? `{${str}}`
: str
)
}
return '_=>' + (is_simple ? str : `(${str})`)
}
================================================
FILE: packages/nuedom/src/compiler/context.js
================================================
import { JS } from './html5.js'
const CONTEXT_RE = /'[^']*'|"[^"]*"|[a-zA-Z_$][a-zA-Z0-9_$]*(?:\.[a-zA-Z_$][a-zA-Z0-9_$]*)*/g
export function addContext(expr, exceptions = ['state']) {
const reserved = new Set([...JS, ...exceptions ])
return expr.replace(CONTEXT_RE, (match, offset, str) => {
if (match.includes('$event')) return match.replaceAll('$event', '$e')
if (match == 'this') return '_'
// Skip string literals
if (match[0] == '"' || match[0] == "'") return match
// Skip property keys
if (str[offset + match.length] == ':') return match
// Skip if preceded by . or /
if (offset > 0 && /[.\/]/.test(str[offset - 1])) return match
const root = match.split('.')[0]
return reserved.has(root) ? match : `_.${match}`
})
}
================================================
FILE: packages/nuedom/src/compiler/document.js
================================================
import { createAST } from './ast.js'
import { tokenize } from './tokenizer.js'
/*
{
doctype: 'html',
script: 'imort { foo, bar } from ...',
meta: {},
lib: []
}
*/
export function parseNue(template) {
const tokens = tokenize(template)
const blocks = parseBlocks(tokens)
const script = []
const page = {}
let lib = []
blocks.forEach((el, i) => {
if (el.doctype) {
page.doctype = el.doctype
} else if (el.tag) {
if (isScript(el)) {
script.push(el.children[0].text.trim())
} else {
lib.push(el)
}
} else if (el.meta) {
const next = blocks[i + 1]
const target = next?.tag && !isScript(next) ? next : page
target.meta = el.meta
}
})
page.script = script.join('\n')
// reserved names
const names = parseNames(page.script)
lib = page.lib = lib.map(el => createAST(el, names))
// root
page.root = lib[0]
// all custom elements
const type = page.doctype || ''
const all_custom = lib.every(ast => ast.is_custom || ast.is)
page.is_lib = all_custom || type.endsWith('lib')
page.is_dhtml = type.includes('dhtml')
|| lib.some(ast => ast.handlers?.length > 0)
|| page.script?.includes('import ')
return page
}
function isScript(block) {
return block.tag?.startsWith('<script')
}
export function parseBlocks(tokens) {
const blocks = []
let i = 0
while (i < tokens.length) {
if (tokens[i].meta) {
blocks.push(tokens[i])
i++
} else {
const result = parseBlock(tokens, i)
if (result.node) blocks.push(result.node)
i = result.next
}
}
return blocks
}
function parseBlock(tokens, i) {
const tag = tokens[i]
if (i >= tokens.length || !tag.startsWith('<') || tag.startsWith('</')) return { next: i + 1 }
i++
const low = tag.toLowerCase()
// !doctype
if (tag.startsWith('<!')) {
const node = { doctype: low.slice(2, -1).replace('doctype', '').trim() }
return { node, next: i++ }
}
if (low.startsWith('<?xml')) {
const node = { xml: true }
return { node, next: i++ }
}
// ignore <style> blocks
if (low.startsWith('<style')) return { next: i }
if (tag.endsWith('/>')) return { node: { tag, children: [] }, next: i }
const children = []
while (i < tokens.length && (tokens[i].meta || !tokens[i].startsWith('</'))) {
if (tokens[i].meta) {
i++
} else if (tokens[i].startsWith('<') && !tokens[i].startsWith('</')) {
const result = parseBlock(tokens, i)
if (result.node) children.push(result.node)
i = result.next
} else {
children.push({ text: tokens[i] })
i++
}
}
if (i < tokens.length) i++ // Skip closing tag
return { node: { tag, children }, next: i }
}
export function parseNames(script) {
const lines = script.trim().split('\n')
const arr = []
for (const line of lines) {
if (line.trim().startsWith('//')) continue
arr.push(
...getFunctionNames(line),
...getVariableNames(line)
)
}
return arr
}
function getVariableNames(line) {
// Match destructuring: import { ... } or const { ... } =
const destructMatch = line.match(/(import|const|var|let)\s*{\s*([^}]+)\s*}/)
if (destructMatch) {
return destructMatch[2].split(',').map(el => {
return el.includes(' as ') ? el.split(' as ')[1].trim() : el.trim()
})
}
// Match regular declarations: const FOO =
const regularMatch = line.match(/(const|var|let)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=/)
if (regularMatch) {
return [regularMatch[2]]
}
return []
}
function getFunctionNames(line) {
const match = line.match(/function\s*([^\(]+)\s*\(/)
return match ? [match[1]] : []
}
================================================
FILE: packages/nuedom/src/compiler/html5.js
================================================
// Web platform constants (HTML5, SVG, DOM APIs)
export const JS = '_ $e document window location localStorage sessionStorage alert Array Boolean Date Error false in Infinity instanceof isFinite isNaN JSON Math NaN new null Number Object parseFloat parseInt String true typeof undefined history navigator screen scrollTo scrollBy innerWidth innerHeight outerWidth outerHeight console'.split(' ')
export const EVENTS = 'click submit change input focus blur keydown keyup keypress mouseover mouseout mousedown mouseup mousemove mouseenter mouseleave wheel scroll resize load unload beforeunload error abort touchstart touchend touchmove touchcancel drag dragstart dragend dragenter dragleave dragover drop animationstart animationend animationiteration transitionend contextmenu dblclick pointerdown pointermove cut copy paste'.split(' ')
export const BOOLEAN = 'disabled checked selected hidden readonly required autofocus autoplay async controls defer loop multiple muted nowrap open reversed scoped seamless sorted translate visibility pointer-events draggable contenteditable'.split(' ')
export const HTML5_TAGS = 'a abbr address area article aside audio b base bdi bdo blockquote body br button canvas caption cite code col colgroup data datalist dd del details dfn dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 head header hr html i iframe img input ins kbd label legend li link main map mark meta meter nav noscript object ol optgroup option output p param picture pre progress q rp rt ruby s samp script section select small source span strong style sub summary sup table tbody td template textarea tfoot th thead time title tr track u ul var video wbr slot portal'.split(' ')
export const SVG_TAGS = 'animate animateMotion animateTransform circle clipPath defs desc ellipse\
feBlend feColorMatrix feComponentTransfer feComposite feConvolveMatrix feDiffuseLighting\
feDisplacementMap feDistantLight feDropShadow feFlood feFuncA feFuncB feFuncG feFuncR\
feGaussianBlur feImage feMerge feMergeNode feMorphology feOffset fePointLight feSpecularLighting\
feSpotLight feTile feTurbulence filter foreignObject g hatch hatchPath image line linearGradient\
marker mask metadata mpath path pattern polygon polyline radialGradient rect set stop style svg\
switch symbol text textPath title tspan use view'.split(' ')
export const SELF_CLOSING = 'img br hr input meta link area base col embed keygen param source track wbr'.split(' ')
// case sensitive attributes (bad casd breaks rendering)
export const STRICT_ATTRS = 'viewBox preserveAspectRatio'.split(' ')
/*
export const ATTR = 'class id style lang dir title accesskey tabindex href src alt type value name for rel target action method placeholder pattern min max step width height poster media sizes srcset content role form download'.split(' ')
export const SVG_ATTR = 'x y cx cy r rx ry d points fill stroke stroke-width transform dx dy text-anchor viewBox preserveAspectRatio clip-path xmlns opacity font-size mask filter'.split(' ')
ATTR.push(...SVG_ATTR)
*/
================================================
FILE: packages/nuedom/src/compiler/tokenizer.js
================================================
import { SELF_CLOSING } from './html5.js'
export function tokenize(template) {
template = template.trim()
const tokens = []
let pos = 0
while (pos < template.length) {
const char = template[pos]
if (char == '<' && template.slice(pos, pos + 4) == '<!--') {
pos = parseComment(template, pos, tokens)
} else if (char == '<') {
if (template.slice(pos, pos + 7) == '<script') {
pos = addScript(template, pos, tokens)
} else {
pos = addTag(template, pos, tokens)
}
} else if (template[pos] == '{') {
pos = addExpr(template, pos, tokens)
} else {
pos = addText(template, pos, tokens)
}
}
return tokens.filter(el => el.meta || el.trim())
}
function parseComment(template, pos, tokens) {
const end = template.indexOf('-->', pos + 4)
if (end == -1) throw new SyntaxError(`Unclosed comment at ${pos}`)
// annotations
const meta = parseAnnotations(template.slice(pos + 4, end))
if (meta) tokens.push({ meta })
return end + 3
}
function parseAnnotations(comment) {
const lines = comment.split('\n')
const ret = {}
lines.forEach(line => {
const trimmed = line.trim()
if (trimmed[0] == '@') {
const match = trimmed.match(/^@(\w+)(?:\s+(.+))?$/)
if (match) {
const [, key, value] = match
ret[key] = value || true
}
}
})
return Object.keys(ret).length ? ret : null
}
function addTag(template, pos, tokens) {
let i = pos + 1
let quote = null
while (i < template.length) {
if ("'\"".includes(template[i]) && (i == 0 || template[i - 1] != '\\')) {
quote = quote == null ? template[i] : null
}
if (template[i] == '>' && quote == null) {
i++
tokens.push(toSelfClosing(template.slice(pos, i)))
return i
}
if (template[i] == '<' && i > pos && quote == null) {
throwSyntaxError(pos, template.slice(pos, i + 10) + "...")
}
i++
}
throwSyntaxError(pos, template.slice(pos, pos + 10) + "...")
}
// <img>, <img src="a">
function toSelfClosing(tag) {
let name = tag.slice(1, -1)
const i = name.indexOf(' ')
if (i > 0) name = name.slice(0, i)
return SELF_CLOSING.includes(name) ? tag.slice(0, -1) + '/>' : tag
}
function addScript(template, pos, tokens) {
const tagEnd = template.indexOf('>', pos)
if (tagEnd == -1) throw new SyntaxError(`Unclosed script tag at ${pos}`)
tokens.push(template.slice(pos, tagEnd + 1)) // Add <script> with attributes
pos = tagEnd + 1
const contentStart = pos
const scriptEnd = template.indexOf('</script>', pos)
if (scriptEnd == -1) throw new SyntaxError(`Missing </script>`)
tokens.push(template.slice(contentStart, scriptEnd)) // Add script content as one token
tokens.push('</script>')
return scriptEnd + 9 // Move past </script>
}
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}</${tag}>`
}
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('<script> error:', script, e)
}
}
function fire(name, wrap) {
const fn = self[name]
if (wrap?.tagName) root = self.root = wrap
if (is_browser && fn && typeof fn == 'function') return fn.call(self, self, update)
}
function mount(wrap) {
fire('onmount', wrap)
const frag = render(ast, self)
const root = frag.firstChild
is_browser ? wrap.replaceWith(root) : wrap.appendChild(root)
if (is_browser) fire('mounted', root)
return root
}
self.mount = function(name, wrap, data) {
if (typeof wrap == 'string') wrap = root?.querySelector(wrap)
const ast = opts.deps?.find(c => name == (c.is || c.tag))
// convert to <div> (TODO: do this at compile time)
if (ast.is_custom) { ast.is = ast.tag; ast.tag = 'div'; delete ast.is_custom }
const block = createNode(ast, data, opts, self)
block.mount(wrap)
}
function render(_ast=ast, data=self) {
return _ast.text || _ast.fn ? renderText(_ast, data) :
_ast.some ? renderIf(_ast, data) :
_ast.for ? renderLoop(_ast, data) :
_ast.is_custom ? renderComponent(_ast, data) :
_ast.tag ? renderTag(_ast, data) :
createFragment()
}
function renderTag(ast, self) {
const tag = ast.svg ? document.createElementNS('http://www.w3.org/2000/svg', ast.tag) :
document.createElement(ast.tag)
setAttributes(tag, ast, self)
if (parent && ast.is_child) setAttributes(tag, parent.ast, parent.self)
const am = tag.classList.length
if (am > (opts.max_class_names || 3)) {
console.error(`More than ${am} class names in class="${tag.classList }"`)
}
const cls = tag.classList?.toString()
if (/[:\[\]]/.test(cls)) {
console.error(`Invalid characters in class name: ${cls}`)
}
ast.handlers?.forEach(h => {
const name = h.name.slice(2) // onclick --> click
tag.addEventListener(name, function(e) {
if (name == 'submit') e.preventDefault()
exec(h.h_fn, self, e)
update()
})
})
ast.children?.forEach((child, i) => {
if (child.slot) {
// slot content from opts
if (opts.slot) return tag.appendChild(renderHTML(opts.slot))
parent?.ast.children?.forEach((node, i) => {
tag.appendChild(render(node, parent.self))
addSpace(tag, node, parent.ast.children[i + 1])
})
} else {
tag.appendChild(render(child, self))
}
addSpace(tag, child, ast.children[i+1])
})
const frag = createFragment()
frag.appendChild(tag)
return frag
}
function renderIf(ast, self) {
const child = ast.some.find(el => {
const fn = el.if || el['else-if']
if (fn) {
const ret = exec(fn, self)
return ret && ret != '[Error]'
} else if (el.else) {
return true
}
})
if (!child) return createFragment()
if (child.tag == 'template') {
const frag = createFragment()
child.children.forEach(node => {
frag.appendChild(render(node, self))
})
return frag
} else {
return render(child, self)
}
}
function renderLoop(impl, self) {
const ast = { ...impl }
const { keys, is_entries, index, fn } = ast.for
delete ast.for
const items = exec(fn, self) || []
const frag = createFragment()
const is_template = ast.tag == 'template'
items.forEach((item, i) => {
// set loop variables
const child = { ...self }
if (index) child[index] = i
if (keys[1]) keys.forEach((key, i ) => child[key] = item[is_entries ? i : key])
else child[keys[0]] = item
if (is_template) {
ast.children.forEach(node => {
frag.appendChild(render(node, child))
})
} else {
frag.appendChild(render(ast, child))
}
})
return frag
}
function renderComponent(ast, self) {
const attr_data = getAttrData(ast, self)
// render function?
const fn = opts.fns && opts.fns[ast.tag]
if (fn) return renderHTML(fn({ ...attr_data, ...self }, opts))
// find component
const comp = findComponent(ast, self)
if (!comp) return renderHTML(renderClientStub(ast.tag, attr_data))
const tag = ast.mount && ast.tag != 'template' ? ast.tag : comp.is ? comp.tag : 'div'
const block = createNode(
{ ...comp, tag, is_custom: false, is_child: true },
attr_data,
opts,
createNode(ast, self, opts, parent)
)
const frag = block.render()
block.fire('onmount', frag.firstChild)
block.fire('mounted')
return frag
}
function findComponent(ast, self) {
let { mount, tag } = ast
if (mount) tag = mount.fn ? exec(mount.fn, self) : mount.val
return opts.deps?.find(c => c != ast && tag == (c.is || c.tag))
}
return {
mount, update, render, ast, self, fire, get root() { return root },
}
}
function renderText(ast, self) {
const val = ast.fn ? exec(ast.fn, self) : ast.text || ''
return ast.html ? renderHTML(val) : document.createTextNode(val)
}
function getAttrData(ast, self) {
const ret = {}
ast.attr?.forEach(a => {
const val = a.fn ? exec(a.fn, { ...self, $concat }) : a.val
if (a.name == 'bind') {
if (typeof val == 'object') Object.assign(ret, val)
} else {
ret[a.name] = val
}
})
return ret
}
function setAttributes(el, ast, self) {
const vars = []
ast.attr?.forEach(a => {
if (a.is_data) return
let { name, val, fn } = a
if (fn) val = exec(fn, { ...self, $concat })
if (a.is_var) {
vars.push({ name, val })
} else if (a.bool) {
if (val) el.setAttribute(name, '')
} else if (name == 'class') {
el.classList.add(...val.trim().split(/ +/))
} else {
el.setAttribute(name, val)
}
})
if (vars.length) {
el.setAttribute('style', vars.map(v => `--${v.name}:${v.val};`).join(''))
}
}
function exec(fn, self, e) {
try {
if (typeof fn == 'string') fn = new Function('_', '$e', 'return ' + fn)
let val = fn(self, e)
if (typeof val == 'string') val = val.replaceAll('undefined', '')
return val == null ? '' : Number.isNaN(val) ? 'N/A' : val
} catch (e) {
console.error('Nue error:', e.message)
return '[Error]'
}
}
function renderHTML(html) {
const frag = document.createDocumentFragment()
frag.innerHTML = html || ''
return frag
}
function $concat(obj) {
return Object.keys(obj).map(key => obj[key] ? key : '').filter(key => !!key).join(' ')
}
function createFragment() {
return document.createDocumentFragment()
}
function addSpace(to, child, next) {
if (child.tag && next?.tag || child.fn && next?.fn) {
to.appendChild(document.createTextNode(' '))
}
}
function renderClientStub(tag, self) {
const json = JSON.stringify(self)
const js = json != '{}' ? `<script type="application/json">${json}</script>` : ''
return `<${tag} nue="${tag}"></${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('<foo/>', { tag: 'foo', is_custom: true })
})
test('SVG tag', () => {
testTag('<path/>', { tag: 'path', svg: true })
})
test('is attrib', () => {
testTag('<p :is="hey">Hello</p>', { tag: 'p', is: 'hey', children: [{ text: 'Hello' }] })
})
test('nested element', () => {
testTag('<foo><p>Hi</p></foo>', {
tag: "foo",
is_custom: true,
children: [{ tag: "p", children: [{ text: "Hi" }]}]
})
})
test('nested tag', () => {
testTag('<p><bar :count=2></p>', {
tag: 'p',
children: [{
tag: 'bar',
attr: [{ name: 'count', fn: '2', is_data: true } ],
is_custom: true,
}],
})
})
test('element with expression', () => {
testTag('<p>{ val } {{ val }}</p>', {
children: [ {fn: '_.val', }, { fn: '_.val', html: true }],
tag: "p",
})
})
test('nested script', () => {
const template = `
<counter>
<script>this.count = 1</script>
</counter>
`
testTag(template, {
tag: "counter",
script: "this.count = 1",
is_custom: true,
})
})
test('client script', () => {
const ast = testTag(`
<body>
<script src="/analytics.js"></script>
<script type="module">alalytics(666)</script>
<script></script>
</body>
`, true)
expect(ast.children.length).toBe(2)
})
test('conditional', () => {
const template = `
<cond-test>
<h3>Hello</h3>
<b :if="am < 100">Lol</b>
<b :else-if="am < 50">Mid</b>
<b :else>Meh</b>
<a :if="foo">Test</a>
</cond-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 = `
<slot-test>
<h3>Hello</h3>
<slot/>
</slot-test>
`
const kids = testTag(template, true).children
expect(kids[1]).toEqual({ slot: true })
})
test('newlines', () => {
const template = `
<textarea
amount="10"
class="bar">
foo
bar
</textarea>
`
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('<div><style></style></div>', { 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 = '<p>Hello</p>'
const js = compileNue(template)
expect(js).toStartWith('export const lib = [')
})
test('multiple components', () => {
const template = `
<comp1>\${ fn(1) }</comp1>
<comp2>\${ fn(2) }</comp2>
`
const js = compileNue(template)
expect(js).toContain("tag: 'comp1'")
expect(js).toContain("tag: 'comp2'")
})
test('function', () => {
const js = compileNue('<b>${ fn(2) }</b>')
expect(js).toInclude('fn: _=>(_.fn(2))')
})
test('quoted function', () => {
const js = compileNue(`<b>\${ 'foo' + "bar" }</b>`)
expect(js).toInclude(`=>('foo' + "bar")`)
})
test('script blocks', () => {
const template = `
<script>
import { format } from './utils.js'
</script>
<script>
function pretty() { }
</script>
<time is="pretty-date">\${ format(date) }</time>
`
const js = compileNue(template)
expect(js).toContain('import { format }')
expect(js).toContain('pretty() { }')
expect(js).toContain('export const lib =')
})
test('<script> block', () => {
const js = compileNue(`
<a>
\${ val }
<script>
this.val = 110
this.setup = function() {
return "done"
}
</script>
</a>
`)
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('<a/>')).toBe('<a></a>')
})
test('render', () => {
expect(renderNue('<hey/><hey>Hello</hey>')).toBe('<div>Hello</div>')
})
test('child', () => {
const html = renderNue('<a><hey/></a> <hey>Hello</hey>')
expect(html).toBe('<a><div>Hello</div></a>')
})
test('<hey>', () => {
const html = renderNue('<hey/> <hey>Hello</hey>')
expect(html).toBe('<div>Hello</div>')
})
test(':is=hey', () => {
const html = renderNue('<hey/> <a :is="hey">Hello</a>')
expect(html).toBe('<a>Hello</a>')
})
test('<app>', () => {
const html = renderNue('<app>Hey</app>')
expect(html).toBe('<div>Hey</div>')
})
test('<body app>', () => {
const html = renderNue('<body :is="app">Hey</body>')
expect(html).toBe('<body>Hey</body>')
})
test('params', () => {
const html = renderNue(`<hey text="foo" :text="'bar'"/> <hey>{ text }</hey>`)
expect(html).toBe('<div text="foo">bar</div>')
})
test('child data', () => {
const template = `
<figure>
<bar-chart :hey="rand()" :value/>
<script>
this.value = 100
rand() { return ['hey'] }
</script>
</figure>
<bar-chart>{ hey[0] } { value }</bar-chart>
`
expect(renderNue(template)).toInclude('<div>hey 100</div>')
})
test('script', () => {
const template = `
<hey/>
<hey>
{ text }
<script>
this.text = 'Hello'
</script>
</hey>
`
const html = renderNue(template)
expect(html).toBe('<div>Hello</div>')
})
test('if-else', () => {
const template = `
<div>
<item :if="ok"/>
<bar :else/>
</div>
<item>Fail</item>
<p :is="bar">Else</p>
`
const html = renderNue(template)
expect(html).toBe('<div><p>Else</p></div>')
})
test('parent & child params', () => {
const template = `
<item :amount="2"/>
<item class="bar" data-amount="{ amount }"/>
`
const html = renderNue(template)
expect(html).toBe('<div class="bar" data-amount="2"></div>')
})
test('class merging', () => {
const template = `
<item class="bar"/>
<item class="foo">
<img class="nested">
</item>
`
const html = renderNue(template)
expect(html).toBe('<div class="foo bar"><img class="nested"></div>')
})
test(':bind', () => {
const template = `
<item :bind="{ data }"/>
<item><h1>{title}</h1><p>{ desc }</p></item>
`
const data = { title: 'Hello', desc: 'World' }
const html = renderNue(template, { data: { data } })
expect(html).toBe('<div><h1>Hello</h1> <p>World</p></div>')
})
test(':bind this', () => {
const html = renderNue(`
<div>
<item :bind="{ this }"/>
<script>
this.title = 'Hello'
this.desc = 'World'
</script>
</div>
<item>
<h1>{ title }</h1>
<p>{ desc }</p>
</item>
`)
expect(html).toInclude('<h1>Hello</h1> <p>World</p>')
})
test('slot', () => {
const template = `
<item :hey="{ title }">
<p>{ desc }</p>
<small>yo</small>
{ desc }
</item>
<item>
<h1>{ hey }</h1>
<slot/>
</item>
`
const html = renderNue(template, { data: { title: 'Hello', desc: 'World' } })
expect(html).toBe('<div><h1>Hello</h1><p>World</p> <small>yo</small>World</div>')
})
test('mount attribute', () => {
const template = `
<a :mount="type" :key="id"/>
<item href="id:{ key }">
<b>Hello</b>
</item>
`
const html = renderNue(template, { data: { type: 'item', id: 1 } })
expect(html).toBe('<a href="id:1"><b>Hello</b></a>')
})
test('<template :mount>', () => {
const template = `
<template mount="item"/>
<item>
<b>Hello</b>
</item>
`
const html = renderNue(template)
expect(html).toBe('<div><b>Hello</b></div>')
})
================================================
FILE: packages/nuedom/test/context.test.js
================================================
import { addContext } from '../src/compiler/context.js'
test('simple variable', () => {
expect(addContext('className')).toBe('_.className')
})
test('handle reserved words', () => {
expect(addContext('Math.PI')).toBe('Math.PI')
})
test('math operations', () => {
expect(addContext('x + y * 2')).toBe('_.x + _.y * 2')
})
test('array access', () => {
expect(addContext('items[0].name')).toBe('_.items[0].name')
})
test('ternary with multiple variables', () => {
expect(addContext('x > 0 ? y : z')).toBe('_.x > 0 ? _.y : _.z')
})
test('plain string', () => {
expect(addContext("'hello'")).toBe("'hello'")
})
test('dollar variable', () => {
expect(addContext("$foo")).toBe("_.$foo")
})
test('this', () => {
expect(addContext("this")).toBe("_")
})
test('$event variable', () => {
expect(addContext("$event")).toBe("$e")
})
test('$event.target', () => {
expect(addContext('$event.target')).toBe('$e.target')
})
test('string expression', () => {
expect(addContext('a + "hello"')).toBe('_.a + "hello"')
})
test('nested function calls', () => {
expect(addContext('format(getName(user))'))
.toBe('_.format(_.getName(_.user))')
})
test('complex #1', () => {
expect(addContext('cute ? prettyDate(date) : Date.now()', ['prettyDate']))
.toBe('_.cute ? prettyDate(_.date) : Date.now()')
})
test('complex #2', () => {
const expr = addContext('router.set({ foo: el.id })')
expect(expr).toBe('_.router.set({ foo: _.el.id })')
})
// Function calls with multiple arguments
test('function calls with args', () => {
expect(addContext('calculate(a, b, Math.max(x, y))'))
.toBe('_.calculate(_.a, _.b, Math.max(_.x, _.y))')
})
// Method chaining
test('method chaining', () => {
expect(addContext('users.filter(u => u.active).map(getName)'))
.toBe('_.users.filter(_.u => _.u.active).map(_.getName)')
})
// Arrow functions
test('arrow functions', () => {
expect(addContext('items.map(item => item.id * factor)'))
.toBe('_.items.map(_.item => _.item.id * _.factor)')
})
// Logical operators
test('logical operators', () => {
expect(addContext('user && user.name || defaultName'))
.toBe('_.user && _.user.name || _.defaultName')
})
// Comparison chains
test('comparison chains', () => {
expect(addContext('min <= value && value <= max'))
.toBe('_.min <= _.value && _.value <= _.max')
})
// Nested objects
test('nested object literals', () => {
expect(addContext('{ user: { name: userName, settings: { theme: currentTheme } } }'))
.toBe('{ user: { name: _.userName, settings: { theme: _.currentTheme } } }')
})
// Array literals
test('array literals', () => {
expect(addContext('[first, second, items[index]]'))
.toBe('[_.first, _.second, _.items[_.index]]')
})
// Regular expressions
test('regex literals', () => {
expect(addContext('/test/.test(input)'))
.toBe('/test/.test(_.input)')
})
// Numbers and operators
test('numeric operations', () => {
expect(addContext('price * 1.2 + tax - discount'))
.toBe('_.price * 1.2 + _.tax - _.discount')
})
================================================
FILE: packages/nuedom/test/document.test.js
================================================
import { parseNue, parseNames } from '../src/compiler/document.js'
test('doctype & root', () => {
const doc = parseNue('<!dhtml> <app/> <section :is="mod"/>')
expect(doc).toMatchObject({
root: { tag: "app", is_custom: true },
doctype: "dhtml",
is_dhtml: true,
is_lib: true,
})
})
test('imports', () => {
const page = parseNue(`
<script>
import { hello } from 'hello.js'
</script>
<a :class="hello" :onclick="hello">{ hello() }</a>
`)
const { root } = page
expect(root.attr[0].fn).toBe('hello')
expect(root.handlers[0].h_fn).toBe('hello($e)')
expect(root.children[0].fn).toBe('hello()')
expect(page.is_dhtml).toBeTrue()
})
test('meta', () => {
const page = parseNue(`
<!-- @license MIT -->
<script>
import { hello } from 'hello.js'
</script>
`)
expect(page.meta.license).toBe('MIT')
})
test('svg document', () => {
const page = parseNue(`
<?xml version="1.0">
<!--
@use [foo, bar]
-->
<svg></svg>
`)
expect(page.lib[0]).toMatchObject({
meta: { use: "[foo, bar]" },
svg: true,
})
})
test('full', () => {
const page = parseNue(`
<!-- @spa -->
<!doctype dhtml>
<script>
import { getContacts } from '/@shared/model.js'
</script>
<!-- @reactive -->
<custom>
<script>this.foo = 10</script>
</custom>
<!-- @author tipiirai -->
<another>
<p>World</p>
</another>
<script>
import { foo as state } from './foo.js'
</script>
`)
expect(page.doctype).toEqual('dhtml')
// meta
const [a, b] = page.lib
expect(page.meta).toEqual({ spa: true })
expect(a.meta).toEqual({ reactive: true })
expect(b.meta).toEqual({ author: 'tipiirai' })
// script
expect(page.script).toInclude('model.js')
expect(page.script).toInclude('foo.js')
})
test('parseNames', () => {
const template = `
import { getContacts } from 'model.js'
import {foo as state, another } from 'foo.js'
// import { nothing } from 'nothing.js'
const { MEME, PRANK } = await import('/kama')
const FOO = [ 100 ]
export function format() {
}
function trick() {
}
`
expect(parseNames(template)).toEqual([
'getContacts', 'state', 'another', 'MEME', 'PRANK', 'FOO', 'format', 'trick'
])
})
================================================
FILE: packages/nuedom/test/domdiff.test.js
================================================
import { createDocument } from '../src/dom/fakedom.js'
import { domdiff } from '../src/dom/diff.js'
// helper functions
const text = (content) => document.createTextNode(content)
const div = (content) => {
const el = document.createElement('div')
if (content) el.appendChild(text(content))
return el
}
const span = (content) => {
const el = document.createElement('span')
if (content) el.appendChild(text(content))
return el
}
const p = (content) => {
const el = document.createElement('p')
if (content) el.appendChild(text(content))
return el
}
const ul = () => document.createElement('ul')
const li = (content, key) => {
const el = document.createElement('li')
if (content) el.appendChild(text(content))
if (key) el.setAttribute('key', key)
return el
}
beforeAll(() => global.document = createDocument() )
test('text update', () => {
const oldDiv = div('1')
const newDiv = div('2')
domdiff(oldDiv, newDiv)
expect(oldDiv.textContent).toBe('2')
})
test('attribute update', () => {
const oldDiv = div()
oldDiv.setAttribute('class', 'old')
const newDiv = div()
newDiv.setAttribute('class', 'new')
domdiff(oldDiv, newDiv)
expect(oldDiv.getAttribute('class')).toBe('new')
})
test('attribute addition', () => {
const oldDiv = div()
const newDiv = div()
newDiv.setAttribute('id', 'test')
domdiff(oldDiv, newDiv)
expect(oldDiv.getAttribute('id')).toBe('test')
})
test('attribute removal', () => {
const oldDiv = div()
oldDiv.setAttribute('id', 'test')
const newDiv = div()
domdiff(oldDiv, newDiv)
expect(oldDiv.hasAttribute('id')).toBe(false)
})
test('element addition', () => {
const oldDiv = div()
const newDiv = div()
newDiv.appendChild(span('New'))
domdiff(oldDiv, newDiv)
expect(oldDiv.children.length).toBe(1)
expect(oldDiv.firstChild.tagName).toBe('SPAN')
expect(oldDiv.textContent).toBe('New')
})
test('element removal', () => {
const oldDiv = div()
oldDiv.appendChild(span('Old'))
const newDiv = div()
domdiff(oldDiv, newDiv)
expect(oldDiv.children.length).toBe(0)
})
test('multiple element addition', () => {
const oldDiv = div()
const newDiv = div()
newDiv.appendChild(span('A'))
newDiv.appendChild(p('B'))
domdiff(oldDiv, newDiv)
expect(oldDiv.children.length).toBe(2)
expect(oldDiv.children[0].tagName).toBe('SPAN')
expect(oldDiv.children[0].textContent).toBe('A')
expect(oldDiv.children[1].tagName).toBe('P')
expect(oldDiv.children[1].textContent).toBe('B')
})
test('multiple element removal', () => {
const oldDiv = div()
oldDiv.appendChild(span('A'))
oldDiv.appendChild(p('B'))
const newDiv = div()
domdiff(oldDiv, newDiv)
expect(oldDiv.children.length).toBe(0)
})
test('element reorder', () => {
const oldDiv = div()
oldDiv.appendChild(span('A'))
oldDiv.appendChild(p('B'))
const newDiv = div()
newDiv.appendChild(p('B'))
newDiv.appendChild(span('A'))
domdiff(oldDiv, newDiv)
expect(oldDiv.children[0].tagName).toBe('P')
expect(oldDiv.children[0].textContent).toBe('B')
expect(oldDiv.children[1].tagName).toBe('SPAN')
expect(oldDiv.children[1].textContent).toBe('A')
})
test('keyed list reorder', () => {
const oldUl = ul()
oldUl.appendChild(li('A', '1'))
oldUl.appendChild(li('B', '2'))
const newUl = ul()
newUl.appendChild(li('B', '2'))
newUl.appendChild(li('A', '1'))
domdiff(oldUl, newUl)
expect(oldUl.children[0].getAttribute('key')).toBe('2')
expect(oldUl.children[0].textContent).toBe('B')
expect(oldUl.children[1].getAttribute('key')).toBe('1')
expect(oldUl.children[1].textContent).toBe('A')
})
test('keyed list addition', () => {
const oldUl = ul()
oldUl.appendChild(li('A', '1'))
const newUl = ul()
newUl.appendChild(li('A', '1'))
newUl.appendChild(li('B', '2'))
domdiff(oldUl, newUl)
expect(oldUl.children.length).toBe(2)
expect(oldUl.children[0].getAttribute('key')).toBe('1')
expect(oldUl.children[0].textContent).toBe('A')
expect(oldUl.children[1].getAttribute('key')).toBe('2')
expect(oldUl.children[1].textContent).toBe('B')
})
test('keyed list removal', () => {
const oldUl = ul()
oldUl.appendChild(li('A', '1'))
oldUl.appendChild(li('B', '2'))
const newUl = ul()
newUl.appendChild(li('A', '1'))
domdiff(oldUl, newUl)
expect(oldUl.children.length).toBe(1)
expect(oldUl.children[0].getAttribute('key')).toBe('1')
expect(oldUl.children[0].textContent).toBe('A')
})
test('nested element update', () => {
const oldDiv = div()
oldDiv.appendChild(span('Hello'))
const newDiv = div()
newDiv.appendChild(span('World'))
domdiff(oldDiv, newDiv)
expect(oldDiv.children[0].textContent).toBe('World')
})
test('nested element addition', () => {
const oldDiv = div()
oldDiv.appendChild(span('A'))
const newDiv = div()
newDiv.appendChild(span('A'))
const nested = div()
nested.appendChild(p('B'))
newDiv.appendChild(nested)
domdiff(oldDiv, newDiv)
expect(oldDiv.children.length).toBe(2)
expect(oldDiv.children[0].tagName).toBe('SPAN')
expect(oldDiv.children[0].textContent).toBe('A')
expect(oldDiv.children[1].tagName).toBe('DIV')
expect(oldDiv.children[1].children[0].tagName).toBe('P')
expect(oldDiv.children[1].children[0].textContent).toBe('B')
})
test('text node addition', () => {
const oldDiv = div()
const newDiv = div('Hello')
domdiff(oldDiv, newDiv)
expect(oldDiv.textContent).toBe('Hello')
})
test('text node removal', () => {
const oldDiv = div('Hello')
const newDiv = div()
domdiff(oldDiv, newDiv)
expect(oldDiv.textContent).toBe('')
})
test('mixed content update', () => {
const oldDiv = div()
oldDiv.appendChild(text('Hello'))
oldDiv.appendChild(span('A'))
const newDiv = div()
newDiv.appendChild(text('World'))
newDiv.appendChild(span('B'))
domdiff(oldDiv, newDiv)
expect(oldDiv.childNodes[0].textContent).toBe('World')
expect(oldDiv.childNodes[1].tagName).toBe('SPAN')
expect(oldDiv.childNodes[1].textContent).toBe('B')
})
test('complete node replacement', () => {
const parent = div()
const oldDiv = div()
oldDiv.appendChild(span('A'))
parent.appendChild(oldDiv)
const newDiv = div()
newDiv.appendChild(p('B'))
domdiff(oldDiv, newDiv)
expect(parent.children[0].tagName).toBe('DIV')
expect(parent.children[0].children[0].tagName).toBe('P')
expect(parent.children[0].children[0].textContent).toBe('B')
})
================================================
FILE: packages/nuedom/test/event.test.js
================================================
import { clickable } from './event.util.js'
test('event handler', () => {
const template = '<button :onclick="counter[0]++">{ counter[0] }</button>'
const data = { counter: [1] }
const root = clickable(template, data)
expect(root.html).toBe('<button>1</button>')
root.click()
expect(data.counter[0]).toBe(2)
expect(root.html).toBe('<button>2</button>')
})
test('event argument', () => {
const template = `
<button :onclick="setName($event)">
{ name }
<script>
setName(e) {
this.name = e.target.tagName
}
</script>
</button>
`
const button = clickable(template)
expect(button.html).toBe('<button></button>')
button.click()
expect(button.html).toBe('<button>BUTTON</button>')
})
test('conditional', () => {
const template = `
<div>
<h3 :if="count">Hello</h3>
<a :onclick="count++"/>
</div>
`
const root = clickable(template, { count: 0 })
expect(root.html).toBe('<div><a></a></div>')
root.click('a')
expect(root.html).toBe('<div><h3>Hello</h3><a></a></div>')
})
test('callback', () => {
const template = `
<div>
<child :callback/>
</div>
<child>
<button :onclick=run/>
<script>
run() {
this.callback()
}
</script>
</child>
`
let counter = 0
const root = clickable(template, { callback: () => counter++ })
root.click()
root.click()
expect(counter).toBe(2)
})
test('method', () => {
const template = `
<a :onclick="increment()">
Count: { count }
<script>
this.count = 0
this.increment = function() {
this.count++
}
</script>
</a>
`
const root = clickable(template)
expect(root.html).toInclude('Count: 0')
root.click('a')
expect(root.html).toInclude('Count: 1')
})
test('method syntax', () => {
const template = `
<a :onclick="hey()">
{ val } { avg }
<script>
hey() {
this.val = 'hey'
}
get avg() {
return 100
}
</script>
</a>
`
const root = clickable(template)
root.click('a') // Await click
const html = root.html
expect(html).toInclude('hey')
expect(html).toInclude('100')
})
test('child/bind updates', () => {
const template = `
<div>
<h1>{ data.hello }</h1>
<child :bind="data"/>
<button :onclick="change"/>
<script>
this.data = {
hello: 'Hello',
world: 'World',
}
change() {
this.data = {
hello: 'Holy',
world: 'Smoke',
}
}
</script>
</div>
<child>
<p>{ world }</p>
</child>
`
const root = clickable(template)
expect(root.html).toInclude('<h1>Hello</h1> <div><p>World</p>')
root.click()
expect(root.html).toInclude('<h1>Holy</h1> <div><p>Smoke</p>')
})
test('event bind shortcut', async () => {
const template = `
<a :onclick disabled="{ disabled }">
<script>
onclick() {
this.disabled = 1
}
</script>
</a>
`
const root = clickable(template)
expect(root.html).toBe('<a></a>')
root.click('a')
expect(root.html).toBe('<a disabled=""></a>')
})
test('loop + if', () => {
const template = `
<div>
<a :each="val in [1,2]" :if="doit">{val}</a>
<button :onclick="doit = true"/>
</div>
`
const root = clickable(template)
expect(root.html).toBe('<div><button></button></div>')
root.click()
expect(root.html).toInclude('<a>1</a><a>2</a>')
})
test.skip('loop :onclick', () => {
const template = `
<div>
<button :each="el, i of [{ id: 2 }]" :onclick="doit = true">
{ el.id } / { i }
</button>
<p :if="doit">Hey</p>
</div>
`
const root = clickable(template)
expect(root.html).toEndWith('</button></div>')
root.click()
expect(root.html).toBe('<div><button>2 / 0</button><p>Hey</p></div>')
})
================================================
FILE: packages/nuedom/test/event.util.js
================================================
import { parseNue } from '../src/compiler/document.js'
import { mountAST } from '../src/dom/render.js'
export function clickable(template, data) {
const { lib } = parseNue(template)
const block = mountAST(lib[0], { data, deps: lib.slice(1) })
const { root } = block
function click(selector='button') {
const el = root.querySelector(selector)
el?.dispatchEvent({ type: 'click' })
}
return { block, click, get html() { return root.innerHTML || '' } }
}
================================================
FILE: packages/nuedom/test/fakedom.test.js
================================================
import { createDocument } from '../src/dom/fakedom.js'
const document = createDocument()
// helper functions
const el = (tag) => document.createElement(tag)
const text = (content) => document.createTextNode(content)
test('innerHTML', () => {
const body = el('body')
const link = el('a')
link.setAttribute('href', '/test')
link.appendChild(text('Click me'))
body.appendChild(link)
expect(body.innerHTML).toBe('<a href="/test">Click me</a>')
})
test('innerHTML handles nested elements', () => {
const div = el('div')
const span = el('span')
span.appendChild(text('Hello'))
div.appendChild(span)
expect(div.innerHTML).toBe('<span>Hello</span>')
})
// firstChild tests
test('firstChild returns first child or text node', () => {
const div = el('div')
const span1 = el('span')
const span2 = el('span')
div.appendChild(span1)
div.appendChild(span2)
expect(div.firstChild).toBe(span1)
// test with text node as first child
const div2 = el('div')
const textNode = text('Hello')
div2.appendChild(textNode)
div2.appendChild(el('span'))
expect(div2.firstChild).toBe(textNode)
})
test('firstChild returns undefined for empty element', () => {
const div = el('div')
expect(div.firstChild).toBeUndefined()
})
// replaceChild tests
test('replaceChild replaces existing child and maintains order', () => {
const div = el('div')
const span1 = el('span')
const span2 = el('span')
const span3 = el('span')
const newP = el('p')
div.appendChild(span1)
div.appendChild(span2)
div.appendChild(span3)
const returned = div.replaceChild(newP, span2)
// check replacement worked
expect(div.children).toContain(newP)
expect(div.children).not.toContain(span2)
expect(newP.parentNode).toBe(div)
expect(span2.parentNode).toBe(null)
// check order maintained
expect(div.children[0]).toBe(span1)
expect(div.children[1]).toBe(newP)
expect(div.children[2]).toBe(span3)
// check return value
expect(returned).toBe(span2)
})
test('attributes work with standard iteration', () => {
const button = el('button')
button.setAttribute('disabled', '')
button.setAttribute('type', 'submit')
const attrs = []
for (let attr of button.attributes) {
attrs.push(`${attr.name}="${attr.value}"`)
}
expect(attrs).toContain('disabled=""')
expect(attrs).toContain('type="submit"')
expect(button.outerHTML).toBe('<button disabled="" type="submit"></button>')
})
test('classList and class attribute work together', () => {
const div = el('div')
div.classList.add('foo', 'bar')
expect(div.classList.length).toBe(2)
expect(div.classList.toString()).toBe('foo bar')
expect(div.outerHTML).toBe('<div class="foo bar"></div>')
})
test('void tags serialize as self-closing', () => {
const div = el('div')
const img = el('img')
img.setAttribute('src', 'test.jpg')
div.appendChild(img)
expect(div.innerHTML).toBe('<img src="test.jpg">')
})
test('SVG elements preserve case-sensitive tag names', () => {
const svg = el('svg')
const foreignObject = el('foreignObject')
svg.appendChild(foreignObject)
expect(svg.innerHTML).toBe('<foreignObject></foreignObject>')
})
test('document fragment works', () => {
const fragment = document.createDocumentFragment()
fragment.appendChild(el('div'))
fragment.appendChild(text('hello'))
expect(fragment.innerHTML).toBe('<div></div>hello')
})
test('textContent', () => {
const div = el('div')
div.appendChild(text('Hello'))
console.info(div.textContent)
// expect(div.textContent).toBe('Hello') // This will probably fail
})
test('textContent getter on elements collects from multiple text children', () => {
const div = el('div')
div.appendChild(text('Hello'))
div.appendChild(text(' World'))
expect(div.textContent).toBe('Hello World')
})
test('textContent getter on elements collects from nested text', () => {
const div = el('div')
const span = el('span')
span.appendChild(text('Hello'))
div.appendChild(span)
expect(div.textContent).toBe('Hello')
})
================================================
FILE: packages/nuedom/test/index.html
================================================
<!doctype html>
<!--
<script src="https://cdn.jsdelivr.net/gh/nuejs/nue@2.0/packages/nuedom/src/nue-jit.js" type="module"></script>
-->
<script src="../src/nue-jit.js" type="module"></script>
<style>
body {
font-family: system-ui;
display: grid;
place-items: center;
min-height: 100vh;
margin: 0;
}
button {
font-family: inherit;
padding: .5em 1.5em;
background-color: black;
border-radius: .25em;
cursor: pointer;
color: white;
border: 0;
}
</style>
<template>
<button :onclick="count++">
Count: <b>{ count }</b>
<script>
this.count = 0
</script>
</button>
</template>
================================================
FILE: packages/nuedom/test/loop.test.js
================================================
import { clickable } from './event.util.js'
import { renderNue } from '../src/dom/render.js'
test('loop index', () => {
const template = '<ul><li :each="(el, i) in items">{i}: {el}</li></ul>'
const html = renderNue(template, { data: { items: ['a', 'b'] } })
expect(html).toBe('<ul><li>0: a</li><li>1: b</li></ul>')
})
test('loop data access', () => {
const template = '<div><p :each="el in items">{el} {foo}</p></div>'
const html = renderNue(template, { data: { foo: 1, items: ['F', 'F'] }})
expect(html).toInclude('<p>F 1</p><p>F 1</p>')
})
test('template loop', () => {
const template = `
<dl>
<template :each="(el, i) in meta">
<dt>{ el.title }</dt>
<dd>{ el.data }</dd>
</template>
</dl>
`
const meta = [
{ title: 'Name', data: 'Alice' },
{ title: 'Age', data: '30' }
]
const html = renderNue(template, { data: { meta } })
expect(html).toBe('<dl><dt>Name</dt><dd>Alice</dd><dt>Age</dt><dd>30</dd></dl>')
})
test('Object.entries()', () => {
const template = `
<dl>
<template :each="[key, val] in Object.entries(items)">
<td>{ key }</td>
<dd>{ val }</dd>
</template>
</dl>
`
const items = { Email: 'm@example.com' }
const html = renderNue(template, { data: { items } })
expect(html).toBe('<dl><td>Email</td><dd>m@example.com</dd></dl>')
})
test('loop deconstruct', () => {
const template = `
<dl>
<template :each="{ key, val } in items">
<td>{ key }</td>
<dd>{ val }</dd>
</template>
</dl>
`
const items = [{ key: 'Email', val: 'm@example.com' }]
const html = renderNue(template, { data: { items } })
expect(html).toBe('<dl><td>Email</td><dd>m@example.com</dd></dl>')
})
test('nested loop', () => {
const template = `
<ul>
<li :each="arr in items">
<p :each="el in arr">{ el }</p>
</li>
</ul>
`
const items = [['A', 'B'], ['C', 'D']]
const html = renderNue(template, { data: { items } })
expect(html).toBe('<ul><li><p>A</p><p>B</p></li><li><p>C</p><p>D</p></li></ul>')
})
test('component loop', () => {
const template = `
<ul>
<item :each="text of arr" :text="{ text }"/>
<script>
this.arr = ['hello', 'world']
</script>
</ul>
<li :is="item">{ text }</li>
`
const html = renderNue(template)
expect(html).toBe('<ul><li>hello</li><li>world</li></ul>')
})
test('slot loop', () => {
const html = renderNue(`
<a>
<child :each="el, i in new Array(2).fill(1)">{i}</child>
</a>
<b :is="child">i: <slot/></b>
`)
expect(html).toBe('<a><b>i: 0</b><b>i: 1</b></a>')
})
================================================
FILE: packages/nuedom/test/render.test.js
================================================
import { renderNue } from '../src/dom/render.js'
jest.spyOn(console, 'error').mockImplementation(() => {})
jest.spyOn(console, 'warn').mockImplementation(() => {})
afterEach(() => console.error.mockClear())
test('element', () => {
expect(renderNue('<div/>')).toBe('<div></div>')
})
test('text', () => {
expect(renderNue('<div>Hello</div>')).toBe('<div>Hello</div>')
})
test('expression', () => {
expect(renderNue('<div>{ 1 + 2 }</div>')).toBe('<div>3</div>')
})
test('NaN values', () => {
expect(renderNue('<div>{ a * b }</div>')).toBe('<div>N/A</div>')
})
test('expression whitespace', () => {
expect(renderNue('<a>{ "a" } { "b" }</a>')).toBe('<a>a b</a>')
})
test('text whitespace', () => {
const template = '<div>{ a } / { b } ({ c }) !</div>'
const html = renderNue(template, { data: { a: 1, b: 2, c: 3 } })
expect(html).toBe('<div>1 / 2 (3) !</div>')
})
test('tag whitespace', () => {
const html = renderNue('<a><b>Hey</b> <em>Yo</em></a>')
expect(html).toInclude('</b> <em>')
})
test('component + text whitespace', () => {
const html = renderNue('<a><note/> Bro</a><note>Yo</note>')
expect(html).toBe('<a><div>Yo</div> Bro</a>')
})
test('errors', () => {
const html = renderNue('<div>{ foo() }</div>')
expect(html).toBe('<div>[Error]</div>')
expect(console.error).toHaveBeenCalled()
})
test('do not render event handlers', () => {
const html = renderNue('<a :onclick="click"></a>', { click: () => true })
expect(html).toBe('<a></a>')
})
test('attributes', () => {
const html = renderNue(`<a class="btn" disabled/>`)
expect(html).toBe('<a class="btn" disabled=""></a>')
})
test('attribute expressions', () => {
const html = renderNue(`<a class="{'f' + 1}"/>`)
expect(html).toBe('<a class="f1"></a>')
})
test('render prop', () => {
const html = renderNue('<h1>{ msg }</h1>', { data: { msg: 'Hello'} })
expect(html).toBe('<h1>Hello</h1>')
})
test('nesting', () => {
const template = '<div><span>{ text }</span></div>'
const html = renderNue(template, { data: { text: 'Nested' } })
expect(html).toBe('<div><span>Nested</span></div>')
})
test('svg', () => {
const template = `
<svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
<foreignobject/>
<path d="M6 8a6 6 0 0 1 12"/>
</svg>
`
const svg = renderNue(template)
expect(svg).toStartWith('<svg xmlns="http://www.w3.org/2000/svg"')
// close tags
expect(svg).toInclude('<path d="M6 8a6 6 0 0 1 12"></path>')
// fix cases
expect(svg).toInclude('viewBox')
expect(svg).toInclude('foreignObject')
})
test('interpolation', () => {
const template = '<div class="item { type }"/>'
const html = renderNue(template, { data: { type: 'active' } })
expect(html).toBe('<div class="item active"></div>')
})
test('html', () => {
const html = renderNue('<div>{{ html }} here</div>', { data: { html: '<b>Bold</b> text' } })
expect(html).toBe('<div><b>Bold</b> text here</div>')
})
test('html false', () => {
const html = renderNue('<a>{{ html }}</a>', { data: { html: false } })
expect(html).toBe('<a></a>')
})
test('class mapping', () => {
const template = '<label class="[ is-active: foo ] bar">Test</label>'
const html = renderNue(template, { data: { foo: true } })
expect(html).toBe('<label class="is-active bar">Test</label>')
})
test('class mapping with functions', () => {
const template = '<div class="[ active: isActive(), error: hasError() ]">Test</div>'
const data = { isActive: () => true, hasError: () => true }
const html = renderNue(template, { data })
expect(html).toBe('<div class="active error">Test</div>')
})
test(':var-* attributes', () => {
const template = '<a --index="1" --random="{ random }"></a>'
const html = renderNue(template, { data: { random: 100 } })
expect(html).toBe('<a style="--index:1;--random:100;"></a>')
})
test('max class names', () => {
const tmpl = '<a class="{ class }"/>'
renderNue(tmpl, { data: { class: 'foo bar baz boo' }})
renderNue(tmpl, { data: { class: 'foo bar' }, max_class_names: 1 })
expect(console.error).toHaveBeenCalledTimes(2)
})
test('invalid character warnings', () => {
renderNue('<a class="hover:md"/>')
renderNue('<a class="bar"/>')
expect(console.error).toHaveBeenCalledTimes(1)
})
test('template if', () => {
const template = `
<dl>
<template :if="flag">
<dt>Foo</dt>
</template>
<template :else>
<dt>Bar</dt>
</template>
</dl>
`
expect(renderNue(template, { data: { flag: true } })).toBe('<dl><dt>Foo</dt></dl>')
expect(renderNue(template)).toBe('<dl><dt>Bar</dt></dl>')
})
test('bad if clause', () => {
expect(renderNue('<div><b :if="bad.clause"/></div>')).toBe('<div></div>')
})
test('component conditional', () => {
const template = `
<div>
<note :if="obj.error"/>
</div>
<note>Hello</note>
`
const html = renderNue(template, { data: { obj: { error: true } } })
expect(html).toBe('<div><div>Hello</div></div>')
})
test('if-else', () => {
const template = `
<div>
<h3>Hello</h3>
<b :if="am < 10">Small</b>
<b :else-if="am < 50">Mid</b>
<strong :else>Big</strong>
<a :if="none">Empty</a>
</div>`
const html = renderNue(template, { data: { am: 30 } })
expect(html).toBe('<div><h3>Hello</h3><b>Mid</b></div>')
})
test('passtrough scripts', () => {
const html = renderNue(`
<body>
<script src="/analytics.js"></script>
<script type="module">track(666)</script>
<script>// ignored</script>
</body>
`)
expect(html).toInclude('analytics.js')
expect(html).toInclude('type="module')
})
test('render functions', () => {
const custom = (data) => `<h1>${ data.hello }, ${ data.who }</h1>`
const template = `<div><custom hello="Hello"/></div>`
const html = renderNue(template, { data: { who: 'World' }, fns: { custom } })
expect(html).toBe('<div><h1>Hello, World</h1></div>')
})
test('JSON stubs', () => {
const template = `<div><foo hello="Hello"/></div>`
const html = renderNue(template)
expect(html).toInclude('<foo nue="foo">')
expect(html).toInclude('<script type="application/json">{"hello":"Hello"}')
})
================================================
FILE: packages/nuedom/test/tokenizer.test.js
================================================
import { tokenize } from '../src/compiler/tokenizer.js'
test('tag', () => {
expect(tokenize('<p>Hello</p>')).toEqual(['<p>', 'Hello', '</p>'])
})
test('self-closing tag', () => {
expect(tokenize('<div/>')).toEqual(['<div/>'])
})
test('expressions', () => {
const tokens = tokenize('<span>{ text } foo {{ html }} bar {{{ html }}}</span>')
expect(tokens).toEqual(
[ "<span>", "{ text }", " foo ", "{{ html }}", " bar ", "{{ html }}", "</span>" ]
)
})
test('nested tags', () => {
expect(tokenize('<div><p>Hi</p></div>')).toEqual(['<div>', '<p>', 'Hi', '</p>', '</div>'])
})
test('open tags', () => {
expect(tokenize('<hr>')).toEqual(['<hr/>'])
expect(tokenize('<img src>')).toEqual(['<img src/>'])
expect(tokenize('<hr><a></a>')).toEqual(['<hr/>', '<a>', '</a>'])
})
test('complex', () => {
const els = tokenize(`
<div>
<p foo="10">Hi</p>
<span><b>there</b></span>
</div>
`)
expect(els.length).toBe(10)
})
test('simple, unquoted attributes', () => {
const els = tokenize('<p :onclick=hey>Hi</p>')
expect(els[0]).toBe('<p :onclick=hey>')
expect(els.length).toBe(3)
})
test('class helper', () => {
const els = tokenize('<p :class="{ foo: true }">Hi</p>')
expect(els.length).toBe(3)
})
test('single quoted attr', () => {
const els = tokenize(`<p :class='{ boo || "baz" }'>Hi</p>`)
expect(els.length).toBe(3)
})
test('complex attribute', () => {
const els = tokenize('<p :class="{ foo: true } baz { bar }">Hi</p>')
expect(els.length).toBe(3)
})
test('elem with attributes', () => {
expect(tokenize('<div class=test disabled>')).toEqual(['<div class=test disabled>'])
})
test('newlines', () => {
const els = tokenize(`
<div
amount="10"
class="bar"/>
`)
expect(els.length).toBe(1)
})
test('if and angle brackets', () => {
expect(tokenize('<a :if="var > 10"/>').length).toBe(1)
expect(tokenize('<a :if="{ var < 10 }"/>').length).toBe(1)
expect(tokenize('<b :if="am < 50"/>').length).toBe(1)
})
test('unclosed tag throws', () => {
expect(() => tokenize('<a href="boo"<b/>')).toThrow(SyntaxError)
})
test('comments', () => {
const els = tokenize(`
<h1>Hello</h1>
<!--
<b>World</p>
-->
`)
expect(els.join('').trim()).toEqual('<h1>Hello</h1>')
})
test('annotations', () => {
const els = tokenize(`
<!--
@license MIT
@author tipiirai
-->
<h1>Hello</h1>
`)
expect(els[0].meta).toEqual({license: "MIT", author: "tipiirai" })
})
test('scripts with expressions', () => {
const tokens = tokenize('<a><script>`{expr}`</script></a>')
expect(tokens.length).toBe(5)
expect(tokens[2]).toBe('`{expr}`')
})
test('scripts with inner HTML', () => {
const tokens = tokenize('<a><script>"<b>B</b>"</script></a>')
expect(tokens.length).toBe(5)
expect(tokens[2]).toBe('"<b>B</b>"')
})
================================================
FILE: packages/nueglow/Makefile
================================================
run-tests:
bun test
================================================
FILE: packages/nueglow/README.md
================================================
# Nueglow: CSS first syntax highlighting
Nueglow is syntax highlighting that works with your design system. It generates semantic HTML that your CSS can style. One minuscule highlighter for all languages.
<a href="https://nuejs.org/blog/nueglow">
<img src="https://nuejs.org/img/glow-dark-big.png"></a>
## The highlighter problem
Popular syntax highlighters can be troublesome:
**Massive codebases** - tools like Shiki ship 14MB and 44 packages. Each language needs its own grammar file with thousands of cryptic regex rules.
**Theme lock-in** - Themes come as giant JSON files with 300+ predefined colors. You can't easily adapt them to your brand or design system.
**Grammar complexity** - HTML grammar alone has 2000+ lines of regex patterns. Adding a new language means wiring another massive grammar file.
These syntax highlighters were built for code editors, not websites. They assume you want their themes and their markup. Your design system becomes an afterthought.
## Why Nueglow
**Universal language support** - Works with JavaScript, TypeScript, Python, HTML, CSS, YAML, JSON, Markdown, Bash, SQL, and virtually any other syntax. No grammar files needed.
**Language-agnostic parsing** - Instead of per-language rules, Nueglow recognizes common patterns: strings, comments, keywords, operators. This works across all programming languages.
**Semantic HTML output** - Keywords become `<b>`, strings become `<em>`, comments become `<sup>`. Your CSS controls the appearance.
**Design system friendly** - Only 10-15 HTML elements to style. Fits naturally into any design system without fighting existing patterns.
**Lightweight** - Complete highlighting for all languages in under 3KB of CSS. No JavaScript runtime, no theme bundles.
<img src="https://nuejs.org/img/glow-light-big.png">
See the [Nue website](https://nuejs.org/docs/nueglow) for details and documentation.
================================================
FILE: packages/nueglow/css/build.js
================================================
// Running: bun css/build.js
import { promises as fs } from 'node:fs'
async function minify(names, toname) {
const min = await (await Bun.build({
entrypoints: names.map(name => `css/${name}.css`),
minify: true,
throw: true,
experimentalCss: true,
})).outputs.map(async file => await file.text())
const to = `minified/${toname}.css`
await fs.mkdir('minified', { recursive: true })
await fs.writeFile(to, min)
console.log('>', to, (await fs.stat(to)).size)
}
await minify(['syntax', 'markers'], 'syntax')
await minify(['syntax'], 'syntax.nano')
================================================
FILE: packages/nueglow/css/light.css
================================================
/* example light mode */
pre {
--glow-bg-color: #f9f9f9;
--glow-base-color: #555;
/* brand coloring */
--glow-primary-color: #0068d6;
--glow-secondary-color: #bd2864;
--glow-accent-color: #456aff;
/* rare case. make it "pop" */
--glow-special-color: #7820bc;
/* shades of gray */
--glow-char-color: #8e989c;
--glow-comment-color: #9aa1a3;
--glow-counter-color: #bbb;
/* selection color */
--glow-marked-color: #51c6fe29;
}
================================================
FILE: packages/nueglow/css/markers.css
================================================
/* styling for specially marked lines */
pre {
--glow-line-color: 50, 180, 250;
--glow-del-color: 250, 110, 130;
--glow-ins-color: 50, 210, 190;
--glow-line-opacity: 0.15;
--padd: var(--glow-padding, 1.5em);
ins, del, dfn {
min-width: calc(100% + calc(var(--padd) * 2));
margin-left: calc(var(--padd) * -1);
padding-left: var(--padd);
border-left: .2em solid white;
display: inline-block;
position: relative;
width: 100%;
:first-child { margin-left: -.2em; }
/* plus & minus characters */
&:before {
position: absolute;
left: 95%;
}
/* with line numbers */
span & {
margin-left: calc(-3.5em - var(--padd));
padding-left: calc(3.5em + var(--padd));
/* &:before { left: calc(var(--padd) - 1.5em); } */
}
}
ins {
border-color: rgb(var(--glow-ins-color));
background-color: rgba(var(--glow-ins-color), var(--glow-line-opacity));
&:before {
content: '+';
color: rgb(var(--glow-ins-color));
}
}
del {
border-color: rgb(var(--glow-del-color));
background-color: rgba(var(--glow-del-color), var(--glow-line-opacity));
border-radius: 0;
&:before {
content: '-';
color: rgb(var(--glow-del-color));
}
}
dfn {
border-color: rgb(var(--glow-line-color));
background-color: rgba(var(--glow-line-color), var(--glow-line-opacity));
}
}
================================================
FILE: packages/nueglow/css/syntax.css
================================================
pre {
background-color: var(--glow-bg-color, #20293A);
color: var(--glow-base-color, #a2aab1);
padding: var(--glow-padding, 1.5em);
counter-reset: line-counter 0;
font-family: monospace;
line-height: 1.7;
overflow-x: auto;
/* reset */
* {
font-weight: 400;
font-style: inherit;
text-decoration: inherit
}
/* primary accent color */
b { color: var(--glow-primary-color, #7dd3fc) }
/* secondary accent color */
em { color: var(--glow-secondary-color, #f472b6) }
/* special emphasis */
strong { color: var(--glow-accent-color, #419fff) }
/* brackets, special characters */
i { color: var(--glow-char-color, #64748b) }
/* vawy error message */
u {
text-decoration: underline wavy var(--glow-error-color, red);
text-decoration-thickness: .15em;
text-underline-offset: .5em;
}
/* comments */
sup {
color: var(--glow-comment-color, #6f7a7d);
font-size: inherit;
vertical-align: inherit;
font-style: italic;
}
/* special */
label { font-weight: bold; color: var(--glow-special-color, #fff); }
/* marked code */
mark {
background-color: var(--glow-marked-color, #2dd4bf26);
color: unset;
border-radius: .2em;
padding: .3em .4em;
margin: -.3em -.4em;
}
/* line numbers */
span {
counter-increment: line-counter 1;
&:before {
color: var(--glow-counter-color, #475569);
content: counter(line-counter);
display: inline-block;
text-align: right;
padding-right: 1em;
margin-right: 1em;
width: 2.5em;
}
}
/* erroneous line number styling */
span:has(u):before {
background-color: var(--glow-error-color, red);
border-radius: .2em;
font-weight: bold;
color: white;
}
}
================================================
FILE: packages/nueglow/index.js
================================================
const MIXED_HTML = ['html', 'jsx', 'php', 'astro', 'dhtml', 'vue', 'svelte', 'hb']
const LINE_COMMENT = { clojure: ';;', lua: '--', python: '#' }
const PREFIXES = { '+': 'ins', '-': 'del', '>': 'dfn' }
const MARK = /(••?)([^•]+)\1/g // ALT + q
const NL = '\n'
const COMMON_WORDS = 'null|true|false|undefined|import|from|async|await|package|begin\
|interface|class|new|int|func|function|get|set|export|default|const|var|let\
|return|yield|for|while|defer|if|then|else|elif|fi|int|string|number|def|public|static|void\
|continue|break|switch|case|final|finally|try|catch|while|super|long|float\
|throw|fun|val|use|fn|my|end|local|until|next|bool|ns|defn|puts|require|each'
// Implement most~50% of words to cover 95% of cases
const SPECIAL_WORDS = {
cpp: 'cout|cin|using|namespace',
python: 'None|nonlocal|lambda',
go: 'chan|fallthrough'
}
// special rules (growing list)
const RULES = {
css: [
{ tag: 'strong', re: /#[0-9a-f]{3,7}/gi },
{ tag: 'label', re: /!important/gi },
{ tag: 'em', re: /--[\w\d\-]+/gi },
],
json: [
{ tag: 'b', re: /(".+"):/gi },
],
yaml: [
{ tag: 'b', re: /([\w ]+):/gi },
],
}
const HTML_TAGS = [
// line comment
{ tag: 'sup', re: /# .+/ },
{ tag: 'label', re: /\[([a-z\-]+)/g, lang: ['md', 'toml'], shift: true },
// string value (keep second on the list)
{ tag: 'em', re: /'[^']*'|"[^"]*"/g, is_string: true },
// HTML tag name
{ tag: 'strong', re: /<([\w\-]+ )/g, shift: true, lang: MIXED_HTML },
{ tag: 'strong', re: /<\/?([\w\-]+)>/g, shift: true, lang: MIXED_HTML },
// ALL CAPS (constants)
// { tag: 'b', re: /\b[A-Z]{2,}\b/g },
// @special
{ tag: 'label', re: /\B@[\w\-]+/gi },
// char
{ tag: 'i', re: /[^\w •]/g },
// variable name
{ tag: 'b', re: /\b([a-z][\w\-]+)\s*[:=\(!\[]/gi },
// property name
{ tag: 'b', re: /"\w+":/g },
// function name
{ tag: 'b', re: /([\w]+)\(/gi },
// numeric value
{ tag: 'em', re: /\b\d+\.?[%\w\b]*/g },
// variable name
{ tag: 'b', re: /([\w]+)\./g, lang: ['js'] },
]
function getTags(lang) {
const tags = HTML_TAGS.filter(el => !el.lang || el.lang.includes(lang))
// custom keywords
if (!['yaml', 'html', 'json'].includes(lang)) {
const w = SPECIAL_WORDS[lang]
const words = (w ? w + '|' : '') + COMMON_WORDS
const re = new RegExp(`\\b(${words})\\b`, 'gi')
tags.splice(4, 0, { tag: 'strong', re })
}
// custom rules
const rules = RULES[lang]
if (rules) tags.unshift(...rules)
return tags
}
function encode(str) {
return str.replaceAll('<', '<').replaceAll('>', '>')
}
// wrap token
function elem(name, str) {
if (str == '<') str = '<'
else if (str == '>') str = '>'
return `<${name}>${str}</${name}>`
}
/*
Markdown/MDX requires a special treatment, because it's so
different from others (not a programming language)
*/
function isMD(lang) {
return ['md', 'mdx', 'nuemark'].includes(lang)
}
function getMDTags(str) {
const s = str.trim()
const c = s[0]
// divider
if (s.startsWith('---')) return [{ tag: 'i', re: /-+/ }]
// line comment
if (s.startsWith('// ')) return [{ tag: 'sup', re: /.+/ }]
if (['![', '[!'].includes(s.slice(0, 2))) return [{ tag: 'em', re: /.+/ }]
if (['import', 'export'].includes(s.slice(0, 6))) return getTags('js')
// HTML
if (c == '<') return getTags('html')
// heading
if (c == '#') return [{ tag: 'label', re: /.+/ }]
// quote
if (c == '>') return [{ tag: 'i', re: />/ }, { tag: 'sup', re: / .+/ }]
// front matter / yaml
if (/^\w+: /.exec(s)) return getTags('yaml')
// component
if (c == '[' && s.endsWith(']')) {
return s[1] == '.' ? [{ tag: 'label', re: /\w+/g }] : getTags('md')
}
// lists, links, images, fenced code
return [
// inline code
{ tag: 'strong', re: /\`.+\`/g },
// image
{ tag: 'em', re: /^(!.+)/g, shift: true },
// list
{ tag: 'b', re: /[\*\_\[\]\(\)<>]+/g },
]
}
export function parseRow(row, lang) {
const tags = isMD(lang) ? getMDTags(row) : getTags(lang)
const tokens = []
// line comment (language specific)
const re = new RegExp(`${LINE_COMMENT[lang] || '//'} .+`)
tags.unshift({ tag: 'sup', re })
for (const el of tags) {
const { re, shift } = el
row.replace(re, function(match, start, n) {
if (arguments.length == 4) {
const more = shift ? match.indexOf(start) : 0
match = start; start = n + more
}
const end = start + match.length
tokens.push({ start, end, ...el })
})
}
return tokens.sort((a, b) => a.start - b.start)
}
function renderString(str) {
return encode(str).replace(/\$?\{([^\}]+)\}/g, function(_, content) {
return elem('i', _.replace(content, elem('b', content)))
})
}
// exported for testing purposes
export function renderRow(row, lang, mark = true) {
if (!row) return ''
const els = parseRow(row, lang)
const ret = []
var index = 0
for (var i = 0, max = 0, len = els.length, el, next; (el = els[i]); i++) {
const { start, end } = el
next = els[i + 1] || []
// skip overlappings
if (start < max) continue
if (start == next[0] && next[1] > end) continue
if (end > max) max = end; else continue
// construct final result
ret.push(row.substring(index, start))
const code = row.substring(start, end)
ret.push(elem(el.tag, el.is_string ? renderString(code) : code))
index = end
}
ret.push(row.substring(index))
const res = ret.join('')
return !mark ? res : res.replace(MARK, (_, marker, content) => {
return elem(marker[1] ? 'u' : 'mark', content)
})
}
// comment start & end
const COMMENT = [/(\/\* |^ *{# |<!--|'''|=begin)/, /(\*\/|#}|-->|'''|=end)$/]
export function parseSyntax(lines, lang, prefix = true) {
const [comm_start, comm_end] = COMMENT
const html = []
// multi-line comment
let comment
function endComment() {
html.push({ comment })
comment = null
}
lines.forEach((line, i) => {
if (!comment) {
if (comm_start.test(line)) {
comment = [line]
if (comm_end.test(line) && line?.trim() != "'''") endComment()
} else {
// highlighted line
const is_md = isMD(lang)
const c = line[0]
let wrap = prefix && (is_md ? (c == '|' && 'dfn') : PREFIXES[c])
if (wrap && is_md && line == '---') wrap = null
if (wrap) line = (line[1] == ' ' ? ' ' : '') + line.slice(1)
// escape character
if (prefix && c == '\\') line = line.slice(1)
html.push({ line, wrap })
}
} else {
comment.push(line)
if (comm_end.test(line)) endComment()
}
})
return html
}
// code, { language: 'js', numbered: true }
export function glow(str, opts = { prefix: true, mark: true }) {
if (typeof opts == 'string') opts = { language: opts }
const lines = Array.isArray(str) ? str : str.trim().split(/\r?\n/)
if (!lines[0]) return ''
// language
let lang = opts.language
if (!lang && lines[0][0] == '<') lang = 'html'
const html = []
function push(line) {
html.push(opts.numbered ? elem('span', line) : line)
}
parseSyntax(lines, lang, opts.prefix).forEach(function(block) {
let { line, comment, wrap } = block
// EOL comment
if (comment) {
return comment.forEach(el => push(elem('sup', encode(el))))
} else {
line = renderRow(line, lang, opts.mark)
}
if (wrap) line = elem(wrap, line)
push(line)
})
return `<code language="${lang || '*'}">${html.join(NL)}</code>`
}
================================================
FILE: packages/nueglow/package.json
================================================
{
"name": "nue-glow",
"version": "0.2.5",
"description": "Markdown syntax highlighter for CSS developers",
"homepage": "https://nuejs.org/blog/introducing-glow/",
"license": "MIT",
"type": "module",
"repository": {
"url": "https://github.com/nuejs/nue",
"directory": "packages/nueglow",
"type": "git"
},
"engines": {
"bun": ">= 1",
"node": ">= 18"
},
"scripts": {
"css": "bun ./css/build.js",
"test": "node --experimental-vm-modules ../../node_modules/jest/bin/jest.js --runInBand"
},
"files": ["index.js"],
"jest": {
"setupFilesAfterEnv": [
"<rootDir>/../../setup-jest.js"
],
"collectCoverageFrom": [
"<rootDir>/src/**"
]
}
}
================================================
FILE: packages/nueglow/test/generate.js
================================================
import { promises as fs } from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { glow } from '../src/glow.js'
const root = process.argv[2] || dirname(fileURLToPath(import.meta.url))
// Nue / html
const HTML = `
<figure @name="img" class=•"baz { foo } \${ bar }"•>
<img loading="lazy" :alt="alt" :src="_ || src">
<!-- HTML comment here -->
<p>I finally made it to the public</p>
<figcaption :if="caption">{{ caption }}</figcaption>
<script>
•constructor(data)• {
this.caption = data.caption || ''
}
</script>
</figure>
`
const JSX = `
import { FormEvent } from 'react';
/*
Multi-line comment goes here
*/
export default function Page() {
async function onSubmit(event: FormEvent<Element>) {
+ const response = await fetch('/api/submit', {
+ method: 'POST',
body: formData,
});
}
return (
<form onSubmit=•{onSubmit}•>
> <input type="text" name="name" />
<button type="submit">Submit</button>
</form>
);
}`
const CSS = `
@import "../css/dark.css";
/* Let's check out CSS code */
.syntax {
border: 1px solid #fff1 !important;
background-color: var(--base-600);
border-radius: var(--radius);
margin-bottom: 3em;
@media(width < 900) {
transform: scale(1.1);
filter: blur(4px);
}
@starting-style {
transition: transform 4s;
}
}
`
const JAVASCRIPT = `
"use strict"
// import some UI stuff
import { layout } from 'components/layout'
// environment
const ENV = {
scripts: ['lol.js'],
styles: ['lma\\no.css'],
desc: undefined
}
export default function({ val }) {
const fooo = val.split('\\n') // 30px
return \`<div class='node'></div>\`
}
`
// Markdown
const MARKDOWN = `
---
title: Lightning CSS might yield our thinking
tags: [ •css, design systems• ]
pubDate: 2024-02-12
---
# This is something about Lightning CSS
I'm baby else umami wolf yield batch iceland
adaptogen. Iceland **chambray** raclette stumptown

> Air plant adaptogen artisan gastropub deep v dreamcatcher
> Pinterest intelligentsia gluten-free truffaut.
* first
* second
[grid]
nollie: "Something"
list: [ foo, bar ]
foo: 10
bar: 30
`
const YAML = `
title: Do this or else that yield happens
tags: [ function, default, const ]
date: 2024-02-12
more: "strings"
count: 10
xmas: true
# Comment here
Documentation:
Hello World: /syntax-test
Nothing goes: /morphine/boss "hello"
list:
- Michelangelo "boost"
`
const NUEMARK = `
---
title: Noel's cringe content
|description: Not much to say
unlisted: true
---
# Lets get magical
I'm baby truffaut umami wolf small batch iceland
adaptogen. Iceland **chambray** raclette stumptown
// line comment here
[table head="Foo | Bar | Baz"]
- Content first | + | + | +
- Content collections | + | + | +
- Hot-reloading | + | + | +
- AI content generation | + | + | +
> This is my blockquote right here
[.listbox]
* Nothing here to see
* This one is a banger

[image loading="eager"]
| small: "/img/explainer-tall.png"
src: "/img/explainer.png"
hidden: true
width: 800
`
const MDX = `
import {Chart} from './snowfall.js'
export const year = 2023
# Last year’s snowfall
In {year}, the snowfall was above average.
It was followed by a warm spring which caused
flood conditions in many of the nearby rivers.

> Air plant adaptogen artisan gastropub deep v dreamcatcher
> Pinterest intelligentsia gluten-free truffaut.
<Chart year={year} color="#fcb32c" />
<Elemment { ...attr }>
<p class="epic">Yo</p>
</Element>
`
const SHELL = `
#!/bin/bash
myfile = 'cars.txt'
touch $myfile
if [ -f $myfile ]; then
rm cars.txt
echo "$myfile deleted"
fi
# open demo on the browser
open "http://localhost:8080"
`
const TOML = `
# This is a TOML document
title = "TOML Example"
[owner]
name = "Tom Preston-Werner"
dob = 1979-05-27T07:32:00-08:00
[database]
enabled = true
ports = [ 8000, 8001, 8002 ]
data = [ ["delta", "phi"], [3.14] ]
temp_targets = { cpu = 79.5, case = 72.0 }
`
const ZIG = `
const std = @import("std");
const parseInt = std.fmt.parseInt;
test "parse integers" {
const input = "123 67 89,99";
const ally = std.testing.allocator;
// Ensure the list is freed at scope exit
defer list.deinit();
var it = std.mem.tokenizeAny(u8, input, " ,");
while (it.next()) |num| {
const n = try parseInt(u32, num, 10);
try list.append(n); // EOL comment
}
}
`
const CPP = `
#include <iostream>
using namespace std;
int main() {
int first_number, second_number, sum;
cout << "Enter two integers: ";
cin >> first_number >> second_number;
// sum of two numbers in stored
sum = first_number + second_number;
// prints sum
cout << first_number << " + "
<< second_number << " = " << sum;
return 0;
}
`
const GO = `
package main
import "fmt"
// fibonacci is a function that returns a function
func fibonacci() func() int {
f2, f1 := 0, 1
return func() int {
f := f2
f2, f1 = f1, f+f1
return f
}
}
func main() {
f := fibonacci()
for i := 0; i < 10; i++ {
fmt.Println(f())
}
}
`
const JSON = `
{
"author": "John Doe <john.doe@gmail.com>",
"keywords": ["json", "es5"],
"version": 1.5,
"keywords": ["json", "json5"],
"version": 1.7,
"scripts": {
"test": "mocha --ui exports --reporter spec",
"build": "./lib/cli.js -c package.json5",
}
}
`
const JSON5 = `
{
// this is a JSON5 snippet
author: 'John Doe <john.doe@gmail.com>',
- keywords: ['json', 'es5'],
- version: 1.5,
+ keywords: ['json', 'json5'],
+ version: 1.7,
scripts: {
test: 'mocha --ui exports --reporter spec',
build: './lib/cli.js -c package.json5',
}
}
`
const TS = `
// user interface
interface User { name: string; id: number; }
// account interface
class UserAccount {
name: string;
id: number;
constructor(name: string, id: number) {
this.name = name;
this.id = id;
}
}
const user: User = new UserAccount("Murphy", 1);
`
const STYLED = `
import styled from 'styled-components';
const Wrapper = styled.section\`
background: papayawhip;
color: \${aquasky},
padding: 4em;
\`;
// Wrapper becomes a React component
render(
<Wrapper defaultOpened="yes">
<Title>
Hello World!
</Title>
</Wrapper>
);
`
const ASTRO = `
\\---
import MyComponent from "./MyComponent.astro";
const items = ["Dog", "Cat", "Platypus"];
\\---
<ul>
> {items.map((item) => (
<li>{item}</li>
))}
</ul>
<!-- renders as <div>Hello!</div> -->
<Element>
- <p>Hello</p>
+ <p>Hello!</p>
</Element>
`
const HASKELL = `
putTodo :: (Int, String) -> IO ()
putTodo (n, todo) = putStrLn (show n ++ ": " ++ todo)
prompt :: [String] -> IO ()
prompt todos = do
putStrLn ""
putStrLn "Current TODO list:"
mapM_ putTodo (zip [0..] todos)
delete :: Int -> [a] -> Maybe [a]
`
const PYTHON = `
# Function definition
def find_square(num):
result = num * num
return result
'''
This is a multiline comment
'''
square = find_square(3) // 2
# Weirdoes
if (False) continue
elif (True) nonlocal + zoo
else None
print('Square:', square)
`
const JAVA = `
// Importing generic Classes/Files
import java.io.*;
class GFG {
// Function to find the biggest of three numbers
static int biggestOfThree(int x, int y, int z) {
return z > (x > y ? x : y) ? z : ((x > y) ? x : y);
}
// Main driver function
public static void main(String[] args) {
int a, b, c;
a = 5; b = 10; c = 3;
// Calling the above function in main
largest = biggestOfThree(a, b, c);
}
}
`
const KOTLIN = `
@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
val job = GlobalScope.launch {
// root coroutine with launch
println("Throwing exception from launch")
throw IndexOutOfBoundsException()
}
try {
deferred.await()
println("Unreached")
} catch (e: ArithmeticException) {
println("Caught ArithmeticException")
}
}
`
const RUST = `
use std::fmt::{ Debug, Display };
// all drinks are emptied
fn compare_prints<T: Debug + Display>(t: &T) {
println!("Debug: \`{:?}\`", t);
}
fn compare_types<T: Debug, U: Debug>(t: &T, u: &U) {
println!("t: \`{:?}\`", t);
}
`
const PERL = `
#!/usr/bin/perl
use warnings;
use Path::Tiny;
# foo/bar
my $dir = path('foo','bar');
# Iterate over the content of foo/bar
my $iter = $dir->iterator;
while (my $file = $iter->()) {
# Print out the file name and path
print "$file";
}
`
const LUA = `
\\-- This here is a comment
function perm (a)
local n = table.getn(a)
return coroutine.wrap(function () permgen(a, n) end)
end
\\-- Another function
function printResult (a)
for i,v in ipairs(a) do
io.write(v, " ")
end
io.write("hello")
end
`
const RUBY = `
# line comment here
def get_numbers_stack(list)
stack = [[0, []]]
output = []
=begin
Ruby multiline comments are pretty weirdoes
Or maybe not??
=end
until stack.empty?
index, taken = stack.pop
next output << taken if index == list.size
stack.unshift [index + 1, taken]
stack.unshift [index + 1, taken + [list[index]]]
end
output
end
`
const PHP = `
<!DOCTYPE html>
<!-- HTML comment -->
<form method="get" action="target_proccessor.php">
<input type="search" name="search">
<input type="submit" name="submit" value="Search">
<?php
// inline PHP comment
$camp = array("zero" => "free", "one" => "code" );
print_r($camp);
?>
</form>
`
const CSHARP = `
public void MyTaskAsync(string[] files) {
MyTaskWorker worker = new MyTaskWorker(MyTaskWorker);
AsyncCallback fooback = new AsyncCallback(MyTask);
lock (_sync) {
if (_myTaskIsRunning)
throw new OperationException(
"The control is busy."
);
// one-line comment here
AsyncOperation async = Async.CreateOperation(null);
bool cancelled;
worker.BeginInvoke(files, context, out cancelled);
_myTaskIsRunning = true;
_myTaskContext = context;
}
}
`
const CLOJURE = `
(ns clojure.examples.hello
(:genclass))
;; This program displays Hello World
(defn Example []
(println [+ 1 2 3]))
(Example)
:dev-http {8080 "public"}
:builds
{:app
{:target :browser
:output-dir "public/app/js"
`
const NIM = `
import std/strformat
type
Person = object
name: string
age: Natural # Ensures the age is positive
let people = [
Person(name: "John", age: 45),
Person(name: "Kate", age: 30)
]
for person in people:
# Type-safe string interpolation,
# evaluated at compile time.
echo(fmt"{person.name} is {person.age} years old")
`
const CRYSTAL = `
# A very basic HTTP server
require "http/server"
server = HTTP::Server.new do |context|
context.response.content_type = "text/plain"
context.response.print "Hello world, got #{context}"
end
puts "Listening http://127.0.0.1:8080"
server.listen(8080)
`
const JULIA = `
function finalize_ref(r::AbstractRemoteRef)
# Check if the finalizer is already run
if islocked(client_refs) || 100
# delay finalizer for later
finalizer(finalize_ref, r)
return nothing # really nothing
end
t = @task begin; sleep(5); println('done'); end
# lock should always be followed by try
Threads.@threads for i = 1:10
a[i] = Threads.threadid()
end
end
`
const HB = `
{#
Mixed Django style comment
#}
<h1>
{{#if quotaFull}}
Please come back tomorrow.
{{/if}}
</h1>
<!-- handlebars example -->
<ul>
{{#each serialList}}
<li>{{this}}</li>
{{/each}}
</ul>
`
const SVELTE = `
<script>
// line comment
import Info from './Info.svelte';
const pkg = {
name: 'svelte',
version: 3,
speed: 'blazing',
website: 'https://svelte.dev'
};
</script>
<!-- layout goes here -->
<p>These styles...</p>
<Nested />
<Info {...pkg} />
<style>
/* CSS comment */
p {
color: purple;
font-family: 'Comic Sans MS', cursive;
font-size: 2em;
}
</style>
`
const SQL = `
SELECT
date_trunc('week', orderdate),
•count(1)•
FROM orders
•WHERE orderdate between• '2024-01-01' AND '2024-02-01'
RANK() OVER (ORDER ••BY __ order_amount DESC••)
INNER JOIN payment_status p ON o.status_id = p.id;
`
async function renderPage(items) {
const html = [
'<meta charset="utf-8">',
'<meta name="viewport" content="width=device-width, initial-scale=1.0">',
'<link rel="stylesheet" href="glow-test.css">',
'<body class="is-dark">'
]
items.forEach(opts => {
const { title } = opts
const language = opts.lang || title.toLowerCase()
const code = glow(opts.code, { language, numbered: true })
html.push(`
<div class="syntax ${opts.class || ''}">
<header><h2>${opts.title || language}</h2></header>
<pre glow>${code}</pre>
</div>
`)
})
html.push('</body>')
// save
const path = join(root, 'glow-test.html')
await fs.writeFile(path, html.join('\n'), 'utf-8')
console.info('wrote', path)
}
await renderPage([
{ title: 'Astro', code: ASTRO, },
{ title: 'C#', code: CSHARP },
{ title: 'C++', code: CPP, lang: 'cpp', },
{ title: 'Clojure Script', code: CLOJURE, lang: 'clojure' },
{ title: 'Crystal', code: CRYSTAL, lang: 'crystal' },
{ title: 'CSS', lang: 'css', code: CSS },
{ title: 'GO', code: GO, lang: 'go', },
{ title: 'Handlebars', code: HB, lang: 'hb' },
{ title: 'Haskell', code: HASKELL, },
{ title: 'HTML', lang: 'html', code: HTML, },
{ title: 'Java', code: JAVA, lang: 'java' },
{ title: 'JavaScript', code: JAVASCRIPT, lang: 'js', },
{ title: 'JSON', code: JSON, lang: 'json', },
{ title: 'JSON5', code: JSON5, lang: 'json5', },
{ title: 'JSX', code: JSX, lang: 'jsx' },
{ title: 'Julia', code: JULIA, lang: 'julia' },
{ title: 'Kotlin', code: KOTLIN, lang: 'java' },
{ title: 'Lua', code: LUA, lang: 'lua' },
{ title: 'Markdown', code: MARKDOWN, lang: 'md', },
{ title: 'MDX', code: MDX, lang: 'mdx', },
{ title: 'Nim', code: NIM, lang: 'nim' },
{ title: 'Nuemark', code: NUEMARK, lang: 'nuemark', },
{ title: 'Perl', code: PERL, lang: 'perl' },
{ title: 'PHP', code: PHP, lang: 'php' },
{ title: 'Python', code: PYTHON, lang: 'python', },
{ title: 'Ruby', code: RUBY, lang: 'ruby' },
{ title: 'Rust', code: RUST, lang: 'rust' },
{ title: 'Shell', code: SHELL, lang: 'sh', },
{ title: 'SQL', code: SQL, class: '_editing-demo' },
{ title: 'Styled component', code: STYLED, lang: 'jsx', },
{ title: 'Svelte', code: SVELTE },
{ title: 'TOML', code: TOML, lang: 'toml', },
{ title: 'TypeScript', code: TS, lang: 'ts', },
{ title: 'ZIG', code: ZIG, lang: 'zig', },
{ title: 'YAML', code: YAML, lang: 'yaml', },
]//.filter(el => ['md'].includes(el.lang))
// ]
)
================================================
FILE: packages/nueglow/test/glow-test.css
================================================
@import "../css/syntax.css";
@import "../css/markers.css";
/*@import "../css/light.css";*/
*, *::before, *::after { box-sizing: border-box; }
:root { --gap: clamp(1em, 5vmin, 3rem); }
body {
font-family: sf pro display;
margin: var(--gap);
gap: var(--gap);
.syntax { break-inside: avoid; }
@media (min-width: 1000px) {
column-count: 2;
}
}
/* wrapper */
.syntax {
border-radius: 6px;
margin-bottom: var(--gap);
header { padding: .7em 1.5em; }
h2 {
letter-spacing: 0.02em;
font-weight: 400;
font-size: 90%;
text-align: center;
margin: .0;
}
}
/* dark == default */
body {
background: #111729;
color: #fff;
.syntax {
background-color: #20293A;
border-color: #fff1;
box-shadow: none;
header { border-bottom: 1px solid #fff1; }
}
}
.is-light {
background: #f3f8fa;
color: #444;
.syntax {
background-color: #fff;
border: 1px solid #eee;
box-shadow: 0 0 .5em #eee;
header { border-bottom: 1px solid #eee; }
}
}
================================================
FILE: packages/nueglow/test/glow.test.js
================================================
import { parseRow, parseSyntax, renderRow, glow } from '..'
test('HTML', () => {
const row = '<div class="hello">'
// parse
const [char, prop, ...rest] = parseRow(row)
expect(char.tag).toBe('i')
expect(prop.tag).toBe('strong')
expect(prop.start).toBe(5)
expect(rest.length).toBeGreaterThan(4)
// render
const html = renderRow(row)
expect(html).toStartWith('<i><</i>')
expect(html).toInclude('<strong>class</strong>')
expect(html).toInclude('<em>"hello"</em>')
})
test('Emphasis', () => {
const html = renderRow('Hey •[img]• girl')
expect(html).toInclude('Hey <mark>')
expect(html).toInclude('</i></mark> girl')
})
/* multiline comments */
test('parse HTML comment', () => {
const blocks = parseSyntax(['<div>', '<!--', 'comment', '-->', '</div>'])
expect(blocks[1].comment[0]).toBe('<!--')
})
test('parse JS comment', () => {
const blocks = parseSyntax(['/* First */', 'function() {', '/*', 'Second', '*/'])
expect(blocks[0].comment).toEqual(['/* First */'])
expect(blocks[2].line).toEqual('/*')
})
/* prefix and mark */
test('disable mark', () => {
const html = renderRow('Hey •[img]• girl', undefined, false)
expect(html).toInclude('Hey •')
expect(html).toInclude('• girl')
})
test('escape prefixes', () => {
const blocks = parseSyntax([
'\\+ not really adding a line',
'\\- not really removing a line',
'\\| not really marking a line'
], 'md')
expect(blocks[0].line).toEqual('+ not really adding a line')
expect(blocks[1].line).toEqual('- not really removing a line')
expect(blocks[2].line).toEqual('| not really marking a line')
})
test('disable prefixes', () => {
const blocks = parseSyntax([
'+ not really adding a line',
'- not really removing a line',
'> not really marking a line'
], undefined, false)
expect(blocks[0].wrap).toEqual(false)
expect(blocks[1].wrap).toEqual(false)
expect(blocks[2].wrap).toEqual(false)
})
================================================
FILE: packages/nuekit/Makefile
================================================
tests:
cd test && bun test
================================================
FILE: packages/nuekit/README.md
================================================
# The UNIX of the Web
Web development became complicated. Hundreds of packages, 400MB of dependencies, hours of configuration before writing a single line of code. We forgot that it doesn't have to be this way.
## How web development should work
**Instant start** - Create `index.html` or `index.md` and you're running. No setup, no configuration, no waiting.
**Single-page apps** - Write semantic HTML with dynamic expressions. Import business logic from pure JavaScript modules. Let your design system handle presentation.
**Content sites** - Front pages, documentation, blogs, marketing pages. Write Nuemark content, add layout modules for structure, trust your design system for consistency.
**Universal hot reload** - Content, CSS, layouts, data, components, server routes, configurations. Save and watch the browser update instantly.
**Complete system** - Content sites, SPAs, server routes, backend models.
One tool, complete control. The UNIX philosophy applied to web development.
**Nue is the entire ecosystem in 1MB**
Visit [Nue website](https://nuejs.org) for comprehensive documentation.
## Migration from React/Next.js
**Less scaffolding** - From 500MB+ of node_modules to 1MB global install. From complex project setups to just `index.html` to get started.
**Pure separation** - Business logic in JS modules. Structure in HTML. Design in CSS. No more mixed concerns in components.
**Faster everything** - Builds in milliseconds. Hot reload across frontend and backend. Pages 10x smaller.
See the [migration guide](https://nuejs.org/docs/migration) for the complete story.
================================================
FILE: packages/nuekit/client/error.js
================================================
function renderDialog(e) {
return `
<dialog id="nuerror">
<header>
<h2>${ e.title }</h2>
<p>${ e.text } at ${ e.path } (${ e.line } / ${ e.column })</p>
</header>
<pre>
${ e.lineText }
</pre>
<footer>
<button popovertarget="nuerror">Close</button>
</footer>
</dialog>
`
}
export async function showError(error) {
window.nuerror?.remove()
document.body.insertAdjacentHTML('beforeend', renderDialog(error))
window.nuerror.showPopover()
}
================================================
FILE: packages/nuekit/client/hmr.js
================================================
function createConnection() {
const server = new WebSocket(`ws://${location.host}`)
server.onmessage = async function(e) {
const asset = JSON.parse(e.data)
return (asset.is_yaml || asset.is_js || asset.is_ts) ? location.reload()
: asset.error ? await handleError(asset)
: asset.is_md ? await reloadContent(asset)
: asset.is_html ? await reloadHTML(asset)
: asset.is_svg ? reloadVisual(asset)
: asset.is_css ? reloadCSS(asset)
: null
}
// reconnect after 1 second
server.onclose = function() {
console.log('HMR reconnecting...')
setTimeout(createConnection, 3000)
}
server.onerror = function() {
server.close()
}
}
createConnection()
async function handleError(asset) {
const { showError } = await import('./error.js')
const { error, path } = asset
showError({ ...error, path })
}
async function reloadContent(asset) {
const { url } = asset
if (url != location.pathname) return location.href = url
// domdiff
const { mountAll } = await import('./mount.js')
const { domdiff } = await import('/@nue/nue.js')
const { title, body } = parsePage(asset.content)
if (title) document.title = title
const lib = asset.ast?.lib
// focused HMR
if (lib?.length == 1) {
const { tag } = lib[0]
domdiff($(tag), body.querySelector(tag))
// diff everything
} else {
domdiff($('body'), body)
}
await mountAll()
}
let reload_count = 0
function reloadVisual(asset) {
// HMR mode
if (location.pathname.endsWith('.svg')) return reloadSVG(asset.content)
// reload <svg> and <object> tags
const { url } = asset
function reload(el, attr) {
if (el) el[attr] = `${url}?${reload_count++}`
}
reload($(`object[data*='${url}']`), 'data')
reload($(`img[src*='${url}']`), 'src')
}
function reloadSVG(html) {
const svg = html.slice(html.indexOf('<svg '), html.indexOf('</svg>') + 6)
document.body.innerHTML = svg
}
function reloadCSS(asset) {
const { url, content } = asset
const orig = $(`[href="${url}"]`)
const style = createStyle(url, content)
if (orig) orig.replaceWith(style)
else document.head.appendChild(style)
}
async function reloadHTML(asset) {
const { ast } = asset
return ast.is_dhtml ? await reloadComponents(asset)
: ast.is_lib ? location.reload()
: await reloadContent(asset)
}
async function reloadComponents(asset) {
const { mountAll } = await import('./mount.js')
const state = saveState()
await mountAll(asset.path)
restoreState(state)
}
/***** helper functions *****/
function createStyle(url, content) {
const el = document.createElement('style')
el.setAttribute('href', url)
el.innerHTML = content
return el
}
// state before remounting
function saveState() {
const formdata = [...document.forms].map(form => new FormData(form))
const el = $('[popover]')
const popover = el?.checkVisibility() && el.id
const dialog = $('dialog[open]')?.id
return { formdata, popover, dialog }
}
function restoreState({ formdata, popover, dialog }) {
formdata.forEach((data, i) => deserialize(document.forms[i], data))
// re-open popover
if (popover) window[popover]?.showPopover()
// re-open dialog
if (dialog) {
const el = window[dialog]
if (el) { el.close(); el.showModal() }
}
}
function deserialize(form, data) {
for (const [key, val] of data.entries()) {
const el = form.elements[key]
if (el.type == 'checkbox') el.checked = !!val
else el.value = val
}
}
function parsePage(html) {
const root = document.createElement('html')
root.innerHTML = html
return { title: $('title', root)?.textContent, body: $('body', root) }
}
function $(query, root=document) {
return root.querySelector(query)
}
================================================
FILE: packages/nuekit/client/mount.js
================================================
// both args are optional
export async function mountAll(reload_path) {
const roots = document.querySelectorAll('[nue]')
const deps = roots.length ? await importComponents(reload_path) : []
if (!deps.length) return
const { mount } = await import('/@nue/nue.js')
for (const root of [...roots]) {
const name = root.getAttribute('nue') || root.tagName.toLowerCase()
const comp = deps.find(a => [a.is, a.tag].includes(name))
if (comp) {
const node = mount(comp, { root, deps, data: getData(root) })
node.root.setAttribute('nue', name)
}
}
}
function getData(root) {
if (root.firstChild?.tagName == 'SCRIPT') root.after(root.firstChild)
const script = root.nextElementSibling
if (script?.type == 'application/json') return JSON.parse(script.textContent)
}
export function getImportPaths() {
const el = document.querySelector('[name="libs"]')
return el ? el.getAttribute('content').split(' ') : []
}
let reload_count = 0
async function importComponents(reload_path) {
const comps = []
for (let path of getImportPaths()) {
const count = path == reload_path ? `?${ reload_count++ }` : ''
const { lib } = await import(`/${path}.js${count}`)
if (lib) comps.push(...lib)
}
return comps
}
// initial page load
addEventListener('route', () => mountAll())
addEventListener('DOMContentLoaded', () => dispatchEvent(new Event('route')))
================================================
FILE: packages/nuekit/client/transitions.js
================================================
// Client-side routing and view transitions for multipage applications
function $(query, root = document) {
return root.querySelector(query)
}
function $$(query, root = document) {
return [...root.querySelectorAll(query)]
}
const scrollPos = {}
const cache = {}
export async function loadPage(path) {
dispatchEvent(new Event('before:route'))
// DOM of the new page
const dom = createDOM(await fetchHTML(path))
// update page metadata and scripts
await updatePageHead(dom)
// update stylesheets
const sheets = updatePageStyles(dom)
await loadStylesheets(sheets)
// inline style tag (single)
updateInlineStyle(dom)
// update page content - keep the working logic
const ignoreMain = updateContent($('main'), $('main', dom))
updateContent($('body'), $('body', dom), ignoreMain)
dispatchRouteEvents()
setActive(path)
}
async function updatePageHead(dom) {
// title
const title = $('title', dom)?.textContent
if (title) document.title = title
// update component list
updateMeta('libs', dom)
// load scripts (modules are loaded only once by the browser)
for (const script of $$('script[src]', dom)) {
await import(script.getAttribute('src'))
}
}
function updateMeta(name, dom) {
$(`meta[name="${name}"]`)?.remove()
const meta = $(`meta[name="${name}"]`, dom)
if (meta) $('head').appendChild(meta)
}
function handlePageScroll() {
const { hash } = location
const el = hash && $(hash)
const scrollTop = el ?
el.offsetTop - (parseInt(getComputedStyle(el).scrollMarginTop) || 0) :
0
scrollTo(0, scrollTop)
}
function dispatchRouteEvents() {
dispatchEvent(new Event('route'))
const [_, app] = location.pathname.split('/')
dispatchEvent(new Event(`route:${app || 'home'}`))
}
// setup linking
export function onclick(root, fn) {
root.addEventListener('click', e => {
const el = e.target.closest('[href]')
if (!el) return
const path = el.getAttribute('href')
const target = el.getAttribute('target')
const filename = path?.split('/')?.pop()?.split(/[#?]/)?.shift()
if (shouldIgnoreClick(e, path, target, filename)) return
// all good
if (path != location.pathname) fn(el.pathname, el)
e.preventDefault()
})
}
function shouldIgnoreClick(e, path, target, filename) {
return e.defaultPrevented || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey ||
!path || path[0] == '#' || path.includes('//') || path.startsWith('mailto:') ||
(filename?.includes('.') && !filename.endsWith('.html')) || !!target
}
export function toRelative(path) {
const curr = location.pathname
return curr.slice(0, curr.lastIndexOf('/') + 1) + path
}
export function setActive(path, attrname = 'aria-current') {
if (path[0] != '/') path = toRelative(path)
// remove old selections
$$(`[${attrname}]`).forEach(el => el.removeAttribute(attrname))
// add new ones
$$('a').forEach(el => {
if (!el.hash && el.pathname == path) {
el.setAttribute(attrname, 'page')
}
})
}
export function setupTransitions() {
// view transition fallback (Safari, Firefox) • caniuse.com/view-transitions
if (!document.startViewTransition) {
document.startViewTransition = (fn) => fn()
}
// Fix: window.onpopstate, event.state == null?
// https://stackoverflow.com/questions/11092736/window-onpopstate-event-state-null
history.pushState({ path: location.pathname }, 0)
// save scroll position whenever user scrolls
addEventListener('scroll', () => {
scrollPos[location.pathname] = window.scrollY
})
// autoroute / document clicks
onclick(document, async (path, el) => {
const img = $('img', el)
if (img) img.style.viewTransitionName = 'active-image'
document.startViewTransition(async () => {
await loadPage(path)
history.pushState({ path }, 0, path)
handlePageScroll()
})
})
// initial active
setActive(location.pathname)
// back button
addEventListener('popstate', e => {
const { path } = e.state || {}
if (path) {
const pos = scrollPos[path]
document.startViewTransition(async () => {
await loadPage(path)
scrollTo(0, pos || 0)
})
}
})
}
/* -------- utilities ---------- */
function haveSameChildren(a, b) {
if (a.children.length != b.children.length) return false
for (let i = 0; i < a.children.length; i++) {
if (a.children[i].tagName != b.children[i].tagName) return false
}
return true
}
// smart DOM diffing and updating
export function updateContent(current, incoming, ignoreMain) {
if (!current || !incoming) return true
current.className = incoming.className
if (haveSameChildren(current, incoming)) {
Array.from(current.children).forEach((el, i) => {
if (!(ignoreMain && el.tagName == 'MAIN')) {
updateElement(el, incoming.children[i])
}
})
return true
} else {
current.innerHTML = incoming.innerHTML
return false
}
}
function updateElement(current, incoming) {
const currentHTML = current.outerHTML.replace(' aria-current="page"', '')
if (currentHTML != incoming.outerHTML) {
current.replaceWith(incoming.cloneNode(true))
}
}
function updatePageStyles(dom) {
const sheets = findNewStyles($$('link'), $$('link', dom))
sheets.forEach(style => $('head').appendChild(style))
return sheets
}
export function findNewStyles(current, incoming) {
// disable styles not in incoming
current.forEach(el => {
const href = el.getAttribute('href')
el.disabled = !incoming.find(style => style.getAttribute('href') == href)
})
// return styles not in current
return incoming.filter(el => {
const href = el.getAttribute('href')
return !current.find(style => style.getAttribute('href') == href)
})
}
function updateInlineStyle(dom) {
$$('style').forEach(el => el.remove())
$$('style', dom).forEach(el => $('head').appendChild(el))
}
async function fetchHTML(path) {
let html = cache[path]
if (html) return html
const resp = await fetch(path)
html = await resp.text()
if (resp.status == 404 && html?.trim()[0] != '<') {
const title = document.title = 'Page not found'
$('article').innerHTML = `<section><h1>${title}</h1></section>`
} else {
cache[path] = html
}
return html
}
function createDOM(html) {
const parser = new DOMParser()
return parser.parseFromString(html, 'text/html')
}
async function loadStylesheets(linkElements) {
const promises = linkElements.map(link => loadStylesheet(link.href))
await Promise.all(promises)
}
function loadStylesheet(href) {
return new Promise(resolve => {
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = href
link.onload = resolve
$('head').appendChild(link)
})
}
// browser environment
if (typeof window == 'object') setupTransitions()
================================================
FILE: packages/nuekit/package.json
================================================
{
"name": "nuekit",
"version": "2.0.0-beta.2",
"description": "The UNIX of the Web",
"homepage": "https://nuejs.org",
"license": "MIT",
"type": "module",
"repository": {
"url": "https://github.com/nuejs/nue",
"directory": "packages/nuekit",
"type": "git"
},
"bin": {
"nue": "./src/cli.js"
},
"engines": {
"bun": ">= 1.2.2"
},
"scripts": {
"test": "cd test && bun test"
},
"dependencies": {
"nue-edgeserver": "^0.1.0",
"nuestate": "^0.1.0",
"nueyaml": "^0.1.0",
"nuedom": "^0.1.0",
"nuemark": "^0.7.0",
"nue-glow": "^0.2.0"
},
"files": [ "src", "client", "favicon.ico" ]
}
================================================
FILE: packages/nuekit/src/asset.js
================================================
import { join } from 'node:path'
import { parseNuemark } from 'nuemark'
import { parseYAML } from 'nueyaml'
import { parseNue } from 'nuedom'
import { renderMD, renderHTML } from './render/page'
import { getCollections } from './collections'
import { mergeConf, mergeData } from './conf'
import { listDependencies } from './deps'
import { mergeSharedData } from './site'
import { renderSVG } from './render/svg'
import { minifyCSS } from './tools/css'
export function createAsset(file, site={}) {
const { files=[], conf={} } = site
let cachedObj = null
function flush() {
cachedObj = null
file.flush()
}
function toAssets(paths) {
const arr = paths.map(path => files.find(file => file.path == path))
return arr.map(file => createAsset(file, site))
}
function getDeps(opts={}) {
const { include, exclude } = opts
const paths = files.map(f => f.path)
const deps = listDependencies(file.path, { paths, include, exclude })
return toAssets(deps)
}
async function config() {
const asset = getDeps().find(f => f.base == 'app.yaml')
return asset ? mergeConf(conf, await asset.parse()) : conf
}
async function data() {
const assets = getDeps()
const app_files = assets.filter(f => f.is_yaml && f.name != 'site' && f.basedir != '@shared')
const app_data = await Promise.all(app_files.map(f => f.parse()))
const ret = mergeData([conf, ...app_data])
// content collections
const colls = conf.collections
if (colls) {
const md_paths = files.filter(f => f.is_md).map(f => f.path)
Object.assign(ret, await getCollections(toAssets(md_paths), colls))
}
// shared data, functions, and transformation
await mergeSharedData(assets, ret)
return ret
}
// list all dependencies (public method)
async function assets() {
return getDeps(await config())
}
async function parse() {
if (!cachedObj) {
const str = await file.text()
cachedObj = file.is_js || file.is_ts ? await import(join(process.cwd(), file.path) + '?' + Math.random())
: file.is_json ? JSON.parsek(str)
: file.is_md ? parseNuemark(str)
: file.is_yaml ? parseYAML(str)
: parseNue(str)
}
return cachedObj
}
async function toExt() {
if (file.is_html) {
const { is_dhtml } = await parse()
if (is_dhtml) return '.html.js'
}
return file.is_md ? '.html' : file.is_ts ? '.js' : file.ext
}
async function contentType() {
const types = {
'.html.js': 'application/javascript',
'.html': 'text/html; charset=utf-8',
'.js': 'application/javascript',
'.svg': 'image/svg+xml',
'.css': 'text/css',
}
return types[await toExt()]
}
async function components(force_html) {
const { is_dhtml=false } = await parse()
const ret = []
for (const asset of (await assets()).filter(el => el.is_html)) {
const ast = await asset.parse()
const { doctype='' } = ast
if (ast.is_lib) {
const same_type = is_dhtml == ast.is_dhtml
const isomorphic = doctype.startsWith('html+dhtml')
const forced = force_html && !ast.is_dhtml
if (isomorphic || same_type || forced) ret.push(...ast.lib)
}
}
return ret
}
async function render(params) {
const asset = createAsset(file, site)
const { is_prod } = conf
if (file.is_svg) {
const { svg } = await config()
if (svg?.process) return await renderSVG(asset, { fonts: svg.fonts, ...params })
}
return file.is_js && is_prod || file.is_ts ? compileJS(file.rootpath, is_prod)
: file.is_css && is_prod ? minifyCSS(await file.text())
: file.is_html ? await renderHTML(asset)
: file.is_md ? await renderMD(asset)
: null
}
return { ...file, flush, config, data, assets, parse, components, toExt, contentType, render }
}
export async function compileJS(path, minify, bundle) {
const result = await Bun.build({
external: bundle ? undefined : ['*'],
entrypoints: [path],
target: 'browser',
minify
})
const [ js ] = result.outputs
return await js.text()
}
================================================
FILE: packages/nuekit/src/cli.js
================================================
#!/usr/bin/env bun
import { styleText } from 'node:util'
import { version } from './system'
// [-npe] --> [-n, -p, -e]
export function expandArgs(args) {
const arr = []
args.forEach(str => {
if (str[0] == '-' && str[1] != '-' && str[2]) {
str.slice(1).split('').forEach(el => arr.push('-' + el))
} else {
arr.push(str)
}
})
return arr
}
export function getArgs(argv) {
const commands = ['serve', 'dev', 'build', 'preview', 'create', 'push']
// default values
const args = { paths: [] }
let opt
expandArgs(argv).forEach((arg) => {
// skip
if (arg == '--') {
// command
} else if (!args.cmd && commands.includes(arg)) {
args.is_prod = arg == 'build'
args.cmd = arg
// options
} else if (!opt && arg[0] == '-') {
// global options
if (['-h', '--help'].includes(arg)) args.help = true
else if (['-v', '--version'].includes(arg)) args.version = true
else if (['-s', '--silent'].includes(arg)) args.silent = true
else if (['--verbose'].includes(arg)) args.verbose = true
// dev & preview options
else if (['-p', '--port'].includes(arg)) opt = 'port'
// build options
else if (['-n', '--dry-run'].includes(arg)) args.dryrun = true
else if (['-i', '--init'].includes(arg)) args.init = true
else if (['--clean'].includes(arg)) args.clean = true
// values
// bad argument
else throw `Unknown option: "${arg}"`
// values
} else if (opt) {
args[opt] = 1 * arg || arg
opt = null
} else {
args.paths.push(arg)
}
})
if (opt) throw `${opt} not set`
return args
}
const HELP = `
Usage
nue [command] [options] [file_matches]
nue -v or --version
nue -h or --help
Commands
serve Start development server (default command)
build Build a production site
preview Preview the production site
create Use a project starter template
Options
-p or --port Serve/preview the site on this port
-n or --dry-run Show what would be built. Does not build anything
-s or --silent Suppress output messages
-i or --init build Nue runtime files (rare need)
--verbose Show detailed output
--clean Clean output directory before building
File matches
Only build files that match the rest of the arguments. For example:
"nue build .ts .css" will build all TypeScript and CSS files
Examples
# serve current directory
nue
# build all Markdown and CSS files
nue build .md .css
# build with verbose output
nue build --verbose
# preview on specific port
nue preview --port 8080
# more examples
https://nuejs.org/docs/cli
┏━┓┏┓┏┳━━┓
┃┏┓┫┃┃┃┃━┫ ${version}
┃┃┃┃┗┛┃┃━┫ nuejs.org
┗┛┗┻━━┻━━┛
`
function format(line) {
return line.match(/^\w[\w ]+$/) ? styleText(['bold', 'white'],line) // titles
: '┏┃┗'.includes(line.trim()[0]) ? styleText(['green'], line) // ascii art
: line.includes('#') ? styleText(['cyan'], line) // comments
: styleText(['gray'], line)
}
export function printHelp() {
console.info(HELP.split('\n').map(format).join('\n'))
}
function printVersion() {
console.log(`Nue ${ version } • Bun ${ Bun.version }`)
}
async function run(args) {
// help
if (args.help) return printHelp()
// version
printVersion()
if (args.version) return
// command
const { cmd, paths } = args
// create
if (cmd == 'create') {
const { create } = await import('./cmd/create')
const [ name, dir ] = paths
return await create(name, { dir })
}
// push (private repo ATM)
if (cmd == 'push') {
const { fswalk } = await import('./tools/fswalk')
const { push } = await import('nuepush')
const files = await fswalk('.dist')
return await push(files, args)
}
// config
const { readSiteConf } = await import('./conf')
const conf = await readSiteConf(args)
if (!conf) return console.error('Not a Nue directory')
// preview
if (cmd == 'preview') {
const { preview } = await import('./cmd/preview')
return await preview(conf, args)
}
// site
const { createSite } = await import('./site')
const site = await createSite(conf)
// build
if (cmd == 'build' || paths.length) {
const { build } = await import('./cmd/build')
await build(site, args)
// dev | serve
} else if (!cmd || cmd == 'dev' || cmd == 'serve') {
const { serve } = await import('./cmd/serve')
await serve(site, args)
}
}
const { argv } = process
if (argv[1].endsWith('cli.js')) {
const args = getArgs(argv.slice(2))
await run(args)
}
================================================
FILE: packages/nuekit/src/cmd/build.js
================================================
import { mkdir, rmdir, writeFile, unlink } from 'node:fs/promises'
import { join, sep } from 'node:path'
import { tmpdir } from 'os'
import { generateSitemap, generateFeed } from '../render/feed'
import { createSystemFiles } from '../system'
export async function build(site, args) {
const { paths=[], dryrun, silent } = args
const { conf, assets } = site
const { dist } = conf
// subset
const subset = paths.length ? assets.filter(el => matches(el.path, paths)) : assets
if (dryrun) {
subset.filter(el => !el.is_yaml).forEach(el => console.log(el.path))
return subset
}
// build subset
const start = performance.now()
await buildAll(subset, { ...args, dist })
// sitemap.xml
if (!paths.length && conf.sitemap?.enabled) {
const xml = await generateSitemap(assets, conf)
if (xml) await writeFile(join(dist, 'sitemap.xml'), xml)
}
// feed.xml
if (!paths.length && conf.rss?.enabled) {
const xml = await generateFeed(assets, conf)
if (xml) await writeFile(join(dist, 'feed.xml'), xml)
}
// stats
if (!silent) {
stats(subset).forEach(line => console.log(line))
const time = Math.round(performance.now() - start)
console.log(`-----\nBuilt in: ${time}ms\n`)
}
}
export async function buildAll(subset, args) {
const { dist, init, verbose, clean } = args
// .dist directory
if (clean) await rmdir(dist, { recursive: true, force: true })
await mkdir(dist, { recursive: true })
// .dist/@nue directory
await createSystemFiles(dist, init)
// build files
subset = subset.filter(el => !el.is_yaml && !el.dir.startsWith(`@shared${sep}data`))
await Promise.all(subset.map(async asset => {
try {
if (verbose) console.log(asset.path)
await buildAsset(asset, dist)
} catch (error) {
console.error(`Failed to build ${asset.path}:`, error)
throw error // or return null to continue with others
}
}))
}
export async function buildAsset(asset, dist) {
const result = await asset.render()
if (result?.js) {
const minified = await minifyJS(result.js)
await asset.write(dist, minified, '.html.js')
}
if (result?.html) await asset.write(dist, result.html)
if (typeof result == 'string') {
await asset.write(dist, result, await asset.toExt())
} else if (!asset.is_html) {
await asset.copy(dist)
}
}
// TODO: layout and colors
export function stats(assets) {
const lines = []
const types = {
md: 'Markdown',
js: 'JavaScript',
ts: 'TypeScript',
html: 'HTML',
svg: 'SVG',
css: 'CSS',
png: 'PNG',
webp: 'WebP',
}
for (const type in types) {
const count = assets.filter(el => el.type == type).length
if (count) lines.push(`${types[type]} files: ${count}`)
}
// misc files
const basics = ['yaml', ...Object.keys(types)]
const count = assets.filter(el => !basics.includes(el.type)).length
if (count) lines.push(`Misc files: ${count}`)
return lines
}
export function matches(path, patterns) {
return patterns.some(match => {
return match.startsWith('./') ? path == match.slice(2) : path.includes(match)
})
}
export async function minifyJS(code) {
const path = join(tmpdir(), `temp-${Date.now()}.js`)
await writeFile(path, code)
const result = await Bun.build({
entrypoints: [path],
external: ['*'],
minify: true
})
await unlink(path)
return await result.outputs[0].text()
}
================================================
FILE: packages/nuekit/src/cmd/create.js
================================================
import { mkdir } from 'node:fs/promises'
import { join } from 'node:path'
export const NAMES = 'blog full minimal spa'.split(' ')
export async function create(name, { dir, baseurl }) {
if (!name) return console.log('❌ USAGE: nue create <template-name>')
if (!NAMES.includes(name)) {
return console.log('❌ Choose one: ' + NAMES.join(', '))
}
if (await Bun.file(name).exists()) {
return console.log(`✨ ${name} directory already exists`)
}
try {
// load zip from local or remote
const zip = dir ? await getLocalZip(name, dir) : await fetchZip(name, baseurl)
await unzip(name, zip)
// success message
console.log(`\n🎉 "${name}" directory created. Your next steps:`)
console.log(` cd ${name}`)
console.log(` nue\n`)
return true
} catch (error) {
console.error(`❌ ${error.message}`)
}
}
export async function getLocalZip(name, dir) {
const path = join(dir, `${name}.zip`)
if (!await Bun.file(path).exists()) throw new Error(`${path} not found`)
console.log(`📦 Using local template: ${path}`)
return Bun.file(path)
}
// download from github
//
export async function fetchZip(name, baseurl='https://github.com/nuejs/nue/raw/master/packages/templates') {
const url = `${baseurl}/${name}.zip`
const resp = await fetch(url)
if (resp.status != 200) throw new Error(`${url} not found`)
console.log(`📦 Downloading ${name} template...`)
return resp
}
// unzip.js
export async function unzip(dir, zip) {
const filename = `${dir}.zip`
try {
// write zip file
await Bun.write(filename, zip)
// extract (expects "minimal" directory inside zip)
const cmd = process.platform == 'win32' ? ['tar', '-xf', filename] : ['unzip', '-q', filename]
const proc = Bun.spawn(cmd)
const exitCode = await proc.exited
if (exitCode !== 0) {
const stderr = await new Response(proc.stderr).text()
throw new Error(`Unpacking archive failed with exit code ${exitCode}: ${stderr}`)
}
// clean up
} finally {
try {
await Bun.file(filename).delete()
} catch (e) {console.info(e)}
}
}
================================================
FILE: packages/nuekit/src/cmd/preview.js
================================================
import { extname, join } from 'node:path'
import { createServer } from '../tools/server'
import { getServer } from '../server'
export async function preview(conf, opts) {
const { dist=".dist" } = conf
const port = opts.port || 4040
// dist directory
const has_index = await Bun.file(join(dist, 'index.html')).exists()
if (!has_index) return console.error('run `nue build` first')
// user server
const handler = await getServer(conf?.server)
// dev server
const server = createServer({ port, handler }, url => getFile(dist, url))
const url = server.url.toString()
console.log('Serving on', url)
return { stop() { server.stop() }, url, port, }
}
export async function getFile(dist, url) {
// favicon
if (url == '/favicon.ico') return Bun.file(join(import.meta.dir, '../../favicon.ico'))
// file
let path = join(dist, url)
if (url.endsWith('/')) path += 'index.html'
const ext = extname(path)
if (!ext) path += '.html'
const file = Bun.file(path)
if (await file.exists()) return file
// 404
if (!ext) {
const err_page = Bun.file(join(dist, '404.html'))
if (await err_page.exists()) return err_page
}
}
================================================
FILE: packages/nuekit/src/cmd/serve.js
================================================
import { extname, join } from 'node:path'
import { generateSitemap, generateFeed } from '../render/feed'
import { createServer, broadcast } from '../tools/server'
import { fswatch } from '../tools/fswatch'
import { getSystemFiles } from '../system'
import { getServer } from '../server'
export async function serve(site, { silent }) {
const { conf, assets } = site
const { root, ignore, port } = conf
// user server
const handler = await getServer(conf?.server)
// dev server
const server = createServer({ port, handler }, (url, params) =>
onServe(url, assets, { params, conf })
)
const watcher = fswatch(root, { ignore })
watcher.onupdate = async function(path) {
const asset = await site.update(path)
// site.yaml update
if (asset.base == 'site.yaml') {
const data = asset.content = await asset.parse()
Object.assign(conf, data)
return broadcast(asset)
}
if (asset) {
asset.content = await asset.render({ hmr: true }) || await asset.text()
if (asset.is_html) {
asset.ast = await asset.parse()
delete asset.ast.root
}
broadcast(asset)
}
}
watcher.onremove = async function(path) {
site.remove(path)
broadcast({ remove: path })
}
const url = server.url.toString()
if (!silent) console.log('Serving on', url)
return { stop() { watcher.close(); server.stop() }, url, port, }
}
// server requests
export async function onServe(url, assets, opts={}) {
const { params={}, conf={} } = opts
const asset = findAssetByURL(url, assets)
const ext = extname(url)
// return File || { content, type }
if (asset) {
const result = await asset.render(params)
if (!result) return Bun.file(asset.rootpath)
const content = (ext ? result.js : result.html) || result
const type = ext && params.hmr == null ? await asset.contentType() : 'text/html; charset=utf-8'
return { content, type }
}
// SPA entry page
if (!ext) {
const app = url.split('/')[1]
const spa = assets.find(asset => ['index.html', `${app}/index.html`].includes(asset.path))
if (spa) return (await spa.render()).html
}
// sitemap.xml
if (url == '/sitemap.xml' && conf.sitemap?.enabled) {
const content = await generateSitemap(assets, conf)
return content ? { content, type: 'application/xml; charset=utf-8' } : null
}
// feed.xml
if (url == '/feed.xml' && conf.rss?.enabled) {
const content = await generateFeed(assets, conf)
return content ? { content, type: 'application/xml; charset=utf-8' } : null
}
// custom error page
if (!ext) {
const err = assets.find(asset => asset.name == '404')
return err ? { content: await err.render(), status: 404 } : null
}
// favicon
if (url == '/favicon.ico') return Bun.file(join(import.meta.dir, '../../favicon.ico'))
}
const sysfiles = getSystemFiles()
export function findAssetByURL(url, assets=[]) {
return [...sysfiles, ...assets].find(asset => {
return url.endsWith('.html.js') ? asset.path == url.slice(1, -3)
: asset.url == url
})
}
================================================
FILE: packages/nuekit/src/collections.js
================================================
// assumes is_md
export async function getCollections(files, opts) {
const arr = {}
for (const [name, conf] of Object.entries(opts)) {
arr[name] = await createCollection(files, conf)
}
return arr
}
export async function createCollection(files, conf) {
let matchedPages = matchPages(files, conf.include)
let filteredPages = await filterPages(matchedPages, conf)
return sortPages(filteredPages, conf.sort)
}
function matchPages(files, patterns=[]) {
const ret = []
for (const pattern of patterns) {
for (const page of files) {
if (page.path.includes(pattern)) ret.push(page)
}
}
return ret
}
async function filterPages(files, conf) {
const ret = []
for (const page of files) {
const { meta } = await page.parse()
// require?
if (conf.require && !conf.require.every(field => meta[field])) continue
// tags?
if (conf.tags && !conf.tags.some(tag => meta.tags?.includes(tag))) continue
// skip?
if (conf.skip?.some(field => meta[field])) continue
const { url, dir, slug } = page
ret.push({ ...meta, url, dir, slug })
}
return ret
}
function sortPages(files, sorting) {
if (!sorting) return files
const [field, direction = 'asc'] = sorting.split(' ')
return files.toSorted((a, b) => {
const aVal = a[field]
const bVal = b[field]
const result = aVal < bVal ? -1 : aVal > bVal ? 1 : 0
return direction == 'desc' ? -result : result
})
}
================================================
FILE: packages/nuekit/src/conf.js
================================================
import { readdir } from 'node:fs/promises'
import { join } from 'node:path'
import { parseYAML } from 'nueyaml'
// configuration properties (separate from data)
const SITE_CONF = 'site design server collections production port sitemap links'.split(' ')
const ALL_CONF = SITE_CONF.concat('include exclude meta content import_map svg'.split(' '))
// default skip list
const SKIP = `node_modules .toml .rs .lock package.json .lockb lock.yaml README.md Makefile`.split(' ')
export async function readSiteConf(args={}) {
const { is_prod=false, port, root='.' } = args
const files = await readdir(root)
let conf = {}
if (files.includes('site.yaml')) {
const file = Bun.file(join(root, 'site.yaml'))
conf = await parseYAML(await file.text())
} else {
const index = files.find(name => ['index.md', 'index.html'].includes(name))
if (!index) return null
}
// build ignore list into config
const ignore = [...SKIP, ...(conf.site?.skip || [])]
ignore.push(conf.server?.dir || join('@shared', 'server'))
ignore.push(join('@shared', 'test'))
// production override
if (is_prod && conf.meta) {
Object.assign(conf.meta, conf.production)
delete conf.production
}
if (port) conf.port = port
// dist
conf.dist = join(root, '.dist')
return { ...conf, is_prod, root, ignore }
}
export function mergeData(dataset) {
const ret = {}
dataset.forEach(data => {
data && Object.entries(data).forEach(([key, val]) => {
if (key == 'meta') return Object.assign(ret, val)
if (!ALL_CONF.includes(key)) ret[key] = val
})
})
return ret
}
export function mergeConf(site_conf, app_conf) {
const conf = structuredClone(site_conf)
for (const key in app_conf) mergeValue(conf, key, app_conf[key])
return conf
}
export function mergeValue(conf, key, val) {
if (!ALL_CONF.includes(key) || SITE_CONF.includes(key)) return
// merge meta / content
if (['meta', 'content'].includes(key) && typeof val == 'object') {
return conf[key] = { ...conf[key], ...val }
}
// override (include, exclude, import_map)
return conf[key] = val
}
================================================
FILE: packages/nuekit/src/deps.js
================================================
import { join, normalize, dirname, extname, basename, sep } from 'node:path'
// app, lib, server are @shared, but not auto-included
const AUTO_INCLUDED = ['data', 'design', 'ui'].map(dir => join('@shared', dir))
const ASSET_TYPES = ['.html', '.js', '.ts', '.yaml', '.css']
export function listDependencies(basepath, { paths, exclude=[], include=[] }) {
// folder dependency
let deps = paths.filter(path => isDep(basepath, path, paths))
// extensions
deps = deps.filter(path => ASSET_TYPES.includes(extname(path)))
// exclusions
exclude.forEach(pattern => {
deps = deps.filter(path => !path.includes(pattern))
})
// Re-inclusions
include.forEach(pattern => {
paths.forEach(path => {
if (path.includes(pattern)) deps.push(path)
})
})
return [...new Set(deps)]
}
function isDep(page_path, asset_path, all_paths) {
// self
if (page_path == asset_path) return false
// root level assets (global)
const dir = dirname(asset_path)
if (dir == '.') return true
// shared dir -> auto-included
if (AUTO_INCLUDED.some(dir => asset_path.startsWith(dir + sep))) return true
// SPA: entire app tree
if (basename(page_path) == 'index.html') {
const dir = dirname(page_path)
return dir == '.' ? !all_paths.some(el => extname(el) == '.md') : asset_path.startsWith(dir + sep)
}
// index.md -> home dir
const pagedir = dirname(page_path)
if (pagedir == '.' && basename(page_path) == 'index.md') return dirname(asset_path) == 'home'
// hierarchical inclusion (handles root ui and app ui)
const page_dirs = parseDirs(dirname(page_path))
const asset_dir = dirname(asset_path)
// check if asset is in ui of any parent directory
return page_dirs.some(pageDir => {
const ui_dir = pageDir ? join(pageDir, 'ui') : 'ui'
return asset_dir == ui_dir || asset_dir == pageDir
})
}
// parseDirs('a/b/c') --> ['a', 'a/b', 'a/b/c']
export function parseDirs(dir) {
const els = normalize(dir).split(sep)
return els.map((el, i) => els.slice(0, i + 1).join('/'))
}
================================================
FILE: packages/nuekit/src/file.js
================================================
import { parse, sep, join } from 'node:path'
import { lstat } from 'node:fs/promises'
export async function createFile(root, path) {
try {
const rootpath = join(root, path)
const stat = await lstat(rootpath)
const info = getFileInfo(path)
const file = Bun.file(rootpath)
const mtime = stat.mtime
let cachedText = null
// is_md, is_js, ...
if (stat.isSymbolicLink()) info.is_symlink = true
async function text() {
if (!cachedText) cachedText = await file.text()
return cachedText
}
function flush() {
cachedText = null
}
async function copy(dist) {
const to = join(dist, path)
await Bun.write(to, file)
return to
}
async function write(dist, content, ext) {
const toname = ext ? info.base.replace(info.ext, ext) : info.base
const to = join(dist, info.dir, toname)
await Bun.write(to, content)
return to
}
return { ...info, rootpath, mtime, text, copy, write, flush }
} catch (error) {
console.warn(`Warning: Error reading ${path}: ${error.message}`)
return null
}
}
export function getFileInfo(path) {
const info = parse(path)
delete info.root
const { ext, dir } = info
const type = info.ext.slice(1)
const url = getURL(info)
const slug = getSlug(info)
if (dir.includes(sep)) info.basedir = dir.split(sep)[0]
return { ...info, path, type, url, slug, [`is_${type}`]: true }
}
export function getURL(file) {
let { name, base, ext, dir } = file
if (['.md', '.html'].includes(ext)) {
if (name == 'index') name = ''
ext = ''
}
if (ext == '.ts') ext = '.js'
const els = dir.split(sep)
els.push(name + ext)
return `/${ els.join('/') }`.replace('//', '/')
}
export function getSlug(file) {
let { name, base, ext } = file
return name == 'index' ? '' : ext == '.md' ? name : base
}
================================================
FILE: packages/nuekit/src/render/feed.js
================================================
import { createCollection } from '../collections'
import { trim } from './page'
const XML = '<?xml version="1.0" encoding="UTF-8"?>'
export async function generateSitemap(assets, conf) {
const origin = getOrigin(conf)
const { skip } = conf.sitemap
const pages = []
for (const asset of assets.filter(el => el.is_md)) {
const { meta } = await asset.parse()
const { mtime, url } = asset
// skip?
if (skip?.some(field => meta[field])) continue
pages.push({ mtime, origin, url })
}
return renderSitemap(pages)
}
export async function generateFeed(assets, conf) {
const key = conf.rss.collection
if (!key) return console.warn('rss.collection missing from site.yaml')
const coll = conf.collections?.[key]
const origin = getOrigin(conf)
if (coll) {
const pages = await createCollection(assets.filter(el => el.is_md), coll)
return renderFeed({ ...conf.rss, origin }, pages)
}
}
export function renderSitemap(pages) {
const xml = [ XML, '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' ]
pages.forEach(page => {
if (page.url == '/404') return
xml.push(trim(`
<url>
<loc>${ page.origin }${ page.url }</loc>
<lastmod>${ page.mtime.toISOString().slice(0, 10) }</lastmod>
</url>`))
})
xml.push('</urlset>')
return xml.join('\n')
}
export function renderFeed(meta, pages) {
const xml = [ XML, '<rss version="2.0">', '<channel>' ]
const { title='', description='', origin } = meta
xml.push(`
<link>${ origin }</link>
<title>${ title }</title>
<description>${ description }</description>
`)
pages.forEach(page => {
const date = page.pubDate || page.date || ''
const desc = page.description || page.desc || ''
xml.push(trim(`
<item>
<title>${ page.title }</title>
<description>${ desc }</description>
<pubDate>${ date.toISOString?.().slice(0, 10) }</pubDate>
<link>${ origin }${ page.url }</link>
</item>`))
})
xml.push('</channel>', '</rss>')
return xml.join('\n')
}
function getOrigin(conf) {
const { origin='' } = conf.site || {}
if (!origin) console.warn('site.origin missing from site.yaml')
return origin
}
================================================
FILE: packages/nuekit/src/render/head.js
================================================
import { parse, sep } from 'node:path'
import { elem } from 'nuemark'
import { version } from '../system'
import { minifyCSS } from '../tools/css'
export async function renderHead({ conf, data, assets, libs=[] }) {
const { title, favicon } = data
const head = []
if (title) head.push(elem('title', renderTitle(title, data.title_template)))
if (favicon) head.push(elem('link', { rel: 'icon', href: favicon }))
// meta
head.push(...renderMeta(data, libs))
// @layers
const layers = conf.design?.layers
if (layers) head.push(elem('style', `@layer ${layers.join(', ')}`))
// styles
head.push(...await renderStyles(assets, conf))
// system scripts
const addJS = name => assets.push(parse(`@nue/${name}.js`))
if (conf.site?.view_transitions) addJS('transitions')
if (libs.length) addJS('mount')
if (!conf.is_prod) addJS('hmr')
// all scripts
const scripts = renderScripts(assets)
if (scripts.length || libs.length) {
head.push(importMap(conf.import_map))
head.push(...scripts)
}
// RSS feed
if (conf.rss?.enabled) {
const { title } = conf.rss
const link = elem('link', { rel: 'alternate', type: 'application/rss+xml', title, href: '/feed.xml' })
head.push(link)
}
return head
}
export function renderMeta(data, libs) {
const desc = data.desc || data.description
const props = {
viewport: 'width=device-width,initial-scale=1',
'article:published_time': data.date || data.pubDate,
generator: `Nue v${version} (nuejs.org)`,
'date.updated': new Date().toISOString().slice(0, 16) + 'Z',
'og:title': renderTitle(data.title, data.title_template),
'og:description': desc,
'og:image': ogImage(data),
libs: libs?.join(' '),
description: desc,
'theme-color': '',
author: '',
robots: '',
}
const meta = [elem('meta', { charset: 'utf-8'})]
Object.entries(props).map(([key, val]) => {
const content = data[key] || val
if (content && data[key] !== false) meta.push(elem('meta', { name: key, content }))
})
return meta
}
function renderTitle(title, template) {
const str = template ? template.replace('%s', title) : title
// Strip Markdown formatting (bold only for now)
return str?.replaceAll('**', '')
}
export function renderScripts(assets) {
const scripts = assets.filter(f => ['.js', '.ts'].includes(f.ext) && f.dir != `@shared${sep}data`)
return scripts.map(s => elem('script', { src: `/${s.dir}/${s.name}.js`, type: 'module' }))
}
export async function renderStyles(assets, conf={}) {
const { inline_css } = conf?.design || {}
const css_files = assets.filter(file => file.is_css)
if (conf.is_prod && inline_css) {
const css = await inlineCSS(css_files)
return [ elem('style', css) ]
}
return css_files.map(file => elem('link', { rel: 'stylesheet', href: `/${file.path}` }))
}
export async function inlineCSS(assets, minify=true) {
const css_files = assets.filter(el => el.is_css)
if (!css_files.length) return ''
const css = await Promise.all(css_files.map(file => file.text()))
const str = css.join('\n').trim()
return minifyCSS(str)
}
function importMap(imports) {
return !Object.keys(imports || {})[0] ? ''
: elem('script', { type: 'importmap' }, JSON.stringify({ imports }))
}
function ogImage(data) {
const og = data.og_image || data.og
const { origin='' } = data
if (og) {
const img = og[0] == '/' ? og : `/${data.dir}/${og}`
return (data.is_prod ? origin : '') + img
}
}
================================================
FILE: packages/nuekit/src/render/page.js
================================================
import { renderScripts, renderHead } from './head'
import { renderNue, compileNue } from 'nuedom'
import { elem, renderInline as markdown } from 'nuemark'
const globals = { markdown }
export function renderSlots({ head=[], content='', comps=[], data={}, conf={} }) {
const attr = getAttr(data)
const { scope } = data
const { max_class_names } = conf.design || {}
function slot(name) {
if (data[name] === false) return ''
const comp = comps.find(el => el.is ? el.is == name : el.tag == name)
return comp ? renderNue(comp, { data, deps: comps, globals, max_class_names }) : ''
}
const article = scope == 'article' ? content : `
<article>
${ slot('pagehead') }
${ content }
${ slot('pagefoot') }
</article>
`
const main = scope == 'main' ? content : `
<main>
${ slot('aside') }
${ article }
${ slot('beside') }
</main>
`
const body = scope == 'body' ? content : `
<body${attr.class}>
${ slot('banner') }
${ slot('header') }
${ slot('subheader') }
${ main }
${ slot('footer') }
${ slot('bottom') }
</body>
`
return `
<!doctype html>
<html lang="${attr.language}"${attr.dir}>
<head>
${ head.join('\n\t') }
${ slot('head') }
</head>
${body}
</html>
`
}
// used by renderMD and renderHTML
export async function renderPage({ asset, content, comps, data={}, conf }) {
const { is_dhtml, doctype } = await asset.parse()
const assets = await asset.assets()
const libs = await getLibPaths(assets)
if (is_dhtml) libs.unshift(asset.path)
const head = await renderHead({ conf, data, assets, libs })
return renderSlots({ head, content, comps, data, conf })
}
export async function renderMD(asset) {
const doc = await asset.parse()
const { meta, headings } = doc
const comps = await asset.components()
// data
const conf = await asset.config()
const data = await asset.data()
// content conf
const heading_ids = meta?.heading_ids || conf.content?.heading_ids
const sections = meta?.sections || conf.content?.sections
Object.assign(data, meta, { headings }, fileMeta(asset))
const content = doc.render({
data, sections, heading_ids,
tags: convertToTags(comps, data),
links: conf.links
})
return await renderPage({ content, comps, data, asset, conf })
}
// <!html>
export async function renderHTML(asset) {
const ast = await asset.parse()
const { is_dhtml } = ast
// raw HTML
const is_raw = ast.doctype == 'html' && ['html', 'head'].includes(ast.root.tag)
if (is_raw) return await asset.text()
// library --> skip
if (ast.is_lib) return is_dhtml ? compileNue(ast) : null
// !dhtml -> renderDHTML
if (is_dhtml) return await renderDHTML(asset)
// data
const data = await asset.data()
Object.assign(data, ast.meta, fileMeta(asset))
// root
const conf = await asset.config()
const root = createWrapper(ast.lib, conf.content?.sections)
data.scope = root.tag
// content
const comps = await asset.components()
const { max_class_names } = conf.design || {}
const content = renderNue(root, { data, deps: comps, globals, max_class_names })
// page
return await renderPage({ content, comps, data, asset, conf })
}
function createWrapper(lib, use_sections) {
if (lib.length == 1) return lib[0]
const wrap = { tag: use_sections ? 'section' : 'article', children: lib }
// hoist child scripts into parent
const script = []
lib.forEach(el => {
if (el.script) script.push(el.script)
delete el.script
})
wrap.script = script.join('\n')
return wrap
}
// <!dhtml>
export async function renderDHTML(asset) {
const doc = await asset.parse()
const comps = await asset.components(true)
const data = await asset.data()
const conf = await asset.config()
const root = createWrapper(doc.lib, conf.content?.sections)
// force component name
if (!root.is_custom && !root.is) root.is = 'default-app'
// server-side html
const content = root.is_custom ? elem('div', { nue: root.tag }) : elem(root.tag, { nue: root.is })
data.scope = root.tag
// "state" to importmap (if <body>)
const map = conf.import_map ??= {}
map.state = '/@nue/state.js'
const html = await renderPage({ asset, content, comps, data, conf })
return { html, js: compileNue({ ...doc, lib: [ root ]} ) }
}
/***** Helper functions *****/
function fileMeta(asset) {
const { url, slug, dir } = asset
return { url, slug, dir }
}
// custom components as Markdown extensions (tags)
function convertToTags(deps, data) {
const tags = {}
deps.forEach(ast => {
if (!ast.is_custom && !ast.is) return
const name = ast.is || ast.tag
// if (ast.is_custom) { delete ast.is_custom; ast.tag = 'div' }
tags[name] = function(args) {
const { attr, blocks } = this
return renderNue(ast, {
data: { ...args, ...attr, attr, blocks },
slot: this.innerHTML,
globals,
deps
})
}
})
return tags
}
async function getLibPaths(assets) {
const paths = []
for (const asset of assets.filter(el => el.is_html)) {
const { doctype } = await asset.parse()
if (doctype?.includes('dhtml lib')) paths.push(asset.path)
}
return paths
}
function getAttr(data) {
const { language = 'en-US', direction } = data
return {
class: data.class ? ` class="${data.class}"` : '',
dir: direction ? ` dir="${direction}"` : '',
language,
}
}
export function trim(str) {
return str.replace(/^\s*[\r\n]/gm, '').replace(/^ {4}/gm, '')
}
================================================
FILE: packages/nuekit/src/render/svg.js
================================================
import { parseYAMLArray } from 'nueyaml'
import { renderNue } from 'nuedom'
import { elem } from 'nuemark'
import { inlineCSS } from './head'
import { trim } from './page'
// opts: { hmr, fonts }
export async function renderSVG(asset, opts={}) {
const { root } = await asset.parse()
const hmr = opts.hmr != null
const fonts = await renderFonts(opts.fonts, hmr)
const deps = await asset.components()
const data = await asset.data()
transform(root)
const styles = getStyles(await asset.assets(), parseYAMLArray('tmp: ' + root.meta?.css))
if (hmr) {
const { base } = asset
const body = renderNue(root, { deps, data })
return renderHMR({ body, base, fonts, styles })
} else {
const css = [...fonts, await inlineCSS(styles)]
if (css[0]) {
const kids = root.children ??= []
kids.unshift({ tag: 'style', children: [{ text: `<![CDATA[${css.join('\n')}]]>` }] })
}
return renderNue(root, { deps, data })
}
}
export function getStyles(assets, patterns) {
if (!patterns?.length) return []
return assets.filter(asset =>
asset.is_css && patterns.some(match => asset.path.includes(match))
)
}
export function renderHMR({ body, base, fonts, styles }) {
const links = styles.map(file => {
return elem('link', { rel: 'stylesheet', href: `${file.url}` })
})
return trim(`
<!doctype html>
<html>
<head>
<title>${base} (dev mode)</title>
<style>
${ fonts.join('\n') }
body { margin: 0 }
</style>
${ links.join('\n')}
<script type="module" src="/@nue/hmr.js"></script>
</head>
<body>${body}</body>
</html>
`)
}
export async function renderFonts(conf, external) {
if (!conf) return []
const fn = external ? renderFont : renderInlineFont
const promises = Object.entries(conf).map(([name, path]) => fn(name, path))
try {
return await Promise.all(promises)
} catch(e) {
console.error('Font file not found', e.path)
return []
}
}
function renderFont(name, path) {
if (!path.startsWith('data') && path[0] != '/') path = '/' + path
return `@font-face { font-family: '${name}'; src: url('${path}')}`
}
async function renderInlineFont(name, path) {
const buffer = await Bun.file(path).arrayBuffer()
const data = 'data:font/woff2;base64,' + Buffer.from(buffer).toString('base64')
return renderFont(name, data)
}
/*** SVG transformations ***/
function transform(svg) {
pushAttr(svg, 'xmlns', 'http://www.w3.org/2000/svg')
pushViewport(svg)
svg.children?.forEach(el => {
if (el.tag == 'html') convertHTMLTag(el)
})
}
function pushAttr(svg, name, val) {
const attr = svg.attr ??= []
if (!attr.find(el => el.name == name)) {
attr.push({ name, val })
}
}
function pushViewport(svg) {
const { attr=[] } = svg
const find = (name) => attr.find(el => el.name == name)
const box = find('viewBox')
const w = find('width')
const h = find('height')
if (!box && w && h) attr.push({ name: 'viewBox', val: `0 0 ${w.val} ${h.val}` })
}
export function convertHTMLTag(tag) {
tag.tag = 'foreignObject'
Object.entries({ x: 0, y: 0, width: '100%', height: '100%'}).forEach(([name, val]) => {
pushAttr(tag, name, val)
})
tag.children?.forEach(el => pushAttr(el, 'xmlns', 'http://www.w3.org/1999/xhtml'))
return tag
}
================================================
FILE: packages/nuekit/src/server/README.md
================================================
# User server
Development environment for the user's server-side code. Not to confuse with tools/server.js, which is the Nue development server.
## Files
**index.js** - Main entry point. Returns either a local worker runtime or proxy to remote server
**worker.js** - Creates local worker environment that mimics edge runtime APIs
**proxy.js** - Creates proxy to remote server for testing against live environments
**env.js** - Simplified mocks for real server models (done later)
================================================
FILE: packages/nuekit/src/server/index.js
================================================
import { createWorker } from './worker.js'
import { createProxy } from './proxy.js'
export async function getServer(opts={}) {
return opts.url ? createProxy(opts) : await createWorker(opts)
}
================================================
FILE: packages/nuekit/src/server/model.js
================================================
import { readdir, mkdir, readFile, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
const SESSIONS_PATH = join(process.cwd(), '.nue', 'sessions.json')
const NOW = Date.now()
const DAY = 86400000
function createModel(items) {
items.forEach((el, i) => {
el.created = NOW - DAY * i
el.id = i + 1
})
async function create(obj) {
const id = items.length + 1
const created = Date.now()
const item = { id, created, ...obj }
items.unshift(item)
return item
}
// implemented with true event sourcing later
async function getAll() {
return items
}
async function size() {
return items.length
}
async function get(id) {
const item = items.find(el => el.id == id)
return {
...item,
async update(data) {
Object.assign(item, data)
return item
},
async remove() {
const i = items.indexOf(item)
items.splice(i, 1)
}
}
}
return { getAll, size, create, get }
}
async function saveSessions(sessions) {
await mkdir(join(process.cwd(), '.nue'), { recursive: true })
await writeFile(SESSIONS_PATH, JSON.stringify([...sessions], null, 2))
}
async function readSessions() {
try {
const data = await readFile(SESSIONS_PATH, 'utf-8')
return new Set(JSON.parse(data))
} catch {
return new Set()
}
}
// specialized models
async function createUserModel(items) {
const users = createModel(items)
const sessions = await readSessions()
async function login(email, password) {
const user = (await users.getAll()).find(el => el.email == email)
// mock: plaintext passwords. production uses hashed
if (user?.password == password) {
const sessionId = crypto.randomUUID()
sessions.add(sessionId)
await saveSessions(sessions)
return { sessionId, user }
}
}
async function authenticate(sessionId) {
return sessions.has(sessionId)
}
async function logout(sessionId) {
sessions.delete(sessionId)
await saveSessions(sessions)
}
return { ...users, login, logout, authenticate }
}
export async function createEnv(dir) {
const files = await readdir(dir)
const env = {}
for (const file of files) {
if (file.endsWith('.json')) {
const type = file.replace('.json', '')
const path = join(dir, file)
const items = JSON.parse(await readFile(path, 'utf8'))
const model = env[type] = type == 'users' ? await createUserModel(items) : createModel(items)
console.log(`Model "${type}" loaded (${ await model.size() } records)`)
}
}
return env
}
================================================
FILE: packages/nuekit/src/server/proxy.j
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
SYMBOL INDEX (480 symbols across 76 files)
FILE: packages/nuedom/bin/serve.js
method fetch (line 12) | fetch(req) {
FILE: packages/nuedom/src/compiler/ast.js
function createAST (line 8) | function createAST(block, imports) {
function parseOpeningTag (line 50) | function parseOpeningTag(tag) {
function parseChildren (line 57) | function parseChildren(arr, imports) {
function mergeConditionals (line 65) | function mergeConditionals(arr) {
function parseText (line 83) | function parseText(text, imports) {
function convertFunctions (line 98) | function convertFunctions(script) {
function convertGetters (line 107) | function convertGetters(script) {
FILE: packages/nuedom/src/compiler/attributes.js
function tokenizeAttr (line 6) | function tokenizeAttr(attrStr) {
function parseAttributes (line 12) | function parseAttributes(attrs, imports) {
function parseNameVal (line 80) | function parseNameVal(attr) {
function parseFor (line 89) | function parseFor(str, imports) {
function parseForArgs (line 97) | function parseForArgs(args) {
function parseExpression (line 106) | function parseExpression(str, attr_name, imports) {
function parseClassHelper (line 121) | function parseClassHelper(val, imports) {
FILE: packages/nuedom/src/compiler/compiler.js
function compileNue (line 7) | function compileNue(template) {
function compileDoc (line 12) | function compileDoc(doc) {
constant RE_FN (line 26) | const RE_FN = /(script|h_fn|fn):\s*(['"`])([^\2]*?)\2/g
function compileJS (line 28) | function compileJS(js) {
function compileFn (line 36) | function compileFn(str, is_event) {
FILE: packages/nuedom/src/compiler/context.js
constant CONTEXT_RE (line 5) | const CONTEXT_RE = /'[^']*'|"[^"]*"|[a-zA-Z_$][a-zA-Z0-9_$]*(?:\.[a-zA-Z...
function addContext (line 7) | function addContext(expr, exceptions = ['state']) {
FILE: packages/nuedom/src/compiler/document.js
function parseNue (line 13) | function parseNue(template) {
function isScript (line 61) | function isScript(block) {
function parseBlocks (line 65) | function parseBlocks(tokens) {
function parseBlock (line 84) | function parseBlock(tokens, i) {
function parseNames (line 129) | function parseNames(script) {
function getVariableNames (line 142) | function getVariableNames(line) {
function getFunctionNames (line 160) | function getFunctionNames(line) {
FILE: packages/nuedom/src/compiler/html5.js
constant EVENTS (line 6) | const EVENTS = 'click submit change input focus blur keydown keyup keypr...
constant BOOLEAN (line 8) | const BOOLEAN = 'disabled checked selected hidden readonly required auto...
constant HTML5_TAGS (line 11) | const HTML5_TAGS = 'a abbr address area article aside audio b base bdi b...
constant SVG_TAGS (line 14) | const SVG_TAGS = 'animate animateMotion animateTransform circle clipPath...
constant SELF_CLOSING (line 22) | const SELF_CLOSING = 'img br hr input meta link area base col embed keyg...
constant STRICT_ATTRS (line 25) | const STRICT_ATTRS = 'viewBox preserveAspectRatio'.split(' ')
FILE: packages/nuedom/src/compiler/tokenizer.js
function tokenize (line 5) | function tokenize(template) {
function parseComment (line 33) | function parseComment(template, pos, tokens) {
function parseAnnotations (line 45) | function parseAnnotations(comment) {
function addTag (line 61) | function addTag(template, pos, tokens) {
function toSelfClosing (line 84) | function toSelfClosing(tag) {
function addScript (line 91) | function addScript(template, pos, tokens) {
function addExpr (line 106) | function addExpr(template, pos, tokens) {
function addText (line 126) | function addText(template, pos, tokens) {
function throwSyntaxError (line 136) | function throwSyntaxError(pos, snippet) {
FILE: packages/nuedom/src/dom/diff.js
function domdiff (line 26) | function domdiff(prev, next) {
function updateAttributes (line 46) | function updateAttributes(prev, next) {
function diffChildren (line 57) | function diffChildren(prev, kids) {
function diffChildrenByKey (line 69) | function diffChildrenByKey(prev, kids) {
FILE: packages/nuedom/src/dom/fakedom.js
function createNode (line 7) | function createNode(nodeType, tagName = null) {
class AttributeMap (line 20) | class AttributeMap extends Map {
method [Symbol.iterator] (line 21) | *[Symbol.iterator]() {
function createClassList (line 28) | function createClassList() {
function serialize (line 40) | function serialize(node) {
function createElement (line 69) | function createElement(tag, nodeType = 1) {
function createDocument (line 202) | function createDocument() {
FILE: packages/nuedom/src/dom/node.js
function createNode (line 9) | function createNode(ast, data={}, opts={}, parent) {
function renderText (line 216) | function renderText(ast, self) {
function getAttrData (line 221) | function getAttrData(ast, self) {
function setAttributes (line 235) | function setAttributes(el, ast, self) {
function exec (line 259) | function exec(fn, self, e) {
function renderHTML (line 272) | function renderHTML(html) {
function $concat (line 278) | function $concat(obj) {
function createFragment (line 282) | function createFragment() {
function addSpace (line 286) | function addSpace(to, child, next) {
function renderClientStub (line 292) | function renderClientStub(tag, self) {
FILE: packages/nuedom/src/dom/render.js
function renderAST (line 6) | function renderAST(ast, opts={}) {
function renderNue (line 11) | function renderNue(template, opts={}) {
function mountAST (line 20) | function mountAST(ast, opts) {
FILE: packages/nuedom/src/nue.js
function mount (line 7) | function mount(ast, opts) {
FILE: packages/nuedom/test/ast.test.js
function testTag (line 7) | function testTag(template, expected) {
FILE: packages/nuedom/test/event.util.js
function clickable (line 6) | function clickable(template, data) {
FILE: packages/nueglow/css/build.js
function minify (line 4) | async function minify(names, toname) {
FILE: packages/nueglow/index.js
constant MIXED_HTML (line 2) | const MIXED_HTML = ['html', 'jsx', 'php', 'astro', 'dhtml', 'vue', 'svel...
constant LINE_COMMENT (line 3) | const LINE_COMMENT = { clojure: ';;', lua: '--', python: '#' }
constant PREFIXES (line 4) | const PREFIXES = { '+': 'ins', '-': 'del', '>': 'dfn' }
constant MARK (line 5) | const MARK = /(••?)([^•]+)\1/g // ALT + q
constant COMMON_WORDS (line 8) | const COMMON_WORDS = 'null|true|false|undefined|import|from|async|await|...
constant SPECIAL_WORDS (line 15) | const SPECIAL_WORDS = {
constant RULES (line 22) | const RULES = {
constant HTML_TAGS (line 38) | const HTML_TAGS = [
function getTags (line 78) | function getTags(lang) {
function encode (line 96) | function encode(str) {
function elem (line 101) | function elem(name, str) {
function isMD (line 111) | function isMD(lang) {
function getMDTags (line 115) | function getMDTags(str) {
function parseRow (line 160) | function parseRow(row, lang) {
function renderString (line 184) | function renderString(str) {
function renderRow (line 192) | function renderRow(row, lang, mark = true) {
constant COMMENT (line 226) | const COMMENT = [/(\/\* |^ *{# |<!--|'''|=begin)/, /(\*\/|#}|-->|'''|=en...
function parseSyntax (line 228) | function parseSyntax(lines, lang, prefix = true) {
function glow (line 272) | function glow(str, opts = { prefix: true, mark: true }) {
FILE: packages/nueglow/test/generate.js
constant HTML (line 12) | const HTML = `
constant JSX (line 29) | const JSX = `
constant CSS (line 51) | const CSS = `
constant JAVASCRIPT (line 71) | const JAVASCRIPT = `
constant MARKDOWN (line 91) | const MARKDOWN = `
constant YAML (line 117) | const YAML = `
constant NUEMARK (line 134) | const NUEMARK = `
constant MDX (line 168) | const MDX = `
constant SHELL (line 192) | const SHELL = `
constant TOML (line 207) | const TOML = `
constant ZIG (line 223) | const ZIG = `
constant CPP (line 242) | const CPP = `
constant JSON (line 287) | const JSON = `
constant JSON5 (line 302) | const JSON5 = `
constant STYLED (line 337) | const STYLED = `
constant ASTRO (line 356) | const ASTRO = `
constant HASKELL (line 375) | const HASKELL = `
constant PYTHON (line 388) | const PYTHON = `
constant JAVA (line 408) | const JAVA = `
constant KOTLIN (line 430) | const KOTLIN = `
constant RUST (line 448) | const RUST = `
constant PERL (line 460) | const PERL = `
constant LUA (line 478) | const LUA = `
constant RUBY (line 495) | const RUBY = `
constant PHP (line 516) | const PHP = `
constant CSHARP (line 532) | const CSHARP = `
constant CLOJURE (line 556) | const CLOJURE = `
constant NIM (line 572) | const NIM = `
constant CRYSTAL (line 591) | const CRYSTAL = `
constant JULIA (line 604) | const JULIA = `
constant SVELTE (line 642) | const SVELTE = `
constant SQL (line 670) | const SQL = `
function renderPage (line 684) | async function renderPage(items) {
FILE: packages/nuekit/client/error.js
function renderDialog (line 2) | function renderDialog(e) {
function showError (line 21) | async function showError(error) {
FILE: packages/nuekit/client/hmr.js
function createConnection (line 3) | function createConnection() {
function handleError (line 32) | async function handleError(asset) {
function reloadContent (line 38) | async function reloadContent(asset) {
function reloadVisual (line 67) | function reloadVisual(asset) {
function reloadSVG (line 83) | function reloadSVG(html) {
function reloadCSS (line 88) | function reloadCSS(asset) {
function reloadHTML (line 99) | async function reloadHTML(asset) {
function reloadComponents (line 107) | async function reloadComponents(asset) {
function createStyle (line 117) | function createStyle(url, content) {
function saveState (line 125) | function saveState() {
function restoreState (line 133) | function restoreState({ formdata, popover, dialog }) {
function deserialize (line 146) | function deserialize(form, data) {
function parsePage (line 154) | function parsePage(html) {
function $ (line 160) | function $(query, root=document) {
FILE: packages/nuekit/client/mount.js
function mountAll (line 3) | async function mountAll(reload_path) {
function getData (line 21) | function getData(root) {
function getImportPaths (line 27) | function getImportPaths() {
function importComponents (line 34) | async function importComponents(reload_path) {
FILE: packages/nuekit/client/transitions.js
function $ (line 3) | function $(query, root = document) {
function $$ (line 7) | function $$(query, root = document) {
function loadPage (line 14) | async function loadPage(path) {
function updatePageHead (line 39) | async function updatePageHead(dom) {
function updateMeta (line 54) | function updateMeta(name, dom) {
function handlePageScroll (line 60) | function handlePageScroll() {
function dispatchRouteEvents (line 69) | function dispatchRouteEvents() {
function onclick (line 76) | function onclick(root, fn) {
function shouldIgnoreClick (line 93) | function shouldIgnoreClick(e, path, target, filename) {
function toRelative (line 99) | function toRelative(path) {
function setActive (line 104) | function setActive(path, attrname = 'aria-current') {
function setupTransitions (line 118) | function setupTransitions() {
function haveSameChildren (line 165) | function haveSameChildren(a, b) {
function updateContent (line 176) | function updateContent(current, incoming, ignoreMain) {
function updateElement (line 194) | function updateElement(current, incoming) {
function updatePageStyles (line 201) | function updatePageStyles(dom) {
function findNewStyles (line 207) | function findNewStyles(current, incoming) {
function updateInlineStyle (line 221) | function updateInlineStyle(dom) {
function fetchHTML (line 226) | async function fetchHTML(path) {
function createDOM (line 243) | function createDOM(html) {
function loadStylesheets (line 248) | async function loadStylesheets(linkElements) {
function loadStylesheet (line 253) | function loadStylesheet(href) {
FILE: packages/nuekit/src/asset.js
function createAsset (line 17) | function createAsset(file, site={}) {
function compileJS (line 140) | async function compileJS(path, minify, bundle) {
FILE: packages/nuekit/src/cli.js
function expandArgs (line 7) | function expandArgs(args) {
function getArgs (line 21) | function getArgs(argv) {
constant HELP (line 75) | const HELP = `
function format (line 122) | function format(line) {
function printHelp (line 129) | function printHelp() {
function printVersion (line 134) | function printVersion() {
function run (line 138) | async function run(args) {
FILE: packages/nuekit/src/cmd/build.js
function build (line 9) | async function build(site, args) {
function buildAll (line 47) | async function buildAll(subset, args) {
function buildAsset (line 72) | async function buildAsset(asset, dist) {
function stats (line 92) | function stats(assets) {
function matches (line 119) | function matches(path, patterns) {
function minifyJS (line 125) | async function minifyJS(code) {
FILE: packages/nuekit/src/cmd/create.js
constant NAMES (line 5) | const NAMES = 'blog full minimal spa'.split(' ')
function create (line 7) | async function create(name, { dir, baseurl }) {
function getLocalZip (line 37) | async function getLocalZip(name, dir) {
function fetchZip (line 46) | async function fetchZip(name, baseurl='https://github.com/nuejs/nue/raw/...
function unzip (line 56) | async function unzip(dir, zip) {
FILE: packages/nuekit/src/cmd/preview.js
function preview (line 8) | async function preview(conf, opts) {
function getFile (line 29) | async function getFile(dist, url) {
FILE: packages/nuekit/src/cmd/serve.js
function serve (line 12) | async function serve(site, { silent }) {
function onServe (line 60) | async function onServe(url, assets, opts={}) {
function findAssetByURL (line 107) | function findAssetByURL(url, assets=[]) {
FILE: packages/nuekit/src/collections.js
function getCollections (line 3) | async function getCollections(files, opts) {
function createCollection (line 13) | async function createCollection(files, conf) {
function matchPages (line 19) | function matchPages(files, patterns=[]) {
function filterPages (line 31) | async function filterPages(files, conf) {
function sortPages (line 53) | function sortPages(files, sorting) {
FILE: packages/nuekit/src/conf.js
constant SITE_CONF (line 8) | const SITE_CONF = 'site design server collections production port sitema...
constant ALL_CONF (line 10) | const ALL_CONF = SITE_CONF.concat('include exclude meta content import_m...
constant SKIP (line 13) | const SKIP = `node_modules .toml .rs .lock package.json .lockb lock.yaml...
function readSiteConf (line 16) | async function readSiteConf(args={}) {
function mergeData (line 51) | function mergeData(dataset) {
function mergeConf (line 64) | function mergeConf(site_conf, app_conf) {
function mergeValue (line 70) | function mergeValue(conf, key, val) {
FILE: packages/nuekit/src/deps.js
constant AUTO_INCLUDED (line 5) | const AUTO_INCLUDED = ['data', 'design', 'ui'].map(dir => join('@shared'...
constant ASSET_TYPES (line 7) | const ASSET_TYPES = ['.html', '.js', '.ts', '.yaml', '.css']
function listDependencies (line 10) | function listDependencies(basepath, { paths, exclude=[], include=[] }) {
function isDep (line 34) | function isDep(page_path, asset_path, all_paths) {
function parseDirs (line 68) | function parseDirs(dir) {
FILE: packages/nuekit/src/file.js
function createFile (line 5) | async function createFile(root, path) {
function getFileInfo (line 47) | function getFileInfo(path) {
function getURL (line 61) | function getURL(file) {
function getSlug (line 76) | function getSlug(file) {
FILE: packages/nuekit/src/render/feed.js
constant XML (line 5) | const XML = '<?xml version="1.0" encoding="UTF-8"?>'
function generateSitemap (line 7) | async function generateSitemap(assets, conf) {
function generateFeed (line 24) | async function generateFeed(assets, conf) {
function renderSitemap (line 38) | function renderSitemap(pages) {
function renderFeed (line 55) | function renderFeed(meta, pages) {
function getOrigin (line 83) | function getOrigin(conf) {
FILE: packages/nuekit/src/render/head.js
function renderHead (line 8) | async function renderHead({ conf, data, assets, libs=[] }) {
function renderMeta (line 51) | function renderMeta(data, libs) {
function renderTitle (line 81) | function renderTitle(title, template) {
function renderScripts (line 88) | function renderScripts(assets) {
function renderStyles (line 93) | async function renderStyles(assets, conf={}) {
function inlineCSS (line 105) | async function inlineCSS(assets, minify=true) {
function importMap (line 115) | function importMap(imports) {
function ogImage (line 120) | function ogImage(data) {
FILE: packages/nuekit/src/render/page.js
function renderSlots (line 8) | function renderSlots({ head=[], content='', comps=[], data={}, conf={} }) {
function renderPage (line 62) | async function renderPage({ asset, content, comps, data={}, conf }) {
function renderMD (line 74) | async function renderMD(asset) {
function renderHTML (line 99) | async function renderHTML(asset) {
function createWrapper (line 132) | function createWrapper(lib, use_sections) {
function renderDHTML (line 149) | async function renderDHTML(asset) {
function fileMeta (line 173) | function fileMeta(asset) {
function convertToTags (line 179) | function convertToTags(deps, data) {
function getLibPaths (line 202) | async function getLibPaths(assets) {
function getAttr (line 211) | function getAttr(data) {
function trim (line 220) | function trim(str) {
FILE: packages/nuekit/src/render/svg.js
function renderSVG (line 12) | async function renderSVG(asset, opts={}) {
function getStyles (line 40) | function getStyles(assets, patterns) {
function renderHMR (line 48) | function renderHMR({ body, base, fonts, styles }) {
function renderFonts (line 73) | async function renderFonts(conf, external) {
function renderFont (line 86) | function renderFont(name, path) {
function renderInlineFont (line 91) | async function renderInlineFont(name, path) {
function transform (line 99) | function transform(svg) {
function pushAttr (line 108) | function pushAttr(svg, name, val) {
function pushViewport (line 115) | function pushViewport(svg) {
function convertHTMLTag (line 124) | function convertHTMLTag(tag) {
FILE: packages/nuekit/src/server/index.js
function getServer (line 5) | async function getServer(opts={}) {
FILE: packages/nuekit/src/server/model.js
constant SESSIONS_PATH (line 5) | const SESSIONS_PATH = join(process.cwd(), '.nue', 'sessions.json')
constant NOW (line 6) | const NOW = Date.now()
constant DAY (line 7) | const DAY = 86400000
function createModel (line 10) | function createModel(items) {
function saveSessions (line 55) | async function saveSessions(sessions) {
function readSessions (line 60) | async function readSessions() {
function createUserModel (line 72) | async function createUserModel(items) {
function createEnv (line 102) | async function createEnv(dir) {
FILE: packages/nuekit/src/server/proxy.js
function createProxy (line 2) | function createProxy(opts) {
FILE: packages/nuekit/src/server/worker.js
function importWorker (line 9) | async function importWorker({ dir, reload }) {
function createWorker (line 22) | async function createWorker(opts = {}) {
function getCFHeaders (line 48) | function getCFHeaders() {
FILE: packages/nuekit/src/site.js
function createSite (line 7) | async function createSite(conf) {
function sortAssets (line 56) | function sortAssets(items) {
function mergeSharedData (line 73) | async function mergeSharedData(assets, data={}) {
FILE: packages/nuekit/src/system.js
function getClientFiles (line 11) | function getClientFiles() {
function getPackages (line 18) | function getPackages() {
function resolve (line 25) | function resolve(pkg) {
function getSystemFiles (line 29) | function getSystemFiles(is_prod) {
function createSystemFiles (line 52) | async function createSystemFiles(dist, force) {
FILE: packages/nuekit/src/tools/css.js
function minifyCSS (line 1) | function minifyCSS(str) {
function parseCSS (line 5) | function parseCSS(str) {
function serialize (line 83) | function serialize(node) {
function tokenize (line 112) | function tokenize(css) {
function readTextToken (line 173) | function readTextToken(css, start) {
FILE: packages/nuekit/src/tools/fswalk.js
function matches (line 5) | function matches(path, patterns) {
function isSkipped (line 9) | function isSkipped(path) {
function warn (line 14) | function warn(message, path) {
function walkDirectory (line 18) | async function walkDirectory(dir, root, opts) {
function fswalk (line 57) | async function fswalk(root = '.', opts = {}) {
FILE: packages/nuekit/src/tools/fswatch.js
function fswatch (line 7) | function fswatch(root, opts = {}) {
function isEditorBackup (line 56) | function isEditorBackup(path) {
function createDeduplicator (line 61) | function createDeduplicator() {
FILE: packages/nuekit/src/tools/server.js
function createServer (line 2) | function createServer({ port=4000, handler }, callback) {
method open (line 49) | open(ws) {
method close (line 53) | close(ws) {
function broadcast (line 59) | function broadcast(data) {
FILE: packages/nuekit/test/cmd/build.test.js
constant CONF (line 23) | const CONF = {
FILE: packages/nuekit/test/cmd/serve.test.js
method render (line 20) | render() { return 'hey' }
method render (line 27) | render() { return { html: 'hey' }}
method render (line 33) | render() { return 'error' }
FILE: packages/nuekit/test/collections.test.js
method text (line 85) | async text() { return '# Hello' }
method text (line 87) | async text() {
FILE: packages/nuekit/test/file.test.js
function testURL (line 9) | function testURL(path, expected) {
FILE: packages/nuekit/test/fswatch.test.js
function waitForEvents (line 12) | async function waitForEvents(array, expectedCount, maxWait = 1000) {
FILE: packages/nuekit/test/render/asset-render.test.js
method text (line 7) | async text() { return '<h1>Hey { slug }</h1> <p>{{ markdown("*boo*") }}<...
method text (line 22) | async text() { return '<main><user-list/></main>' }
method text (line 24) | async text() { return '<ul :is="user-list"/>' }
method text (line 47) | async text() { return '<!dhtml lib> <comp>Again</comp>' }
method text (line 53) | async text() { return '<!dhtml> <h1>Hey</h1> <p>World</p> <comp/>' }
FILE: packages/nuekit/test/render/head.test.js
method text (line 15) | async text() { return '' }
method text (line 16) | async text() { return 'body {}' }
method text (line 17) | async text() { return '' }
FILE: packages/nuekit/test/render/md.test.js
method text (line 8) | async text() { return '<!html lib><header class="foo"/> <navi/>' }
method text (line 9) | async text() { return '<!html lib><footer/>' }
method text (line 10) | async text() { return '<!dhtml lib> <foo/>' }
method text (line 14) | async text() { return '# Hello' }
method text (line 37) | async text() {
method text (line 42) | async text() { return ['# Hello', '[custom.blue]', ' World'].join('\n') }
method text (line 52) | async text() {
method text (line 68) | async text() { return '# Hey' }
FILE: packages/nuekit/test/render/page.test.js
method assets (line 34) | async assets() { return [] }
method parse (line 35) | async parse() { return {} }
method config (line 46) | async config() { return {} }
method data (line 48) | async data() {
method parse (line 51) | async parse() {
method assets (line 56) | async assets() {
method components (line 61) | async components() {
method data (line 77) | async data() {return {} }
method config (line 78) | async config() { return {} }
method assets (line 79) | async assets() { return [] }
method parse (line 81) | async parse() {
method components (line 85) | async components() {
method parse (line 99) | async parse() {
method text (line 103) | async text() {
method data (line 115) | async data() { return {} }
method assets (line 116) | async assets() { return [] }
method config (line 117) | async config() { return {} }
method components (line 118) | async components() { return [] }
method parse (line 120) | async parse() {
FILE: packages/nuekit/test/render/svg.test.js
method data (line 40) | async data() { return {} }
method components (line 41) | async components() { return [] }
method config (line 42) | async config() { return {} }
method parse (line 46) | async parse() {
method assets (line 52) | async assets() {
FILE: packages/nuekit/test/site.test.js
method parse (line 40) | parse() { return { port: 100 } }
method parse (line 41) | parse() { return { default: function(data) { data.port = 200 }} }
FILE: packages/nuekit/test/test-utils.js
function writeAll (line 10) | async function writeAll(items) {
function write (line 26) | async function write(path, content) {
function toYAML (line 39) | function toYAML(data) {
function removeAll (line 47) | async function removeAll() {
function fileset (line 54) | async function fileset(dir) {
FILE: packages/nuemark/index.js
constant EOL (line 5) | const EOL = /\r\n|\r|\n/
function nuemark (line 7) | function nuemark(content, opts) {
function parseNuemark (line 11) | function parseNuemark(content) {
FILE: packages/nuemark/src/parse-blocks.js
function parseBlocks (line 8) | function parseBlocks(lines, capture) {
function processNestedBlocks (line 167) | function processNestedBlocks(block, capture) {
function parseHeading (line 210) | function parseHeading(str) {
function getFootnoteIds (line 219) | function getFootnoteIds(blocks) {
function parseRef (line 224) | function parseRef(str) {
function getListItem (line 234) | function getListItem(line) {
function getBreak (line 240) | function getBreak(str) {
function isYAML (line 250) | function isYAML(str) {
function parseTableRow (line 256) | function parseTableRow(line) {
function addListEntry (line 260) | function addListEntry({ entries }, line) {
FILE: packages/nuemark/src/parse-document.js
function parseDocument (line 10) | function parseDocument(lines) {
function sectionize (line 64) | function sectionize(blocks = []) {
function renderFootnotes (line 89) | function renderFootnotes(arr) {
function getTitle (line 96) | function getTitle(blocks) {
function stripMeta (line 102) | function stripMeta(lines) {
function parseReflinks (line 122) | function parseReflinks(links = {}) {
FILE: packages/nuemark/src/parse-inline.js
constant FORMATTING (line 6) | const FORMATTING = {
constant ESCAPED (line 23) | const ESCAPED = { '<': '<', '>': '>' }
constant SIGNIFICANT (line 26) | const SIGNIFICANT = /[\*_\["`~\\|•{\\<>]|!\[/
constant PARSERS (line 30) | const PARSERS = [
function isValidName (line 131) | function isValidName(name) {
function parseInline (line 144) | function parseInline(str) {
function parseLink (line 170) | function parseLink(str, is_reflink) {
function parseLinkTitle (line 216) | function parseLinkTitle(href) {
FILE: packages/nuemark/src/parse-tag.js
constant ATTR (line 2) | const ATTR = 'id is class style hidden disabled popovertarget popover'.s...
function parseTag (line 7) | function parseTag(input) {
function parseValue (line 42) | function parseValue(val) {
function valueGetter (line 53) | function valueGetter(input) {
function parseSpecs (line 68) | function parseSpecs(str) {
function parseAttr (line 79) | function parseAttr(str) {
FILE: packages/nuemark/src/render-blocks.js
function renderLines (line 10) | function renderLines(lines, opts) {
function renderBlocks (line 15) | function renderBlocks(blocks, opts = {}) {
function renderBlock (line 19) | function renderBlock(block, opts) {
function renderList (line 37) | function renderList({ items, numbered }, opts) {
function renderHeading (line 42) | function renderHeading(h, opts = {}) {
function createHeadingId (line 54) | function createHeadingId(text) {
function renderContent (line 61) | function renderContent(lines, opts) {
function renderCode (line 66) | function renderCode({ name, code, attr, data }, opts) {
constant SELF_CLOSING (line 87) | const SELF_CLOSING = ['img', 'source', 'meta', 'link']
function elem (line 89) | function elem(name, attr, body) {
function renderAttrs (line 100) | function renderAttrs(attr) {
FILE: packages/nuemark/src/render-inline.js
function renderToken (line 7) | function renderToken(token, opts = {}) {
function formatText (line 20) | function formatText({ tag, body }, opts) {
function renderCode (line 25) | function renderCode(code) {
function renderImage (line 29) | function renderImage(img) {
function renderLink (line 34) | function renderLink(link, opts) {
function renderTokens (line 50) | function renderTokens(tokens, opts) {
function renderInline (line 54) | function renderInline(str, opts) {
function renderVariable (line 58) | function renderVariable(expr, data) {
FILE: packages/nuemark/src/render-tag.js
constant TAGS (line 11) | const TAGS = {
method codeblock (line 13) | codeblock() {
method accordion (line 18) | accordion({ name, open }) {
method block (line 29) | block() {
method button (line 40) | button(data) {
method define (line 48) | define() {
method image (line 61) | image() {
method list (line 83) | list() {
method svg (line 91) | svg(data) {
method icon (line 96) | icon(data) {
method table (line 100) | table() {
method video (line 109) | video() {
method object (line 117) | object() {
function readIcon (line 143) | function readIcon(path, icon_dir) {
function renderIcon (line 160) | function renderIcon(name, symbol, icon_dir) {
function renderTag (line 165) | function renderTag(tag, opts = {}) {
function renderIsland (line 189) | function renderIsland(tag, all_data) {
constant MIME (line 203) | const MIME = {
function getMimeType (line 212) | function getMimeType(path = '') {
function createPicture (line 217) | function createPicture(img_attr, data) {
function getListItems (line 230) | function getListItems(arr) {
function parseSize (line 234) | function parseSize(data) {
function extractData (line 242) | function extractData(data, all_data) {
function getVideoAttrs (line 252) | function getVideoAttrs(data) {
function wrap (line 262) | function wrap(name, html) {
function getInnerHTML (line 268) | function getInnerHTML(blocks = [], opts) {
function renderTable (line 277) | function renderTable(table, opts) {
function parseTable (line 306) | function parseTable(lines) {
FILE: packages/nuemark/test/block.test.js
function beforerender (line 255) | function beforerender(block) {
FILE: packages/nuemark/test/tag.test.js
method print (line 53) | print(data) {
FILE: packages/nueserver/nueserver.js
function createContext (line 6) | function createContext(req, env = {}) {
function matches (line 33) | function matches(method, path) {
function matchPath (line 46) | function matchPath(pattern, path) {
function fetch (line 99) | async function fetch(request, env = {}) {
FILE: packages/nuestate/src/state.js
constant CONTEXTS (line 4) | const CONTEXTS = ['path_params', 'query', 'session', 'local', 'emit_only...
method setup (line 12) | setup(args={}) {
method data (line 21) | get data() {
method on (line 30) | on(names, fn) {
method off (line 35) | off(names, fn) {
method emit (line 40) | emit(name, val) {
method set (line 46) | set(data, is_popstate) {
method init (line 56) | init() {
method clear (line 60) | clear() {
method get (line 69) | get(api, prop) {
method set (line 73) | set(api, prop, val) {
function translate (line 81) | function translate(obj) {
function pushURLState (line 92) | function pushURLState(at, data) {
function onpopstate (line 103) | function onpopstate({ state }) {
function autolink (line 109) | function autolink(root=document) {
function getChanges (line 120) | function getChanges(orig, data) {
function fire (line 135) | function fire(changes) {
function save (line 144) | function save(changes) {
function getFnIndex (line 161) | function getFnIndex(names, fn) {
function getRouteParams (line 165) | function getRouteParams(route) {
function getPathData (line 169) | function getPathData(route, pathname) {
function getQueryData (line 184) | function getQueryData(params, search) {
function getURLData (line 194) | function getURLData({ pathname, search }) {
function renderPath (line 202) | function renderPath(route, data={}) {
function renderQuery (line 208) | function renderQuery(params, data={}) {
constant KEY (line 222) | const KEY = '$state'
function getStoreData (line 224) | function getStoreData(store) {
function setStoreValue (line 228) | function setStoreValue(store, key, val) {
FILE: packages/nuestate/test/browser.test.js
function click (line 17) | function click(pathname) {
FILE: packages/nueyaml/nueyaml.js
function stripComments (line 2) | function stripComments(line) {
function measureIndent (line 12) | function measureIndent(line) {
function detectIndentSize (line 21) | function detectIndentSize(lines) {
function validateIndentation (line 32) | function validateIndentation(lines) {
function isNumber (line 65) | function isNumber(str) {
function parseValue (line 70) | function parseValue(raw) {
function parseYAMLArray (line 93) | function parseYAMLArray(line) {
function detectStructure (line 101) | function detectStructure(lines) {
function buildObject (line 169) | function buildObject(blocks) {
function parseYAML (line 298) | function parseYAML(text) {
FILE: packages/templates/full/@shared/lib/crud.js
function post (line 2) | async function post(route, data) {
function get (line 13) | async function get(route, params) {
function del (line 22) | async function del(route) {
function getAuthHeader (line 29) | function getAuthHeader() {
function catchError (line 34) | function catchError(resp) {
FILE: packages/www/@shared/data/topics.js
function getCategory (line 15) | function getCategory(topics, slug) {
function parseEntry (line 23) | function parseEntry(el) {
FILE: packages/www/@shared/lib/console/console.js
function createConsole (line 2) | function createConsole(text, callback) {
function runLines (line 38) | function runLines(lines, callback, isActive) {
function runCommand (line 60) | function runCommand(text, callback, isActive) {
function processResponse (line 85) | function processResponse(str, callback, isActive) {
function parseDelay (line 101) | function parseDelay(text) {
FILE: packages/www/@shared/lib/video/controller.js
function createVideo (line 7) | function createVideo({ videoId, poster, width, height }) {
function createSource (line 24) | function createSource(base, use_hls) {
function getQuality (line 33) | function getQuality() {
Condensed preview — 321 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (880K chars).
[
{
"path": ".editorconfig",
"chars": 155,
"preview": "root = true\ncharset = utf-8\nend_of_line = lf\nindent_size = 2\ntab_width = 2\nindent_style = space\ninsert_final_newline = t"
},
{
"path": ".github/ISSUE_TEMPLATE/bug-report.md",
"chars": 622,
"preview": "---\nname: 🐞 Bug Report\nabout: Create a bug report to help us improve Nue\ntype: bug\n---\n\n<!-- Please search to see if an "
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 205,
"preview": "blank_issues_enabled: true\ncontact_links:\n - name: 💬 Discussions\n url: https://github.com/nuejs/nue/discussions\n "
},
{
"path": ".github/ISSUE_TEMPLATE/feature-request.md",
"chars": 544,
"preview": "---\nname: 💡 Feature Request or Improvement\nabout: Suggest an idea or improvement\nlabels: new feature, improvement\ntype: "
},
{
"path": ".github/workflows/build-template-zips.yml",
"chars": 755,
"preview": "name: Build template zips\non:\n push:\n branches: [ master, 2.0 ]\n paths:\n - 'packages/templates/**'\npermissio"
},
{
"path": ".github/workflows/links.yaml.disabled",
"chars": 2578,
"preview": "name: Check Links\n\non:\n push:\n branches:\n - master\n paths:\n - '**/README.md'\n - 'packages/nuejs.or"
},
{
"path": ".github/workflows/push-examples.yaml.disabled",
"chars": 2521,
"preview": "name: Push examples to web server\n\non:\n push:\n branches:\n - master\n paths:\n - packages/examples/**\n wo"
},
{
"path": ".github/workflows/push-site.yaml.disabled",
"chars": 947,
"preview": "name: Push site to web server\n\non:\n push:\n branches:\n - master\n paths:\n - packages/nuejs.org/**\n workf"
},
{
"path": ".github/workflows/repo/build-page/action.yaml",
"chars": 645,
"preview": "name: Repository nue page build\ndescription: |\n Internal composite action to install dependencies, register the `nue` c"
},
{
"path": ".github/workflows/test.yaml",
"chars": 709,
"preview": "name: Test\n\non:\n push:\n branches:\n - master\n paths-ignore:\n - .github/**\n - packages/templates/**\n"
},
{
"path": ".gitignore",
"chars": 98,
"preview": "node_modules\n\ncoverage\n.dist\n.nue\ndist\n_test\ntest_dir\nperformance.js\n\n.DS_Store\n.idea\n.vscode\n.env"
},
{
"path": ".prettierrc.yaml",
"chars": 336,
"preview": "printWidth: 100\ntabWidth: 2\nuseTabs: false\nsemi: false\nsingleQuote: true\nquoteProps: 'as-needed'\ntrailingComma: 'es5'\nbr"
},
{
"path": "CONTRIBUTING.md",
"chars": 54,
"preview": "\n\n## Details here\n\nhttps://nuejs.org/docs/contributing"
},
{
"path": "LICENSE",
"chars": 1089,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2025-present, Tero Piirainen\n\nPermission is hereby granted, free of charge, to any "
},
{
"path": "bunfig.toml",
"chars": 69,
"preview": "[test]\ncoveragePathIgnorePatterns = [\"**/test/**\", \"**/test_dir/**\"]\n"
},
{
"path": "package.json",
"chars": 82,
"preview": "{\n \"name\": \"nue-monorepo\",\n \"private\": true,\n \"workspaces\": [ \"packages/*\" ]\n}\n"
},
{
"path": "packages/nuedom/Makefile",
"chars": 112,
"preview": "\nrun-tests:\n\tbun test\n\ndemo:\n\t@ bun bin/serve\n\nminify:\n\t@ bun bin/minify.js\n\ncoverage:\n\tbun test --coverage\n\n\n\n\n"
},
{
"path": "packages/nuedom/README.md",
"chars": 1630,
"preview": "\n# Nuedom: HTML first UI assembly\nNuedom (or just \"Nue\") is a markup language that extends HTML with just enough syntax "
},
{
"path": "packages/nuedom/bin/minify.js",
"chars": 802,
"preview": "\nimport { version } from '../package.json' with { type: 'json' }\nconst year = new Date().getFullYear()\n\nconst banner = `"
},
{
"path": "packages/nuedom/bin/serve.js",
"chars": 461,
"preview": "\nconst server = Bun.serve({\n port: 4400,\n\n routes: {\n '/favicon.ico': async () => {\n return new Response('', {"
},
{
"path": "packages/nuedom/package.json",
"chars": 498,
"preview": "{\n \"name\": \"nuedom\",\n \"version\": \"0.1.0\",\n \"description\": \"HTML first UI assembly\",\n \"homepage\": \"https://nuejs.org/"
},
{
"path": "packages/nuedom/src/compiler/ast.js",
"chars": 3491,
"preview": "\nimport { HTML5_TAGS, SVG_TAGS } from './html5.js'\nimport { parseAttributes } from './attributes.js'\nimport { addContext"
},
{
"path": "packages/nuedom/src/compiler/attributes.js",
"chars": 3830,
"preview": "\nimport { BOOLEAN, EVENTS, STRICT_ATTRS } from './html5.js'\nimport { addContext } from './context.js'\n\n\nexport function "
},
{
"path": "packages/nuedom/src/compiler/compiler.js",
"chars": 1230,
"preview": "\n/* Compiles template string to JavaScript object (AST) */\nimport { inspect } from 'node:util'\nimport { parseNue } from "
},
{
"path": "packages/nuedom/src/compiler/context.js",
"chars": 783,
"preview": "\nimport { JS } from './html5.js'\n\n\nconst CONTEXT_RE = /'[^']*'|\"[^\"]*\"|[a-zA-Z_$][a-zA-Z0-9_$]*(?:\\.[a-zA-Z_$][a-zA-Z0-9"
},
{
"path": "packages/nuedom/src/compiler/document.js",
"chars": 3694,
"preview": "\nimport { createAST } from './ast.js'\nimport { tokenize } from './tokenizer.js'\n\n/*\n {\n doctype: 'html',\n script:"
},
{
"path": "packages/nuedom/src/compiler/html5.js",
"chars": 3074,
"preview": "\n// Web platform constants (HTML5, SVG, DOM APIs)\n\nexport const JS = '_ $e document window location localStorage session"
},
{
"path": "packages/nuedom/src/compiler/tokenizer.js",
"chars": 3718,
"preview": "\nimport { SELF_CLOSING } from './html5.js'\n\n\nexport function tokenize(template) {\n template = template.trim()\n const t"
},
{
"path": "packages/nuedom/src/dom/diff.js",
"chars": 2361,
"preview": "\n// Nue • (c) 2025 Tero Piirainen & contributors, MIT Licensed\n\n/*\n Universal DOM diff optimized for simplicity and per"
},
{
"path": "packages/nuedom/src/dom/fakedom.js",
"chars": 5272,
"preview": "\n// TODO: use html5.js\nconst caseSensitive = ['foreignObject', 'clipPath', 'linearGradient', 'radialGradient', 'textPath"
},
{
"path": "packages/nuedom/src/dom/node.js",
"chars": 7835,
"preview": "\n// Nue • (c) 2025 Tero Piirainen & contributors, MIT Licensed\n\nimport { domdiff } from './diff.js'\n\nconst is_browser = "
},
{
"path": "packages/nuedom/src/dom/render.js",
"chars": 910,
"preview": "\nimport { createDocument } from './fakedom.js'\nimport { parseNue } from '../compiler/document.js'\nimport { createNode } "
},
{
"path": "packages/nuedom/src/index.js",
"chars": 216,
"preview": "\n// Server-side API\nexport { renderNue } from './dom/render.js'\nexport { parseNue } from './compiler/document.js'\nexport"
},
{
"path": "packages/nuedom/src/nue-jit.js",
"chars": 400,
"preview": "\n// The browser API with compiler\nimport { parseNue } from './compiler/document.js'\nimport { createNode } from './dom/no"
},
{
"path": "packages/nuedom/src/nue.js",
"chars": 295,
"preview": "\n// The browser API\nimport { createNode } from './dom/node.js'\n\nexport { domdiff } from './dom/diff.js'\n\nexport function"
},
{
"path": "packages/nuedom/test/ast.test.js",
"chars": 3493,
"preview": "\nimport { createAST, convertFunctions, convertGetters } from '../src/compiler/ast.js'\nimport { tokenize } from '../src/c"
},
{
"path": "packages/nuedom/test/attributes.test.js",
"chars": 3805,
"preview": "\nimport {\n tokenizeAttr,\n parseAttributes,\n parseClassHelper,\n parseExpression,\n parseFor,\n parseForArgs,\n} from '"
},
{
"path": "packages/nuedom/test/compiler.test.js",
"chars": 1866,
"preview": "\nimport { compileFn } from '../src/compiler/compiler.js'\nimport { compileNue } from '..'\n\n\ntest('empty template', () =>"
},
{
"path": "packages/nuedom/test/component.test.js",
"chars": 3782,
"preview": "\nimport { renderNue } from '../src/dom/render.js'\n\ntest('self-closing', () => {\n expect(renderNue('<a/>')).toBe('<a></a"
},
{
"path": "packages/nuedom/test/context.test.js",
"chars": 3035,
"preview": "\nimport { addContext } from '../src/compiler/context.js'\n\ntest('simple variable', () => {\n expect(addContext('className"
},
{
"path": "packages/nuedom/test/document.test.js",
"chars": 2319,
"preview": "\nimport { parseNue, parseNames } from '../src/compiler/document.js'\n\ntest('doctype & root', () => {\n const doc = parseN"
},
{
"path": "packages/nuedom/test/domdiff.test.js",
"chars": 6386,
"preview": "\nimport { createDocument } from '../src/dom/fakedom.js'\nimport { domdiff } from '../src/dom/diff.js'\n\n// helper function"
},
{
"path": "packages/nuedom/test/event.test.js",
"chars": 3951,
"preview": "\nimport { clickable } from './event.util.js'\n\n\ntest('event handler', () => {\n const template = '<button :onclick=\"count"
},
{
"path": "packages/nuedom/test/event.util.js",
"chars": 478,
"preview": "\nimport { parseNue } from '../src/compiler/document.js'\nimport { mountAST } from '../src/dom/render.js'\n\n\nexport functio"
},
{
"path": "packages/nuedom/test/fakedom.test.js",
"chars": 4027,
"preview": "\nimport { createDocument } from '../src/dom/fakedom.js'\n\nconst document = createDocument()\n\n// helper functions\nconst el"
},
{
"path": "packages/nuedom/test/index.html",
"chars": 645,
"preview": "\n<!doctype html>\n\n<!--\n<script src=\"https://cdn.jsdelivr.net/gh/nuejs/nue@2.0/packages/nuedom/src/nue-jit.js\" type=\"modu"
},
{
"path": "packages/nuedom/test/loop.test.js",
"chars": 2641,
"preview": "\nimport { clickable } from './event.util.js'\nimport { renderNue } from '../src/dom/render.js'\n\ntest('loop index', () => "
},
{
"path": "packages/nuedom/test/render.test.js",
"chars": 6171,
"preview": "\nimport { renderNue } from '../src/dom/render.js'\n\njest.spyOn(console, 'error').mockImplementation(() => {})\njest.spyOn("
},
{
"path": "packages/nuedom/test/tokenizer.test.js",
"chars": 2843,
"preview": "\nimport { tokenize } from '../src/compiler/tokenizer.js'\n\n\ntest('tag', () => {\n expect(tokenize('<p>Hello</p>')).toEqua"
},
{
"path": "packages/nueglow/Makefile",
"chars": 21,
"preview": "\nrun-tests:\n\tbun test"
},
{
"path": "packages/nueglow/README.md",
"chars": 1901,
"preview": "\n# Nueglow: CSS first syntax highlighting\nNueglow is syntax highlighting that works with your design system. It generate"
},
{
"path": "packages/nueglow/css/build.js",
"chars": 576,
"preview": "// Running: bun css/build.js\nimport { promises as fs } from 'node:fs'\n\nasync function minify(names, toname) {\n const mi"
},
{
"path": "packages/nueglow/css/light.css",
"chars": 456,
"preview": "\n/* example light mode */\n\npre {\n --glow-bg-color: #f9f9f9;\n --glow-base-color: #555;\n\n /* brand coloring */\n --glow"
},
{
"path": "packages/nueglow/css/markers.css",
"chars": 1412,
"preview": "\n/* styling for specially marked lines */\n\npre {\n --glow-line-color: 50, 180, 250;\n --glow-del-color: 250, 110, 130;\n "
},
{
"path": "packages/nueglow/css/syntax.css",
"chars": 1768,
"preview": "\n\npre {\n background-color: var(--glow-bg-color, #20293A);\n color: var(--glow-base-color, #a2aab1);\n padding: var(--gl"
},
{
"path": "packages/nueglow/index.js",
"chars": 7533,
"preview": "\nconst MIXED_HTML = ['html', 'jsx', 'php', 'astro', 'dhtml', 'vue', 'svelte', 'hb']\nconst LINE_COMMENT = { clojure: ';;'"
},
{
"path": "packages/nueglow/package.json",
"chars": 716,
"preview": "{\n \"name\": \"nue-glow\",\n \"version\": \"0.2.5\",\n \"description\": \"Markdown syntax highlighter for CSS developers\",\n \"home"
},
{
"path": "packages/nueglow/test/generate.js",
"chars": 14724,
"preview": "import { promises as fs } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:u"
},
{
"path": "packages/nueglow/test/glow-test.css",
"chars": 1014,
"preview": "\n@import \"../css/syntax.css\";\n@import \"../css/markers.css\";\n/*@import \"../css/light.css\";*/\n\n*, *::before, *::after { bo"
},
{
"path": "packages/nueglow/test/glow.test.js",
"chars": 1937,
"preview": "import { parseRow, parseSyntax, renderRow, glow } from '..'\n\n\ntest('HTML', () => {\n const row = '<div class=\"hello\">'\n\n"
},
{
"path": "packages/nuekit/Makefile",
"chars": 28,
"preview": "\ntests:\n\tcd test && bun test"
},
{
"path": "packages/nuekit/README.md",
"chars": 1607,
"preview": "\n# The UNIX of the Web\nWeb development became complicated. Hundreds of packages, 400MB of dependencies, hours of configu"
},
{
"path": "packages/nuekit/client/error.js",
"chars": 527,
"preview": "\nfunction renderDialog(e) {\n return `\n <dialog id=\"nuerror\">\n <header>\n <h2>${ e.title }</h2>\n <p"
},
{
"path": "packages/nuekit/client/hmr.js",
"chars": 3741,
"preview": "\n\nfunction createConnection() {\n const server = new WebSocket(`ws://${location.host}`)\n\n server.onmessage = async func"
},
{
"path": "packages/nuekit/client/mount.js",
"chars": 1404,
"preview": "\n// both args are optional\nexport async function mountAll(reload_path) {\n const roots = document.querySelectorAll('[nue"
},
{
"path": "packages/nuekit/client/transitions.js",
"chars": 6841,
"preview": "// Client-side routing and view transitions for multipage applications\n\nfunction $(query, root = document) {\n return ro"
},
{
"path": "packages/nuekit/package.json",
"chars": 655,
"preview": "{\n \"name\": \"nuekit\",\n \"version\": \"2.0.0-beta.2\",\n \"description\": \"The UNIX of the Web\",\n \"homepage\": \"https://nuejs."
},
{
"path": "packages/nuekit/src/asset.js",
"chars": 4147,
"preview": "\nimport { join } from 'node:path'\n\nimport { parseNuemark } from 'nuemark'\nimport { parseYAML } from 'nueyaml'\nimport { p"
},
{
"path": "packages/nuekit/src/cli.js",
"chars": 4632,
"preview": "#!/usr/bin/env bun\n\nimport { styleText } from 'node:util'\nimport { version } from './system'\n\n// [-npe] --> [-n, -p, -e]"
},
{
"path": "packages/nuekit/src/cmd/build.js",
"chars": 3422,
"preview": "\nimport { mkdir, rmdir, writeFile, unlink } from 'node:fs/promises'\nimport { join, sep } from 'node:path'\nimport { tmpdi"
},
{
"path": "packages/nuekit/src/cmd/create.js",
"chars": 2110,
"preview": "\nimport { mkdir } from 'node:fs/promises'\nimport { join } from 'node:path'\n\nexport const NAMES = 'blog full minimal spa'"
},
{
"path": "packages/nuekit/src/cmd/preview.js",
"chars": 1172,
"preview": "\nimport { extname, join } from 'node:path'\n\nimport { createServer } from '../tools/server'\nimport { getServer } from '.."
},
{
"path": "packages/nuekit/src/cmd/serve.js",
"chars": 3083,
"preview": "\nimport { extname, join } from 'node:path'\n\nimport { generateSitemap, generateFeed } from '../render/feed'\nimport { crea"
},
{
"path": "packages/nuekit/src/collections.js",
"chars": 1452,
"preview": "\n// assumes is_md\nexport async function getCollections(files, opts) {\n const arr = {}\n\n for (const [name, conf] of Obj"
},
{
"path": "packages/nuekit/src/conf.js",
"chars": 2117,
"preview": "\nimport { readdir } from 'node:fs/promises'\nimport { join } from 'node:path'\n\nimport { parseYAML } from 'nueyaml'\n\n// co"
},
{
"path": "packages/nuekit/src/deps.js",
"chars": 2041,
"preview": "\nimport { join, normalize, dirname, extname, basename, sep } from 'node:path'\n\n// app, lib, server are @shared, but not "
},
{
"path": "packages/nuekit/src/file.js",
"chars": 1873,
"preview": "\nimport { parse, sep, join } from 'node:path'\nimport { lstat } from 'node:fs/promises'\n\nexport async function createFile"
},
{
"path": "packages/nuekit/src/render/feed.js",
"chars": 2197,
"preview": "\nimport { createCollection } from '../collections'\nimport { trim } from './page'\n\nconst XML = '<?xml version=\"1.0\" encod"
},
{
"path": "packages/nuekit/src/render/head.js",
"chars": 3495,
"preview": "\nimport { parse, sep } from 'node:path'\n\nimport { elem } from 'nuemark'\nimport { version } from '../system'\nimport { min"
},
{
"path": "packages/nuekit/src/render/page.js",
"chars": 5591,
"preview": "\nimport { renderScripts, renderHead } from './head'\nimport { renderNue, compileNue } from 'nuedom'\nimport { elem, render"
},
{
"path": "packages/nuekit/src/render/svg.js",
"chars": 3348,
"preview": "\nimport { parseYAMLArray } from 'nueyaml'\n\nimport { renderNue } from 'nuedom'\nimport { elem } from 'nuemark'\n\nimport { i"
},
{
"path": "packages/nuekit/src/server/README.md",
"chars": 491,
"preview": "\n# User server\nDevelopment environment for the user's server-side code. Not to confuse with tools/server.js, which is th"
},
{
"path": "packages/nuekit/src/server/index.js",
"chars": 196,
"preview": "\nimport { createWorker } from './worker.js'\nimport { createProxy } from './proxy.js'\n\nexport async function getServer(op"
},
{
"path": "packages/nuekit/src/server/model.js",
"chars": 2610,
"preview": "\nimport { readdir, mkdir, readFile, writeFile } from 'node:fs/promises'\nimport { join } from 'node:path'\n\nconst SESSIONS"
},
{
"path": "packages/nuekit/src/server/proxy.js",
"chars": 513,
"preview": "\nexport function createProxy(opts) {\n\n return async function(req) {\n const url = new URL(req.url)\n const match = "
},
{
"path": "packages/nuekit/src/server/worker.js",
"chars": 1993,
"preview": "\nimport { join } from 'node:path'\nimport { existsSync } from 'fs'\n\nimport { routes, fetch, matches } from 'nue-edgeserve"
},
{
"path": "packages/nuekit/src/site.js",
"chars": 2110,
"preview": "\nimport { parse, sep } from 'node:path'\nimport { fswalk } from './tools/fswalk'\nimport { createAsset } from './asset'\nim"
},
{
"path": "packages/nuekit/src/system.js",
"chars": 1854,
"preview": "\nimport { mkdir, rm } from 'node:fs/promises'\nimport { fileURLToPath } from 'node:url'\nimport { join } from 'node:path'\n"
},
{
"path": "packages/nuekit/src/tools/README.md",
"chars": 287,
"preview": "\n# Tools\nGeneric utilities that can potentially be used outside Nue.\n\n## Files\n\n**css.js** - CSS parser & minifier. Turn"
},
{
"path": "packages/nuekit/src/tools/css.js",
"chars": 4894,
"preview": "export function minifyCSS(str) {\n return parseCSS(str).map(serialize).join('')\n}\n\nexport function parseCSS(str) {\n con"
},
{
"path": "packages/nuekit/src/tools/fswalk.js",
"chars": 1676,
"preview": "\nimport { readdir, stat } from 'node:fs/promises'\nimport { parse, join, relative } from 'node:path'\n\nexport function mat"
},
{
"path": "packages/nuekit/src/tools/fswatch.js",
"chars": 1718,
"preview": "\nimport { promises as fs, watch } from 'node:fs'\nimport { join, extname } from 'node:path'\nimport { fswalk, matches } fr"
},
{
"path": "packages/nuekit/src/tools/server.js",
"chars": 1621,
"preview": "\nexport function createServer({ port=4000, handler }, callback) {\n\n async function fetch(req) {\n const { pathname, s"
},
{
"path": "packages/nuekit/test/cli.test.js",
"chars": 551,
"preview": "\nimport { expandArgs, getArgs } from '../src/cli'\n\ntest('expandArgs', () => {\n expect(expandArgs(['--silent', '-pve']))"
},
{
"path": "packages/nuekit/test/cmd/build.test.js",
"chars": 4492,
"preview": "\nimport { join } from 'node:path'\nimport { build, matches, stats, buildAsset, buildAll, minifyJS } from '../../src/cmd/b"
},
{
"path": "packages/nuekit/test/cmd/create.test.js",
"chars": 921,
"preview": "\nimport { create, unzip, getLocalZip, fetchZip } from '../../src/cmd/create'\nimport { rm, readdir } from 'node:fs/promis"
},
{
"path": "packages/nuekit/test/cmd/serve.test.js",
"chars": 2249,
"preview": "\nimport { findAssetByURL, onServe } from '../../src/cmd/serve.js'\n\ntest('find /', () => {\n const asset = findAssetByURL"
},
{
"path": "packages/nuekit/test/collections.test.js",
"chars": 2329,
"preview": "\nimport { getCollections } from '../src/collections.js'\nimport { createAsset } from '../src/asset'\n\n// Mock page objects"
},
{
"path": "packages/nuekit/test/conf.test.js",
"chars": 2674,
"preview": "\nimport { readSiteConf, mergeData, mergeValue } from '../src/conf'\nimport { testDir, writeAll, removeAll } from './test-"
},
{
"path": "packages/nuekit/test/css.test.js",
"chars": 4252,
"preview": "\nimport { tokenize, parseCSS, minifyCSS } from '../src/tools/css'\n\ndescribe('tokenizer', () => {\n\n test('comments', () "
},
{
"path": "packages/nuekit/test/deps.test.js",
"chars": 2810,
"preview": "\nimport { test, expect } from 'bun:test'\nimport { listDependencies, parseDirs } from '../src/deps'\n\nconst paths = [\n\n /"
},
{
"path": "packages/nuekit/test/file.test.js",
"chars": 1736,
"preview": "\nimport { join, parse } from 'node:path'\n\nimport { testDir, write, removeAll } from './test-utils'\nimport { createFile, "
},
{
"path": "packages/nuekit/test/fswalk.test.js",
"chars": 2506,
"preview": "\nimport { mkdir } from 'node:fs/promises'\nimport { join } from 'node:path'\n\nimport { testDir, writeAll, removeAll } from"
},
{
"path": "packages/nuekit/test/fswatch.test.js",
"chars": 4090,
"preview": "import { test, expect } from 'bun:test'\nimport { promises as fs } from 'node:fs'\nimport { join } from 'node:path'\n\nimpor"
},
{
"path": "packages/nuekit/test/render/asset-render.test.js",
"chars": 1880,
"preview": "\nimport { createAsset } from '../../src/asset'\n\n\ntest('index.html', async () => {\n const page = createAsset({\n async"
},
{
"path": "packages/nuekit/test/render/feed.test.js",
"chars": 2211,
"preview": "\nimport { generateSitemap, generateFeed, renderSitemap, renderFeed } from '../../src/render/feed'\n\n\ntest('generateSitema"
},
{
"path": "packages/nuekit/test/render/head.test.js",
"chars": 2321,
"preview": "\nimport { renderScripts, renderStyles, renderMeta, renderHead, } from '../../src/render/head'\n\ntest('renderScripts', () "
},
{
"path": "packages/nuekit/test/render/md.test.js",
"chars": 1964,
"preview": "\nimport { createAsset } from '../../src/asset'\n\n\ntest('MD: page assets', async () => {\n\n const files = [\n { is_html:"
},
{
"path": "packages/nuekit/test/render/page.test.js",
"chars": 2890,
"preview": "\nimport {\n renderMD,\n renderPage,\n renderSlots,\n renderDHTML,\n renderHTML,\n\n} from '../../src/render/page'\n\n\ntest('"
},
{
"path": "packages/nuekit/test/render/svg.test.js",
"chars": 2403,
"preview": "\nimport { renderFonts, renderHMR, renderSVG, convertHTMLTag } from '../../src/render/svg'\n\nconst testfile = import.meta."
},
{
"path": "packages/nuekit/test/server/model.test.js",
"chars": 953,
"preview": "\nimport { testDir, writeAll, removeAll } from '../test-utils'\nimport { createEnv } from '../../src/server/model'\n\n\nafter"
},
{
"path": "packages/nuekit/test/server/proxy.test.js",
"chars": 1205,
"preview": "\nimport { createProxy } from '../../src/server/proxy.js'\n\ntest('proxy forwards matching requests', async () => {\n globa"
},
{
"path": "packages/nuekit/test/server/worker.test.js",
"chars": 1021,
"preview": "\nimport { importWorker, createWorker } from '../../src/server/worker'\nimport { testDir, write, removeAll } from '../test"
},
{
"path": "packages/nuekit/test/server.test.js",
"chars": 3085,
"preview": "\nimport { testDir, writeAll, removeAll } from './test-utils'\nimport { broadcast, createServer } from '../src/tools/serve"
},
{
"path": "packages/nuekit/test/site.test.js",
"chars": 1035,
"preview": "\nimport { sortAssets, mergeSharedData } from '../src/site'\n\ntest('sortAssets', () => {\n const sorted = sortAssets([\n "
},
{
"path": "packages/nuekit/test/system.test.js",
"chars": 674,
"preview": "\nimport { version, getSystemFiles, createSystemFiles } from '../src/system'\nimport { testDir, removeAll } from './test-u"
},
{
"path": "packages/nuekit/test/test-utils.js",
"chars": 1449,
"preview": "\nimport { writeFile, mkdir, rmdir } from 'node:fs/promises'\nimport { join } from 'node:path'\n\nimport { fswalk } from '.."
},
{
"path": "packages/nuemark/.npmignore",
"chars": 6,
"preview": "test/\n"
},
{
"path": "packages/nuemark/Makefile",
"chars": 35,
"preview": "tests:\n\tcd test && bun test # block"
},
{
"path": "packages/nuemark/README.md",
"chars": 2349,
"preview": "\n# Nuemark: Content first web development\nNuemark is a Markdown-based authoring format for rich, interactive content. It"
},
{
"path": "packages/nuemark/index.js",
"chars": 490,
"preview": "\nimport { parseDocument } from './src/parse-document.js'\nimport { renderLines } from './src/render-blocks.js'\n\nconst EOL"
},
{
"path": "packages/nuemark/package.json",
"chars": 692,
"preview": "{\n \"name\": \"nuemark\",\n \"version\": \"0.7.0\",\n \"description\": \"Markdown flavour for rich, interactive websites\",\n \"home"
},
{
"path": "packages/nuemark/src/parse-blocks.js",
"chars": 6275,
"preview": "\nimport { parseYAML } from 'nueyaml'\n\nimport { parseInline, parseLinkTitle } from './parse-inline.js'\nimport { parseTag "
},
{
"path": "packages/nuemark/src/parse-document.js",
"chars": 3349,
"preview": "\nimport { parseYAML } from 'nueyaml'\n\nimport { parseBlocks } from './parse-blocks.js'\nimport { parseLinkTitle } from './"
},
{
"path": "packages/nuemark/src/parse-inline.js",
"chars": 4836,
"preview": "\n/* Inline tokenizer */\nimport { parseAttr, parseTag } from './parse-tag.js'\n\n\nexport const FORMATTING = {\n '***': 'EM'"
},
{
"path": "packages/nuemark/src/parse-tag.js",
"chars": 2281,
"preview": "\nconst ATTR = 'id is class style hidden disabled popovertarget popover'.split(' ')\n\n/*\n --> { name, attr, data }\n*/\nexp"
},
{
"path": "packages/nuemark/src/render-blocks.js",
"chars": 3125,
"preview": "\nimport { glow } from 'nue-glow'\n\nimport { parseBlocks } from './parse-blocks.js'\nimport { renderInline, renderTokens } "
},
{
"path": "packages/nuemark/src/render-inline.js",
"chars": 1822,
"preview": "\nimport { ESCAPED, parseInline } from './parse-inline.js'\nimport { elem } from './render-blocks.js'\nimport { renderTag }"
},
{
"path": "packages/nuemark/src/render-tag.js",
"chars": 8714,
"preview": "\nimport { join, extname } from 'node:path'\nimport { readFileSync } from 'node:fs'\n\nimport { sectionize } from './parse-d"
},
{
"path": "packages/nuemark/test/block.test.js",
"chars": 8028,
"preview": "\nimport { nuemark } from '../index.js'\nimport { getBreak, parseBlocks, parseHeading } from '../src/parse-blocks.js'\nimpo"
},
{
"path": "packages/nuemark/test/document.test.js",
"chars": 2734,
"preview": "\nimport { parseBlocks } from '../src/parse-blocks.js'\nimport { parseDocument, sectionize, stripMeta } from '../src/parse"
},
{
"path": "packages/nuemark/test/inline.test.js",
"chars": 8697,
"preview": "\nimport { parseInline, parseLink } from '../src/parse-inline.js'\nimport { renderInline, renderToken, renderTokens } from"
},
{
"path": "packages/nuemark/test/tag.test.js",
"chars": 8855,
"preview": "\nimport { dirname, join, relative } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nimport { parseAttr, parse"
},
{
"path": "packages/nueserver/Makefile",
"chars": 21,
"preview": "\nrun-tests:\n\tbun test"
},
{
"path": "packages/nueserver/README.md",
"chars": 3497,
"preview": "\n# Nueserver: Edge first development\nNueserver is an HTTP server built for edge deployment. Write code locally, deploy g"
},
{
"path": "packages/nueserver/nueserver.js",
"chars": 3510,
"preview": "\n// Nueserver: Edge-first HTTP server\nexport const routes = []\n\n// Context for each request\nfunction createContext(req, "
},
{
"path": "packages/nueserver/package.json",
"chars": 372,
"preview": "{\n \"name\": \"nue-edgeserver\",\n \"version\": \"0.1.0\",\n \"description\": \"Edge first server development\",\n \"homepage\": \"htt"
},
{
"path": "packages/nueserver/test/route.test.js",
"chars": 1266,
"preview": "\nimport { matchPath } from '..'\n\nconst cases = [\n // Basic matching\n ['/', '/', true, {}],\n ['/users', '/users', true"
},
{
"path": "packages/nueserver/test/server.test.js",
"chars": 1760,
"preview": "import { fetch, routes } from '..'\n\n// clear routes\nafterAll(() => routes.length = 0)\n\ntest('GET', async () => {\n get('"
},
{
"path": "packages/nuestate/Makefile",
"chars": 32,
"preview": "\nrun-tests:\n\tcd test && bun test"
},
{
"path": "packages/nuestate/README.md",
"chars": 2165,
"preview": "\n# Nuestate: URL-first state management\nNuestate puts your application state in the URL by default. This makes bookmarki"
},
{
"path": "packages/nuestate/package.json",
"chars": 354,
"preview": "{\n \"name\": \"nuestate\",\n \"version\": \"0.1.1\",\n \"description\": \"URL-first state management\",\n \"homepage\": \"https://nuej"
},
{
"path": "packages/nuestate/src/state.js",
"chars": 5145,
"preview": "\nif (typeof global == 'object') global.window = null\n\nconst CONTEXTS = ['path_params', 'query', 'session', 'local', 'emi"
},
{
"path": "packages/nuestate/test/browser.test.js",
"chars": 2255,
"preview": "import { test, expect, mock } from 'bun:test'\nimport { state } from '../src/state.js'\n\nglobal.sessionStorage = {}\nglobal"
},
{
"path": "packages/nuestate/test/index.html",
"chars": 723,
"preview": "\n<p>\n <a href=\"/test/\">Home</a>\n</p>\n\n<p>\n <a href=\"/test/customers/\">/customers/</a>\n <a href=\"/test/users/\">/users/"
},
{
"path": "packages/nuestate/test/state.test.js",
"chars": 2538,
"preview": "\nimport { getPathData, getQueryData, renderPath, renderQuery, api, state } from '../src/state.js'\n\nbeforeAll(() => globa"
},
{
"path": "packages/nueyaml/Makefile",
"chars": 16,
"preview": "\ntest:\n\tbun test"
},
{
"path": "packages/nueyaml/README.md",
"chars": 1259,
"preview": "# Nueyaml: YAML without the problems\nNueyaml is YAML stripped down to its essence so you can write complex configuration"
},
{
"path": "packages/nueyaml/nueyaml.js",
"chars": 8882,
"preview": "\nexport function stripComments(line) {\n // Full line comment\n if (line.trim().startsWith('#')) return ''\n\n // Inline "
},
{
"path": "packages/nueyaml/nueyaml.test.js",
"chars": 6632,
"preview": "import {\n stripComments,\n measureIndent,\n isNumber,\n parseValue,\n parseYAMLArray,\n detectStructure,\n buildObject,"
},
{
"path": "packages/nueyaml/package.json",
"chars": 352,
"preview": "{\n \"name\": \"nueyaml\",\n \"version\": \"0.1.0\",\n \"description\": \"YAML without the problems\",\n \"homepage\": \"https://nuejs."
},
{
"path": "packages/templates/blog/index.css",
"chars": 2746,
"preview": "\n/* settings */\n@layer base, components;\n\n@layer base {\n\n *, *:before, *:after {\n box-sizing: border-box;\n }\n\n /*"
},
{
"path": "packages/templates/blog/index.html",
"chars": 496,
"preview": "\n<!doctype html>\n\n<article>\n <h1>Design engineering blog</h1>\n\n <ul>\n <li :each=\"page in blog\">\n <a href=\"{ pa"
},
{
"path": "packages/templates/blog/layout.html",
"chars": 145,
"preview": "\n<!doctype html lib>\n\n<header>\n <a href=\"/\">{ author }</a>\n</header>\n\n<footer>\n © Copyright { new Date().getFullYear()"
},
{
"path": "packages/templates/blog/posts/components.html",
"chars": 339,
"preview": "\n\n<header :is=\"pagehead\">\n { formatDate(date) }\n\n <script>\n formatDate(date) {\n return new Date(date).toLocale"
},
{
"path": "packages/templates/blog/posts/css-is-awesome.md",
"chars": 6481,
"preview": "---\ndate: 2025-08-05\n---\n\n# Modern CSS is awesome\nCSS has transformed dramatically over the past decade, but the JavaScr"
},
{
"path": "packages/templates/blog/posts/design-engineering.md",
"chars": 4519,
"preview": "---\ndate: 2025-08-15\n---\n\n# What is design engineering?\nWeb development split into two camps: those who design and those"
},
{
"path": "packages/templates/blog/posts/design-systems.md",
"chars": 4852,
"preview": "---\ndate: 2025-08-01\n---\n\n# What is a CSS design system?\nModern CSS has everything needed for real design systems. Varia"
},
{
"path": "packages/templates/blog/posts/extreme-performance.md",
"chars": 6429,
"preview": "\n---\ndate: 2025-09-15\n---\n\n# Extreme performance\nToday the web performance industry optimizes the wrong things. Complex "
},
{
"path": "packages/templates/blog/site.yaml",
"chars": 394,
"preview": "\nmeta:\n title: Emma Bennet / Design Engineering Blog\n title_template: %s / Emma Bennet\n author: Emma Bennet\n\nsite:\n "
},
{
"path": "packages/templates/full/.gitignore",
"chars": 11,
"preview": ".dist\n.nue\n"
},
{
"path": "packages/templates/full/404.md",
"chars": 37,
"preview": "\n# 404 Not found\nPage not implemented"
},
{
"path": "packages/templates/full/@shared/design/README.md",
"chars": 498,
"preview": "\n# Basic rules\n\n**Element selectors for 90% of things** class names only for the missing elements in the HTML spec.\n\n**P"
},
{
"path": "packages/templates/full/@shared/design/base.css",
"chars": 429,
"preview": "\n@layer base, component, modifier;\n\n@layer base {\n\n :root {\n\n /* base colors */\n --base-200: #E2E8F0;\n --base-"
},
{
"path": "packages/templates/full/@shared/design/button.css",
"chars": 525,
"preview": "\n@layer component {\n\n button, .button {\n background-color: var(--base-800);\n text-align: center;\n font-family:"
},
{
"path": "packages/templates/full/@shared/design/components.css",
"chars": 297,
"preview": "\n\n@layer component {\n\n .logo {\n font-weight: 550;\n b { color: var(--red) }\n &:hover { color: black }\n }\n\n\n ."
},
{
"path": "packages/templates/full/@shared/design/content.css",
"chars": 1410,
"preview": "\n/* Content elements (headings, paragraphs, lists, images, blockquotes, ...) */\n\n@layer base {\n *, *:before, *:after {\n"
},
{
"path": "packages/templates/full/@shared/design/dialog.css",
"chars": 208,
"preview": "\ndialog {\n margin-top: 10em;\n padding: 1.5em;\n max-width: 100%;\n width: 25em;\n\n > :first-child { margin: 0 }\n\n &::"
},
{
"path": "packages/templates/full/@shared/design/document.css",
"chars": 1055,
"preview": "\n/* For content heavy documents only (excluded from single-page-apps) */\n\n@layer base {\n\n article {\n flex-direction:"
},
{
"path": "packages/templates/full/@shared/design/figure.css",
"chars": 435,
"preview": "\n@layer component {\n\n /* placeholder images */\n figure {\n background-color: var(--base-200);\n margin: var(--m) 0"
},
{
"path": "packages/templates/full/@shared/design/form.css",
"chars": 711,
"preview": "\n@layer base {\n\n form {\n flex-direction: column;\n margin-block: 1em;\n display: flex;\n gap: 1em;\n\n }\n\n lab"
},
{
"path": "packages/templates/full/@shared/design/layout.css",
"chars": 717,
"preview": "\n@layer base {\n body {\n max-width: 1000px;\n flex-direction: column;\n min-height: 100vh;\n margin: 0 auto;\n "
},
{
"path": "packages/templates/full/@shared/design/modifier.css",
"chars": 390,
"preview": "\n/* universal modifiers */\n@layer modifier {\n\n /* size */\n .thin { max-width: 25em }\n .wide { max-width: inher"
},
{
"path": "packages/templates/full/@shared/design/syntax.css",
"chars": 1176,
"preview": "\n@layer base {\n pre {\n border: 1px solid var(--base-200);\n counter-reset: line-number 0;\n color: var(--base-60"
},
{
"path": "packages/templates/full/@shared/design/table.css",
"chars": 473,
"preview": "\n@layer base {\n\n table {\n border-collapse: collapse;\n width: 100%;\n\n tr {\n vertical-align: baseline;\n "
},
{
"path": "packages/templates/full/@shared/lib/crud.js",
"chars": 889,
"preview": "\nexport async function post(route, data) {\n const resp = await fetch(route, {\n headers: { 'Content-Type': 'applicati"
},
{
"path": "packages/templates/full/@shared/server/data/leads.json",
"chars": 12970,
"preview": "[\n {\n \"email\": \"sarah.johnson@gmail.com\",\n \"country\": \"US\",\n \"subscribed\": true,\n \"source\": \"www.google.com"
},
{
"path": "packages/templates/full/@shared/server/data/users.json",
"chars": 61,
"preview": "[\n { \"email\": \"admin@example.com\", \"password\": \"demo123\" }\n]"
},
{
"path": "packages/templates/full/@shared/server/index.js",
"chars": 1541,
"preview": "\n\n// login\npost('/api/login', async (c) => {\n const { users } = c.env\n const { email, password } = await c.req.json()\n"
},
{
"path": "packages/templates/full/@shared/ui/components.html",
"chars": 144,
"preview": "\n<figure :is=\"placeholder\" class=\"{ class }\">\n <div --height=\"{ height }px\"/>\n</figure>\n\n<logo class=\"logo\">\n <b>■</b>"
},
{
"path": "packages/templates/full/@shared/ui/footer.html",
"chars": 139,
"preview": "<!doctype html lib>\n\n<footer>\n © 2025 <logo/>\n\n <nav>\n <a href=\"/legal/\">Legal</a>\n <a href=\"/terms/\">Terms</a>\n"
},
{
"path": "packages/templates/full/@shared/ui/header.html",
"chars": 247,
"preview": "<!doctype html lib>\n\n<header>\n <nav>\n <a href=\"/\"><logo/></a>\n <a href=\"/docs/\">Docs</a>\n <a href=\"/blog/\">Blo"
},
{
"path": "packages/templates/full/@shared/ui/isomorphic.html",
"chars": 406,
"preview": "<!doctype html+dhtml lib>\n\n<!--\n Used on server layouts and in client app\n-->\n\n<time :is=\"pretty-date\">\n { pretty }\n\n "
},
{
"path": "packages/templates/full/Makefile",
"chars": 102,
"preview": "\ninit:\n\tcd @shared/server && bun db/init/load-data autoload\n\ntest:\n\tcd @shared/server && bun test\n\n\n\n"
},
{
"path": "packages/templates/full/admin/app.yaml",
"chars": 26,
"preview": "\nexclude: [ document.css ]"
},
{
"path": "packages/templates/full/admin/index.html",
"chars": 931,
"preview": "<!doctype dhtml>\n\n<script>\n import { get, post } from 'crud'\n import { state } from 'state'\n\n state.setup({\n query"
},
{
"path": "packages/templates/full/admin/ui/lead.html",
"chars": 1123,
"preview": "<!doctype dhtml lib>\n\n<script>\n import { state } from 'state'\n import { get } from 'crud'\n</script>\n\n<article :is=\"con"
},
{
"path": "packages/templates/full/admin/ui/leads.html",
"chars": 1937,
"preview": "<!doctype dhtml lib>\n\n<script>\n import { state } from 'state'\n</script>\n\n<article :is=\"contact-list\">\n\n <h1>Leads</h1>"
},
{
"path": "packages/templates/full/admin/ui/shared.html",
"chars": 1310,
"preview": "<!doctype dhtml lib>\n\n<script>\n import { state } from 'state'\n import { del } from 'crud'\n</script>\n\n<!-- pretty date "
},
{
"path": "packages/templates/full/blog/components.html",
"chars": 312,
"preview": "\n<pagehead>\n <pretty-date :date=\"date\"/> • <span>{ author }</span>\n</pagehead>\n\n\n<table :is=\"blog-entries\">\n <tr :each"
},
{
"path": "packages/templates/full/blog/css-beats-js.md",
"chars": 7346,
"preview": "\n---\ndate: 2025-08-05\n---\n\n# Why CSS beats CSS-in-JS\nCSS-in-JS promised to solve CSS problems by moving styles into Java"
},
{
"path": "packages/templates/full/blog/css-is-awesome.md",
"chars": 6476,
"preview": "---\ndate: 2025-08-05\n---\n\n# Modern CSS is awesome\nCSS has transformed dramatically over the past decade, but the JavaScr"
},
{
"path": "packages/templates/full/blog/design-engineering.md",
"chars": 4514,
"preview": "---\ndate: 2025-08-15\n---\n\n# What is design engineering?\nWeb development split into two camps: those who design and those"
},
{
"path": "packages/templates/full/blog/design-systems.md",
"chars": 4847,
"preview": "---\ndate: 2025-08-01\n---\n\n# What is a CSS design system?\nModern CSS has everything needed for real design systems. Varia"
},
{
"path": "packages/templates/full/blog/extreme-performance.md",
"chars": 6424,
"preview": "\n---\ndate: 2025-09-15\n---\n\n# Extreme performance\nToday the web performance industry optimizes the wrong things. Complex "
},
{
"path": "packages/templates/full/blog/index.md",
"chars": 61,
"preview": "\n---\npagehead: false\nskip: true\n---\n\n# Blog\n\n[blog-entries]\n\n"
},
{
"path": "packages/templates/full/blog/web-standards.md",
"chars": 3924,
"preview": "\n---\ndate: 2025-09-05\nauthor: Maude Bonnet\n---\n\n# Web standards never get old\nHTML from 2006 still works. CSS only grows"
},
{
"path": "packages/templates/full/contact/contact.html",
"chars": 1046,
"preview": "<!doctype dhtml lib>\n\n<script>\n import { post } from 'crud'\n</script>\n\n<form :is=\"contact-form\" :onsubmit=\"submit\" clas"
},
{
"path": "packages/templates/full/contact/index.md",
"chars": 85,
"preview": "\n\n# Contact us\n\n[.columns]\n [contact-form]\n ---\n [placeholder.yellow height=\"500\"]"
},
{
"path": "packages/templates/full/contact/thanks.md",
"chars": 121,
"preview": "\n# Thank you\nWe got your message and will be in touch soon. Someone from our team will get back to you within 24 hours.\n"
}
]
// ... and 121 more files (download for full content)
About this extraction
This page contains the full source code of the nuejs/nue GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 321 files (803.8 KB), approximately 221.0k tokens, and a symbol index with 480 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.