leaf-lang 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ahmad A.
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.
package/README.md ADDED
@@ -0,0 +1,327 @@
1
+ # 🍃 Leaf
2
+
3
+ > HTML without angle brackets. Write structure with braces, compile to clean HTML.
4
+
5
+ ```leaf
6
+ leaf {
7
+ head {
8
+ title "Hello World"
9
+ meta charset="UTF-8"
10
+ }
11
+ body {
12
+ h1 "Hello from Leaf!"
13
+ p "No angle brackets. Just clean, readable markup."
14
+ a href="https://github.com" target="_blank" "GitHub"
15
+ img src="photo.jpg" alt="A photo"
16
+ }
17
+ }
18
+ ```
19
+
20
+ Compiles to:
21
+
22
+ ```html
23
+ <!DOCTYPE html>
24
+ <html>
25
+ <head>
26
+ <title>Hello World</title>
27
+ <meta charset="UTF-8">
28
+ </head>
29
+ <body>
30
+ <h1>Hello from Leaf!</h1>
31
+ <p>No angle brackets. Just clean, readable markup.</p>
32
+ <a href="https://github.com" target="_blank">GitHub</a>
33
+ <img src="photo.jpg" alt="A photo">
34
+ </body>
35
+ </html>
36
+ ```
37
+
38
+ ---
39
+
40
+ ## Install
41
+
42
+ ```bash
43
+ npm install -g leaf-lang
44
+ ```
45
+
46
+ Or locally in your project:
47
+
48
+ ```bash
49
+ npm install leaf-lang
50
+ ```
51
+
52
+ ---
53
+
54
+ ## CLI usage
55
+
56
+ ```bash
57
+ # Compile a .leaf file → .html file (same name)
58
+ leafc index.leaf
59
+
60
+ # Compile to a specific output file
61
+ leafc index.leaf -o dist/index.html
62
+
63
+ # Watch and recompile on save
64
+ leafc --watch index.leaf
65
+
66
+ # Print HTML to stdout (pipe-friendly)
67
+ leafc index.leaf --stdout
68
+
69
+ # Show version
70
+ leafc --version
71
+ ```
72
+
73
+ ---
74
+
75
+ ## Node.js API
76
+
77
+ ```js
78
+ const { compile, parse } = require("leaf-lang");
79
+
80
+ // Compile Leaf source to HTML string
81
+ const html = compile(`
82
+ leaf {
83
+ head { title "My Page" }
84
+ body { h1 "Hello" }
85
+ }
86
+ `);
87
+
88
+ console.log(html);
89
+ // → <!DOCTYPE html>\n<html>...
90
+
91
+ // Parse to AST only (no render)
92
+ const ast = parse(`div { p "hi" }`);
93
+ console.log(ast.nodes);
94
+ ```
95
+
96
+ ---
97
+
98
+ ## Syntax reference
99
+
100
+ ### Root element
101
+
102
+ ```leaf
103
+ leaf { } → <html></html>
104
+ ```
105
+
106
+ `leaf` is the only special keyword — it maps to `<html>`. All other tags are standard HTML tag names.
107
+
108
+ ### Nesting with braces
109
+
110
+ ```leaf
111
+ div class="container" {
112
+ section id="hero" {
113
+ h1 "Title"
114
+ p "Paragraph"
115
+ }
116
+ }
117
+ ```
118
+
119
+ ### Inline text
120
+
121
+ ```leaf
122
+ p "Some text"
123
+ h1 "Page title"
124
+ span "inline"
125
+ ```
126
+
127
+ ### Attributes
128
+
129
+ ```leaf
130
+ a href="https://example.com" target="_blank" "Link text"
131
+ input type="email" placeholder="you@example.com"
132
+ div class="box" id="main" style="color:red"
133
+ ```
134
+
135
+ Boolean attributes (no value):
136
+
137
+ ```leaf
138
+ input type="checkbox" checked
139
+ video controls autoplay
140
+ ```
141
+
142
+ ### Self-closing (void) tags
143
+
144
+ These close automatically — no `{ }` needed:
145
+
146
+ ```
147
+ br hr img input meta link source track wbr
148
+ area base col embed param
149
+ ```
150
+
151
+ ```leaf
152
+ img src="photo.jpg" alt="Photo"
153
+ meta charset="UTF-8"
154
+ br
155
+ hr
156
+ input type="text" placeholder="Name"
157
+ ```
158
+
159
+ ### Comments
160
+
161
+ ```leaf
162
+ # This is a comment — removed from HTML output
163
+ div {
164
+ # h1 "This line is commented out"
165
+ p "Only this renders"
166
+ }
167
+ ```
168
+
169
+ ### CSS and style
170
+
171
+ Pass CSS inline with `style=`:
172
+
173
+ ```leaf
174
+ div style="background:#1D9E75;padding:2rem;border-radius:8px" {
175
+ p style="color:#fff" "Green box"
176
+ }
177
+ ```
178
+
179
+ Or use a `<style>` block:
180
+
181
+ ```leaf
182
+ head {
183
+ style "
184
+ body { font-family: sans-serif; }
185
+ h1 { color: #1D9E75; }
186
+ "
187
+ }
188
+ ```
189
+
190
+ ### Links — every type
191
+
192
+ ```leaf
193
+ a href="https://github.com" target="_blank" "External link"
194
+ a href="#section" "Anchor link"
195
+ a href="mailto:you@example.com" "Email link"
196
+ a href="tel:+60123456789" "Phone link"
197
+ a href="/file.zip" download "Download"
198
+ ```
199
+
200
+ ### Images
201
+
202
+ ```leaf
203
+ img src="photo.jpg" alt="Description"
204
+
205
+ # Image as a link
206
+ a href="https://github.com" target="_blank" {
207
+ img src="banner.jpg" alt="Visit GitHub"
208
+ }
209
+
210
+ # Figure with caption
211
+ figure {
212
+ img src="photo.jpg" alt="A photo"
213
+ figcaption "Photo caption here"
214
+ }
215
+ ```
216
+
217
+ ### Forms
218
+
219
+ ```leaf
220
+ form action="/submit" method="post" {
221
+ label "Name"
222
+ input type="text" name="name" placeholder="Your name"
223
+
224
+ label "Email"
225
+ input type="email" name="email"
226
+
227
+ label "Message"
228
+ textarea name="msg" rows="4"
229
+
230
+ button type="submit" "Send"
231
+ }
232
+ ```
233
+
234
+ ### Tables
235
+
236
+ ```leaf
237
+ table {
238
+ thead {
239
+ tr {
240
+ th "Name"
241
+ th "Role"
242
+ }
243
+ }
244
+ tbody {
245
+ tr {
246
+ td "Ahmad"
247
+ td "Developer"
248
+ }
249
+ }
250
+ }
251
+ ```
252
+
253
+ ### All supported semantic tags
254
+
255
+ `nav` `header` `footer` `main` `section` `article` `aside`
256
+ `details` `summary` `figure` `figcaption` `blockquote`
257
+ `ul` `ol` `li` `dl` `dt` `dd`
258
+ `strong` `em` `code` `pre` `mark` `del` `ins` `sub` `sup`
259
+ `video` `audio` `source` `iframe` `canvas` `progress` `meter`
260
+ `script` `noscript` `template`
261
+
262
+ ---
263
+
264
+ ## Full example
265
+
266
+ ```leaf
267
+ leaf {
268
+ head {
269
+ title "My Portfolio"
270
+ meta charset="UTF-8"
271
+ meta name="viewport" content="width=device-width,initial-scale=1"
272
+ style "
273
+ :root {
274
+ --accent: #1D9E75;
275
+ --dark: #0a2e20;
276
+ }
277
+ body { margin: 0; font-family: 'Segoe UI', sans-serif; }
278
+ nav { background: var(--dark); padding: 1rem 2rem; display: flex; justify-content: space-between; }
279
+ nav a { color: #9FE1CB; text-decoration: none; margin-left: 1rem; }
280
+ .hero { padding: 5rem 2rem; text-align: center; background: #f7fdf9; }
281
+ h1 { color: var(--dark); font-size: 2.5rem; }
282
+ .cta { display: inline-block; background: var(--accent); color: #fff; padding: 12px 28px; border-radius: 8px; text-decoration: none; margin-top: 1rem; }
283
+ "
284
+ }
285
+ body {
286
+ nav {
287
+ span "Ahmad"
288
+ div {
289
+ a href="#work" "Work"
290
+ a href="#contact" "Contact"
291
+ }
292
+ }
293
+ div class="hero" {
294
+ h1 "Hello, I build things."
295
+ p "Minecraft mods. Android apps. Web tools."
296
+ a class="cta" href="#work" "See my work"
297
+ }
298
+ }
299
+ }
300
+ ```
301
+
302
+ ---
303
+
304
+ ## Editor support
305
+
306
+ The [Leaf IDE](https://alamien060512-alt.github.io) is a browser-based editor with:
307
+ - Syntax highlighting for `.leaf` files
308
+ - Live HTML output panel
309
+ - Preview tab
310
+ - Import / export `.leaf` and `.html`
311
+
312
+ ---
313
+
314
+ ## Roadmap
315
+
316
+ - [ ] VS Code extension with syntax highlighting
317
+ - [ ] `leafc --watch` with live-reload server
318
+ - [ ] Import / include other `.leaf` files
319
+ - [ ] Template variables `{{ name }}`
320
+ - [ ] Loops `for item in items { }`
321
+ - [ ] Conditional `if condition { }`
322
+
323
+ ---
324
+
325
+ ## License
326
+
327
+ MIT © Ahmad A.
package/bin/leafc.js ADDED
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+ const { compile } = require("../lib/compiler");
7
+
8
+
9
+ const green = (s) => `\x1b[32m${s}\x1b[0m`;
10
+ const red = (s) => `\x1b[31m${s}\x1b[0m`;
11
+ const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
12
+ const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
13
+ const bold = (s) => `\x1b[1m${s}\x1b[0m`;
14
+ const dim = (s) => `\x1b[2m${s}\x1b[0m`;
15
+
16
+ function printHelp() {
17
+ console.log(`
18
+ ${bold("leafc")} — Leaf language compiler
19
+
20
+ ${bold("Usage:")}
21
+ leafc <file.leaf> Compile to <file.html>
22
+ leafc <file.leaf> -o out.html Compile to a specific output file
23
+ leafc <file.leaf> --stdout Print HTML to stdout
24
+ leafc --watch <file.leaf> Watch file and recompile on change
25
+ leafc --version Print version
26
+ leafc --help Show this help
27
+
28
+ ${bold("Examples:")}
29
+ leafc index.leaf → index.html
30
+ leafc index.leaf -o dist/index.html
31
+ leafc index.leaf --stdout | prettier --parser html
32
+ leafc --watch index.leaf
33
+
34
+ ${bold("Syntax quick reference:")}
35
+ leaf { } → <html></html> (root element)
36
+ head { } → <head></head>
37
+ body { } → <body></body>
38
+ div class="box" { } → <div class="box"></div>
39
+ p "text" → <p>text</p>
40
+ a href="url" "text" → <a href="url">text</a>
41
+ img src="x" alt="" → <img src="x" alt=""> (self-closing)
42
+ # comment → (removed from output)
43
+ `);
44
+ }
45
+
46
+ function printVersion() {
47
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "../package.json"), "utf8"));
48
+ console.log(`leafc v${pkg.version}`);
49
+ }
50
+
51
+ function compileFile(inputPath, outputPath, toStdout) {
52
+ if (!fs.existsSync(inputPath)) {
53
+ console.error(red(`✗ File not found: ${inputPath}`));
54
+ process.exit(1);
55
+ }
56
+
57
+ const source = fs.readFileSync(inputPath, "utf8");
58
+ let html;
59
+
60
+ try {
61
+ html = compile(source);
62
+ } catch (err) {
63
+ console.error(red(`✗ Compile error: ${err.message}`));
64
+ process.exit(1);
65
+ }
66
+
67
+ if (toStdout) {
68
+ process.stdout.write(html);
69
+ return;
70
+ }
71
+
72
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
73
+ fs.writeFileSync(outputPath, html, "utf8");
74
+
75
+ const inLines = source.split("\n").length;
76
+ const outLines = html.split("\n").length;
77
+ const size = (Buffer.byteLength(html, "utf8") / 1024).toFixed(1);
78
+
79
+ console.log(
80
+ green("✓") +
81
+ ` ${cyan(path.basename(inputPath))} → ${cyan(path.basename(outputPath))}` +
82
+ dim(` (${inLines} lines → ${outLines} lines, ${size} KB)`)
83
+ );
84
+ }
85
+
86
+ function watchFile(inputPath, outputPath) {
87
+ compileFile(inputPath, outputPath, false);
88
+ console.log(yellow(`⟳ Watching ${inputPath} ...`));
89
+
90
+ let debounce;
91
+ fs.watch(inputPath, () => {
92
+ clearTimeout(debounce);
93
+ debounce = setTimeout(() => {
94
+ process.stdout.write("\r" + " ".repeat(60) + "\r");
95
+ compileFile(inputPath, outputPath, false);
96
+ }, 80);
97
+ });
98
+ }
99
+
100
+
101
+ const args = process.argv.slice(2);
102
+
103
+ if (!args.length || args.includes("--help") || args.includes("-h")) {
104
+ printHelp();
105
+ process.exit(0);
106
+ }
107
+
108
+ if (args.includes("--version") || args.includes("-v")) {
109
+ printVersion();
110
+ process.exit(0);
111
+ }
112
+
113
+ const watch = args.includes("--watch");
114
+ const toStdout = args.includes("--stdout");
115
+ const oIdx = args.indexOf("-o");
116
+
117
+ const files = args.filter((a) => !a.startsWith("-") && (oIdx === -1 || args[oIdx + 1] !== a));
118
+ const inputFile = files[0];
119
+
120
+ if (!inputFile) {
121
+ console.error(red("✗ No input file specified. Run leafc --help for usage."));
122
+ process.exit(1);
123
+ }
124
+
125
+ const ext = path.extname(inputFile);
126
+ const base = inputFile.slice(0, inputFile.length - ext.length);
127
+ const outputFile = oIdx !== -1 ? args[oIdx + 1] : base + ".html";
128
+
129
+ if (watch) {
130
+ watchFile(inputFile, outputFile);
131
+ } else {
132
+ compileFile(inputFile, outputFile, toStdout);
133
+ }
@@ -0,0 +1,44 @@
1
+ leaf {
2
+ head {
3
+ title "Contact — Leaf example"
4
+ meta charset="UTF-8"
5
+ meta name="viewport" content="width=device-width,initial-scale=1"
6
+ style "
7
+ body { font-family: 'Segoe UI', sans-serif; padding: 2rem; max-width: 520px; margin: auto; color: #1a1a1a; }
8
+ h2 { color: #0a2e20; margin-bottom: 1.2rem; }
9
+ label { display: block; margin-top: 1rem; font-size: 13px; color: #555; margin-bottom: 3px; }
10
+ input, textarea, select { width: 100%; padding: 9px 12px; border: 1px solid #ccc; border-radius: 7px; font-size: 14px; font-family: inherit; }
11
+ input:focus, textarea:focus, select:focus { outline: none; border-color: #1D9E75; box-shadow: 0 0 0 3px #1D9E7520; }
12
+ .row { display: flex; gap: 12px; }
13
+ .row > div { flex: 1; }
14
+ .submit { margin-top: 1.5rem; width: 100%; padding: 12px; background: #1D9E75; color: #fff; border: none; border-radius: 8px; font-size: 15px; font-weight: 600; cursor: pointer; }
15
+ .submit:hover { background: #0F6E56; }
16
+ "
17
+ }
18
+ body {
19
+ h2 "Contact Us"
20
+ form action="/submit" method="post" {
21
+ div class="row" {
22
+ div {
23
+ label "First name"
24
+ input type="text" name="fname" placeholder="Ahmad"
25
+ }
26
+ div {
27
+ label "Last name"
28
+ input type="text" name="lname" placeholder="A."
29
+ }
30
+ }
31
+ label "Email"
32
+ input type="email" name="email" placeholder="you@example.com"
33
+ label "Subject"
34
+ select name="subject" {
35
+ option "General enquiry"
36
+ option "Bug report"
37
+ option "Feature request"
38
+ }
39
+ label "Message"
40
+ textarea name="message" rows="5" placeholder="Write your message..."
41
+ button type="submit" class="submit" "Send Message"
42
+ }
43
+ }
44
+ }
@@ -0,0 +1,103 @@
1
+ leaf {
2
+ head {
3
+ title "Leaf Lang"
4
+ meta charset="UTF-8"
5
+ meta name="viewport" content="width=device-width,initial-scale=1"
6
+ meta name="description" content="Leaf — HTML without angle brackets"
7
+ style "
8
+ :root {
9
+ --green-900: #0a2e20;
10
+ --green-700: #0F6E56;
11
+ --green-500: #1D9E75;
12
+ --green-100: #e1f5ee;
13
+ --green-50: #f7fdf9;
14
+ --border: #e8f5ef;
15
+ --muted: #666;
16
+ --radius-sm: 8px;
17
+ --radius-md: 12px;
18
+ --radius-lg: 16px;
19
+ }
20
+
21
+ *, *::before, *::after { box-sizing: border-box; }
22
+ body { margin: 0; font-family: 'Segoe UI', sans-serif; color: #1a1a1a; background: #fff; }
23
+
24
+ nav { background: var(--green-900); display: flex; align-items: center; justify-content: space-between; padding: 1rem 2rem; }
25
+ .logo { color: #fff; font-weight: 800; font-size: 1.1rem; }
26
+ nav a { color: #9FE1CB; text-decoration: none; margin-left: 1.2rem; font-size: 14px; }
27
+
28
+ .hero { padding: 5rem 2rem 4rem; text-align: center; background: var(--green-50); border-bottom: 1px solid var(--border); }
29
+ .badge { display: inline-block; background: var(--green-100); color: var(--green-700); font-size: 12px; font-weight: 700; padding: 4px 14px; border-radius: 20px; margin-bottom: 1rem; }
30
+ .hero h1 { font-size: 2.6rem; font-weight: 800; color: var(--green-900); margin: .4rem 0 1rem; line-height: 1.2; }
31
+ .accent { color: var(--green-500); }
32
+ .sub { color: var(--muted); font-size: 1rem; max-width: 500px; margin: 0 auto 2rem; }
33
+ .btns { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }
34
+ .btn { display: inline-block; padding: 11px 26px; border-radius: var(--radius-sm); text-decoration: none; font-size: 14px; font-weight: 600; }
35
+ .btn-p { background: var(--green-500); color: #fff; }
36
+ .btn-g { border: 2px solid var(--green-500); color: var(--green-500); }
37
+
38
+ .features { padding: 4rem 2rem; max-width: 900px; margin: 0 auto; }
39
+ .sec-tag { font-size: 11px; font-weight: 700; color: var(--green-500); letter-spacing: .1em; text-transform: uppercase; margin-bottom: .4rem; }
40
+ .sec-title { font-size: 1.7rem; font-weight: 800; color: var(--green-900); margin-bottom: 2rem; }
41
+ .cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 14px; }
42
+ .card { border: 1px solid var(--border); border-radius: var(--radius-md); padding: 1.4rem; }
43
+ .card h3 { font-size: 14px; font-weight: 700; color: var(--green-900); margin: 0 0 .3rem; }
44
+ .card p { font-size: 13px; color: var(--muted); line-height: 1.55; margin: 0; }
45
+
46
+ footer { background: var(--green-900); color: #5DCAA5; text-align: center; padding: 2rem; font-size: 13px; }
47
+ footer a { color: #9FE1CB; text-decoration: none; }
48
+ "
49
+ }
50
+ body {
51
+ nav {
52
+ span class="logo" "🍃 Leaf"
53
+ div {
54
+ a href="#features" "Features"
55
+ a href="https://github.com/alamien060512-alt/leaf-lang" target="_blank" "GitHub"
56
+ }
57
+ }
58
+
59
+ div class="hero" {
60
+ div class="badge" "v1.0.0 — stable"
61
+ h1 {
62
+ span "Write HTML "
63
+ span class="accent" "like code."
64
+ }
65
+ p class="sub" "Leaf is a templating language that compiles to clean HTML. No angle brackets — just braces and indentation."
66
+ div class="btns" {
67
+ a class="btn btn-p" href="https://github.com/alamien060512-alt/leaf-lang" "Get Started"
68
+ a class="btn btn-g" href="#features" "Learn More"
69
+ }
70
+ }
71
+
72
+ div class="features" id="features" {
73
+ p class="sec-tag" "Why Leaf"
74
+ p class="sec-title" "Everything HTML has, cleaner."
75
+ div class="cards" {
76
+ div class="card" {
77
+ h3 "{ } Brace syntax"
78
+ p "Nest elements with curly braces. Familiar to any developer. No closing tags to forget."
79
+ }
80
+ div class="card" {
81
+ h3 "All HTML tags"
82
+ p "Every tag works: div, section, img, form, table, video, audio, canvas — all of them."
83
+ }
84
+ div class="card" {
85
+ h3 "Full CSS support"
86
+ p "Use style= with any CSS value. Background images, animations, variables — all supported."
87
+ }
88
+ div class="card" {
89
+ h3 "Zero dependencies"
90
+ p "The compiler is a single 200-line JS file. No build pipeline needed."
91
+ }
92
+ }
93
+ }
94
+
95
+ footer {
96
+ p {
97
+ span "Built with "
98
+ a href="https://github.com/alamien060512-alt/leaf-lang" "Leaf"
99
+ span " — MIT License"
100
+ }
101
+ }
102
+ }
103
+ }
@@ -0,0 +1,25 @@
1
+ leaf {
2
+ head {
3
+ title "My Leaf Page"
4
+ meta charset="UTF-8"
5
+ meta name="viewport" content="width=device-width,initial-scale=1"
6
+ style "
7
+ body {
8
+ font-family: 'Segoe UI', sans-serif;
9
+ padding: 2rem;
10
+ max-width: 700px;
11
+ margin: auto;
12
+ color: #1a1a1a;
13
+ }
14
+ h1 { color: #1D9E75; }
15
+ a { color: #1D9E75; }
16
+ "
17
+ }
18
+ body {
19
+ h1 "Hello from Leaf!"
20
+ p "Leaf compiles to clean HTML. No angle brackets needed."
21
+ a href="https://github.com/alamien060512-alt/leaf-lang" target="_blank" "View on GitHub"
22
+ br
23
+ img src="https://picsum.photos/seed/leaf/700/300" alt="Leaf example image" style="width:100%;border-radius:10px;margin-top:1rem"
24
+ }
25
+ }
@@ -0,0 +1,215 @@
1
+ "use strict";
2
+
3
+
4
+ const VOID_TAGS = new Set([
5
+ "area", "base", "br", "col", "embed", "hr", "img", "input",
6
+ "link", "meta", "param", "source", "track", "wbr"
7
+ ]);
8
+
9
+
10
+ const ALL_TAGS = new Set([
11
+ "a","abbr","address","article","aside","audio","b","blockquote","body","br",
12
+ "button","canvas","caption","cite","code","col","colgroup","data","datalist",
13
+ "dd","del","details","dfn","dialog","div","dl","dt","em","embed","fieldset",
14
+ "figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","head",
15
+ "header","hr","html","i","iframe","img","input","ins","kbd","label","legend",
16
+ "li","link","main","map","mark","menu","meta","meter","nav","noscript","object",
17
+ "ol","optgroup","option","output","p","param","picture","pre","progress","q",
18
+ "rp","rt","ruby","s","samp","script","section","select","small","source","span",
19
+ "strong","style","sub","summary","sup","table","tbody","td","template","textarea",
20
+ "tfoot","th","thead","time","title","tr","track","u","ul","var","video","wbr",
21
+ "leaf"
22
+ ]);
23
+
24
+
25
+ function tokenize(src) {
26
+ const tokens = [];
27
+ let i = 0;
28
+
29
+ while (i < src.length) {
30
+ const c = src[i];
31
+
32
+
33
+ if (/\s/.test(c)) { i++; continue; }
34
+
35
+
36
+ if (c === "#") {
37
+ while (i < src.length && src[i] !== "\n") i++;
38
+ continue;
39
+ }
40
+
41
+
42
+ if (c === "{") { tokens.push({ t: "open" }); i++; continue; }
43
+ if (c === "}") { tokens.push({ t: "close" }); i++; continue; }
44
+
45
+
46
+ if (c === "=") { tokens.push({ t: "eq" }); i++; continue; }
47
+
48
+
49
+ if (c === '"') {
50
+ i++;
51
+ let s = "";
52
+ while (i < src.length && src[i] !== '"') {
53
+ if (src[i] === "\\") { i++; s += src[i] || ""; }
54
+ else { s += src[i]; }
55
+ i++;
56
+ }
57
+ i++;
58
+ tokens.push({ t: "str", v: s });
59
+ continue;
60
+ }
61
+
62
+
63
+ let word = "";
64
+ while (i < src.length && !/[\s{}"#=]/.test(src[i])) word += src[i++];
65
+ if (word) tokens.push({ t: "word", v: word });
66
+ }
67
+
68
+ return tokens;
69
+ }
70
+
71
+
72
+ function looksLikeTag(tokens, pos) {
73
+ if (pos >= tokens.length) return false;
74
+ const tok = tokens[pos];
75
+ if (tok.t !== "word") return false;
76
+ if (!ALL_TAGS.has(tok.v.toLowerCase())) return false;
77
+ if (tokens[pos + 1]?.t === "eq") return false;
78
+ return true;
79
+ }
80
+
81
+ function parseNode(tokens, pos) {
82
+ if (tokens[pos].t !== "word") return null;
83
+
84
+ const tag = tokens[pos].v;
85
+ pos++;
86
+ const isVoid = VOID_TAGS.has(tag);
87
+ const attrs = {};
88
+ let text = null;
89
+
90
+
91
+ while (pos < tokens.length) {
92
+ const cur = tokens[pos];
93
+
94
+ if (cur.t === "open" || cur.t === "close") break;
95
+
96
+
97
+ if (cur.t === "str") { text = cur.v; pos++; break; }
98
+
99
+
100
+ if (cur.t === "word" && tokens[pos + 1]?.t === "eq") {
101
+ const key = cur.v;
102
+ pos += 2;
103
+ if (tokens[pos]?.t === "str") { attrs[key] = tokens[pos].v; pos++; }
104
+ else if (tokens[pos]?.t === "word") { attrs[key] = tokens[pos].v; pos++; }
105
+ else { attrs[key] = ""; }
106
+ if (isVoid && looksLikeTag(tokens, pos)) break;
107
+ continue;
108
+ }
109
+
110
+
111
+ if (cur.t === "word") {
112
+ if (looksLikeTag(tokens, pos)) break;
113
+ attrs[cur.v] = null;
114
+ pos++;
115
+ if (isVoid && looksLikeTag(tokens, pos)) break;
116
+ continue;
117
+ }
118
+
119
+ pos++;
120
+ }
121
+
122
+
123
+ let children = [];
124
+ if (!isVoid && pos < tokens.length && tokens[pos]?.t === "open") {
125
+ pos++;
126
+ const result = parseBlock(tokens, pos);
127
+ children = result.nodes;
128
+ pos = result.pos;
129
+ if (pos < tokens.length && tokens[pos]?.t === "close") pos++;
130
+ }
131
+
132
+ return { node: { tag, attrs, text, children }, pos };
133
+ }
134
+
135
+ function parseBlock(tokens, pos) {
136
+ const nodes = [];
137
+ while (pos < tokens.length && tokens[pos].t !== "close") {
138
+ if (tokens[pos].t !== "word") { pos++; continue; }
139
+ const result = parseNode(tokens, pos);
140
+ if (!result) { pos++; continue; }
141
+ nodes.push(result.node);
142
+ pos = result.pos;
143
+ }
144
+ return { nodes, pos };
145
+ }
146
+
147
+
148
+ function escapeHtml(s) {
149
+ return String(s)
150
+ .replace(/&/g, "&amp;")
151
+ .replace(/</g, "&lt;")
152
+ .replace(/>/g, "&gt;");
153
+ }
154
+
155
+ function escapeAttr(s) {
156
+ return String(s).replace(/"/g, "&quot;");
157
+ }
158
+
159
+ function renderAttrs(attrs) {
160
+ let s = "";
161
+ for (const [k, v] of Object.entries(attrs)) {
162
+ s += v === null || v === "" ? ` ${k}` : ` ${k}="${escapeAttr(v)}"`;
163
+ }
164
+ return s;
165
+ }
166
+
167
+ function renderNode(node, depth) {
168
+ const tag = node.tag === "leaf" ? "html" : node.tag;
169
+ const indent = " ".repeat(depth);
170
+ const attrs = renderAttrs(node.attrs);
171
+
172
+ if (VOID_TAGS.has(tag)) return `${indent}<${tag}${attrs}>`;
173
+
174
+ const parts = [];
175
+ if (node.text !== null && node.text !== undefined) parts.push(escapeHtml(node.text));
176
+ for (const child of node.children) parts.push(renderNode(child, depth + 1));
177
+
178
+ if (!parts.length) return `${indent}<${tag}${attrs}></${tag}>`;
179
+ if (parts.length === 1 && !parts[0].includes("\n")) return `${indent}<${tag}${attrs}>${parts[0]}</${tag}>`;
180
+ return `${indent}<${tag}${attrs}>\n${parts.join("\n")}\n${indent}</${tag}>`;
181
+ }
182
+
183
+
184
+
185
+ /**
186
+ * Compile a Leaf source string to HTML.
187
+ * @param {string} source Leaf source code
188
+ * @returns {string} Full HTML output including <!DOCTYPE html>
189
+ */
190
+ function compile(source) {
191
+ if (typeof source !== "string") throw new TypeError("source must be a string");
192
+
193
+ const tokens = tokenize(source);
194
+ const { nodes } = parseBlock(tokens, 0);
195
+
196
+ if (!nodes.length) throw new Error("Empty Leaf source — nothing to compile");
197
+
198
+ if (nodes[0].tag === "leaf") {
199
+ return "<!DOCTYPE html>\n" + renderNode(nodes[0], 0);
200
+ }
201
+
202
+ return nodes.map((n) => renderNode(n, 0)).join("\n");
203
+ }
204
+
205
+ /**
206
+ * Parse Leaf source into an AST without rendering.
207
+ * @param {string} source
208
+ * @returns {{ nodes: Array }}
209
+ */
210
+ function parse(source) {
211
+ const tokens = tokenize(source);
212
+ return parseBlock(tokens, 0);
213
+ }
214
+
215
+ module.exports = { compile, parse, tokenize };
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "leaf-lang",
3
+ "version": "1.0.0",
4
+ "description": "Leaf — a clean templating language that compiles to HTML. No angle brackets, just braces.",
5
+ "main": "lib/compiler.js",
6
+ "bin": {
7
+ "leafc": "bin/leafc.js"
8
+ },
9
+ "scripts": {
10
+ "test": "node test/compiler.test.js",
11
+ "build": "echo 'No build step needed — pure JS'",
12
+ "prepublishOnly": "npm test"
13
+ },
14
+ "keywords": [
15
+ "leaf",
16
+ "html",
17
+ "template",
18
+ "compiler",
19
+ "preprocessor",
20
+ "markup",
21
+ "web",
22
+ "static-site"
23
+ ],
24
+ "author": "Ahmad A. <al.amien060512@gmail.com>",
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/alamien060512-alt/leaf-lang.git"
29
+ },
30
+ "bugs": {
31
+ "url": "https://github.com/alamien060512-alt/leaf-lang/issues"
32
+ },
33
+ "homepage": "https://github.com/alamien060512-alt/leaf-lang#readme",
34
+ "engines": {
35
+ "node": ">=14.0.0"
36
+ },
37
+ "files": [
38
+ "bin/",
39
+ "lib/",
40
+ "examples/",
41
+ "README.md",
42
+ "LICENSE"
43
+ ],
44
+ "devDependencies": {}
45
+ }