nattoppet 4.0.1 → 4.1.0

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/package.json CHANGED
@@ -1,50 +1,32 @@
1
1
  {
2
2
  "name": "nattoppet",
3
- "version": "4.0.1",
3
+ "version": "4.1.0",
4
4
  "description": "A tiny macro-based markup language for blogging",
5
5
  "author": "ylxdzsw <ylxdzsw@gmail.com>",
6
6
  "license": "MIT",
7
7
  "type": "module",
8
8
  "files": [
9
- "nattoppet.ts",
10
- "nattoppet-dev.ts",
11
- "nattoppet-init.ts",
12
- "nattoppet-native.ts",
13
- "compiler.ts",
14
- "stdlib.ts",
15
- "common.ymd",
16
- "vue.ymd",
17
- "form.ymd",
18
- "katex.ymd",
19
- "koa.ymd",
20
- "ppt.ymd",
21
- "tml.ymd",
22
- "vue.css",
23
- "form.css",
24
- "form.js",
25
- "koa.css",
26
- "ppt.css",
27
- "ppt.js",
28
- "tml.css",
29
- "tml.js",
30
- "katex.css",
9
+ "dist",
31
10
  "GUIDE.md",
32
- "README.md"
11
+ "readme.md"
33
12
  ],
34
13
  "bin": {
35
- "nattoppet": "./nattoppet.ts",
36
- "nattoppet-dev": "./nattoppet-dev.ts",
37
- "nattoppet-init": "./nattoppet-init.ts",
38
- "nattoppet-native": "./nattoppet-native.ts"
14
+ "nattoppet": "./dist/nattoppet.js",
15
+ "nattoppet-dev": "./dist/nattoppet-dev.js",
16
+ "nattoppet-init": "./dist/nattoppet-init.js",
17
+ "nattoppet-native": "./dist/nattoppet-native.js"
39
18
  },
40
19
  "scripts": {
41
- "build": "bun build ./nattoppet.ts --outdir=./dist --target=bun",
42
- "dev": "bun run --watch ./nattoppet-dev.ts",
43
- "test": "bun test",
44
- "test:watch": "bun test --watch",
45
- "compile": "bun run ./nattoppet.ts",
46
- "init": "bun run ./nattoppet-init.ts",
47
- "native": "bun run ./nattoppet-native.ts"
20
+ "build": "node scripts/build-package.mjs",
21
+ "build:binary": "bun build ./nattoppet.ts --compile --outfile=./binary-dist/nattoppet",
22
+ "prepack": "npm run build",
23
+ "dev": "node nattoppet-dev.ts",
24
+ "test": "node --test",
25
+ "test:bun": "bun test",
26
+ "test:watch": "node --test --watch",
27
+ "compile": "node nattoppet.ts",
28
+ "init": "node nattoppet-init.ts",
29
+ "native": "node nattoppet-native.ts"
48
30
  },
49
31
  "dependencies": {
50
32
  "coffeescript": "^2.7.0",
@@ -53,8 +35,10 @@
53
35
  "less": "^4.2.0",
54
36
  "marked": "^9.1.6"
55
37
  },
56
- "devDependencies": { },
38
+ "devDependencies": {
39
+ "esbuild": "^0.25.0"
40
+ },
57
41
  "engines": {
58
- "bun": ">=1.0.0"
42
+ "node": ">=22.18.0"
59
43
  }
60
44
  }
package/readme.md CHANGED
@@ -5,36 +5,42 @@ A tiny markup language and themes for making documents, slides, or web apps as s
5
5
 
6
6
  ### Usage
7
7
 
8
+ Install Nattoppet with Node.js 22.18 or newer:
9
+
10
+ ```bash
11
+ npm install -g nattoppet
12
+ ```
13
+
8
14
  Compile a nattoppet file:
9
15
 
10
16
  ```bash
11
- bun run nattoppet.ts in.ymd > out.html
17
+ nattoppet in.ymd > out.html
12
18
  ```
13
19
 
14
20
  Compile a nattoppet file but don't minimize (for debugging):
