devabhasha 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 +974 -0
- package/package.json +47 -0
- package/src/analyzer.js +125 -0
- package/src/bundler.js +129 -0
- package/src/cli.js +99 -0
- package/src/codegen.js +864 -0
- package/src/devserver.js +148 -0
- package/src/errors.js +71 -0
- package/src/index.js +30 -0
- package/src/io-browser.js +31 -0
- package/src/io-node.js +102 -0
- package/src/karaka-web.js +49 -0
- package/src/keywords.js +64 -0
- package/src/lexer.js +140 -0
- package/src/parser.js +687 -0
- package/src/server-node.js +120 -0
- package/src/server.js +182 -0
- package/src/stdlib.js +137 -0
- package/src/style.js +103 -0
- package/src/symbols.js +194 -0
- package/src/vibhakti.js +87 -0
package/src/devserver.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// devserver.js — a zero-dependency dev server with live reload for .deva web
|
|
2
|
+
// programs. Serves an HTML page that runs the compiled program, watches the
|
|
3
|
+
// source (and its directory, catching आयात imports), recompiles on change, and
|
|
4
|
+
// pushes a reload signal to the browser over Server-Sent Events. Compile errors
|
|
5
|
+
// are shown on the page instead of a blank screen.
|
|
6
|
+
//
|
|
7
|
+
// Uses only Node built-ins (http, fs) — no external packages — so it runs in
|
|
8
|
+
// the same dependency-free environment as the rest of the toolchain.
|
|
9
|
+
|
|
10
|
+
import { createServer } from 'http';
|
|
11
|
+
import { readFileSync, watch } from 'fs';
|
|
12
|
+
import { dirname, basename } from 'path';
|
|
13
|
+
import { bundle } from './bundler.js';
|
|
14
|
+
import { DevabhashaError, formatError } from './errors.js';
|
|
15
|
+
|
|
16
|
+
// Build the browser bundle for the entry file. Returns { ok, code | error }.
|
|
17
|
+
function buildBundle(entry) {
|
|
18
|
+
try {
|
|
19
|
+
const code = bundle(entry, { includeRuntime: true });
|
|
20
|
+
return { ok: true, code };
|
|
21
|
+
} catch (e) {
|
|
22
|
+
const src = (() => { try { return readFileSync(entry, 'utf8'); } catch { return ''; } })();
|
|
23
|
+
const msg = e instanceof DevabhashaError ? formatError(e, e.source || src) : ('दोषः: ' + e.message);
|
|
24
|
+
return { ok: false, error: msg };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// The HTML shell. #मूलम् is the mount root; the live-reload client listens on
|
|
29
|
+
// /__live (SSE) and reloads on the 'reload' event. A compile error is rendered
|
|
30
|
+
// into the page (passed via a global the bundle replaces) rather than crashing.
|
|
31
|
+
function pageHtml(entryName) {
|
|
32
|
+
return `<!DOCTYPE html>
|
|
33
|
+
<html lang="sa">
|
|
34
|
+
<head>
|
|
35
|
+
<meta charset="utf-8">
|
|
36
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
37
|
+
<title>${entryName} — देवभाषा</title>
|
|
38
|
+
<style>
|
|
39
|
+
body { margin: 0; font-family: system-ui, -apple-system, sans-serif; background: #f4f1ea; }
|
|
40
|
+
#__err { white-space: pre-wrap; font-family: ui-monospace, monospace; color: #a33;
|
|
41
|
+
background: #fff5f5; border: 1px solid #f3c0c0; border-radius: 8px;
|
|
42
|
+
margin: 20px; padding: 16px 20px; font-size: 13px; }
|
|
43
|
+
#मूलम् { min-height: 100vh; }
|
|
44
|
+
</style>
|
|
45
|
+
</head>
|
|
46
|
+
<body>
|
|
47
|
+
<div id="मूलम्"></div>
|
|
48
|
+
<script>
|
|
49
|
+
// live reload over Server-Sent Events
|
|
50
|
+
try {
|
|
51
|
+
const es = new EventSource('/__live');
|
|
52
|
+
es.addEventListener('reload', () => location.reload());
|
|
53
|
+
} catch (e) {}
|
|
54
|
+
</script>
|
|
55
|
+
<script src="/__bundle.js"></script>
|
|
56
|
+
</body>
|
|
57
|
+
</html>`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Wrap a compiled bundle so the DOM runtime mounts to #मूलम् by default and
|
|
61
|
+
// any runtime error is shown on the page.
|
|
62
|
+
function wrapBundle(code) {
|
|
63
|
+
return `(function(){
|
|
64
|
+
var __root = document.getElementById('मूलम्');
|
|
65
|
+
try {
|
|
66
|
+
${code}
|
|
67
|
+
} catch (e) {
|
|
68
|
+
var d = document.createElement('div'); d.id = '__err';
|
|
69
|
+
d.textContent = 'दोषः (runtime): ' + (e && e.message || e);
|
|
70
|
+
document.body.appendChild(d);
|
|
71
|
+
console.error(e);
|
|
72
|
+
}
|
|
73
|
+
})();`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function errorPageScript(message) {
|
|
77
|
+
return `(function(){
|
|
78
|
+
var d = document.createElement('div'); d.id = '__err';
|
|
79
|
+
d.textContent = ${JSON.stringify(message)};
|
|
80
|
+
document.body.appendChild(d);
|
|
81
|
+
})();`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function serve(entry, { port = 5173, open = false } = {}) {
|
|
85
|
+
const entryName = basename(entry);
|
|
86
|
+
const clients = new Set(); // open SSE responses
|
|
87
|
+
|
|
88
|
+
const server = createServer((req, res) => {
|
|
89
|
+
if (req.url === '/' || req.url === '/index.html') {
|
|
90
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
91
|
+
res.end(pageHtml(entryName));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (req.url === '/__bundle.js') {
|
|
95
|
+
const built = buildBundle(entry);
|
|
96
|
+
res.writeHead(200, { 'Content-Type': 'application/javascript; charset=utf-8' });
|
|
97
|
+
res.end(built.ok ? wrapBundle(built.code) : errorPageScript(built.error));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (req.url === '/__live') {
|
|
101
|
+
res.writeHead(200, {
|
|
102
|
+
'Content-Type': 'text/event-stream',
|
|
103
|
+
'Cache-Control': 'no-cache',
|
|
104
|
+
Connection: 'keep-alive',
|
|
105
|
+
});
|
|
106
|
+
res.write('retry: 1000\n\n');
|
|
107
|
+
clients.add(res);
|
|
108
|
+
req.on('close', () => clients.delete(res));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
res.writeHead(404); res.end('not found');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
function notifyReload() {
|
|
115
|
+
for (const res of clients) { try { res.write('event: reload\ndata: 1\n\n'); } catch {} }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Watch the entry's directory so edits to it OR its imports trigger a reload.
|
|
119
|
+
// Debounced — editors often fire several events per save.
|
|
120
|
+
let timer = null;
|
|
121
|
+
const dir = dirname(entry) || '.';
|
|
122
|
+
let watcher = null;
|
|
123
|
+
try {
|
|
124
|
+
watcher = watch(dir, { persistent: true }, (_evt, fname) => {
|
|
125
|
+
if (fname && !/\.deva$/.test(fname)) return; // only .deva changes
|
|
126
|
+
clearTimeout(timer);
|
|
127
|
+
timer = setTimeout(() => {
|
|
128
|
+
const built = buildBundle(entry);
|
|
129
|
+
console.log(built.ok ? `↻ recompiled ${entryName}` : `✗ ${entryName} has errors (shown in browser)`);
|
|
130
|
+
notifyReload();
|
|
131
|
+
}, 60);
|
|
132
|
+
});
|
|
133
|
+
} catch (e) {
|
|
134
|
+
console.error('चेतावनी: file watching unavailable — live reload disabled.');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
server.listen(port, () => {
|
|
138
|
+
// initial build feedback
|
|
139
|
+
const built = buildBundle(entry);
|
|
140
|
+
if (!built.ok) console.log(`✗ ${entryName} has errors (shown in browser):\n` + built.error);
|
|
141
|
+
console.log(`\n देवभाषा dev server\n ▸ http://localhost:${port}/\n watching ${entryName} for changes (live reload on)\n`);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
server,
|
|
146
|
+
close() { if (watcher) watcher.close(); for (const r of clients) { try { r.end(); } catch {} } server.close(); },
|
|
147
|
+
};
|
|
148
|
+
}
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// errors.js — structured compiler errors with source-context formatting.
|
|
2
|
+
//
|
|
3
|
+
// A good error shows WHERE the problem is, in the source, with a caret.
|
|
4
|
+
// DevabhashaError carries the position; formatError renders the offending
|
|
5
|
+
// line and points at the column:
|
|
6
|
+
//
|
|
7
|
+
// दोषः (parse error): expected ')' but found ';'
|
|
8
|
+
// line 3, column 12
|
|
9
|
+
//
|
|
10
|
+
// ३ | दर्शय(अ + ब;
|
|
11
|
+
// | ^
|
|
12
|
+
//
|
|
13
|
+
// kind is a short tag ('lex' | 'parse' | 'codegen') used for the heading.
|
|
14
|
+
|
|
15
|
+
export class DevabhashaError extends Error {
|
|
16
|
+
constructor(message, { line = null, col = null, kind = 'parse' } = {}) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = 'DevabhashaError';
|
|
19
|
+
this.line = line;
|
|
20
|
+
this.col = col;
|
|
21
|
+
this.kind = kind;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const KIND_LABEL = {
|
|
26
|
+
lex: 'अक्षरदोषः (lex error)',
|
|
27
|
+
parse: 'पाठदोषः (parse error)',
|
|
28
|
+
codegen: 'कूटदोषः (codegen error)',
|
|
29
|
+
runtime: 'क्रियादोषः (runtime error)',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Convert a Devanagari/ASCII digit line number into Devanagari for display.
|
|
33
|
+
const DEVA_DIGITS = '०१२३४५६७८९';
|
|
34
|
+
function toDeva(n) {
|
|
35
|
+
return String(n).split('').map(d => /[0-9]/.test(d) ? DEVA_DIGITS[+d] : d).join('');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Render an error with source context. `source` is the original program text.
|
|
39
|
+
export function formatError(err, source) {
|
|
40
|
+
const label = KIND_LABEL[err.kind] || 'दोषः (error)';
|
|
41
|
+
let out = `${label}: ${err.message}`;
|
|
42
|
+
|
|
43
|
+
if (err.line == null) return out;
|
|
44
|
+
|
|
45
|
+
out += `\n line ${err.line}, column ${err.col ?? '?'}`;
|
|
46
|
+
|
|
47
|
+
if (source != null) {
|
|
48
|
+
const lines = source.split('\n');
|
|
49
|
+
const srcLine = lines[err.line - 1];
|
|
50
|
+
if (srcLine !== undefined) {
|
|
51
|
+
const gutter = toDeva(err.line);
|
|
52
|
+
const pad = ' '.repeat(gutter.length);
|
|
53
|
+
out += `\n\n ${gutter} | ${srcLine}`;
|
|
54
|
+
if (err.col != null) {
|
|
55
|
+
// caret: account for the gutter and the column (1-based)
|
|
56
|
+
const caretPad = ' '.repeat(Math.max(0, err.col - 1));
|
|
57
|
+
out += `\n ${pad} | ${caretPad}^`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Helper used by the lexer/parser to raise a positioned error.
|
|
65
|
+
export function raise(message, pos, kind = 'parse') {
|
|
66
|
+
throw new DevabhashaError(message, {
|
|
67
|
+
line: pos && pos.line,
|
|
68
|
+
col: pos && pos.col,
|
|
69
|
+
kind,
|
|
70
|
+
});
|
|
71
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// index.js — public API: compile(source) -> javascript string.
|
|
2
|
+
|
|
3
|
+
import { tokenize } from './lexer.js';
|
|
4
|
+
import { parse } from './parser.js';
|
|
5
|
+
import { generate } from './codegen.js';
|
|
6
|
+
|
|
7
|
+
export function compile(source, options = {}) {
|
|
8
|
+
const tokens = tokenize(source);
|
|
9
|
+
const ast = parse(tokens);
|
|
10
|
+
return generate(ast, options);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Like compile, but returns { code, exports, imports } for the bundler.
|
|
14
|
+
// Never includes the runtime (the bundler adds it once for the whole program).
|
|
15
|
+
export function compileModule(source) {
|
|
16
|
+
const tokens = tokenize(source);
|
|
17
|
+
const ast = parse(tokens);
|
|
18
|
+
return generate(ast, { includeRuntime: false, withMeta: true });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Like compile, but also returns a Source Map v3 object: { code, map }.
|
|
22
|
+
export function compileWithMap(source, options = {}) {
|
|
23
|
+
const tokens = tokenize(source);
|
|
24
|
+
const ast = parse(tokens);
|
|
25
|
+
return generate(ast, { ...options, sourceMap: true });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { tokenize, parse, generate };
|
|
29
|
+
export { PRELUDE } from './codegen.js';
|
|
30
|
+
export { DevabhashaError, formatError } from './errors.js';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// io-browser.js — the browser backend for the layered I/O interface (__IO).
|
|
2
|
+
//
|
|
3
|
+
// The frontend half of a full-stack Devabhāṣā app runs in the browser and uses
|
|
4
|
+
// जाल (network) to talk to its backend. The codegen lowers जाल.आनय /
|
|
5
|
+
// आनयप्रदत्त to __IO.net.fetch / fetchJson, so a browser page that runs a
|
|
6
|
+
// compiled frontend needs an __IO whose `net` uses the browser's native fetch.
|
|
7
|
+
//
|
|
8
|
+
// File ops have no meaning in a browser, so सञ्चिका.* returns a clear failure
|
|
9
|
+
// Result rather than throwing — keeping the same program runnable on either
|
|
10
|
+
// host, with the Result telling it what isn't available here.
|
|
11
|
+
//
|
|
12
|
+
// Exported as a source string (like IO_NODE_SOURCE) so the server can prepend
|
|
13
|
+
// it to the frontend bundle it serves.
|
|
14
|
+
|
|
15
|
+
export const IO_BROWSER_SOURCE = `// --- देवभाषा I/O (browser backend) ---
|
|
16
|
+
const __IO = (() => {
|
|
17
|
+
const ok = (v) => ({ "सफल": true, "मूल्यम्": v, "दोषः": null });
|
|
18
|
+
const err = (e) => ({ "सफल": false, "मूल्यम्": null, "दोषः": String(e && e.message || e) });
|
|
19
|
+
const noFile = async () => err('file I/O is not available in the browser');
|
|
20
|
+
return {
|
|
21
|
+
file: {
|
|
22
|
+
read: noFile, write: noFile, exists: async () => ok(false),
|
|
23
|
+
remove: noFile, list: noFile, readJson: noFile, writeJson: noFile,
|
|
24
|
+
},
|
|
25
|
+
net: {
|
|
26
|
+
async fetch(url, options){ try { const res = await fetch(url, options||undefined); const text = await res.text(); return ok({ "स्थितिः": res.status, "पाठः": text, "सफलम्": res.ok }); } catch(e){ return err(e); } },
|
|
27
|
+
async fetchJson(url, options){ try { const res = await fetch(url, options||undefined); const text = await res.text(); try { return ok({ "स्थितिः": res.status, "प्रदत्तम्": JSON.parse(text), "सफलम्": res.ok }); } catch(e){ return err('JSON: '+(e&&e.message||e)); } } catch(e){ return err(e); } },
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
})();
|
|
31
|
+
`;
|
package/src/io-node.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// io-node.js — the Node backend for the __IO interface.
|
|
2
|
+
//
|
|
3
|
+
// Layered design: a Devabhāṣā program calls सञ्चिका.पठ / जाल.आनय, which the
|
|
4
|
+
// codegen lowers to __IO.file.read / __IO.net.fetch. The program is bound to
|
|
5
|
+
// the __IO *interface*, never to Node — this file is one concrete backend.
|
|
6
|
+
// A browser backend or an in-memory test backend can implement the same
|
|
7
|
+
// shape and be injected instead.
|
|
8
|
+
//
|
|
9
|
+
// THE __IO CONTRACT (every operation is async and returns a परिणाम/Result):
|
|
10
|
+
// __IO.file.read(path) → Promise<Result<string>>
|
|
11
|
+
// __IO.file.write(path, data) → Promise<Result<true>>
|
|
12
|
+
// __IO.file.exists(path) → Promise<Result<boolean>>
|
|
13
|
+
// __IO.file.remove(path) → Promise<Result<true>>
|
|
14
|
+
// __IO.file.list(dir) → Promise<Result<string[]>>
|
|
15
|
+
// __IO.net.fetch(url, options?) → Promise<Result<{ status, text, ok }>>
|
|
16
|
+
//
|
|
17
|
+
// A Result is { सफल: boolean, मूल्यम्: value, दोषः: error } — the same shape
|
|
18
|
+
// the __RT prelude produces, so awaited results inspect with फल.सफल etc.
|
|
19
|
+
|
|
20
|
+
import { readFile, writeFile, unlink, readdir, access } from 'fs/promises';
|
|
21
|
+
|
|
22
|
+
const ok = (v) => ({ 'सफल': true, 'मूल्यम्': v, 'दोषः': null });
|
|
23
|
+
const err = (e) => ({ 'सफल': false, 'मूल्यम्': null, 'दोषः': String(e && e.message || e) });
|
|
24
|
+
|
|
25
|
+
export const __IO = {
|
|
26
|
+
file: {
|
|
27
|
+
async read(path) {
|
|
28
|
+
try { return ok(await readFile(path, 'utf8')); }
|
|
29
|
+
catch (e) { return err(e); }
|
|
30
|
+
},
|
|
31
|
+
async write(path, data) {
|
|
32
|
+
try { await writeFile(path, String(data), 'utf8'); return ok(true); }
|
|
33
|
+
catch (e) { return err(e); }
|
|
34
|
+
},
|
|
35
|
+
async exists(path) {
|
|
36
|
+
try { await access(path); return ok(true); }
|
|
37
|
+
catch { return ok(false); } // absence is not an error — it's `false`
|
|
38
|
+
},
|
|
39
|
+
async remove(path) {
|
|
40
|
+
try { await unlink(path); return ok(true); }
|
|
41
|
+
catch (e) { return err(e); }
|
|
42
|
+
},
|
|
43
|
+
async list(dir) {
|
|
44
|
+
try { return ok(await readdir(dir)); }
|
|
45
|
+
catch (e) { return err(e); }
|
|
46
|
+
},
|
|
47
|
+
async readJson(path) { // read + parse JSON → परिणाम
|
|
48
|
+
try {
|
|
49
|
+
const text = await readFile(path, 'utf8');
|
|
50
|
+
try { return ok(JSON.parse(text)); }
|
|
51
|
+
catch (e) { return err('JSON: ' + (e && e.message || e)); }
|
|
52
|
+
} catch (e) { return err(e); }
|
|
53
|
+
},
|
|
54
|
+
async writeJson(path, value) { // serialize + write
|
|
55
|
+
try { await writeFile(path, JSON.stringify(value, null, 2), 'utf8'); return ok(true); }
|
|
56
|
+
catch (e) { return err(e); }
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
net: {
|
|
60
|
+
async fetch(url, options) {
|
|
61
|
+
try {
|
|
62
|
+
const res = await fetch(url, options || undefined);
|
|
63
|
+
const text = await res.text();
|
|
64
|
+
return ok({ 'स्थितिः': res.status, 'पाठः': text, 'सफलम्': res.ok });
|
|
65
|
+
} catch (e) { return err(e); }
|
|
66
|
+
},
|
|
67
|
+
async fetchJson(url, options) { // fetch + parse JSON → परिणाम
|
|
68
|
+
try {
|
|
69
|
+
const res = await fetch(url, options || undefined);
|
|
70
|
+
const text = await res.text();
|
|
71
|
+
try { return ok({ 'स्थितिः': res.status, 'प्रदत्तम्': JSON.parse(text), 'सफलम्': res.ok }); }
|
|
72
|
+
catch (e) { return err('JSON: ' + (e && e.message || e)); }
|
|
73
|
+
} catch (e) { return err(e); }
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// A source string that defines the same __IO for embedding in `build` output.
|
|
79
|
+
// (For build, we inline the backend so the produced .js is self-contained
|
|
80
|
+
// when run under Node.)
|
|
81
|
+
export const IO_NODE_SOURCE = `// --- देवभाषा I/O (Node backend) ---
|
|
82
|
+
const __IO = (() => {
|
|
83
|
+
const { readFile, writeFile, unlink, readdir, access } = require('fs/promises');
|
|
84
|
+
const ok = (v) => ({ "सफल": true, "मूल्यम्": v, "दोषः": null });
|
|
85
|
+
const err = (e) => ({ "सफल": false, "मूल्यम्": null, "दोषः": String(e && e.message || e) });
|
|
86
|
+
return {
|
|
87
|
+
file: {
|
|
88
|
+
async read(p){ try { return ok(await readFile(p,'utf8')); } catch(e){ return err(e); } },
|
|
89
|
+
async write(p,d){ try { await writeFile(p,String(d),'utf8'); return ok(true); } catch(e){ return err(e); } },
|
|
90
|
+
async exists(p){ try { await access(p); return ok(true); } catch { return ok(false); } },
|
|
91
|
+
async remove(p){ try { await unlink(p); return ok(true); } catch(e){ return err(e); } },
|
|
92
|
+
async list(dir){ try { return ok(await readdir(dir)); } catch(e){ return err(e); } },
|
|
93
|
+
async readJson(p){ try { const t = await readFile(p,'utf8'); try { return ok(JSON.parse(t)); } catch(e){ return err('JSON: '+(e&&e.message||e)); } } catch(e){ return err(e); } },
|
|
94
|
+
async writeJson(p,v){ try { await writeFile(p,JSON.stringify(v,null,2),'utf8'); return ok(true); } catch(e){ return err(e); } },
|
|
95
|
+
},
|
|
96
|
+
net: {
|
|
97
|
+
async fetch(url, options){ try { const res = await fetch(url, options||undefined); const text = await res.text(); return ok({ "स्थितिः": res.status, "पाठः": text, "सफलम्": res.ok }); } catch(e){ return err(e); } },
|
|
98
|
+
async fetchJson(url, options){ try { const res = await fetch(url, options||undefined); const text = await res.text(); try { return ok({ "स्थितिः": res.status, "प्रदत्तम्": JSON.parse(text), "सफलम्": res.ok }); } catch(e){ return err('JSON: '+(e&&e.message||e)); } } catch(e){ return err(e); } },
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
})();
|
|
102
|
+
`;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// karaka-web.js — maps kāraka roles to web/DOM construction semantics,
|
|
2
|
+
// and defines which stems name which DOM concepts.
|
|
3
|
+
//
|
|
4
|
+
// The signature verb रचय (racaya, "construct") takes a BAG of case-marked
|
|
5
|
+
// arguments in ANY ORDER and assembles a DOM element. Each kāraka fills a
|
|
6
|
+
// distinct slot, so order is irrelevant — exactly the Pāṇinian promise.
|
|
7
|
+
|
|
8
|
+
import { KARAKA } from './vibhakti.js';
|
|
9
|
+
|
|
10
|
+
// kāraka → which slot of a DOM construction it fills.
|
|
11
|
+
export const KARAKA_TO_SLOT = {
|
|
12
|
+
[KARAKA.KARTR]: 'tag', // कर्तृ (nom) — what the element IS (div, button…)
|
|
13
|
+
[KARAKA.KARMAN]: 'content', // कर्म (acc) — content placed into it
|
|
14
|
+
[KARAKA.KARANA]: 'handler', // करण (instr)— the handler function (instrument)
|
|
15
|
+
[KARAKA.SAMPRADANA]: 'event', // सम्प्रदान (dat) — event it responds to
|
|
16
|
+
[KARAKA.ADHIKARANA]: 'parent', // अधिकरण (loc) — where it mounts (locus)
|
|
17
|
+
[KARAKA.APADANA]: 'source', // अपादान (abl) — data source it derives from
|
|
18
|
+
[KARAKA.SAMBANDHA]: 'prop', // सम्बन्ध (gen) — attribute/property relation
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Stem vocabulary: nominative-case stems that name HTML tags.
|
|
22
|
+
// The user writes these inflected (पटः, मूलकम्…); the engine reads the
|
|
23
|
+
// ending for the role and this table for the tag name.
|
|
24
|
+
export const TAG_STEMS = {
|
|
25
|
+
'पट': 'button', // paṭa — "cloth/panel" → button
|
|
26
|
+
'मूल': 'div', // mūla — "root/base" → div (generic container)
|
|
27
|
+
'शीर्ष': 'h1', // śīrṣa — "head" → heading
|
|
28
|
+
'वाक्य': 'p', // vākya — "sentence" → paragraph
|
|
29
|
+
'सूची': 'ul', // sūcī — "list" → list
|
|
30
|
+
'पङ्क्ति':'li', // paṅkti — "row/line" → list item
|
|
31
|
+
'पीठ': 'input', // pīṭha — "seat/field" → input
|
|
32
|
+
'चित्र': 'img', // citra — "picture" → image
|
|
33
|
+
'सेतु': 'a', // setu — "bridge/link" → anchor
|
|
34
|
+
'क्षेत्र':'span', // kṣetra — "field/area"→ span
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Event stems (used in सम्प्रदान / dative position).
|
|
38
|
+
export const EVENT_STEMS = {
|
|
39
|
+
'स्पर्श': 'click', // sparśa — "touch" → click
|
|
40
|
+
'परिवर्तन':'change', // parivartana → change
|
|
41
|
+
'निवेश': 'input', // niveśa — "entry" → input
|
|
42
|
+
'प्रेषण': 'submit', // preṣaṇa — "sending"→ submit
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Construction / action verbs.
|
|
46
|
+
export const KARAKA_VERBS = {
|
|
47
|
+
'रचय': 'CONSTRUCT', // racaya — "construct" → build DOM element from kārakas
|
|
48
|
+
'योजय': 'ATTACH', // yojaya — "join" → mount (also legacy keyword)
|
|
49
|
+
};
|
package/src/keywords.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// keywords.js — the Sanskrit-language surface layer.
|
|
2
|
+
// Edit vocabulary here without touching the lexer/parser/codegen.
|
|
3
|
+
//
|
|
4
|
+
// Each keyword maps a Devanagari word -> an internal token type.
|
|
5
|
+
// The internal token types are language-neutral so the rest of the
|
|
6
|
+
// compiler never sees Sanskrit directly.
|
|
7
|
+
|
|
8
|
+
export const KEYWORDS = {
|
|
9
|
+
// declarations
|
|
10
|
+
'चर': 'LET', // cara — "it varies" → mutable binding
|
|
11
|
+
'नियत': 'CONST', // niyata — "fixed" → constant
|
|
12
|
+
'कार्य': 'FUNC', // kārya — "work to be done" → function
|
|
13
|
+
'फलम्': 'RETURN', // phalam — "fruit/result" → return
|
|
14
|
+
|
|
15
|
+
// control flow
|
|
16
|
+
'यदि': 'IF', // yadi — if
|
|
17
|
+
'अन्यथा': 'ELSE', // anyathā — otherwise
|
|
18
|
+
'यावत्': 'WHILE', // yāvat — "as long as" → while
|
|
19
|
+
'प्रत्येकम्': 'FOR', // pratyekam — "for each" → for-of
|
|
20
|
+
'भङ्ग': 'BREAK', // bhaṅga — "breaking" → break
|
|
21
|
+
'अनुवृत्तम्': 'CONTINUE', // anuvṛttam — "continuing" → continue
|
|
22
|
+
|
|
23
|
+
// literals
|
|
24
|
+
'सत्यम्': 'TRUE', // satyam — true
|
|
25
|
+
'असत्यम्': 'FALSE', // asatyam — false
|
|
26
|
+
'शून्यम्': 'NULL', // śūnyam — "void/zero" → null
|
|
27
|
+
|
|
28
|
+
// web / DOM layer
|
|
29
|
+
'दर्शय': 'PRINT', // darśaya — "cause to show" → console.log
|
|
30
|
+
'अङ्गम्': 'ELEMENT', // aṅgam — "limb/part" → createElement
|
|
31
|
+
'योजय': 'MOUNT', // yojaya — "join/attach" → append to DOM
|
|
32
|
+
'श्रोता': 'LISTEN', // śrotā — "listener" → addEventListener
|
|
33
|
+
'रचय': 'CONSTRUCT', // racaya — "construct" → kāraka-based DOM builder
|
|
34
|
+
'कोष': 'OBJECT', // kośa — "treasury/dictionary" → object literal
|
|
35
|
+
'रूप': 'STYLE', // rūpa — "form/appearance" → style block
|
|
36
|
+
'रूपनाम': 'STYLENAME', // rūpanāma — "form-name" → named reusable style
|
|
37
|
+
'भाव': 'STATE', // bhāva — "state/condition" → reactive state cell
|
|
38
|
+
'दृश्य': 'VIEW', // dṛśya — "view/visible" → reactive view region
|
|
39
|
+
'निर्यात': 'EXPORT', // niryāta — "sending out" → export
|
|
40
|
+
'आयात': 'IMPORT', // āyāta — "incoming" → import
|
|
41
|
+
'आ': 'FROM', // ā — "from" → module source preposition
|
|
42
|
+
'असमकालिक': 'ASYNC', // asamakālika — "asynchronous" → async function
|
|
43
|
+
'प्रतीक्षा': 'AWAIT', // pratīkṣā — "waiting" → await
|
|
44
|
+
'अथवा': 'ORELSE', // athavā — "or else" → Result value-or-fallback
|
|
45
|
+
'सूत्र': 'SUTRA', // sūtra — "thread" → a reactive reference (lazy, live)
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Reverse map for error messages / pretty-printing.
|
|
49
|
+
export const TOKEN_TO_WORD = Object.fromEntries(
|
|
50
|
+
Object.entries(KEYWORDS).map(([word, tok]) => [tok, word])
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// Multi-character operators must be listed longest-first so the lexer
|
|
54
|
+
// matches '==' before '=', '+=' before '+', etc.
|
|
55
|
+
export const OPERATORS = [
|
|
56
|
+
'===', '!==', '??',
|
|
57
|
+
'==', '!=', '<=', '>=', '&&', '||',
|
|
58
|
+
'+=', '-=', '*=', '/=', '%=', '++', '--',
|
|
59
|
+
'+', '-', '*', '/', '%', '=', '<', '>', '!', '?',
|
|
60
|
+
'(', ')', '{', '}', '[', ']', ',', ';', '.', ':',
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
// Devanagari danda (।) is accepted as a statement terminator, like ';'.
|
|
64
|
+
export const DANDA = '।';
|
package/src/lexer.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// lexer.js — turns Devanagari source text into a flat token stream.
|
|
2
|
+
|
|
3
|
+
import { KEYWORDS, OPERATORS, DANDA } from './keywords.js';
|
|
4
|
+
import { DevabhashaError } from './errors.js';
|
|
5
|
+
|
|
6
|
+
// Devanagari block: U+0900–U+097F. We treat a "word" as a run of
|
|
7
|
+
// Devanagari letters, virama, matras, anusvara, etc. plus ASCII letters.
|
|
8
|
+
// We exclude Devanagari punctuation: danda । (U+0964) and double danda ॥
|
|
9
|
+
// (U+0965), which are statement terminators, not identifier characters.
|
|
10
|
+
const DEVA = /[\u0900-\u0963\u0966-\u097F]/;
|
|
11
|
+
const DEVA_DIGITS = '०१२३४५६७८९';
|
|
12
|
+
|
|
13
|
+
function isWordChar(ch) {
|
|
14
|
+
return DEVA.test(ch) || /[A-Za-z_]/.test(ch);
|
|
15
|
+
}
|
|
16
|
+
function isDigit(ch) {
|
|
17
|
+
return /[0-9]/.test(ch) || DEVA_DIGITS.includes(ch);
|
|
18
|
+
}
|
|
19
|
+
function normalizeDigit(ch) {
|
|
20
|
+
const i = DEVA_DIGITS.indexOf(ch);
|
|
21
|
+
return i >= 0 ? String(i) : ch;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function tokenize(src) {
|
|
25
|
+
const tokens = [];
|
|
26
|
+
let i = 0;
|
|
27
|
+
let line = 1;
|
|
28
|
+
let col = 1;
|
|
29
|
+
|
|
30
|
+
const push = (type, value) => tokens.push({ type, value, line, col });
|
|
31
|
+
const advance = (n = 1) => { i += n; col += n; };
|
|
32
|
+
|
|
33
|
+
while (i < src.length) {
|
|
34
|
+
const ch = src[i];
|
|
35
|
+
|
|
36
|
+
// whitespace
|
|
37
|
+
if (ch === '\n') { line++; col = 1; i++; continue; }
|
|
38
|
+
if (/\s/.test(ch)) { advance(); continue; }
|
|
39
|
+
|
|
40
|
+
// comments: # to end of line
|
|
41
|
+
if (ch === '#') {
|
|
42
|
+
while (i < src.length && src[i] !== '\n') i++;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// danda → statement terminator
|
|
47
|
+
if (ch === DANDA) { push('SEMI', ';'); advance(); continue; }
|
|
48
|
+
|
|
49
|
+
// strings: "..." with \ escapes (plain — no interpolation by default).
|
|
50
|
+
// Interpolation is opt-in via the पाठ"…{expr}…" marker (handled below in
|
|
51
|
+
// the word branch), so existing strings containing literal braces are safe.
|
|
52
|
+
if (ch === '"' || ch === "'") {
|
|
53
|
+
const quote = ch;
|
|
54
|
+
const startLine = line, startCol = col;
|
|
55
|
+
advance();
|
|
56
|
+
let str = '';
|
|
57
|
+
while (i < src.length && src[i] !== quote) {
|
|
58
|
+
if (src[i] === '\\') { str += src[i] + src[i + 1]; advance(2); }
|
|
59
|
+
else if (src[i] === '\n') { line++; col = 1; str += '\n'; i++; }
|
|
60
|
+
else { str += src[i]; advance(); }
|
|
61
|
+
}
|
|
62
|
+
advance(); // closing quote
|
|
63
|
+
tokens.push({ type: 'STRING', value: str, line: startLine, col: startCol });
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// numbers (supports Devanagari digits and decimals)
|
|
68
|
+
if (isDigit(ch)) {
|
|
69
|
+
let num = '';
|
|
70
|
+
while (i < src.length && (isDigit(src[i]) || src[i] === '.')) {
|
|
71
|
+
num += normalizeDigit(src[i]);
|
|
72
|
+
advance();
|
|
73
|
+
}
|
|
74
|
+
push('NUMBER', num);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// words → keyword or identifier
|
|
79
|
+
if (isWordChar(ch)) {
|
|
80
|
+
const wStartLine = line, wStartCol = col;
|
|
81
|
+
let word = '';
|
|
82
|
+
while (i < src.length && (isWordChar(src[i]) || isDigit(src[i]))) {
|
|
83
|
+
word += src[i];
|
|
84
|
+
advance();
|
|
85
|
+
}
|
|
86
|
+
// पाठ"…{expr}…" — an interpolated string. Only when पाठ is immediately
|
|
87
|
+
// followed by a quote (no space), so the plain identifier पाठ is unaffected.
|
|
88
|
+
if (word === 'पाठ' && (src[i] === '"' || src[i] === "'")) {
|
|
89
|
+
const quote = src[i];
|
|
90
|
+
advance(); // opening quote
|
|
91
|
+
const chunks = [''];
|
|
92
|
+
const exprs = [];
|
|
93
|
+
while (i < src.length && src[i] !== quote) {
|
|
94
|
+
if (src[i] === '\\') {
|
|
95
|
+
const nxt = src[i + 1];
|
|
96
|
+
if (nxt === '{' || nxt === '}') { chunks[chunks.length - 1] += nxt; advance(2); }
|
|
97
|
+
else { chunks[chunks.length - 1] += src[i] + nxt; advance(2); }
|
|
98
|
+
} else if (src[i] === '{') {
|
|
99
|
+
advance(); // {
|
|
100
|
+
let depth = 1, expr = '';
|
|
101
|
+
while (i < src.length && depth > 0) {
|
|
102
|
+
if (src[i] === '{') depth++;
|
|
103
|
+
else if (src[i] === '}') { depth--; if (depth === 0) break; }
|
|
104
|
+
if (src[i] === '\n') { line++; col = 1; expr += '\n'; i++; }
|
|
105
|
+
else { expr += src[i]; advance(); }
|
|
106
|
+
}
|
|
107
|
+
advance(); // }
|
|
108
|
+
exprs.push(expr);
|
|
109
|
+
chunks.push('');
|
|
110
|
+
} else if (src[i] === '\n') { line++; col = 1; chunks[chunks.length - 1] += '\n'; i++; }
|
|
111
|
+
else { chunks[chunks.length - 1] += src[i]; advance(); }
|
|
112
|
+
}
|
|
113
|
+
advance(); // closing quote
|
|
114
|
+
tokens.push({ type: 'TEMPLATE', chunks, exprs, line: wStartLine, col: wStartCol });
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (KEYWORDS[word]) push(KEYWORDS[word], word);
|
|
118
|
+
else push('IDENT', word);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// operators / punctuation
|
|
123
|
+
const three = src.slice(i, i + 3);
|
|
124
|
+
const two = src.slice(i, i + 2);
|
|
125
|
+
if (OPERATORS.includes(three)) { push('OP', three); advance(3); continue; }
|
|
126
|
+
if (OPERATORS.includes(two)) {
|
|
127
|
+
const t = two === ';' ? 'SEMI' : 'OP';
|
|
128
|
+
push(t, two); advance(2); continue;
|
|
129
|
+
}
|
|
130
|
+
if (OPERATORS.includes(ch)) {
|
|
131
|
+
const t = ch === ';' ? 'SEMI' : 'OP';
|
|
132
|
+
push(t, ch); advance(); continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
throw new DevabhashaError(`अज्ञातं चिह्नम् (unknown character) '${ch}'`, { line, col, kind: 'lex' });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
push('EOF', null);
|
|
139
|
+
return tokens;
|
|
140
|
+
}
|