thyscript 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/bin/thy-node.js +332 -0
- package/bin/thy.js +828 -0
- package/bin/thys.js +25 -0
- package/examples/calculator.thy +274 -0
- package/examples/calculator.thy.js +628 -0
- package/examples/hello.thy +34 -0
- package/examples/login.thy +382 -0
- package/examples/login.thy.js +710 -0
- package/package.json +54 -0
- package/scripts/_class.js +18 -0
- package/scripts/_class2.js +11 -0
- package/scripts/_class3.js +21 -0
- package/scripts/_dstruct.js +20 -0
- package/scripts/_dstruct2.js +24 -0
- package/scripts/_scaffold.js +16 -0
- package/scripts/_scaffold2.js +16 -0
- package/scripts/_scaffold3.js +26 -0
- package/scripts/_tpl_compile.js +13 -0
- package/scripts/_tpl_test.js +9 -0
- package/scripts/_typeof.js +26 -0
- package/scripts/_typeof2.js +25 -0
- package/scripts/patch_genmethod.js +44 -0
- package/scripts/smoke_test.js +167 -0
- package/scripts/smoke_test2.js +151 -0
- package/scripts/test_control_panel.js +58 -0
- package/src/compiler/CodeGenerator.js +1449 -0
- package/src/compiler/JITCompiler.js +325 -0
- package/src/database/DatabaseEngine.js +931 -0
- package/src/errors/ErrorDetector.js +305 -0
- package/src/errors/ErrorMessages.js +497 -0
- package/src/index.js +169 -0
- package/src/lexer/Lexer.js +342 -0
- package/src/lexer/TokenTypes.js +1234 -0
- package/src/memory/MemoryManager.js +283 -0
- package/src/parser/AST.js +606 -0
- package/src/parser/Parser.js +1942 -0
- package/src/runtime/ThyRuntime.js +265 -0
- package/src/runtime/stdlib/ArrayLib.js +207 -0
- package/src/runtime/stdlib/FileLib.js +168 -0
- package/src/runtime/stdlib/MathLib.js +411 -0
- package/src/runtime/stdlib/StringLib.js +387 -0
- package/src/server/ControlPanel.js +1188 -0
- package/src/server/HTMLCSSEngine.js +866 -0
- package/src/server/SecurityEngine.js +714 -0
- package/src/server/ServerEngine.js +738 -0
- package/tests/test1_math.thy +82 -0
- package/tests/test2_oop.thy +133 -0
- package/tests/test3_server.thy +143 -0
- package/tests/test4_functions.thy +159 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ThyStringLib - ThyScript Standard String Library
|
|
5
|
+
* Provides rich string manipulation: formatting, encoding, compression,
|
|
6
|
+
* fuzzy matching, CSV/JSON, regex, NLP basics, and more.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const zlib = require('zlib');
|
|
10
|
+
const { promisify } = require('util');
|
|
11
|
+
|
|
12
|
+
const gzipAsync = promisify(zlib.gzip);
|
|
13
|
+
const gunzipAsync = promisify(zlib.gunzip);
|
|
14
|
+
|
|
15
|
+
class ThyStringLib {
|
|
16
|
+
// ─── Basic Ops ────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
trim(str) { return str.trim(); }
|
|
19
|
+
trimStart(str) { return str.trimStart(); }
|
|
20
|
+
trimEnd(str) { return str.trimEnd(); }
|
|
21
|
+
upper(str) { return str.toUpperCase(); }
|
|
22
|
+
lower(str) { return str.toLowerCase(); }
|
|
23
|
+
replace(str, from, to) { return str.split(from).join(to); }
|
|
24
|
+
split(str, sep, limit) { return limit !== undefined ? str.split(sep, limit) : str.split(sep); }
|
|
25
|
+
join(arr, sep = '') { return arr.join(sep); }
|
|
26
|
+
slice(str, start, end) { return str.slice(start, end); }
|
|
27
|
+
indexOf(str, sub, pos) { return str.indexOf(sub, pos); }
|
|
28
|
+
lastIndexOf(str, sub) { return str.lastIndexOf(sub); }
|
|
29
|
+
includes(str, sub) { return str.includes(sub); }
|
|
30
|
+
startsWith(str, prefix){ return str.startsWith(prefix); }
|
|
31
|
+
endsWith(str, suffix) { return str.endsWith(suffix); }
|
|
32
|
+
padStart(str, len, ch) { return str.padStart(len, ch); }
|
|
33
|
+
padEnd(str, len, ch) { return str.padEnd(len, ch); }
|
|
34
|
+
repeat(str, n) { return str.repeat(n); }
|
|
35
|
+
|
|
36
|
+
/** Reverse a string. */
|
|
37
|
+
reverse(str) { return [...str].reverse().join(''); }
|
|
38
|
+
|
|
39
|
+
// ─── Formatting ───────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* printf-style format: %s (string), %d (int), %f (float), %j (json).
|
|
43
|
+
* @param {string} template
|
|
44
|
+
* @param {...*} args
|
|
45
|
+
* @returns {string}
|
|
46
|
+
*/
|
|
47
|
+
format(template, ...args) {
|
|
48
|
+
let i = 0;
|
|
49
|
+
return template.replace(/%([sdfj%])/g, (_, spec) => {
|
|
50
|
+
if (spec === '%') return '%';
|
|
51
|
+
const arg = args[i++];
|
|
52
|
+
if (spec === 's') return String(arg ?? '');
|
|
53
|
+
if (spec === 'd') return String(Math.round(Number(arg)));
|
|
54
|
+
if (spec === 'f') return Number(arg).toFixed(6);
|
|
55
|
+
if (spec === 'j') return JSON.stringify(arg);
|
|
56
|
+
return _;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Template interpolation using {{key}} placeholders.
|
|
62
|
+
* @param {string} template
|
|
63
|
+
* @param {object} obj
|
|
64
|
+
* @returns {string}
|
|
65
|
+
*/
|
|
66
|
+
interpolate(template, obj) {
|
|
67
|
+
return template.replace(/\{\{(\s*[\w.]+\s*)\}\}/g, (_, key) => {
|
|
68
|
+
const parts = key.trim().split('.');
|
|
69
|
+
let val = obj;
|
|
70
|
+
for (const p of parts) val = val?.[p];
|
|
71
|
+
return val == null ? '' : String(val);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ─── Case Conversions ─────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
/** Convert camelCase to snake_case. */
|
|
78
|
+
camelToSnake(str) {
|
|
79
|
+
return str
|
|
80
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
|
|
81
|
+
.replace(/([a-z\d])([A-Z])/g, '$1_$2')
|
|
82
|
+
.toLowerCase();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Convert snake_case to camelCase. */
|
|
86
|
+
snakeToCamel(str) {
|
|
87
|
+
return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Capitalize first letter. */
|
|
91
|
+
capitalize(str) {
|
|
92
|
+
if (!str) return str;
|
|
93
|
+
return str[0].toUpperCase() + str.slice(1).toLowerCase();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Title case every word. */
|
|
97
|
+
titleCase(str) {
|
|
98
|
+
return str.replace(/\w\S*/g, w => w[0].toUpperCase() + w.slice(1).toLowerCase());
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ─── Truncation ───────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Truncate a string to len characters, appending suffix if needed.
|
|
105
|
+
* @param {string} str
|
|
106
|
+
* @param {number} len
|
|
107
|
+
* @param {string} suffix
|
|
108
|
+
* @returns {string}
|
|
109
|
+
*/
|
|
110
|
+
truncate(str, len, suffix = '...') {
|
|
111
|
+
if (str.length <= len) return str;
|
|
112
|
+
return str.slice(0, len - suffix.length) + suffix;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ─── Counts ───────────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
wordCount(str) { return str.trim() === '' ? 0 : str.trim().split(/\s+/).length; }
|
|
118
|
+
charCount(str) { return str.length; }
|
|
119
|
+
lineCount(str) { return str.split('\n').length; }
|
|
120
|
+
|
|
121
|
+
// ─── Encoding ─────────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
/** Encode string to base64. */
|
|
124
|
+
toBase64(str) { return Buffer.from(str, 'utf8').toString('base64'); }
|
|
125
|
+
|
|
126
|
+
/** Decode base64 string. */
|
|
127
|
+
fromBase64(b64) { return Buffer.from(b64, 'base64').toString('utf8'); }
|
|
128
|
+
|
|
129
|
+
// ─── HTML Entities ────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
/** Escape HTML special characters. */
|
|
132
|
+
escape(str) {
|
|
133
|
+
return str
|
|
134
|
+
.replace(/&/g, '&')
|
|
135
|
+
.replace(/</g, '<')
|
|
136
|
+
.replace(/>/g, '>')
|
|
137
|
+
.replace(/"/g, '"')
|
|
138
|
+
.replace(/'/g, ''');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Unescape HTML entities. */
|
|
142
|
+
unescape(str) {
|
|
143
|
+
return str
|
|
144
|
+
.replace(/&/g, '&')
|
|
145
|
+
.replace(/</g, '<')
|
|
146
|
+
.replace(/>/g, '>')
|
|
147
|
+
.replace(/"/g, '"')
|
|
148
|
+
.replace(/'/g, "'");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ─── Slugify ─────────────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Convert a string to a URL-safe slug.
|
|
155
|
+
* @param {string} str
|
|
156
|
+
* @returns {string}
|
|
157
|
+
*/
|
|
158
|
+
slugify(str) {
|
|
159
|
+
return str
|
|
160
|
+
.normalize('NFD')
|
|
161
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
162
|
+
.toLowerCase()
|
|
163
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
164
|
+
.trim()
|
|
165
|
+
.replace(/[\s-]+/g, '-');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ─── Edit Distance ────────────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Levenshtein edit distance between two strings.
|
|
172
|
+
* @param {string} a
|
|
173
|
+
* @param {string} b
|
|
174
|
+
* @returns {number}
|
|
175
|
+
*/
|
|
176
|
+
levenshtein(a, b) {
|
|
177
|
+
const m = a.length, n = b.length;
|
|
178
|
+
const dp = Array.from({ length: m + 1 }, (_, i) =>
|
|
179
|
+
Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0))
|
|
180
|
+
);
|
|
181
|
+
for (let i = 1; i <= m; i++) {
|
|
182
|
+
for (let j = 1; j <= n; j++) {
|
|
183
|
+
dp[i][j] = a[i - 1] === b[j - 1]
|
|
184
|
+
? dp[i - 1][j - 1]
|
|
185
|
+
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return dp[m][n];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ─── Fuzzy Match ─────────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Fuzzy match: returns a similarity score in [0, 1].
|
|
195
|
+
* Uses normalised Levenshtein distance.
|
|
196
|
+
* @param {string} needle
|
|
197
|
+
* @param {string} haystack
|
|
198
|
+
* @returns {number}
|
|
199
|
+
*/
|
|
200
|
+
fuzzyMatch(needle, haystack) {
|
|
201
|
+
needle = needle.toLowerCase();
|
|
202
|
+
haystack = haystack.toLowerCase();
|
|
203
|
+
if (needle === haystack) return 1;
|
|
204
|
+
if (needle.length === 0) return 0;
|
|
205
|
+
const dist = this.levenshtein(needle, haystack);
|
|
206
|
+
return 1 - dist / Math.max(needle.length, haystack.length);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ─── CSV ─────────────────────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Parse a CSV string into a 2D array. Handles quoted fields.
|
|
213
|
+
* @param {string} str
|
|
214
|
+
* @param {string} delimiter
|
|
215
|
+
* @returns {string[][]}
|
|
216
|
+
*/
|
|
217
|
+
parseCSV(str, delimiter = ',') {
|
|
218
|
+
const rows = [];
|
|
219
|
+
const lines = str.split('\n');
|
|
220
|
+
for (const line of lines) {
|
|
221
|
+
if (!line.trim()) continue;
|
|
222
|
+
const row = [];
|
|
223
|
+
let inQuote = false, cell = '';
|
|
224
|
+
for (let i = 0; i < line.length; i++) {
|
|
225
|
+
const ch = line[i];
|
|
226
|
+
if (ch === '"') {
|
|
227
|
+
if (inQuote && line[i + 1] === '"') { cell += '"'; i++; }
|
|
228
|
+
else inQuote = !inQuote;
|
|
229
|
+
} else if (ch === delimiter && !inQuote) {
|
|
230
|
+
row.push(cell); cell = '';
|
|
231
|
+
} else {
|
|
232
|
+
cell += ch;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
row.push(cell);
|
|
236
|
+
rows.push(row);
|
|
237
|
+
}
|
|
238
|
+
return rows;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Convert 2D array to CSV string. Quotes fields containing delimiter/newline.
|
|
243
|
+
* @param {string[][]} data
|
|
244
|
+
* @param {string} delimiter
|
|
245
|
+
* @returns {string}
|
|
246
|
+
*/
|
|
247
|
+
stringifyCSV(data, delimiter = ',') {
|
|
248
|
+
return data.map(row =>
|
|
249
|
+
row.map(cell => {
|
|
250
|
+
const s = String(cell ?? '');
|
|
251
|
+
return s.includes(delimiter) || s.includes('\n') || s.includes('"')
|
|
252
|
+
? `"${s.replace(/"/g, '""')}"`
|
|
253
|
+
: s;
|
|
254
|
+
}).join(delimiter)
|
|
255
|
+
).join('\n');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ─── JSON ─────────────────────────────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Parse JSON string with error context.
|
|
262
|
+
* @param {string} str
|
|
263
|
+
* @returns {*}
|
|
264
|
+
*/
|
|
265
|
+
parseJSON(str) {
|
|
266
|
+
try {
|
|
267
|
+
return JSON.parse(str);
|
|
268
|
+
} catch (e) {
|
|
269
|
+
throw new SyntaxError(`ThyStringLib.parseJSON: ${e.message}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Stringify to JSON, optionally pretty-printed.
|
|
275
|
+
* @param {*} val
|
|
276
|
+
* @param {boolean} pretty
|
|
277
|
+
* @returns {string}
|
|
278
|
+
*/
|
|
279
|
+
stringifyJSON(val, pretty = false) {
|
|
280
|
+
return pretty ? JSON.stringify(val, null, 2) : JSON.stringify(val);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ─── Compression ─────────────────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Compress a string using gzip. Returns a Buffer.
|
|
287
|
+
* @param {string} str
|
|
288
|
+
* @returns {Promise<Buffer>}
|
|
289
|
+
*/
|
|
290
|
+
async compress(str) {
|
|
291
|
+
return gzipAsync(Buffer.from(str, 'utf8'));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Decompress a gzip Buffer to string.
|
|
296
|
+
* @param {Buffer} buf
|
|
297
|
+
* @returns {Promise<string>}
|
|
298
|
+
*/
|
|
299
|
+
async decompress(buf) {
|
|
300
|
+
const out = await gunzipAsync(buf);
|
|
301
|
+
return out.toString('utf8');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ─── Regex ────────────────────────────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Return all regex matches in str.
|
|
308
|
+
* @param {string} str
|
|
309
|
+
* @param {RegExp|string} pattern
|
|
310
|
+
* @param {string} flags
|
|
311
|
+
* @returns {RegExpMatchArray[]}
|
|
312
|
+
*/
|
|
313
|
+
regexMatch(str, pattern, flags = 'g') {
|
|
314
|
+
const re = pattern instanceof RegExp ? pattern : new RegExp(pattern, flags);
|
|
315
|
+
return [...str.matchAll(re)];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Test if str matches pattern.
|
|
320
|
+
* @param {string} str
|
|
321
|
+
* @param {RegExp|string} pattern
|
|
322
|
+
* @returns {boolean}
|
|
323
|
+
*/
|
|
324
|
+
regexTest(str, pattern) {
|
|
325
|
+
const re = pattern instanceof RegExp ? pattern : new RegExp(pattern);
|
|
326
|
+
return re.test(str);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Replace using regex.
|
|
331
|
+
* @param {string} str
|
|
332
|
+
* @param {RegExp|string} pattern
|
|
333
|
+
* @param {string|function} replacement
|
|
334
|
+
* @param {string} flags
|
|
335
|
+
* @returns {string}
|
|
336
|
+
*/
|
|
337
|
+
regexReplace(str, pattern, replacement, flags = 'g') {
|
|
338
|
+
const re = pattern instanceof RegExp ? pattern : new RegExp(pattern, flags);
|
|
339
|
+
return str.replace(re, replacement);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ─── Tokenize ────────────────────────────────────────────────────────────
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Split string into word tokens (strips punctuation).
|
|
346
|
+
* @param {string} str
|
|
347
|
+
* @returns {string[]}
|
|
348
|
+
*/
|
|
349
|
+
tokenize(str) {
|
|
350
|
+
return str.toLowerCase().match(/\b[a-z']+\b/g) ?? [];
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ─── Sentiment ───────────────────────────────────────────────────────────
|
|
354
|
+
|
|
355
|
+
static _positiveWords = new Set([
|
|
356
|
+
'good','great','excellent','amazing','wonderful','fantastic','happy',
|
|
357
|
+
'love','best','awesome','beautiful','nice','perfect','superb','joy',
|
|
358
|
+
'brilliant','delightful','outstanding','positive','success','win',
|
|
359
|
+
'helpful','innovative','effective','enjoy','like','fun','exciting',
|
|
360
|
+
]);
|
|
361
|
+
|
|
362
|
+
static _negativeWords = new Set([
|
|
363
|
+
'bad','terrible','awful','horrible','hate','worst','ugly','poor',
|
|
364
|
+
'failure','sad','angry','frustrating','boring','waste','broken',
|
|
365
|
+
'useless','annoying','negative','wrong','error','problem','fail',
|
|
366
|
+
'difficult','confusing','disappointing','slow','crash','bug',
|
|
367
|
+
]);
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Simple bag-of-words sentiment analysis.
|
|
371
|
+
* @param {string} str
|
|
372
|
+
* @returns {{ score: number, label: 'positive'|'negative'|'neutral', pos: number, neg: number }}
|
|
373
|
+
*/
|
|
374
|
+
sentiment(str) {
|
|
375
|
+
const tokens = this.tokenize(str);
|
|
376
|
+
let pos = 0, neg = 0;
|
|
377
|
+
for (const t of tokens) {
|
|
378
|
+
if (ThyStringLib._positiveWords.has(t)) pos++;
|
|
379
|
+
if (ThyStringLib._negativeWords.has(t)) neg++;
|
|
380
|
+
}
|
|
381
|
+
const score = pos - neg;
|
|
382
|
+
const label = score > 0 ? 'positive' : score < 0 ? 'negative' : 'neutral';
|
|
383
|
+
return { score, label, pos, neg };
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
module.exports = ThyStringLib;
|