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/codegen.js
ADDED
|
@@ -0,0 +1,864 @@
|
|
|
1
|
+
// codegen.js — walks the AST and emits JavaScript source text.
|
|
2
|
+
|
|
3
|
+
import { METHODS, PROPERTIES, GLOBALS, MATH_CONSTANTS } from './stdlib.js';
|
|
4
|
+
import { styleProp, styleValue, isStyleWord } from './style.js';
|
|
5
|
+
import { DevabhashaError } from './errors.js';
|
|
6
|
+
|
|
7
|
+
export const PRELUDE = `// --- देवभाषा prelude (host-independent) ---
|
|
8
|
+
const __RT = {
|
|
9
|
+
// परिणाम (Result): explicit success/failure values for fallible operations.
|
|
10
|
+
// keys are the raw Sanskrit field names, since member access (फल.सफल) emits
|
|
11
|
+
// the property name unchanged.
|
|
12
|
+
ok(v) { return { "सफल": true, "मूल्यम्": v, "दोषः": null }; },
|
|
13
|
+
err(e) { return { "सफल": false, "मूल्यम्": null, "दोषः": e }; },
|
|
14
|
+
// result अथवा fallback — the Result's value if सफल, else the (lazy) fallback.
|
|
15
|
+
// A non-Result value is returned as-is (so अथवा is also a null/Err guard).
|
|
16
|
+
orElse(r, fb) {
|
|
17
|
+
if (r && typeof r === "object" && "सफल" in r) return r["सफल"] ? r["मूल्यम्"] : fb();
|
|
18
|
+
return r == null ? fb() : r;
|
|
19
|
+
},
|
|
20
|
+
// प्रकारः — a value's kind, named in Sanskrit (for reflection / tests).
|
|
21
|
+
typeOf(v) {
|
|
22
|
+
if (v === null || v === undefined) return "रिक्त"; // null/undefined
|
|
23
|
+
if (Array.isArray(v)) return "सूची"; // array
|
|
24
|
+
const t = typeof v;
|
|
25
|
+
if (t === "number") return "अङ्क"; // number
|
|
26
|
+
if (t === "string") return "वाक्"; // string
|
|
27
|
+
if (t === "boolean") return "सत्यासत्य"; // boolean
|
|
28
|
+
if (t === "function") return "कार्य"; // function
|
|
29
|
+
return "कोष"; // object/record
|
|
30
|
+
},
|
|
31
|
+
// प्रदत्त (data) — JSON parse/serialize, both returning परिणाम since
|
|
32
|
+
// JSON.parse throws and the language has no exceptions.
|
|
33
|
+
json: {
|
|
34
|
+
विश्लेषय(text) { // parse
|
|
35
|
+
try { return __RT.ok(JSON.parse(text)); }
|
|
36
|
+
catch (e) { return __RT.err(String(e && e.message || e)); }
|
|
37
|
+
},
|
|
38
|
+
सूत्रय(v, pretty) { // stringify (pretty by default)
|
|
39
|
+
try { return __RT.ok(JSON.stringify(v, null, pretty === false ? undefined : 2)); }
|
|
40
|
+
catch (e) { return __RT.err(String(e && e.message || e)); }
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
// अङ्कय — parse a string to a number → परिणाम (Err if not a number).
|
|
44
|
+
toNumber(s) {
|
|
45
|
+
const n = Number(s);
|
|
46
|
+
return Number.isNaN(n) ? __RT.err("अङ्कः न (not a number): " + s) : __RT.ok(n);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
const RUNTIME = `// --- देवभाषा runtime ---
|
|
52
|
+
const __DB = {
|
|
53
|
+
el(tag, ...rest) {
|
|
54
|
+
const node = document.createElement(tag);
|
|
55
|
+
for (const r of rest) {
|
|
56
|
+
if (r == null) continue;
|
|
57
|
+
if (typeof r === 'object' && !(r instanceof Node) && !Array.isArray(r)) {
|
|
58
|
+
// props/attrs object
|
|
59
|
+
for (const [k, v] of Object.entries(r)) {
|
|
60
|
+
if (k.startsWith('on') && typeof v === 'function') {
|
|
61
|
+
node.addEventListener(k.slice(2).toLowerCase(), v);
|
|
62
|
+
} else if (k === 'style' && typeof v === 'object') {
|
|
63
|
+
Object.assign(node.style, v);
|
|
64
|
+
} else {
|
|
65
|
+
node.setAttribute(k, v);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} else if (Array.isArray(r)) {
|
|
69
|
+
r.forEach(c => node.append(c instanceof Node ? c : document.createTextNode(String(c))));
|
|
70
|
+
} else {
|
|
71
|
+
node.append(r instanceof Node ? r : document.createTextNode(String(r)));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return node;
|
|
75
|
+
},
|
|
76
|
+
mount(node, target) {
|
|
77
|
+
const t = typeof target === 'string' ? document.querySelector(target) : (target || document.body);
|
|
78
|
+
t.append(node);
|
|
79
|
+
return node;
|
|
80
|
+
},
|
|
81
|
+
listen(node, event, handler) {
|
|
82
|
+
node.addEventListener(event, handler);
|
|
83
|
+
return node;
|
|
84
|
+
},
|
|
85
|
+
construct({ tag, content, contentBind, event, handler, parent, prop, source, children, style, styleBind }) {
|
|
86
|
+
const node = document.createElement(tag);
|
|
87
|
+
if (contentBind != null) {
|
|
88
|
+
// fine-grained: a bound text node that updates in place on dep change
|
|
89
|
+
node.append(__DB.bindText(contentBind));
|
|
90
|
+
} else if (content != null && content.__isSutra) {
|
|
91
|
+
// a सूत्र reactive reference passed as content → bind fine-grained
|
|
92
|
+
node.append(__DB.bindText(content));
|
|
93
|
+
} else if (content != null) {
|
|
94
|
+
if (Array.isArray(content)) content.forEach(c => node.append(c instanceof Node ? c : document.createTextNode(String(c))));
|
|
95
|
+
else node.append(content instanceof Node ? content : document.createTextNode(String(content)));
|
|
96
|
+
}
|
|
97
|
+
if (children) {
|
|
98
|
+
// DOM append moves nodes, so nested child constructs are correctly
|
|
99
|
+
// re-parented into this element (समास composition). An array child is
|
|
100
|
+
// flattened — this is what makes list rendering (.प्रतिचित्रय → nodes) work.
|
|
101
|
+
const appendChild = c => {
|
|
102
|
+
if (c == null) return;
|
|
103
|
+
if (Array.isArray(c)) { c.forEach(appendChild); return; }
|
|
104
|
+
node.append(c instanceof Node ? c : document.createTextNode(String(c)));
|
|
105
|
+
};
|
|
106
|
+
for (const c of children) appendChild(c);
|
|
107
|
+
}
|
|
108
|
+
if (style && typeof style === 'object') {
|
|
109
|
+
Object.assign(node.style, style);
|
|
110
|
+
}
|
|
111
|
+
if (styleBind && typeof styleBind === 'object') {
|
|
112
|
+
// each dynamic style property gets its own effect → only that property
|
|
113
|
+
// updates when its dependencies change (fine-grained, no rebuild).
|
|
114
|
+
for (const [k, thunk] of Object.entries(styleBind)) {
|
|
115
|
+
__DB.effect(() => { node.style[k] = thunk(); });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (prop && typeof prop === 'object') {
|
|
119
|
+
for (const [k, v] of Object.entries(prop)) node.setAttribute(k, v);
|
|
120
|
+
}
|
|
121
|
+
if (event && handler) node.addEventListener(event, handler);
|
|
122
|
+
if (parent != null) {
|
|
123
|
+
const t = typeof parent === 'string' ? document.querySelector(parent) : parent;
|
|
124
|
+
(t || document.body).append(node);
|
|
125
|
+
}
|
|
126
|
+
return node;
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
// ----- reactivity -----
|
|
130
|
+
// A subscriber stack: whatever is on top when a भाव cell is READ becomes a
|
|
131
|
+
// dependency of that subscriber. Both the coarse दृश्य (a whole-view render)
|
|
132
|
+
// and a fine-grained प्रभाव (effect) push themselves here. A subscriber is an
|
|
133
|
+
// object { run, deps } where deps is the set of cells it currently reads.
|
|
134
|
+
_subStack: [],
|
|
135
|
+
_currentSub() { return __DB._subStack.length ? __DB._subStack[__DB._subStack.length - 1] : null; },
|
|
136
|
+
state(initial) {
|
|
137
|
+
let value = initial;
|
|
138
|
+
const subs = new Set(); // subscribers depending on this cell
|
|
139
|
+
const cell = (...args) => {
|
|
140
|
+
if (args.length === 0) { // read — track the current subscriber
|
|
141
|
+
const sub = __DB._currentSub();
|
|
142
|
+
if (sub) { subs.add(sub); if (sub.deps) sub.deps.add(cell); }
|
|
143
|
+
return value;
|
|
144
|
+
}
|
|
145
|
+
const next = args[0]; // write
|
|
146
|
+
// skip re-render only when an unchanged PRIMITIVE is written; object/
|
|
147
|
+
// array state is usually mutated in place, so always re-render those.
|
|
148
|
+
if (next === value && (next === null || typeof next !== 'object')) return value;
|
|
149
|
+
value = next;
|
|
150
|
+
// notify every subscriber (snapshot first — re-running mutates the set)
|
|
151
|
+
for (const sub of Array.from(subs)) {
|
|
152
|
+
if (typeof sub === 'function') sub(); // legacy view render
|
|
153
|
+
else if (sub && sub.run) sub.run(); // effect / binding
|
|
154
|
+
}
|
|
155
|
+
return value;
|
|
156
|
+
};
|
|
157
|
+
cell.__isState = true;
|
|
158
|
+
cell.__unsubscribe = (sub) => subs.delete(sub);
|
|
159
|
+
return cell;
|
|
160
|
+
},
|
|
161
|
+
// प्रभाव — a fine-grained effect. Runs fn now, tracking which भाव cells it
|
|
162
|
+
// reads, and re-runs ONLY fn when any of those change. Before each re-run it
|
|
163
|
+
// unsubscribes from its previous dependencies (so conditional reads don't
|
|
164
|
+
// leave stale subscriptions) and re-tracks fresh ones.
|
|
165
|
+
effect(fn) {
|
|
166
|
+
const sub = {
|
|
167
|
+
deps: new Set(),
|
|
168
|
+
cleanups: [],
|
|
169
|
+
run() {
|
|
170
|
+
// run any registered cleanups from the previous run (teardown)
|
|
171
|
+
for (const c of sub.cleanups) { try { c(); } catch (e) {} }
|
|
172
|
+
sub.cleanups = [];
|
|
173
|
+
// drop old subscriptions, then re-track on this run
|
|
174
|
+
for (const cell of sub.deps) if (cell.__unsubscribe) cell.__unsubscribe(sub);
|
|
175
|
+
sub.deps.clear();
|
|
176
|
+
__DB._subStack.push(sub);
|
|
177
|
+
const prevEffect = __DB._activeEffect; __DB._activeEffect = sub;
|
|
178
|
+
try { fn(); } finally { __DB._activeEffect = prevEffect; __DB._subStack.pop(); }
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
sub.run();
|
|
182
|
+
return sub;
|
|
183
|
+
},
|
|
184
|
+
_activeEffect: null,
|
|
185
|
+
// सफाई — register a cleanup that runs before the current effect's next run
|
|
186
|
+
// (and could run on disposal). The standard teardown hook for timers/listeners.
|
|
187
|
+
onCleanup(fn) { if (__DB._activeEffect) __DB._activeEffect.cleanups.push(fn); },
|
|
188
|
+
// सूत्र — tag a thunk as a reactive reference so content slots / बन्ध bind it.
|
|
189
|
+
sutra(thunk) { thunk.__isSutra = true; return thunk; },
|
|
190
|
+
// आलस्यचित्रम् — a lazy-loaded image. Renders an img showing the placeholder
|
|
191
|
+
// (or nothing) and swaps in the real src only once it scrolls into view, via
|
|
192
|
+
// IntersectionObserver. opts: { alt, placeholder, rootMargin }. Falls back to
|
|
193
|
+
// eager loading where IntersectionObserver is unavailable.
|
|
194
|
+
lazyImage(src, opts) {
|
|
195
|
+
opts = opts || {};
|
|
196
|
+
const img = document.createElement('img');
|
|
197
|
+
if (opts.alt != null) img.setAttribute('alt', opts.alt);
|
|
198
|
+
if (opts.placeholder) img.setAttribute('src', opts.placeholder);
|
|
199
|
+
img.setAttribute('data-src', src);
|
|
200
|
+
img.setAttribute('loading', 'lazy'); // native hint where supported
|
|
201
|
+
const load = () => { if (img.getAttribute('src') !== src) img.setAttribute('src', src); };
|
|
202
|
+
if (typeof IntersectionObserver === 'function') {
|
|
203
|
+
const io = new IntersectionObserver((entries) => {
|
|
204
|
+
for (const e of entries) {
|
|
205
|
+
if (e.isIntersecting) { load(); io.unobserve(img); }
|
|
206
|
+
}
|
|
207
|
+
}, { rootMargin: opts.rootMargin || '200px' });
|
|
208
|
+
io.observe(img);
|
|
209
|
+
} else {
|
|
210
|
+
load(); // no observer → load now
|
|
211
|
+
}
|
|
212
|
+
return img;
|
|
213
|
+
},
|
|
214
|
+
// bindText — fine-grained: a text node whose content is produced by thunk();
|
|
215
|
+
// only this node's text updates when the thunk's dependencies change.
|
|
216
|
+
bindText(thunk) {
|
|
217
|
+
const node = document.createTextNode('');
|
|
218
|
+
__DB.effect(() => { node.textContent = String(thunk()); });
|
|
219
|
+
return node;
|
|
220
|
+
},
|
|
221
|
+
// आवली — keyed list reconciliation. dataThunk() returns the current array;
|
|
222
|
+
// keyFn(item, i) gives a STABLE identity; renderFn(item, i) builds a node for
|
|
223
|
+
// a new key. Wrapped in an effect, so it re-runs when the data signal changes.
|
|
224
|
+
// On each run it reuses the DOM nodes of surviving keys (preserving their
|
|
225
|
+
// state/focus), creates nodes for new keys, removes vanished ones, and
|
|
226
|
+
// reorders children to match the new sequence — without rebuilding everything.
|
|
227
|
+
keyedList(dataThunk, keyFn, renderFn) {
|
|
228
|
+
const host = document.createElement('div');
|
|
229
|
+
host.style.display = 'contents'; // transparent wrapper, no layout box
|
|
230
|
+
let prev = new Map(); // key → node (from the last run)
|
|
231
|
+
__DB.effect(() => {
|
|
232
|
+
const items = dataThunk() || [];
|
|
233
|
+
const next = new Map();
|
|
234
|
+
const ordered = [];
|
|
235
|
+
items.forEach((item, i) => {
|
|
236
|
+
const k = String(keyFn(item, i));
|
|
237
|
+
let node = prev.get(k);
|
|
238
|
+
if (node === undefined) node = renderFn(item, i); // new key → build
|
|
239
|
+
next.set(k, node);
|
|
240
|
+
ordered.push(node);
|
|
241
|
+
});
|
|
242
|
+
// remove nodes whose key vanished
|
|
243
|
+
for (const [k, node] of prev) {
|
|
244
|
+
if (!next.has(k) && node.parentNode === host) host.removeChild(node);
|
|
245
|
+
}
|
|
246
|
+
// place nodes in the new order (reusing/moving existing ones)
|
|
247
|
+
let ref = null; // insert before the previous sibling
|
|
248
|
+
for (let i = ordered.length - 1; i >= 0; i--) {
|
|
249
|
+
const node = ordered[i];
|
|
250
|
+
if (node.nextSibling !== ref || node.parentNode !== host) host.insertBefore(node, ref);
|
|
251
|
+
ref = node;
|
|
252
|
+
}
|
|
253
|
+
prev = next;
|
|
254
|
+
});
|
|
255
|
+
return host;
|
|
256
|
+
},
|
|
257
|
+
view(container, viewFn) {
|
|
258
|
+
const host = container ? (typeof container === 'string' ? document.querySelector(container) : container) : document.body;
|
|
259
|
+
const render = () => {
|
|
260
|
+
__DB._subStack.push(render);
|
|
261
|
+
let out;
|
|
262
|
+
try { out = viewFn(); } finally { __DB._subStack.pop(); }
|
|
263
|
+
host.innerHTML = '';
|
|
264
|
+
const append = c => {
|
|
265
|
+
if (c == null) return;
|
|
266
|
+
if (Array.isArray(c)) { c.forEach(append); return; }
|
|
267
|
+
host.append(c instanceof Node ? c : document.createTextNode(String(c)));
|
|
268
|
+
};
|
|
269
|
+
append(out);
|
|
270
|
+
};
|
|
271
|
+
render();
|
|
272
|
+
return host;
|
|
273
|
+
},
|
|
274
|
+
// ----- timing & input (for animation loops / games) -----
|
|
275
|
+
interval(fn, ms) { return setInterval(fn, ms); },
|
|
276
|
+
clearTimer(id) { clearInterval(id); },
|
|
277
|
+
onKey(fn) {
|
|
278
|
+
const h = (e) => fn(e.key);
|
|
279
|
+
document.addEventListener('keydown', h);
|
|
280
|
+
return h;
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
`;
|
|
284
|
+
|
|
285
|
+
export function generate(ast, { includeRuntime = true, withMeta = false, sourceMap = false } = {}) {
|
|
286
|
+
let out = '';
|
|
287
|
+
// output position tracking (for source maps)
|
|
288
|
+
let outLine = 0; // 0-based
|
|
289
|
+
let outCol = 0; // 0-based
|
|
290
|
+
const emit = (s) => {
|
|
291
|
+
out += s;
|
|
292
|
+
// advance the output cursor
|
|
293
|
+
let nl = -1, from = 0, count = 0, last = -1;
|
|
294
|
+
for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) === 10) { count++; last = i; }
|
|
295
|
+
if (count === 0) { outCol += s.length; }
|
|
296
|
+
else { outLine += count; outCol = s.length - last - 1; }
|
|
297
|
+
};
|
|
298
|
+
// collected mappings: { genLine, genCol, srcLine (1-based), srcCol (1-based) }
|
|
299
|
+
const mappings = [];
|
|
300
|
+
const recordMapping = (node) => {
|
|
301
|
+
if (!sourceMap || !node || node.line == null) return;
|
|
302
|
+
mappings.push({ genLine: outLine, genCol: outCol, srcLine: node.line, srcCol: node.col || 1 });
|
|
303
|
+
};
|
|
304
|
+
// names declared with भाव are reactive cells: reads → x(), writes → x(v)
|
|
305
|
+
const stateNames = new Set();
|
|
306
|
+
// true while generating a दृश्य body (coarse re-render owns updates there)
|
|
307
|
+
let inView = false;
|
|
308
|
+
// does an expression read any भाव cell? If so, a रचय content slot bound to it
|
|
309
|
+
// is DYNAMIC and gets compiled to a fine-grained thunk (auto बन्ध) instead of
|
|
310
|
+
// an eagerly-evaluated value. A function expression's body is its own scope —
|
|
311
|
+
// we don't descend into it (an event handler reading state isn't a text dep).
|
|
312
|
+
function readsState(n) {
|
|
313
|
+
if (!n || typeof n !== 'object') return false;
|
|
314
|
+
if (n.type === 'Identifier') return stateNames.has(n.name);
|
|
315
|
+
if (n.type === 'FuncExpr' || n.type === 'FuncDecl') return false;
|
|
316
|
+
for (const k of Object.keys(n)) {
|
|
317
|
+
if (k === 'line' || k === 'col' || k === 'namePos' || k === 'paramPos') continue;
|
|
318
|
+
const v = n[k];
|
|
319
|
+
if (Array.isArray(v)) { if (v.some(readsState)) return true; }
|
|
320
|
+
else if (v && typeof v === 'object' && v.type) { if (readsState(v)) return true; }
|
|
321
|
+
}
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
// module metadata collected during codegen
|
|
325
|
+
const exports = [];
|
|
326
|
+
const imports = [];
|
|
327
|
+
// names bound to imported namespaces (आयात * रूपेण ग): member access on
|
|
328
|
+
// these uses id(prop), not the stdlib method tables.
|
|
329
|
+
const namespaceAliases = new Set();
|
|
330
|
+
// async-context depth: प्रतीक्षा (await) is only valid when > 0
|
|
331
|
+
let asyncDepth = 0;
|
|
332
|
+
// inside a रूप style-value expression: bare color/style words → CSS literals
|
|
333
|
+
let inStyleValue = false;
|
|
334
|
+
|
|
335
|
+
function gen(node, indent = '') {
|
|
336
|
+
switch (node.type) {
|
|
337
|
+
case 'Program':
|
|
338
|
+
node.body.forEach(s => { emit(indent); genStatement(s, indent); emit('\n'); });
|
|
339
|
+
break;
|
|
340
|
+
default:
|
|
341
|
+
genStatement(node, indent);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function genStatement(node, indent) {
|
|
346
|
+
recordMapping(node);
|
|
347
|
+
switch (node.type) {
|
|
348
|
+
case 'VarDecl': {
|
|
349
|
+
const kw = node.kind === 'CONST' ? 'const' : 'let';
|
|
350
|
+
emit(`${kw} ${id(node.name)}`);
|
|
351
|
+
if (node.init) { emit(' = '); genExpr(node.init); }
|
|
352
|
+
emit(';');
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
case 'StyleDecl': {
|
|
356
|
+
// रूपनाम X = रूप {...} → const X = { ...translated style... };
|
|
357
|
+
emit(`const ${id(node.name)} = `);
|
|
358
|
+
emitStyleObject(node.pairs);
|
|
359
|
+
emit(';');
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
case 'StateDecl': {
|
|
363
|
+
// भाव x = init → const x = __DB.state(init);
|
|
364
|
+
stateNames.add(node.name);
|
|
365
|
+
emit(`const ${id(node.name)} = __DB.state(`);
|
|
366
|
+
if (node.init) genExpr(node.init); else emit('undefined');
|
|
367
|
+
emit(');');
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
case 'View': {
|
|
371
|
+
// दृश्य (container) { body } → __DB.view(container, () => { … return last });
|
|
372
|
+
// inside a दृश्य the coarse re-render owns updates, so content slots stay
|
|
373
|
+
// eager (no fine-grained binding) to avoid double-updating.
|
|
374
|
+
emit('__DB.view(');
|
|
375
|
+
if (node.container) genExpr(node.container); else emit('null');
|
|
376
|
+
emit(', () => ');
|
|
377
|
+
const savedInView = inView; inView = true;
|
|
378
|
+
genViewBody(node.body, indent);
|
|
379
|
+
inView = savedInView;
|
|
380
|
+
emit(');');
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
case 'Export':
|
|
384
|
+
// emit the underlying declaration; the export is recorded as metadata
|
|
385
|
+
exports.push(node.name);
|
|
386
|
+
genStatement(node.decl, indent);
|
|
387
|
+
break;
|
|
388
|
+
case 'Import':
|
|
389
|
+
// imports are resolved by the bundler; record metadata and emit
|
|
390
|
+
// nothing inline (the bundler prepends linked module code).
|
|
391
|
+
imports.push(node);
|
|
392
|
+
if (node.kind === 'namespace') namespaceAliases.add(node.alias);
|
|
393
|
+
break;
|
|
394
|
+
case 'FuncDecl': {
|
|
395
|
+
emit(`${node.async ? 'async ' : ''}function ${id(node.name)}(${node.params.map(id).join(', ')}) `);
|
|
396
|
+
const savedAD = asyncDepth;
|
|
397
|
+
asyncDepth = node.async ? 1 : 0; // entering a function resets context
|
|
398
|
+
genBlock(node.body, indent);
|
|
399
|
+
asyncDepth = savedAD;
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
case 'Return':
|
|
403
|
+
emit('return');
|
|
404
|
+
if (node.argument) { emit(' '); genExpr(node.argument); }
|
|
405
|
+
emit(';');
|
|
406
|
+
break;
|
|
407
|
+
case 'If':
|
|
408
|
+
emit('if (');
|
|
409
|
+
genExpr(node.test);
|
|
410
|
+
emit(') ');
|
|
411
|
+
genBlock(node.consequent, indent);
|
|
412
|
+
if (node.alternate) {
|
|
413
|
+
emit(' else ');
|
|
414
|
+
if (node.alternate.type === 'If') genStatement(node.alternate, indent);
|
|
415
|
+
else genBlock(node.alternate, indent);
|
|
416
|
+
}
|
|
417
|
+
break;
|
|
418
|
+
case 'While':
|
|
419
|
+
emit('while (');
|
|
420
|
+
genExpr(node.test);
|
|
421
|
+
emit(') ');
|
|
422
|
+
genBlock(node.body, indent);
|
|
423
|
+
break;
|
|
424
|
+
case 'ForOf':
|
|
425
|
+
emit(`for (const ${id(node.item)} of `);
|
|
426
|
+
genExpr(node.iterable);
|
|
427
|
+
emit(') ');
|
|
428
|
+
genBlock(node.body, indent);
|
|
429
|
+
break;
|
|
430
|
+
case 'Break': emit('break;'); break;
|
|
431
|
+
case 'Continue': emit('continue;'); break;
|
|
432
|
+
case 'Print':
|
|
433
|
+
emit('console.log(');
|
|
434
|
+
node.args.forEach((a, i) => { if (i) emit(', '); genExpr(a); });
|
|
435
|
+
emit(');');
|
|
436
|
+
break;
|
|
437
|
+
case 'Block':
|
|
438
|
+
genBlock(node, indent);
|
|
439
|
+
break;
|
|
440
|
+
case 'ExpressionStatement':
|
|
441
|
+
genExpr(node.expression);
|
|
442
|
+
emit(';');
|
|
443
|
+
break;
|
|
444
|
+
default:
|
|
445
|
+
throw new Error(`codegen: unknown statement ${node.type}`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function genBlock(block, indent) {
|
|
450
|
+
const inner = indent + ' ';
|
|
451
|
+
emit('{\n');
|
|
452
|
+
block.body.forEach(s => { emit(inner); genStatement(s, inner); emit('\n'); });
|
|
453
|
+
emit(indent + '}');
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// A view body: like a block, but the LAST statement, if it's an expression,
|
|
457
|
+
// becomes the returned value (the thing to render).
|
|
458
|
+
function genViewBody(block, indent) {
|
|
459
|
+
const inner = indent + ' ';
|
|
460
|
+
emit('{\n');
|
|
461
|
+
block.body.forEach((s, i) => {
|
|
462
|
+
const last = i === block.body.length - 1;
|
|
463
|
+
emit(inner);
|
|
464
|
+
if (last && s.type === 'ExpressionStatement') {
|
|
465
|
+
emit('return ');
|
|
466
|
+
genExpr(s.expression);
|
|
467
|
+
emit(';');
|
|
468
|
+
} else {
|
|
469
|
+
genStatement(s, inner);
|
|
470
|
+
}
|
|
471
|
+
emit('\n');
|
|
472
|
+
});
|
|
473
|
+
emit(indent + '}');
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// emit a { } object literal from रूप (key, value) pairs, translating
|
|
477
|
+
// Sanskrit property names and known value words into CSS.
|
|
478
|
+
// emit a style object for the given pairs. Static pairs go into a plain
|
|
479
|
+
// object; pairs whose value reads a भाव cell (outside a view) are emitted
|
|
480
|
+
// separately as styleBind thunks so the runtime updates just that property
|
|
481
|
+
// fine-grained. Returns { hasStatic, hasBind } so the caller can wire slots.
|
|
482
|
+
function partitionStylePairs(pairs) {
|
|
483
|
+
const staticPairs = [], bindPairs = [];
|
|
484
|
+
for (const p of pairs) {
|
|
485
|
+
let dynamic = false;
|
|
486
|
+
if (!inView) {
|
|
487
|
+
if (p.value.kind === 'word') {
|
|
488
|
+
// a bare identifier that is a भाव cell (and not a style keyword) is dynamic
|
|
489
|
+
dynamic = stateNames.has(p.value.value) && !isStyleWord(p.value.value);
|
|
490
|
+
} else {
|
|
491
|
+
dynamic = readsState(p.value.value);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
(dynamic ? bindPairs : staticPairs).push(p);
|
|
495
|
+
}
|
|
496
|
+
return { staticPairs, bindPairs };
|
|
497
|
+
}
|
|
498
|
+
function emitStylePair(p) {
|
|
499
|
+
emit(JSON.stringify(styleProp(p.key)));
|
|
500
|
+
emit(': ');
|
|
501
|
+
if (p.value.kind === 'word') {
|
|
502
|
+
if (isStyleWord(p.value.value)) emit(JSON.stringify(styleValue(p.value.value)));
|
|
503
|
+
else emit(id(p.value.value));
|
|
504
|
+
} else {
|
|
505
|
+
const saved = inStyleValue; inStyleValue = true;
|
|
506
|
+
genExpr(p.value.value);
|
|
507
|
+
inStyleValue = saved;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
function emitStyleObject(pairs) {
|
|
511
|
+
emit('{ ');
|
|
512
|
+
pairs.forEach((p, i) => { if (i) emit(', '); emitStylePair(p); });
|
|
513
|
+
emit(' }');
|
|
514
|
+
}
|
|
515
|
+
// a styleBind object: { "prop": () => (value), ... } — each a reactive thunk
|
|
516
|
+
function emitStyleBindObject(pairs) {
|
|
517
|
+
emit('{ ');
|
|
518
|
+
pairs.forEach((p, i) => {
|
|
519
|
+
if (i) emit(', ');
|
|
520
|
+
emit(JSON.stringify(styleProp(p.key)));
|
|
521
|
+
emit(': () => (');
|
|
522
|
+
const saved = inStyleValue; inStyleValue = true;
|
|
523
|
+
if (p.value.kind === 'word') {
|
|
524
|
+
// a bare state identifier → its reactive read (cell())
|
|
525
|
+
if (isStyleWord(p.value.value) && !stateNames.has(p.value.value)) emit(JSON.stringify(styleValue(p.value.value)));
|
|
526
|
+
else emit(id(p.value.value) + '()');
|
|
527
|
+
} else {
|
|
528
|
+
genExpr(p.value.value);
|
|
529
|
+
}
|
|
530
|
+
inStyleValue = saved;
|
|
531
|
+
emit(')');
|
|
532
|
+
});
|
|
533
|
+
emit(' }');
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function genExpr(node) {
|
|
537
|
+
switch (node.type) {
|
|
538
|
+
case 'Number': emit(node.value); break;
|
|
539
|
+
case 'String': emit(JSON.stringify(unescapeStr(node.value))); break;
|
|
540
|
+
case 'Template': {
|
|
541
|
+
// string interpolation → a JS template literal
|
|
542
|
+
const esc = s => s.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
|
|
543
|
+
emit('`');
|
|
544
|
+
emit(esc(unescapeStr(node.chunks[0])));
|
|
545
|
+
node.parts.forEach((p, idx) => {
|
|
546
|
+
emit('${');
|
|
547
|
+
genExpr(p);
|
|
548
|
+
emit('}');
|
|
549
|
+
emit(esc(unescapeStr(node.chunks[idx + 1] ?? '')));
|
|
550
|
+
});
|
|
551
|
+
emit('`');
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
case 'Boolean': emit(node.value ? 'true' : 'false'); break;
|
|
555
|
+
case 'Null': emit('null'); break;
|
|
556
|
+
case 'Identifier': {
|
|
557
|
+
// inside a रूप style-value expression, a known color/style word is a
|
|
558
|
+
// CSS literal (so ternaries like सक्रियः ? रक्तः : धूसरः work)
|
|
559
|
+
if (inStyleValue && isStyleWord(node.name) && !stateNames.has(node.name)) {
|
|
560
|
+
emit(JSON.stringify(styleValue(node.name)));
|
|
561
|
+
break;
|
|
562
|
+
}
|
|
563
|
+
// a भाव state cell reads as x(); a Sanskrit global builtin
|
|
564
|
+
// (संकेताक्षर → String.fromCharCode); else an ordinary identifier
|
|
565
|
+
if (stateNames.has(node.name)) { emit(id(node.name) + '()'); break; }
|
|
566
|
+
const g = GLOBALS[node.name];
|
|
567
|
+
emit(g !== undefined ? g : id(node.name));
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
case 'FuncExpr': {
|
|
571
|
+
emit(`${node.async ? 'async ' : ''}function (${node.params.map(id).join(', ')}) `);
|
|
572
|
+
const savedAD = asyncDepth;
|
|
573
|
+
asyncDepth = node.async ? 1 : 0;
|
|
574
|
+
genBlock(node.body, '');
|
|
575
|
+
asyncDepth = savedAD;
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
case 'Await':
|
|
579
|
+
if (asyncDepth === 0) {
|
|
580
|
+
throw new DevabhashaError('प्रतीक्षादोषः: प्रतीक्षा (await) is only valid inside an असमकालिक (async) function',
|
|
581
|
+
{ line: node.line, col: node.col, kind: 'codegen' });
|
|
582
|
+
}
|
|
583
|
+
emit('await '); genExpr(node.argument);
|
|
584
|
+
break;
|
|
585
|
+
case 'Array':
|
|
586
|
+
emit('[');
|
|
587
|
+
node.elements.forEach((e, i) => { if (i) emit(', '); genExpr(e); });
|
|
588
|
+
emit(']');
|
|
589
|
+
break;
|
|
590
|
+
case 'Binary':
|
|
591
|
+
emit('('); genExpr(node.left); emit(` ${node.op} `); genExpr(node.right); emit(')');
|
|
592
|
+
break;
|
|
593
|
+
case 'Unary':
|
|
594
|
+
emit(node.op); genExpr(node.argument);
|
|
595
|
+
break;
|
|
596
|
+
case 'Assign':
|
|
597
|
+
// a भाव state cell write: x = v → x(v)
|
|
598
|
+
if (node.target.type === 'Identifier' && stateNames.has(node.target.name)) {
|
|
599
|
+
emit(id(node.target.name) + '('); genExpr(node.value); emit(')');
|
|
600
|
+
} else {
|
|
601
|
+
genExpr(node.target); emit(' = '); genExpr(node.value);
|
|
602
|
+
}
|
|
603
|
+
break;
|
|
604
|
+
case 'Ternary':
|
|
605
|
+
emit('('); genExpr(node.test); emit(' ? ');
|
|
606
|
+
genExpr(node.consequent); emit(' : ');
|
|
607
|
+
genExpr(node.alternate); emit(')');
|
|
608
|
+
break;
|
|
609
|
+
case 'OrElse':
|
|
610
|
+
// result अथवा fallback → __RT.orElse(result, () => (fallback))
|
|
611
|
+
// (lazy thunk; parens so an object-literal fallback isn't read as a block)
|
|
612
|
+
emit('__RT.orElse(');
|
|
613
|
+
genExpr(node.value);
|
|
614
|
+
emit(', () => (');
|
|
615
|
+
genExpr(node.fallback);
|
|
616
|
+
emit('))');
|
|
617
|
+
break;
|
|
618
|
+
case 'Sutra':
|
|
619
|
+
// सूत्र(expr) → a tagged reactive thunk; components bind it fine-grained
|
|
620
|
+
emit('__DB.sutra(() => (');
|
|
621
|
+
genExpr(node.expr);
|
|
622
|
+
emit('))');
|
|
623
|
+
break;
|
|
624
|
+
case 'Update':
|
|
625
|
+
// postfix ++/--; for a भाव cell: x++ → x(x() + 1)
|
|
626
|
+
if (node.target.type === 'Identifier' && stateNames.has(node.target.name)) {
|
|
627
|
+
const nm = id(node.target.name);
|
|
628
|
+
emit(`${nm}(${nm}() ${node.op === '++' ? '+' : '-'} 1)`);
|
|
629
|
+
} else {
|
|
630
|
+
genExpr(node.target); emit(node.op);
|
|
631
|
+
}
|
|
632
|
+
break;
|
|
633
|
+
case 'Call':
|
|
634
|
+
genExpr(node.callee);
|
|
635
|
+
emit('(');
|
|
636
|
+
node.args.forEach((a, i) => { if (i) emit(', '); genExpr(a); });
|
|
637
|
+
emit(')');
|
|
638
|
+
break;
|
|
639
|
+
case 'Member':
|
|
640
|
+
// namespace member (आयात * रूपेण ग → ग.पाई) uses the export key id(),
|
|
641
|
+
// bypassing the stdlib method/constant tables.
|
|
642
|
+
if (!node.computed && node.object.type === 'Identifier'
|
|
643
|
+
&& namespaceAliases.has(node.object.name)) {
|
|
644
|
+
emit(id(node.object.name));
|
|
645
|
+
emit(`[${JSON.stringify(id(node.property))}]`);
|
|
646
|
+
break;
|
|
647
|
+
}
|
|
648
|
+
genExpr(node.object);
|
|
649
|
+
if (node.computed) { emit('['); genExpr(node.property); emit(']'); }
|
|
650
|
+
else {
|
|
651
|
+
// translate Sanskrit stdlib names → JS; pass through otherwise
|
|
652
|
+
const jsName = METHODS[node.property] || PROPERTIES[node.property]
|
|
653
|
+
|| MATH_CONSTANTS[node.property] || node.property;
|
|
654
|
+
emit(`.${jsName}`);
|
|
655
|
+
}
|
|
656
|
+
break;
|
|
657
|
+
case 'ObjectLiteral':
|
|
658
|
+
emit('{ ');
|
|
659
|
+
node.props.forEach((p, i) => {
|
|
660
|
+
if (i) emit(', ');
|
|
661
|
+
emit(JSON.stringify(p.key.value)); emit(': '); genExpr(p.value);
|
|
662
|
+
});
|
|
663
|
+
emit(' }');
|
|
664
|
+
break;
|
|
665
|
+
case 'ElementExpr':
|
|
666
|
+
emit('__DB.el(');
|
|
667
|
+
node.args.forEach((a, i) => { if (i) emit(', '); genExpr(a); });
|
|
668
|
+
emit(')');
|
|
669
|
+
break;
|
|
670
|
+
case 'Construct': {
|
|
671
|
+
// Assemble from role slots; order in source is irrelevant.
|
|
672
|
+
const s = node.slots;
|
|
673
|
+
emit('__DB.construct({ tag: ');
|
|
674
|
+
genExpr(s.tag);
|
|
675
|
+
if (s.content) {
|
|
676
|
+
if (!inView && readsState(s.content)) {
|
|
677
|
+
// dynamic content outside a दृश्य → fine-grained: bind only this node
|
|
678
|
+
emit(', contentBind: () => ('); genExpr(s.content); emit(')');
|
|
679
|
+
} else {
|
|
680
|
+
emit(', content: '); genExpr(s.content);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
if (s.event) { emit(', event: '); genExpr(s.event); }
|
|
684
|
+
if (s.handler) { emit(', handler: '); genExpr(s.handler); }
|
|
685
|
+
if (s.parent) { emit(', parent: '); genExpr(s.parent); }
|
|
686
|
+
if (s.prop) { emit(', prop: '); genExpr(s.prop); }
|
|
687
|
+
if (s.source) { emit(', source: '); genExpr(s.source); }
|
|
688
|
+
// रूप style: optional named base merged with translated inline pairs.
|
|
689
|
+
// Dynamic pairs (reading भाव cells, outside a view) become fine-grained
|
|
690
|
+
// styleBind thunks; static pairs stay a plain object (zero overhead).
|
|
691
|
+
if (node.style && (node.style.base || (node.style.pairs && node.style.pairs.length))) {
|
|
692
|
+
const { base, pairs } = node.style;
|
|
693
|
+
const { staticPairs, bindPairs } = partitionStylePairs(pairs || []);
|
|
694
|
+
if (base || staticPairs.length) {
|
|
695
|
+
emit(', style: ');
|
|
696
|
+
if (base) {
|
|
697
|
+
emit('Object.assign({}, ');
|
|
698
|
+
genExpr(base);
|
|
699
|
+
if (staticPairs.length) { emit(', '); emitStyleObject(staticPairs); }
|
|
700
|
+
emit(')');
|
|
701
|
+
} else {
|
|
702
|
+
emitStyleObject(staticPairs);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (bindPairs.length) { emit(', styleBind: '); emitStyleBindObject(bindPairs); }
|
|
706
|
+
}
|
|
707
|
+
// समास children: a sibling list of nested elements / text
|
|
708
|
+
if (node.children && node.children.length) {
|
|
709
|
+
emit(', children: [');
|
|
710
|
+
node.children.forEach((c, i) => { if (i) emit(', '); genExpr(c); });
|
|
711
|
+
emit(']');
|
|
712
|
+
}
|
|
713
|
+
emit(' })');
|
|
714
|
+
break;
|
|
715
|
+
}
|
|
716
|
+
case 'Mount':
|
|
717
|
+
emit('__DB.mount(');
|
|
718
|
+
node.args.forEach((a, i) => { if (i) emit(', '); genExpr(a); });
|
|
719
|
+
emit(')');
|
|
720
|
+
break;
|
|
721
|
+
case 'Listen':
|
|
722
|
+
emit('__DB.listen(');
|
|
723
|
+
node.args.forEach((a, i) => { if (i) emit(', '); genExpr(a); });
|
|
724
|
+
emit(')');
|
|
725
|
+
break;
|
|
726
|
+
default:
|
|
727
|
+
throw new Error(`codegen: unknown expression ${node.type}`);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (includeRuntime) emit(PRELUDE + RUNTIME + '\n');
|
|
732
|
+
gen(ast);
|
|
733
|
+
|
|
734
|
+
if (sourceMap || withMeta) {
|
|
735
|
+
const result = { code: out, exports, imports };
|
|
736
|
+
if (sourceMap) result.map = buildSourceMap(mappings, out);
|
|
737
|
+
return result;
|
|
738
|
+
}
|
|
739
|
+
return out;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// ----- Source Map v3 (VLQ-encoded) -----
|
|
743
|
+
const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
744
|
+
function vlqEncode(num) {
|
|
745
|
+
let vlq = num < 0 ? ((-num) << 1) | 1 : num << 1;
|
|
746
|
+
let out = '';
|
|
747
|
+
do {
|
|
748
|
+
let digit = vlq & 0b11111;
|
|
749
|
+
vlq >>>= 5;
|
|
750
|
+
if (vlq > 0) digit |= 0b100000;
|
|
751
|
+
out += B64[digit];
|
|
752
|
+
} while (vlq > 0);
|
|
753
|
+
return out;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Build a standard Source Map v3 object from statement-level mappings.
|
|
757
|
+
// `mappings` entries are 0-based genLine/genCol with 1-based source line/col.
|
|
758
|
+
function buildSourceMap(mappings, code, { file = 'out.js', source = 'input.deva' } = {}) {
|
|
759
|
+
// group by generated line, sort by generated column
|
|
760
|
+
const byLine = new Map();
|
|
761
|
+
for (const m of mappings) {
|
|
762
|
+
if (!byLine.has(m.genLine)) byLine.set(m.genLine, []);
|
|
763
|
+
byLine.get(m.genLine).push(m);
|
|
764
|
+
}
|
|
765
|
+
const totalLines = code.split('\n').length;
|
|
766
|
+
|
|
767
|
+
// VLQ deltas are relative to previous values across the whole file:
|
|
768
|
+
// [genColDelta, sourceIndexDelta, srcLineDelta, srcColDelta]
|
|
769
|
+
let prevGenCol = 0, prevSrcLine = 0, prevSrcCol = 0;
|
|
770
|
+
const lineStrings = [];
|
|
771
|
+
for (let line = 0; line < totalLines; line++) {
|
|
772
|
+
const segs = (byLine.get(line) || []).sort((a, b) => a.genCol - b.genCol);
|
|
773
|
+
prevGenCol = 0; // generated column resets each output line
|
|
774
|
+
const parts = [];
|
|
775
|
+
for (const m of segs) {
|
|
776
|
+
const seg =
|
|
777
|
+
vlqEncode(m.genCol - prevGenCol) +
|
|
778
|
+
vlqEncode(0) + // single source, index 0
|
|
779
|
+
vlqEncode((m.srcLine - 1) - prevSrcLine) +
|
|
780
|
+
vlqEncode((m.srcCol - 1) - prevSrcCol);
|
|
781
|
+
prevGenCol = m.genCol;
|
|
782
|
+
prevSrcLine = m.srcLine - 1;
|
|
783
|
+
prevSrcCol = m.srcCol - 1;
|
|
784
|
+
parts.push(seg);
|
|
785
|
+
}
|
|
786
|
+
lineStrings.push(parts.join(','));
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
return {
|
|
790
|
+
version: 3,
|
|
791
|
+
file,
|
|
792
|
+
sources: [source],
|
|
793
|
+
names: [],
|
|
794
|
+
mappings: lineStrings.join(';'),
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// Identifiers are transliterated to stable ASCII so the generated JS is
|
|
799
|
+
// portable and debuggable. The mapping is deterministic, so the same
|
|
800
|
+
// Sanskrit name always yields the same JS name (declaration == call site).
|
|
801
|
+
const TRANSLIT = {
|
|
802
|
+
// independent vowels
|
|
803
|
+
'अ':'a','आ':'aa','इ':'i','ई':'ii','उ':'u','ऊ':'uu','ऋ':'ri','ॠ':'rii',
|
|
804
|
+
'ऌ':'li','ए':'e','ऐ':'ai','ओ':'o','औ':'au',
|
|
805
|
+
// consonants (inherent 'a')
|
|
806
|
+
'क':'ka','ख':'kha','ग':'ga','घ':'gha','ङ':'nga',
|
|
807
|
+
'च':'ca','छ':'cha','ज':'ja','झ':'jha','ञ':'nya',
|
|
808
|
+
'ट':'tta','ठ':'ttha','ड':'dda','ढ':'ddha','ण':'nna',
|
|
809
|
+
'त':'ta','थ':'tha','द':'da','ध':'dha','न':'na',
|
|
810
|
+
'प':'pa','फ':'pha','ब':'ba','भ':'bha','म':'ma',
|
|
811
|
+
'य':'ya','र':'ra','ल':'la','व':'va','श':'sha','ष':'ssa','स':'sa','ह':'ha',
|
|
812
|
+
'ळ':'lla',
|
|
813
|
+
// dependent vowel signs (matras) — replace the inherent 'a'
|
|
814
|
+
'\u093E':'aa','\u093F':'i','\u0940':'ii','\u0941':'u','\u0942':'uu',
|
|
815
|
+
'\u0943':'ri','\u0947':'e','\u0948':'ai','\u094B':'o','\u094C':'au',
|
|
816
|
+
// anusvara / visarga / chandrabindu
|
|
817
|
+
'\u0902':'m','\u0903':'h','\u0901':'n',
|
|
818
|
+
};
|
|
819
|
+
const VIRAMA = '\u094D';
|
|
820
|
+
|
|
821
|
+
export function id(name) {
|
|
822
|
+
let result = '';
|
|
823
|
+
const chars = [...name];
|
|
824
|
+
for (let k = 0; k < chars.length; k++) {
|
|
825
|
+
const ch = chars[k];
|
|
826
|
+
if (/[A-Za-z0-9_$]/.test(ch)) { result += ch; continue; }
|
|
827
|
+
if (ch === VIRAMA) {
|
|
828
|
+
// virama removes the inherent 'a' of the preceding consonant
|
|
829
|
+
if (result.endsWith('a')) result = result.slice(0, -1);
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
const next = chars[k + 1];
|
|
833
|
+
const map = TRANSLIT[ch];
|
|
834
|
+
if (map !== undefined) {
|
|
835
|
+
// a matra replaces the inherent 'a' just emitted for the consonant
|
|
836
|
+
if (/[\u093E-\u094C]/.test(ch) && result.endsWith('a')) {
|
|
837
|
+
result = result.slice(0, -1) + map;
|
|
838
|
+
} else {
|
|
839
|
+
result += map;
|
|
840
|
+
}
|
|
841
|
+
} else {
|
|
842
|
+
result += '_u' + ch.codePointAt(0).toString(16);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
if (/^[0-9]/.test(result)) result = '_' + result;
|
|
846
|
+
// A transliterated name might collide with a JS reserved word
|
|
847
|
+
// (e.g. दो → "do"). Suffix an underscore to keep it a valid identifier.
|
|
848
|
+
if (JS_RESERVED.has(result)) result = result + '_';
|
|
849
|
+
return result || '_';
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
const JS_RESERVED = new Set([
|
|
853
|
+
'do','if','in','for','new','var','let','try','case','else','enum','eval',
|
|
854
|
+
'null','this','true','void','with','break','catch','class','const','false',
|
|
855
|
+
'super','throw','while','yield','delete','export','import','return','switch',
|
|
856
|
+
'typeof','default','extends','finally','continue','debugger','function',
|
|
857
|
+
'arguments','await','async','instanceof',
|
|
858
|
+
]);
|
|
859
|
+
|
|
860
|
+
function unescapeStr(s) {
|
|
861
|
+
return s
|
|
862
|
+
.replace(/\\n/g, '\n').replace(/\\t/g, '\t')
|
|
863
|
+
.replace(/\\"/g, '"').replace(/\\'/g, "'").replace(/\\\\/g, '\\');
|
|
864
|
+
}
|