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 +21 -0
- package/README.md +327 -0
- package/bin/leafc.js +133 -0
- package/examples/form.leaf +44 -0
- package/examples/landing.leaf +103 -0
- package/examples/starter.leaf +25 -0
- package/lib/compiler.js +215 -0
- package/package.json +45 -0
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
|
+
}
|
package/lib/compiler.js
ADDED
|
@@ -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, "&")
|
|
151
|
+
.replace(/</g, "<")
|
|
152
|
+
.replace(/>/g, ">");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function escapeAttr(s) {
|
|
156
|
+
return String(s).replace(/"/g, """);
|
|
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
|
+
}
|