15
21
 
16
22
  ```bash
17
- bun run nattoppet.ts in.ymd --dev > out.html
23
+ nattoppet in.ymd --dev > out.html
18
24
  ```
19
25
 
20
26
  Open a browser to preview a nattoppet file; rebuild every time the page is refreshed:
21
27
 
22
28
  ```bash
23
- bun run nattoppet-dev.ts in.ymd
29
+ nattoppet-dev in.ymd
24
30
  ```
25
31
 
26
32
  Initialize a new project:
27
33
 
28
34
  ```bash
29
- bun run nattoppet-init.ts form
35
+ nattoppet-init form
30
36
  ```
31
37
 
32
38
  Build a double-clickable executable for the page:
33
39
 
34
40
  ```bash
35
- bun run nattoppet-native.ts init
41
+ nattoppet-native init
36
42
  # (edit the metadata as needed)
37
- bun run nattoppet-native.ts build in.ymd
43
+ nattoppet-native build in.ymd
38
44
  ```
39
45
 
40
46
 
package/compiler.ts DELETED
@@ -1,169 +0,0 @@
1
- import vm from "node:vm"
2
- import { extname } from "node:path"
3
- import { fileURLToPath } from "node:url"
4
- import * as fs from "node:fs"
5
-
6
- const pattern = /^(?:\[(.+?)\]([:=])|\[(mixin|#slot)\] ?(.*?)\n)/m
7
-
8
- const fetch_text_file = async (path: string) => {
9
- const filePath = new URL(path, import.meta.url)
10
- return fs.readFileSync(fileURLToPath(filePath), 'utf-8')
11
- }
12
-
13
- const file_exists = (path: string) => {
14
- try {
15
- const filePath = new URL(path, import.meta.url)
16
- fs.accessSync(fileURLToPath(filePath))
17
- return true
18
- } catch {
19
- return false
20
- }
21
- }
22
-
23
- const resolve_mixin_path = (path: string): string => {
24
- if (extname(path)) return path
25
- // Compatibility hack: bare name only resolves to .ymd if exact file doesn't exist
26
- if (file_exists(path)) return path
27
- const ymdPath = path + ".ymd"
28
- if (file_exists(ymdPath)) return ymdPath
29
- throw `mixin not found: ${path}`
30
- }
31
-
32
- // tokenize also process mixins
33
- // mixins are special direvatives
34
- // by default, they are relative to the root directory of nattoppet, unless they are prefixed with "./"
35
- export const tokenize = async (str: string) => {
36
- const tokens: any[] = []
37
-
38
- while (true) {
39
- const m = str.match(pattern)
40
- if (!m || m.index == null) {
41
- if (str) tokens.push({ type: "code", content: str })
42
- break
43
- }
44
-
45
- if (m.index && m.index > 0) {
46
- tokens.push({ type: "code", content: str.substring(0, m.index) })
47
- }
48
-
49
- if (m[3]) {
50
- if (m[3] === 'mixin') {
51
- tokens.push({ type: 'mixin', path: m[4] })
52
- } else if (m[3] === '#slot') {
53
- tokens.push({ type: 'slot' })
54
- }
55
- str = str.substring(m.index + m[0].length)
56
- continue
57
- }
58
-
59
- const name = m[1]
60
- const type = m[2] == ':' ? 'ref' : 'fn'
61
- str = str.substring(m.index + m[0].length)
62
-
63
- const p1 = str.indexOf('\n\n')
64
- const p2 = str.search(pattern)
65
- const p = p1 >= 0 && p2 >= 0 ? Math.min(p1+2, p2) :
66
- p1 < 0 && p2 < 0 ? str.length : Math.max(p1+2, p2)
67
-
68
- const content = str.substring(0, p).trim()
69
- str = str.substring(p)
70
-
71
- tokens.push({ type, name, content })
72
- }
73
-
74
- for (let i = 0; i < tokens.length; i++) if (tokens[i].type == 'mixin') {
75
- const resolvedPath = resolve_mixin_path(tokens[i].path)
76
- const ext = extname(resolvedPath)
77
-
78
- if (ext === ".ymd") {
79
- const text = await fetch_text_file(resolvedPath)
80
- const inlineTokens = await tokenize(text)
81
- const slotIndices = inlineTokens
82
- .map((t: any, idx: number) => t.type === 'slot' ? idx : -1)
83
- .filter((idx: number) => idx >= 0)
84
-
85
- if (slotIndices.length > 1) {
86
- throw `multiple [#slot] in mixin ${resolvedPath}`
87
- } else if (slotIndices.length === 1) {
88
- const slotIdx = slotIndices[0]
89
- const head = inlineTokens.slice(0, slotIdx)
90
- const tail = inlineTokens.slice(slotIdx + 1)
91
- tokens.splice(i, 1, ...head)
92
- tokens.push(...tail)
93
- } else {
94
- tokens.splice(i, 1, ...inlineTokens)
95
- }
96
- } else {
97
- tokens[i] = { type: "raw", content: await fetch_text_file(resolvedPath) }
98
- }
99
-
100
- i -= 1 // revisit i since we replaced it
101
- }
102
-
103
- return tokens
104
- }
105
-
106
- const _interpret = (str: string, env: any, defs: any[]): any => {
107
- const p1 = str.search(/^ /m)
108
- const p2 = str.search(/\[(.+?)\]/)
109
- if (p1 < 0 && p2 < 0) return str
110
-
111
- if (p2 < 0 || (p1 >= 0 && p1 < p2)) {
112
- const head = str.substring(0, p1)
113
- str = str.substring(p1+2)
114
- let p = str.indexOf('\n\n')
115
- if (p < 0) p = str.length
116
- return head + '<p>' + _interpret(str.substring(0, p), env, defs) + '</p>' + _interpret(str.substring(p+2), env, defs)
117
- }
118
-
119
- const name = str.match(/\[(.+?)\]/)![1]
120
- const i = defs.findIndex(x => x.name == name)
121
- if (i < 0) throw `definition ${name} not found`
122
-
123
- const def = defs[i]
124
- switch (def.type) {
125
- case 'fn':
126
- env.remaining = str.substring(p2 + name.length + 2)
127
- env.interpret = (str: string) => { // a "scoped" interpret function capable for interpreting substrings. We preserve the "remaining" property outside.
128
- const remaining = env.remaining
129
- const result = _interpret(str, env, defs) // We gaved full defs available on the call site (dynamic scoping) as the text to be interpreted is part of the call site text.
130
- env.remaining = remaining
131
- return result
132
- }
133
- const result = vm.runInContext('{' + def.content + '}', env)
134
- return str.substring(0, p2) + result + _interpret(env.remaining, env, defs)
135
- case 'ref':
136
- return str.substring(0, p2) +
137
- _interpret(def.content, env, defs.slice(i+1)) + // Exclude self from scope to prevent infinite recursion, but allow forward references
138
- _interpret(str.substring(p2 + name.length + 2), env, defs)
139
- default: throw 'unknown defs type'
140
- }
141
- }
142
-
143
- export const compile = async (str: string, locals: any = {}) => {
144
- const env = vm.createContext(locals)
145
- for (const k in env) if (typeof(env[k]) == 'function')
146
- env[k] = env[k].bind(env)
147
-
148
- let tokens = await tokenize(str)
149
- let output = ''
150
- while (true) {
151
- const token = tokens.shift()
152
- if (!token) return output
153
- switch (token.type) {
154
- case "code":
155
- output += _interpret(token.content, env, tokens)
156
- break
157
- case "raw":
158
- output += token.content
159
- break
160
- }
161
- }
162
- }
163
-
164
- /*
165
- TODO:
166
- 1. support first-class markdown-like nestable lists
167
- 2. cleanup the spaces, carefully define when to trim
168
- 3. require a newline before indent to open paragraph?
169
- */
package/nattoppet-dev.ts DELETED
@@ -1,94 +0,0 @@
1
- import * as fs from "node:fs"
2
- import * as path from "node:path"
3
- import { fileURLToPath } from "node:url"
4
-
5
- const file = process.argv[2]
6
-
7
- if (process.argv.length < 3 || file == "--help") {
8
- console.log("Usage: nattoppet-dev [file] [hook cmd]...")
9
- process.exit(0)
10
- }
11
-
12
- const nattoppetPath = fileURLToPath(new URL("nattoppet.ts", import.meta.url))
13
-
14
- Bun.serve({
15
- port: 3939,
16
- async fetch(request) {
17
- const url = new URL(request.url)
18
-
19
- // Static file serving
20
- if (url.pathname !== '/') {
21
- try {
22
- const filePath = path.join(process.cwd(), decodeURIComponent(url.pathname))
23
- const file = Bun.file(filePath)
24
- if (await file.exists()) {
25
- return new Response(file)
26
- }
27
- } catch (e) {
28
- console.error(e)
29
- }
30
- return new Response(null, { status: 404 })
31
- }
32
-
33
- // Run build hooks if specified
34
- if (process.argv.length > 3) {
35
- const hookProc = Bun.spawn(process.argv.slice(3))
36
- const exitCode = await hookProc.exited
37
-
38
- if (exitCode !== 0) {
39
- console.warn("!!! hook command exit with non-0 status")
40
- return new Response("Hook command failed", { status: 500 })
41
- }
42
- }
43
-
44
- // Compile the nattoppet file
45
- const compileProc = Bun.spawn([
46
- process.execPath,
47
- "run",
48
- nattoppetPath,
49
- file,
50
- "--dev"
51
- ], {
52
- stdout: 'pipe',
53
- stderr: 'pipe'
54
- })
55
-
56
- const [stdout, stderr] = await Promise.all([
57
- new Response(compileProc.stdout).arrayBuffer(),
58
- new Response(compileProc.stderr).text()
59
- ])
60
-
61
- const exitCode = await compileProc.exited
62
-
63
- if (stderr) {
64
- console.error(stderr)
65
- }
66
-
67
- const content = exitCode === 0 ? new Uint8Array(stdout) : new TextEncoder().encode("Compilation failed")
68
-
69
- return new Response(content, {
70
- headers: {
71
- "Content-Type": "text/html"
72
- },
73
- status: 200
74
- })
75
- }
76
- })
77
-
78
- console.log("Server running at http://127.0.0.1:3939")
79
-
80
- // Open browser
81
- const platform = process.platform
82
- const browser = process.env.BROWSER ?? (
83
- platform === 'darwin' ? 'open' :
84
- platform === 'win32' ? 'start' :
85
- 'xdg-open'
86
- )
87
-
88
- const browserCmd = browser.includes('chrom')
89
- ? [browser, "--app=http://127.0.0.1:3939"]
90
- : [browser, "http://127.0.0.1:3939"]
91
-
92
- Bun.spawn(browserCmd, {
93
- stdio: ['ignore', 'ignore', 'ignore']
94
- })
@@ -1,116 +0,0 @@
1
- import * as fs from "node:fs"
2
- import * as path from "node:path"
3
- import { fileURLToPath } from "node:url"
4
-
5
- const main_source = `\
6
- #![windows_subsystem = "windows"]
7
-
8
- use web_view::*;
9
-
10
- fn main() {
11
- static HTML_CONTENT: &str = include_str!("../target/bundle.html");
12
-
13
- web_view::builder()
14
- .title("Nattoppet Native")
15
- .content(Content::Html(HTML_CONTENT))
16
- .size(600, 480)
17
- .resizable(false)
18
- .debug(cfg!(debug_assertions))
19
- .user_data(())
20
- .invoke_handler(handler)
21
- .run()
22
- .unwrap();
23
- }
24
-
25
- fn handler(webview: &mut WebView<()>, arg: &str) -> WVResult {
26
- Ok(())
27
- }
28
- `
29
-
30
- const build_source = `\
31
- #[cfg(windows)]
32
- extern crate winres;
33
-
34
- #[cfg(windows)]
35
- fn main() {
36
- let mut res = winres::WindowsResource::new();
37
- res.set_icon("../icon.ico");
38
- res.compile().unwrap();
39
- }
40
-
41
- #[cfg(not(windows))]
42
- fn main() {}
43
- `
44
-
45
- const cargo_source = `\
46
- [package]
47
- name = "nattoppet_native"
48
- version = "0.1.0"
49
- edition = "2021"
50
-
51
- [dependencies]
52
- web-view = { version = "0.7", features = ["edge"] }
53
-
54
- [target.'cfg(windows)'.build-dependencies]
55
- winres = "0.1"
56
-
57
- [package.metadata.winres]
58
- ProductName = "A nattoppet native app"
59
- `
60
-
61
- const init = () => {
62
- fs.mkdirSync('native', { recursive: true })
63
- fs.mkdirSync('native/src', { recursive: true })
64
- fs.mkdirSync('native/target', { recursive: true })
65
- fs.writeFileSync("native/src/main.rs", main_source)
66
- fs.writeFileSync("native/build.rs", build_source)
67
- fs.writeFileSync("native/Cargo.toml", cargo_source)
68
- }
69
-
70
- const bundle = async () => {
71
- const nattoppetPath = fileURLToPath(new URL("nattoppet.ts", import.meta.url))
72
- const proc = Bun.spawn([
73
- process.execPath,
74
- "run",
75
- nattoppetPath,
76
- process.argv[3]
77
- ], {
78
- stdout: 'pipe',
79
- stderr: 'pipe'
80
- })
81
-
82
- const [stdout, stderr] = await Promise.all([
83
- new Response(proc.stdout).arrayBuffer(),
84
- new Response(proc.stderr).text()
85
- ])
86
-
87
- if (stderr)
88
- console.error(stderr)
89
-
90
- await Bun.write('native/target/bundle.html', stdout)
91
- }
92
-
93
- const build = async () => {
94
- await bundle()
95
- const proc = Bun.spawn(['cargo', 'build', '--release'], {
96
- cwd: 'native'
97
- })
98
-
99
- const exitCode = await proc.exited
100
- if (exitCode != 0) {
101
- console.error("Cargo exit with code: " + exitCode)
102
- } else {
103
- console.log("building finished. Check native/target/release/")
104
- }
105
- }
106
-
107
- const help = () => {
108
- console.log("Usage: nattoppet-native [init|bundle|build] <file.ymd>")
109
- }
110
-
111
- switch (process.argv[2]) {
112
- case 'init': init(); break
113
- case 'bundle': bundle(); break
114
- case 'build': build(); break
115
- default: help(); break
116
- }
package/nattoppet.ts DELETED
@@ -1,35 +0,0 @@
1
- import * as path from "node:path"
2
- import * as fs from "node:fs"
3
- import minifier from "html-minifier-terser"
4
-
5
- import stdlib from "./stdlib.ts"
6
- import * as compiler from "./compiler.ts"
7
-
8
- const compiled = await (async () => {
9
- const file = process.argv[2]
10
- if (file) {
11
- const dir = path.dirname(path.resolve(file))
12
- const code = fs.readFileSync(file, 'utf-8')
13
- return await compiler.compile(code, { ...stdlib, base_dir: dir })
14
- } else {
15
- const dir = process.cwd()
16
- const chunks: Uint8Array[] = []
17
- for await (const chunk of process.stdin) {
18
- chunks.push(chunk)
19
- }
20
- const code = Buffer.concat(chunks).toString('utf-8')
21
- return await compiler.compile(code, { ...stdlib, base_dir: dir })
22
- }
23
- })()
24
-
25
- const minified = process.argv.includes('--dev') ? compiled : await minifier.minify(compiled, {
26
- collapseWhitespace: true,
27
- removeAttributeQuotes: true,
28
- removeComments: true,
29
- removeRedundantAttributes: true,
30
- removeOptionalTags: true,
31
- minifyCSS: true,
32
- minifyJS: true,
33
- })
34
-
35
- process.stdout.write(minified)
package/stdlib.ts DELETED
@@ -1,120 +0,0 @@
1
- import * as path from "node:path"
2
- import * as fs from "node:fs"
3
- import * as zlib from "node:zlib"
4
-
5
- import { marked } from "marked"
6
- import coffee from "coffeescript"
7
- import katex from "katex"
8
- import less from "less"
9
-
10
- function compress_sync(data: ArrayBuffer) {
11
- // Keep using zlib.deflateRawSync for browser compatibility
12
- // Browser uses DecompressionStream("deflate-raw") which expects raw deflate
13
- return zlib.deflateRawSync(new Uint8Array(data), {
14
- flush: zlib.constants.Z_FINISH,
15
- level: zlib.constants.Z_BEST_COMPRESSION
16
- })
17
- }
18
-
19
- export default {
20
- skip(n: number) {
21
- this.remaining = this.remaining.substring(n)
22
- },
23
-
24
- capture_until(delimiter: string) {
25
- const p = this.remaining.indexOf(delimiter)
26
- if (p < 0) {
27
- const result = this.remaining
28
- this.remaining = ""
29
- return result
30
- }
31
- const result = this.remaining.substring(0, p)
32
- this.remaining = this.remaining.substring(p + delimiter.length)
33
- return result
34
- },
35
-
36
- std_call(hascontent = false) {
37
- const opts: string[] = [], args: string[] = []
38
- const parse_option = () => {
39
- const m = this.remaining.match(/^\.([\w\-]+)/)
40
- this.skip(m[0].length)
41
- opts.push(m[1])
42
- }
43
- const parse_argument = () => {
44
- const m = this.remaining.match(/^\((.*?)\)|{(.*?)}/)
45
- this.skip(m[0].length)
46
- args.push(m[1] || m[2])
47
- }
48
- const parse_block = () => {
49
- const m = this.remaining.match(/^(>+)\n?/)
50
- this.skip(m[0].length)
51
- return this.capture_until('<'.repeat(m[1].length))
52
- }
53
-
54
- while (true) {
55
- switch (this.remaining[0]) {
56
- case '.':
57
- parse_option()
58
- break
59
- case '(':
60
- case '{':
61
- parse_argument()
62
- break
63
- case '>':
64
- return { opts, args, block: parse_block() }
65
- case ' ':
66
- if (hascontent)
67
- return { opts, args, block: this.capture_until('\n').slice(1) }
68
- default:
69
- return { opts, args }
70
- }
71
- }
72
- },
73
-
74
- rpath(file: string) {
75
- if (!path.isAbsolute(file))
76
- return path.join(this.base_dir, file)
77
- return file
78
- },
79
-
80
- read(file: string, encoding = "utf-8") {
81
- const filePath = this.rpath(file)
82
- switch (encoding) {
83
- case "utf8":
84
- case "utf-8":
85
- return fs.readFileSync(filePath, 'utf-8')
86
- case "base64":
87
- return Buffer.from(fs.readFileSync(filePath)).toString('base64')
88
- case "compressed-base64":
89
- return Buffer.from(compress_sync(fs.readFileSync(filePath))).toString('base64')
90
- default:
91
- throw "unknown encoding: " + encoding
92
- }
93
- },
94
-
95
- extname(file: string) {
96
- return path.extname(file).slice(1)
97
- },
98
-
99
- basename(file: string) {
100
- return path.basename(file)
101
- },
102
-
103
- render_coffee(str: string, options: any) {
104
- return coffee.compile(str, options)
105
- },
106
-
107
- render_less(str: string) {
108
- let x: any; less.render(str, {}, (err: any, out: any) => !err && (x = out))
109
- if (!x) throw "less failed to compile synchronously"
110
- return x.css
111
- },
112
-
113
- render_markdown(str: string) {
114
- return marked.parse(str)
115
- },
116
-
117
- render_katex(str: string, displayMode = false) {
118
- return katex.renderToString(str, { displayMode, output: 'html' })
119
- }
120
- } as any
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes