nattoppet 4.0.1 → 4.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/GUIDE.md CHANGED
@@ -34,8 +34,8 @@ This guide covers writing `.ymd` files from the perspective of an end user.
34
34
 
35
35
  ### Prerequisites
36
36
 
37
- - [Bun](https://bun.sh) (≥1.0)
38
- - Clone this repository and run `bun install` in the project root.
37
+ - [Node.js](https://nodejs.org/) 22.18 or newer.
38
+ - Install Nattoppet with `npm install -g nattoppet`.
39
39
 
40
40
  ### Your first document
41
41
 
@@ -56,13 +56,13 @@ This line is not indented, so it passes through as raw HTML.
56
56
  Compile it:
57
57
 
58
58
  ```bash
59
- bun run nattoppet.ts input.ymd > output.html
59
+ nattoppet input.ymd > output.html
60
60
  ```
61
61
 
62
62
  Preview with live reload:
63
63
 
64
64
  ```bash
65
- bun run nattoppet-dev.ts input.ymd
65
+ nattoppet-dev input.ymd
66
66
  ```
67
67
 
68
68
  The `--dev` flag disables minification and is useful for debugging. Both compilation modes write to stdout.
@@ -194,7 +194,8 @@ A mixin inlines another file into the current token stream.
194
194
  - If the path has an extension, it is used as-is.
195
195
  - If the path has no extension and a file with that exact name exists, it is used.
196
196
  - Otherwise, `.ymd` is appended (e.g. `[mixin] common` resolves to `common.ymd`).
197
- - **All mixin paths are resolved relative to the nattoppet installation directory** (where `compiler.ts` lives). Paths starting with `./` are **not** treated specially; they also resolve relative to the compiler root.
197
+ - Bare paths such as `common` resolve relative to the nattoppet installation directory.
198
+ - Explicit relative paths beginning with `./` or `../` resolve relative to the input document. Relative paths inside a mixin resolve relative to that mixin.
198
199
 
199
200
  ### Non-`.ymd` mixins
200
201
 
@@ -213,7 +214,7 @@ A mixin can contain a `[#slot]` marker. When the mixin is expanded, the caller's
213
214
  [#slot]
214
215
 
215
216
  [#] -- page.ymd --
216
- [mixin] layout.ymd
217
+ [mixin] ./layout.ymd
217
218
 
218
219
  [title]: My Page
219
220
  <h1>Hello</h1>
@@ -223,7 +224,7 @@ A mixin can contain a `[#slot]` marker. When the mixin is expanded, the caller's
223
224
 
224
225
  ### `common.ymd`
225
226
 
226
- `common.ymd` ships with nattoppet in the project root. Since mixin paths resolve relative to the compiler root, `[mixin] common` finds it automatically. All built-in themes include it, which is why macros like `[require]`, `[img]`, and `[code]` are available in themed documents.
227
+ `common.ymd` ships with nattoppet in the project root. Since bare mixin paths resolve relative to the compiler root, `[mixin] common` finds it automatically. All built-in themes include it, which is why macros like `[require]`, `[img]`, and `[code]` are available in themed documents.
227
228
 
228
229
  **Trap**: If you write a bare `.ymd` without a theme and want `[require]`, you must add `[mixin] common.ymd` yourself. `[require]` is not a compiler built-in; it is a function macro defined in `common.ymd`.
229
230
 
@@ -633,7 +634,7 @@ The delimiter passed to `capture_until` is consumed from `remaining` but is **no
633
634
  | `definition X not found` | You invoked `[X]` but never defined `[X]:` or `[X]=`. | Define the macro, or check for typos. |
634
635
  | `definition X not found` inside a self-referential ref | Reference macros cannot reference themselves. | Use a function macro if you need recursion, or restructure. |
635
636
  | Multiple `[#slot]` error | A mixin contains more than one slot. | Remove extra slots, or split into multiple mixins. |
636
- | `mixin not found: X` | The mixin path could not be resolved. | Check the path; remember all mixin paths resolve relative to the compiler root, not the input file. |
637
+ | `mixin not found: X` | The mixin path could not be resolved. | Use a bare name for a built-in mixin, or begin a project-local path with `./` or `../`. |
637
638
  | Math does not render, or renders inline when it should be display | Missing `>>...<<` block delimiters around multi-line `[$$]`. | Use `[$$]>>...<<`. |
638
639
  | CJK text has unwanted spaces | Hard line breaks between CJK characters are preserved as spaces by HTML. | Wrap the text in `[cn]>>...<<`. |
639
640
  | Paragraphs wrap raw HTML | Indented HTML is treated as prose. | Remove the leading two spaces from HTML lines. |
@@ -651,9 +652,9 @@ The delimiter passed to `capture_until` is consumed from `remaining` but is **no
651
652
  You can turn a `.ymd` file into a desktop application:
652
653
 
653
654
  ```bash
654
- bun run nattoppet-native.ts init # create native/ Rust scaffold
655
- bun run nattoppet-native.ts bundle file.ymd # compile HTML into native/
656
- bun run nattoppet-native.ts build file.ymd # compile HTML and run cargo build --release
655
+ nattoppet-native init # create native/ Rust scaffold
656
+ nattoppet-native bundle file.ymd # compile HTML into native/
657
+ nattoppet-native build file.ymd # compile HTML and run cargo build --release
657
658
  ```
658
659
 
659
660
  The result is a single native binary (Linux/macOS/Windows) that embeds the HTML and renders it with a web-view.
@@ -661,7 +662,7 @@ The result is a single native binary (Linux/macOS/Windows) that embeds the HTML
661
662
  ### Project scaffolding
662
663
 
663
664
  ```bash
664
- bun run nattoppet-init.ts form
665
+ nattoppet-init form
665
666
  ```
666
667
 
667
668
  This generates a starter `main.ymd`, `main.js`, and optionally Rust/WASM boilerplate plus a GitHub Pages deployment workflow.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shiwei Zhang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire as createNattoppetRequire } from 'node:module'; const require = createNattoppetRequire(import.meta.url);
3
+
4
+ // nattoppet-dev.ts
5
+ import * as fs from "node:fs";
6
+ import * as path from "node:path";
7
+ import * as http from "node:http";
8
+ import * as cp from "node:child_process";
9
+ import { fileURLToPath } from "node:url";
10
+ var file = process.argv[2];
11
+ if (process.argv.length < 3 || file == "--help") {
12
+ console.log("Usage: nattoppet-dev [file] [hook cmd]...");
13
+ process.exit(0);
14
+ }
15
+ var runtimeExt = path.extname(fileURLToPath(import.meta.url)) === ".js" ? ".js" : ".ts";
16
+ var nattoppetPath = fileURLToPath(new URL(`nattoppet${runtimeExt}`, import.meta.url));
17
+ var mimeTypes = {
18
+ ".html": "text/html",
19
+ ".css": "text/css",
20
+ ".js": "text/javascript",
21
+ ".wasm": "application/wasm",
22
+ ".json": "application/json",
23
+ ".png": "image/png",
24
+ ".svg": "image/svg+xml"
25
+ };
26
+ http.createServer(async (req, res) => {
27
+ try {
28
+ const url = new URL(req.url, "http://localhost");
29
+ if (url.pathname !== "/") {
30
+ const filePath = path.join(process.cwd(), decodeURIComponent(url.pathname));
31
+ await fs.promises.access(filePath);
32
+ const ext = path.extname(filePath);
33
+ const contentType = mimeTypes[ext] || "application/octet-stream";
34
+ res.writeHead(200, { "Content-Type": contentType });
35
+ fs.createReadStream(filePath).pipe(res);
36
+ return;
37
+ }
38
+ if (process.argv.length > 3) {
39
+ const exitCode = await new Promise((resolve) => {
40
+ const hookProc = cp.spawn(process.argv[3], process.argv.slice(4), {
41
+ stdio: "inherit"
42
+ });
43
+ hookProc.on("close", resolve);
44
+ });
45
+ if (exitCode !== 0) {
46
+ console.warn("!!! hook command exit with non-0 status");
47
+ res.writeHead(500);
48
+ res.end("Hook command failed");
49
+ return;
50
+ }
51
+ }
52
+ const { stdout, stderr } = await new Promise((resolve, reject) => {
53
+ const proc = cp.spawn(process.execPath, [nattoppetPath, file, "--dev"]);
54
+ let stdout2 = "";
55
+ let stderr2 = "";
56
+ proc.stdout.on("data", (d) => stdout2 += d);
57
+ proc.stderr.on("data", (d) => stderr2 += d);
58
+ proc.on("close", (code) => {
59
+ if (code !== 0) reject(new Error(`compilation failed with code ${code}`));
60
+ else resolve({ stdout: stdout2, stderr: stderr2 });
61
+ });
62
+ proc.on("error", reject);
63
+ });
64
+ if (stderr)
65
+ console.error(stderr);
66
+ res.writeHead(200, { "Content-Type": "text/html" });
67
+ res.end(stdout);
68
+ } catch (e) {
69
+ if (e instanceof Error && "code" in e && e.code !== "ENOENT") {
70
+ console.error(e);
71
+ }
72
+ res.writeHead(404);
73
+ res.end("Not found");
74
+ }
75
+ }).listen(3939, () => {
76
+ console.log("Server running at http://127.0.0.1:3939");
77
+ });
78
+ var platform = process.platform;
79
+ var browser = process.env.BROWSER ?? (platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open");
80
+ var browserArgs = browser.includes("chrom") ? [browser, "--app=http://127.0.0.1:3939"] : [browser, "http://127.0.0.1:3939"];
81
+ cp.spawn(browserArgs[0], browserArgs.slice(1), {
82
+ stdio: "ignore",
83
+ detached: true
84
+ }).unref();
@@ -1,16 +1,16 @@
1
- import * as fs from "node:fs"
2
-
3
- const type = process.argv[2]
4
-
5
- const avaliable_templates = ["form"]
6
-
1
+ #!/usr/bin/env node
2
+ import { createRequire as createNattoppetRequire } from 'node:module'; const require = createNattoppetRequire(import.meta.url);
3
+
4
+ // nattoppet-init.ts
5
+ import * as fs from "node:fs";
6
+ import * as readline from "node:readline";
7
+ var type = process.argv[2];
8
+ var avaliable_templates = ["form"];
7
9
  if (process.argv.length < 3 || !avaliable_templates.includes(type)) {
8
- console.log("Usage: nattoppet-init form")
9
- process.exit(0)
10
+ console.log("Usage: nattoppet-init form");
11
+ process.exit(0);
10
12
  }
11
-
12
- fs.writeFileSync("main.ymd", `\
13
- [mixin] ${type}
13
+ fs.writeFileSync("main.ymd", `[mixin] ${type}
14
14
 
15
15
  [title]: Page Title
16
16
 
@@ -23,22 +23,23 @@ fs.writeFileSync("main.ymd", `\
23
23
  [checkbox].female Female
24
24
 
25
25
  [require](main.js)
26
- `)
27
-
28
- fs.writeFileSync("main.js", `\
29
- async function run(args) {
26
+ `);
27
+ fs.writeFileSync("main.js", `async function run(args) {
30
28
  return JSON.stringify(args)
31
29
  }
32
30
 
33
31
  if (typeof Deno != 'undefined')
34
32
  run({}).then(console.log)
35
- `)
36
-
37
- const gen_wasm = prompt("Generate wasm? (give lib name or empty to cancel): ")
38
-
33
+ `);
34
+ var rl = readline.createInterface({
35
+ input: process.stdin,
36
+ output: process.stdout
37
+ });
38
+ var question = (q) => new Promise((resolve) => rl.question(q, resolve));
39
+ var gen_wasm = await question("Generate wasm? (give lib name or empty to cancel): ");
40
+ rl.close();
39
41
  if (gen_wasm) {
40
- fs.writeFileSync(`${gen_wasm}.rs`, `\
41
- #[no_mangle]
42
+ fs.writeFileSync(`${gen_wasm}.rs`, `#[no_mangle]
42
43
  pub unsafe extern fn sum(data: *const u32, len: usize) -> u32 {
43
44
  let mut sum = 0;
44
45
  for i in 0..len {
@@ -58,10 +59,8 @@ pub unsafe extern "C" fn free_memory(ptr: *mut u8, byte_size: usize, alignment:
58
59
  let layout = std::alloc::Layout::from_size_align_unchecked(byte_size, alignment);
59
60
  std::alloc::dealloc(ptr, layout)
60
61
  }
61
- `)
62
-
63
- fs.writeFileSync("Cargo.toml", `\
64
- [package]
62
+ `);
63
+ fs.writeFileSync("Cargo.toml", `[package]
65
64
  name = "${gen_wasm}"
66
65
  version = "0.1.0"
67
66
  authors = ["Shiwei Zhang <ylxdzsw@gmail.com>"]
@@ -78,9 +77,8 @@ strip = true
78
77
 
79
78
  [dependencies]
80
79
  absurd = { git = "https://github.com/ylxdzsw/absurd" }
81
- `)
82
-
83
- fs.appendFileSync("main.js", `
80
+ `);
81
+ fs.appendFileSync("main.js", `
84
82
 
85
83
  async function call_wasm() {
86
84
  await window.wasm_ready
@@ -91,21 +89,21 @@ async function call_wasm() {
91
89
  ${gen_wasm}.free_memory(buffer_ptr, 4 * 8, 4)
92
90
  console.log(sum)
93
91
  }
94
- `)
95
-
96
- fs.appendFileSync("main.ymd", `
92
+ `);
93
+ fs.appendFileSync("main.ymd", `
97
94
  [require](target/wasm32-unknown-unknown/release/${gen_wasm}.wasm)
98
- `)
95
+ `);
99
96
  }
100
-
101
-
102
- const gen_workflow = prompt("Generate github workflow? (y/n): ")
103
-
97
+ var rl2 = readline.createInterface({
98
+ input: process.stdin,
99
+ output: process.stdout
100
+ });
101
+ var gen_workflow = await new Promise((resolve) => rl2.question("Generate github workflow? (y/n): ", resolve));
102
+ rl2.close();
104
103
  if (gen_workflow == "y") {
105
- fs.mkdirSync(".github", { recursive: true })
106
- fs.mkdirSync(".github/workflows", { recursive: true })
107
- fs.writeFileSync(".github/workflows/pages.yml", `\
108
- name: Build and deploy to pages
104
+ fs.mkdirSync(".github", { recursive: true });
105
+ fs.mkdirSync(".github/workflows", { recursive: true });
106
+ fs.writeFileSync(".github/workflows/pages.yml", `name: Build and deploy to pages
109
107
  on: [push]
110
108
 
111
109
  jobs:
@@ -115,10 +113,11 @@ jobs:
115
113
  - name: Checkout
116
114
  uses: actions/checkout@v4
117
115
 
118
- - name: Install Bun
119
- uses: oven-sh/setup-bun@v2
116
+ - name: Install Node.js
117
+ uses: actions/setup-node@v4
120
118
  with:
121
- bun-version: latest
119
+ node-version: 22
120
+
122
121
  ${gen_wasm ? `
123
122
  - name: Install Rust
124
123
  uses: dtolnay/rust-toolchain@nightly
@@ -127,16 +126,15 @@ ${gen_wasm ? `
127
126
 
128
127
  - name: Compile WASM
129
128
  run: cargo build --release --target wasm32-unknown-unknown
130
- ` : ''}
129
+ ` : ""}
131
130
  - name: Build HTML
132
- run: bun run nattoppet.ts main.ymd > index.html
131
+ run: npx --yes nattoppet main.ymd > index.html
133
132
 
134
133
  - name: Deploy
135
134
  uses: JamesIves/github-pages-deploy-action@v4
136
135
  with:
137
136
  branch: gh-pages
138
137
  folder: "."
139
- `)
138
+ `);
140
139
  }
141
-
142
- console.info("Finished")
140
+ console.info("Finished");
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire as createNattoppetRequire } from 'node:module'; const require = createNattoppetRequire(import.meta.url);
3
+
4
+ // nattoppet-native.ts
5
+ import * as fs from "node:fs";
6
+ import * as path from "node:path";
7
+ import * as cp from "node:child_process";
8
+ import { fileURLToPath } from "node:url";
9
+ var main_source = `#![windows_subsystem = "windows"]
10
+
11
+ use web_view::*;
12
+
13
+ fn main() {
14
+ static HTML_CONTENT: &str = include_str!("../target/bundle.html");
15
+
16
+ web_view::builder()
17
+ .title("Nattoppet Native")
18
+ .content(Content::Html(HTML_CONTENT))
19
+ .size(600, 480)
20
+ .resizable(false)
21
+ .debug(cfg!(debug_assertions))
22
+ .user_data(())
23
+ .invoke_handler(handler)
24
+ .run()
25
+ .unwrap();
26
+ }
27
+
28
+ fn handler(webview: &mut WebView<()>, arg: &str) -> WVResult {
29
+ Ok(())
30
+ }
31
+ `;
32
+ var build_source = `#[cfg(windows)]
33
+ extern crate winres;
34
+
35
+ #[cfg(windows)]
36
+ fn main() {
37
+ let mut res = winres::WindowsResource::new();
38
+ res.set_icon("../icon.ico");
39
+ res.compile().unwrap();
40
+ }
41
+
42
+ #[cfg(not(windows))]
43
+ fn main() {}
44
+ `;
45
+ var cargo_source = `[package]
46
+ name = "nattoppet_native"
47
+ version = "0.1.0"
48
+ edition = "2021"
49
+
50
+ [dependencies]
51
+ web-view = { version = "0.7", features = ["edge"] }
52
+
53
+ [target.'cfg(windows)'.build-dependencies]
54
+ winres = "0.1"
55
+
56
+ [package.metadata.winres]
57
+ ProductName = "A nattoppet native app"
58
+ `;
59
+ var init = () => {
60
+ fs.mkdirSync("native", { recursive: true });
61
+ fs.mkdirSync("native/src", { recursive: true });
62
+ fs.mkdirSync("native/target", { recursive: true });
63
+ fs.writeFileSync("native/src/main.rs", main_source);
64
+ fs.writeFileSync("native/build.rs", build_source);
65
+ fs.writeFileSync("native/Cargo.toml", cargo_source);
66
+ };
67
+ var spawn2 = (cmd, args, opts) => {
68
+ return new Promise((resolve, reject) => {
69
+ const proc = cp.spawn(cmd, args, opts);
70
+ let stdout = "";
71
+ let stderr = "";
72
+ proc.stdout.on("data", (d) => stdout += d);
73
+ proc.stderr.on("data", (d) => stderr += d);
74
+ proc.on("close", (code) => {
75
+ if (code !== 0) reject(new Error(stderr));
76
+ else resolve(stdout);
77
+ });
78
+ proc.on("error", reject);
79
+ });
80
+ };
81
+ var bundle = async () => {
82
+ const runtimeExt = path.extname(fileURLToPath(import.meta.url)) === ".js" ? ".js" : ".ts";
83
+ const nattoppetPath = fileURLToPath(new URL(`nattoppet${runtimeExt}`, import.meta.url));
84
+ const stdout = await spawn2(process.execPath, [nattoppetPath, process.argv[3]]);
85
+ fs.writeFileSync("native/target/bundle.html", stdout);
86
+ };
87
+ var build = async () => {
88
+ await bundle();
89
+ await spawn2("cargo", ["build", "--release"], {
90
+ cwd: "native"
91
+ });
92
+ console.log("building finished. Check native/target/release/");
93
+ };
94
+ var help = () => {
95
+ console.log("Usage: nattoppet-native [init|bundle|build] <file.ymd>");
96
+ };
97
+ switch (process.argv[2]) {
98
+ case "init":
99
+ init();
100
+ break;
101
+ case "bundle":
102
+ bundle().catch((e) => {
103
+ console.error(e.message);
104
+ process.exit(1);
105
+ });
106
+ break;
107
+ case "build":
108
+ build().catch((e) => {
109
+ console.error(e.message);
110
+ process.exit(1);
111
+ });
112
+ break;
113
+ default:
114
+ help();
115
+ break;
116
+ }