@flux-lang/core 0.1.4 → 0.1.6-canary.18d439adc
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/README.md +20 -9
- package/dist/ast.d.ts +149 -10
- package/dist/ast.d.ts.map +1 -1
- package/dist/checks.d.ts.map +1 -1
- package/dist/checks.js +184 -2
- package/dist/checks.js.map +1 -1
- package/dist/index.d.ts +10 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/layout.d.ts +28 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +40 -0
- package/dist/layout.js.map +1 -0
- package/dist/parser.d.ts +9 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +939 -29
- package/dist/parser.js.map +1 -1
- package/dist/render.d.ts +174 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +1968 -0
- package/dist/render.js.map +1 -0
- package/dist/runtime/kernel.js +2 -0
- package/dist/runtime/kernel.js.map +1 -1
- package/dist/runtime.d.ts +91 -59
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +103 -372
- package/dist/runtime.js.map +1 -1
- package/dist/transform.d.ts +22 -0
- package/dist/transform.d.ts.map +1 -0
- package/dist/transform.js +383 -0
- package/dist/transform.js.map +1 -0
- package/package.json +2 -1
package/dist/parser.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
/**
|
|
2
4
|
* Token types for the Flux lexer.
|
|
3
5
|
* We keep keywords mostly as identifiers, except for a few special literals.
|
|
@@ -506,8 +508,10 @@ class Lexer {
|
|
|
506
508
|
class Parser {
|
|
507
509
|
tokens;
|
|
508
510
|
current = 0;
|
|
509
|
-
|
|
511
|
+
allowBodyFragments = false;
|
|
512
|
+
constructor(tokens, options = {}) {
|
|
510
513
|
this.tokens = tokens;
|
|
514
|
+
this.allowBodyFragments = options.allowBodyFragments ?? false;
|
|
511
515
|
}
|
|
512
516
|
parseDocument() {
|
|
513
517
|
// document { ... }
|
|
@@ -520,6 +524,11 @@ class Parser {
|
|
|
520
524
|
const rules = [];
|
|
521
525
|
let runtime;
|
|
522
526
|
let materials;
|
|
527
|
+
let assets;
|
|
528
|
+
let tokens;
|
|
529
|
+
let styles;
|
|
530
|
+
const themes = [];
|
|
531
|
+
let body;
|
|
523
532
|
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
524
533
|
if (this.checkIdentifier("meta")) {
|
|
525
534
|
const blockMeta = this.parseMetaBlock();
|
|
@@ -541,9 +550,24 @@ class Parser {
|
|
|
541
550
|
else if (this.checkIdentifier("runtime")) {
|
|
542
551
|
runtime = this.parseRuntimeBlock();
|
|
543
552
|
}
|
|
553
|
+
else if (this.checkIdentifier("assets")) {
|
|
554
|
+
assets = this.parseAssetsBlock();
|
|
555
|
+
}
|
|
544
556
|
else if (this.checkIdentifier("materials")) {
|
|
545
557
|
materials = this.parseMaterialsBlock();
|
|
546
558
|
}
|
|
559
|
+
else if (this.checkIdentifier("tokens")) {
|
|
560
|
+
tokens = this.parseTokensBlock();
|
|
561
|
+
}
|
|
562
|
+
else if (this.checkIdentifier("styles")) {
|
|
563
|
+
styles = this.parseStylesBlock();
|
|
564
|
+
}
|
|
565
|
+
else if (this.checkIdentifier("theme")) {
|
|
566
|
+
themes.push(this.parseThemeBlock());
|
|
567
|
+
}
|
|
568
|
+
else if (this.checkIdentifier("body")) {
|
|
569
|
+
body = this.parseBodyBlock();
|
|
570
|
+
}
|
|
547
571
|
else {
|
|
548
572
|
const tok = this.peek();
|
|
549
573
|
throw this.errorAtToken(tok, `Unexpected top-level construct '${tok.lexeme}'`);
|
|
@@ -558,6 +582,11 @@ class Parser {
|
|
|
558
582
|
rules,
|
|
559
583
|
runtime,
|
|
560
584
|
materials,
|
|
585
|
+
assets,
|
|
586
|
+
tokens,
|
|
587
|
+
styles,
|
|
588
|
+
themes,
|
|
589
|
+
body,
|
|
561
590
|
};
|
|
562
591
|
return doc;
|
|
563
592
|
}
|
|
@@ -567,8 +596,7 @@ class Parser {
|
|
|
567
596
|
this.consume(TokenType.LBrace, "Expected '{' after 'meta'");
|
|
568
597
|
const meta = { version: "0.1.0" };
|
|
569
598
|
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
570
|
-
const
|
|
571
|
-
const key = String(keyTok.value);
|
|
599
|
+
const key = this.parseKeyPath("Expected meta field name");
|
|
572
600
|
this.consume(TokenType.Equals, "Expected '=' after meta field name");
|
|
573
601
|
const valueTok = this.consume(TokenType.String, "Expected string value for meta field");
|
|
574
602
|
meta[key] = String(valueTok.value);
|
|
@@ -994,6 +1022,599 @@ class Parser {
|
|
|
994
1022
|
this.consume(TokenType.RBrace, "Expected '}' after video block");
|
|
995
1023
|
return video;
|
|
996
1024
|
}
|
|
1025
|
+
// --- Assets (v0.2) ---
|
|
1026
|
+
parseAssetsBlock() {
|
|
1027
|
+
this.expectIdentifier("assets", "Expected 'assets'");
|
|
1028
|
+
this.consume(TokenType.LBrace, "Expected '{' after 'assets'");
|
|
1029
|
+
const assets = [];
|
|
1030
|
+
const banks = [];
|
|
1031
|
+
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
1032
|
+
if (this.checkIdentifier("asset")) {
|
|
1033
|
+
assets.push(this.parseAssetDecl());
|
|
1034
|
+
}
|
|
1035
|
+
else if (this.checkIdentifier("bank")) {
|
|
1036
|
+
banks.push(this.parseAssetBankDecl());
|
|
1037
|
+
}
|
|
1038
|
+
else {
|
|
1039
|
+
this.skipStatement();
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
this.consume(TokenType.RBrace, "Expected '}' after assets block");
|
|
1043
|
+
return { assets, banks };
|
|
1044
|
+
}
|
|
1045
|
+
parseAssetDecl() {
|
|
1046
|
+
this.expectIdentifier("asset", "Expected 'asset'");
|
|
1047
|
+
const nameTok = this.consume(TokenType.Identifier, "Expected asset name");
|
|
1048
|
+
const name = String(nameTok.value);
|
|
1049
|
+
this.consume(TokenType.LBrace, "Expected '{' after asset name");
|
|
1050
|
+
const asset = {
|
|
1051
|
+
name,
|
|
1052
|
+
kind: "",
|
|
1053
|
+
path: "",
|
|
1054
|
+
tags: [],
|
|
1055
|
+
};
|
|
1056
|
+
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
1057
|
+
if (this.checkIdentifier("kind")) {
|
|
1058
|
+
this.advance();
|
|
1059
|
+
this.consume(TokenType.Equals, "Expected '=' after 'kind'");
|
|
1060
|
+
const tok = this.peek();
|
|
1061
|
+
if (tok.type === TokenType.String || tok.type === TokenType.Identifier) {
|
|
1062
|
+
this.advance();
|
|
1063
|
+
asset.kind = String(tok.value ?? tok.lexeme);
|
|
1064
|
+
}
|
|
1065
|
+
else {
|
|
1066
|
+
throw this.errorAtToken(tok, "Expected kind identifier or string");
|
|
1067
|
+
}
|
|
1068
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
1069
|
+
}
|
|
1070
|
+
else if (this.checkIdentifier("path")) {
|
|
1071
|
+
this.advance();
|
|
1072
|
+
this.consume(TokenType.Equals, "Expected '=' after 'path'");
|
|
1073
|
+
const tok = this.consume(TokenType.String, "Expected string for path");
|
|
1074
|
+
asset.path = String(tok.value);
|
|
1075
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
1076
|
+
}
|
|
1077
|
+
else if (this.checkIdentifier("tags")) {
|
|
1078
|
+
asset.tags = this.parseIdentifierList();
|
|
1079
|
+
}
|
|
1080
|
+
else if (this.checkIdentifier("weight")) {
|
|
1081
|
+
this.advance();
|
|
1082
|
+
this.consume(TokenType.Equals, "Expected '=' after 'weight'");
|
|
1083
|
+
const tok = this.consumeNumber("Expected numeric weight");
|
|
1084
|
+
asset.weight = Number(tok.value);
|
|
1085
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
1086
|
+
}
|
|
1087
|
+
else if (this.checkIdentifier("meta")) {
|
|
1088
|
+
asset.meta = this.parseMetaMapBlock();
|
|
1089
|
+
}
|
|
1090
|
+
else {
|
|
1091
|
+
this.skipStatement();
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
this.consume(TokenType.RBrace, "Expected '}' after asset block");
|
|
1095
|
+
return asset;
|
|
1096
|
+
}
|
|
1097
|
+
parseAssetBankDecl() {
|
|
1098
|
+
this.expectIdentifier("bank", "Expected 'bank'");
|
|
1099
|
+
const nameTok = this.consume(TokenType.Identifier, "Expected bank name");
|
|
1100
|
+
const name = String(nameTok.value);
|
|
1101
|
+
this.consume(TokenType.LBrace, "Expected '{' after bank name");
|
|
1102
|
+
const bank = {
|
|
1103
|
+
name,
|
|
1104
|
+
kind: "",
|
|
1105
|
+
root: "",
|
|
1106
|
+
include: "",
|
|
1107
|
+
tags: [],
|
|
1108
|
+
};
|
|
1109
|
+
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
1110
|
+
if (this.checkIdentifier("kind")) {
|
|
1111
|
+
this.advance();
|
|
1112
|
+
this.consume(TokenType.Equals, "Expected '=' after 'kind'");
|
|
1113
|
+
const tok = this.peek();
|
|
1114
|
+
if (tok.type === TokenType.String || tok.type === TokenType.Identifier) {
|
|
1115
|
+
this.advance();
|
|
1116
|
+
bank.kind = String(tok.value ?? tok.lexeme);
|
|
1117
|
+
}
|
|
1118
|
+
else {
|
|
1119
|
+
throw this.errorAtToken(tok, "Expected kind identifier or string");
|
|
1120
|
+
}
|
|
1121
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
1122
|
+
}
|
|
1123
|
+
else if (this.checkIdentifier("root")) {
|
|
1124
|
+
this.advance();
|
|
1125
|
+
this.consume(TokenType.Equals, "Expected '=' after 'root'");
|
|
1126
|
+
const tok = this.consume(TokenType.String, "Expected string for root");
|
|
1127
|
+
bank.root = String(tok.value);
|
|
1128
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
1129
|
+
}
|
|
1130
|
+
else if (this.checkIdentifier("include")) {
|
|
1131
|
+
this.advance();
|
|
1132
|
+
this.consume(TokenType.Equals, "Expected '=' after 'include'");
|
|
1133
|
+
const tok = this.consume(TokenType.String, "Expected string for include");
|
|
1134
|
+
bank.include = String(tok.value);
|
|
1135
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
1136
|
+
}
|
|
1137
|
+
else if (this.checkIdentifier("tags")) {
|
|
1138
|
+
bank.tags = this.parseIdentifierList();
|
|
1139
|
+
}
|
|
1140
|
+
else if (this.checkIdentifier("strategy")) {
|
|
1141
|
+
this.advance();
|
|
1142
|
+
this.consume(TokenType.Equals, "Expected '=' after 'strategy'");
|
|
1143
|
+
const tok = this.peek();
|
|
1144
|
+
if (tok.type !== TokenType.Identifier && tok.type !== TokenType.String) {
|
|
1145
|
+
throw this.errorAtToken(tok, "Expected strategy identifier or string");
|
|
1146
|
+
}
|
|
1147
|
+
this.advance();
|
|
1148
|
+
const raw = String(tok.value ?? tok.lexeme);
|
|
1149
|
+
if (raw !== "weighted" && raw !== "uniform") {
|
|
1150
|
+
throw this.errorAtToken(tok, `Unknown asset strategy '${raw}'`);
|
|
1151
|
+
}
|
|
1152
|
+
bank.strategy = raw;
|
|
1153
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
1154
|
+
}
|
|
1155
|
+
else {
|
|
1156
|
+
this.skipStatement();
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
this.consume(TokenType.RBrace, "Expected '}' after bank block");
|
|
1160
|
+
return bank;
|
|
1161
|
+
}
|
|
1162
|
+
parseMetaMapBlock() {
|
|
1163
|
+
this.expectIdentifier("meta", "Expected 'meta'");
|
|
1164
|
+
this.consume(TokenType.LBrace, "Expected '{' after 'meta'");
|
|
1165
|
+
const meta = {};
|
|
1166
|
+
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
1167
|
+
const key = this.parseKeyPath("Expected meta field name");
|
|
1168
|
+
this.consume(TokenType.Equals, "Expected '=' after meta field name");
|
|
1169
|
+
const value = this.parseValueLiteral();
|
|
1170
|
+
meta[key] = value;
|
|
1171
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
1172
|
+
}
|
|
1173
|
+
this.consume(TokenType.RBrace, "Expected '}' after meta block");
|
|
1174
|
+
return meta;
|
|
1175
|
+
}
|
|
1176
|
+
// --- Body (v0.2) ---
|
|
1177
|
+
parseBodyBlock() {
|
|
1178
|
+
this.expectIdentifier("body", "Expected 'body'");
|
|
1179
|
+
this.consume(TokenType.LBrace, "Expected '{' after 'body'");
|
|
1180
|
+
const nodes = [];
|
|
1181
|
+
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
1182
|
+
const node = this.parseDocumentNode();
|
|
1183
|
+
if (!this.allowBodyFragments && node.kind !== "page") {
|
|
1184
|
+
throw this.errorAtToken(this.peek(), "Body block must contain page nodes at the top level");
|
|
1185
|
+
}
|
|
1186
|
+
nodes.push(node);
|
|
1187
|
+
}
|
|
1188
|
+
this.consume(TokenType.RBrace, "Expected '}' after body block");
|
|
1189
|
+
return { nodes };
|
|
1190
|
+
}
|
|
1191
|
+
parseDocumentNode() {
|
|
1192
|
+
const kindTok = this.consume(TokenType.Identifier, "Expected node kind");
|
|
1193
|
+
const kind = String(kindTok.value);
|
|
1194
|
+
const idTok = this.consume(TokenType.Identifier, "Expected node id");
|
|
1195
|
+
const id = String(idTok.value);
|
|
1196
|
+
this.consume(TokenType.LBrace, "Expected '{' after node id");
|
|
1197
|
+
const props = {};
|
|
1198
|
+
const children = [];
|
|
1199
|
+
let refresh;
|
|
1200
|
+
let transition;
|
|
1201
|
+
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
1202
|
+
if (this.checkIdentifier("refresh")) {
|
|
1203
|
+
refresh = this.parseRefreshPolicy();
|
|
1204
|
+
continue;
|
|
1205
|
+
}
|
|
1206
|
+
if (this.checkIdentifier("transition")) {
|
|
1207
|
+
transition = this.parseTransitionSpec();
|
|
1208
|
+
continue;
|
|
1209
|
+
}
|
|
1210
|
+
if (this.looksLikeNodeDecl()) {
|
|
1211
|
+
children.push(this.parseDocumentNode());
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
if (this.check(TokenType.Identifier)) {
|
|
1215
|
+
const key = this.parseKeyPath("Expected property name");
|
|
1216
|
+
this.consume(TokenType.Equals, "Expected '=' after property name");
|
|
1217
|
+
if (this.match(TokenType.At)) {
|
|
1218
|
+
const expr = this.parseExpr();
|
|
1219
|
+
props[key] = { kind: "DynamicValue", expr };
|
|
1220
|
+
}
|
|
1221
|
+
else {
|
|
1222
|
+
const value = this.parseValueLiteral();
|
|
1223
|
+
props[key] = { kind: "LiteralValue", value };
|
|
1224
|
+
}
|
|
1225
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
1226
|
+
continue;
|
|
1227
|
+
}
|
|
1228
|
+
this.skipStatement();
|
|
1229
|
+
}
|
|
1230
|
+
const endTok = this.consume(TokenType.RBrace, "Expected '}' after node block");
|
|
1231
|
+
return {
|
|
1232
|
+
id,
|
|
1233
|
+
kind,
|
|
1234
|
+
props,
|
|
1235
|
+
children,
|
|
1236
|
+
refresh,
|
|
1237
|
+
transition,
|
|
1238
|
+
loc: { line: kindTok.line, column: kindTok.column, endLine: endTok.line, endColumn: endTok.column },
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
parseTokensBlock() {
|
|
1242
|
+
this.expectIdentifier("tokens", "Expected 'tokens'");
|
|
1243
|
+
this.consume(TokenType.LBrace, "Expected '{' after 'tokens'");
|
|
1244
|
+
const tokens = {};
|
|
1245
|
+
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
1246
|
+
const key = this.parseKeyPath("Expected token name");
|
|
1247
|
+
this.consume(TokenType.Equals, "Expected '=' after token name");
|
|
1248
|
+
const value = this.parseValueLiteral();
|
|
1249
|
+
tokens[key] = value;
|
|
1250
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
1251
|
+
}
|
|
1252
|
+
this.consume(TokenType.RBrace, "Expected '}' after tokens block");
|
|
1253
|
+
return { tokens };
|
|
1254
|
+
}
|
|
1255
|
+
parseStylesBlock() {
|
|
1256
|
+
this.expectIdentifier("styles", "Expected 'styles'");
|
|
1257
|
+
this.consume(TokenType.LBrace, "Expected '{' after 'styles'");
|
|
1258
|
+
const styles = [];
|
|
1259
|
+
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
1260
|
+
const nameTok = this.consume(TokenType.Identifier, "Expected style name");
|
|
1261
|
+
const name = String(nameTok.value);
|
|
1262
|
+
let extendsName;
|
|
1263
|
+
if (this.match(TokenType.Colon)) {
|
|
1264
|
+
const baseTok = this.consume(TokenType.Identifier, "Expected base style name");
|
|
1265
|
+
extendsName = String(baseTok.value);
|
|
1266
|
+
}
|
|
1267
|
+
this.consume(TokenType.LBrace, "Expected '{' after style name");
|
|
1268
|
+
const props = this.parsePropertyMap();
|
|
1269
|
+
this.consume(TokenType.RBrace, "Expected '}' after style block");
|
|
1270
|
+
styles.push({ name, extends: extendsName, props });
|
|
1271
|
+
}
|
|
1272
|
+
this.consume(TokenType.RBrace, "Expected '}' after styles block");
|
|
1273
|
+
return { styles };
|
|
1274
|
+
}
|
|
1275
|
+
parseThemeBlock() {
|
|
1276
|
+
this.expectIdentifier("theme", "Expected 'theme'");
|
|
1277
|
+
let name;
|
|
1278
|
+
if (this.check(TokenType.String)) {
|
|
1279
|
+
name = String(this.advance().value);
|
|
1280
|
+
}
|
|
1281
|
+
else if (this.check(TokenType.Identifier)) {
|
|
1282
|
+
name = String(this.advance().value);
|
|
1283
|
+
}
|
|
1284
|
+
else {
|
|
1285
|
+
throw this.errorAtToken(this.peek(), "Expected theme name");
|
|
1286
|
+
}
|
|
1287
|
+
this.consume(TokenType.LBrace, "Expected '{' after theme name");
|
|
1288
|
+
let tokens;
|
|
1289
|
+
let styles;
|
|
1290
|
+
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
1291
|
+
if (this.checkIdentifier("tokens")) {
|
|
1292
|
+
tokens = this.parseTokensBlock();
|
|
1293
|
+
}
|
|
1294
|
+
else if (this.checkIdentifier("styles")) {
|
|
1295
|
+
styles = this.parseStylesBlock();
|
|
1296
|
+
}
|
|
1297
|
+
else {
|
|
1298
|
+
const tok = this.peek();
|
|
1299
|
+
throw this.errorAtToken(tok, `Unexpected theme field '${tok.lexeme}'`);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
this.consume(TokenType.RBrace, "Expected '}' after theme block");
|
|
1303
|
+
return { name, tokens, styles };
|
|
1304
|
+
}
|
|
1305
|
+
parseRefreshPolicy() {
|
|
1306
|
+
this.expectIdentifier("refresh", "Expected 'refresh'");
|
|
1307
|
+
this.consume(TokenType.Equals, "Expected '=' after 'refresh'");
|
|
1308
|
+
if (this.checkIdentifier("never")) {
|
|
1309
|
+
this.advance();
|
|
1310
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
1311
|
+
return { kind: "never" };
|
|
1312
|
+
}
|
|
1313
|
+
if (this.checkIdentifier("docstep") || this.checkIdentifier("onDocstep")) {
|
|
1314
|
+
this.advance();
|
|
1315
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
1316
|
+
return { kind: "docstep" };
|
|
1317
|
+
}
|
|
1318
|
+
if (this.checkIdentifier("onLoad")) {
|
|
1319
|
+
this.advance();
|
|
1320
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
1321
|
+
return { kind: "never" };
|
|
1322
|
+
}
|
|
1323
|
+
if (this.checkIdentifier("every")) {
|
|
1324
|
+
this.advance();
|
|
1325
|
+
this.consume(TokenType.LParen, "Expected '(' after 'every'");
|
|
1326
|
+
const intervalSec = this.parseTimeSeconds("Expected refresh interval");
|
|
1327
|
+
let phaseSec = 0;
|
|
1328
|
+
if (this.match(TokenType.Comma)) {
|
|
1329
|
+
if (!this.checkIdentifier("phase")) {
|
|
1330
|
+
throw this.errorAtToken(this.peek(), "Expected 'phase' in every(...)");
|
|
1331
|
+
}
|
|
1332
|
+
this.advance();
|
|
1333
|
+
this.consume(TokenType.Equals, "Expected '=' after 'phase'");
|
|
1334
|
+
phaseSec = this.parseTimeSeconds("Expected phase duration", true);
|
|
1335
|
+
}
|
|
1336
|
+
this.consume(TokenType.RParen, "Expected ')' after every(...)");
|
|
1337
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
1338
|
+
return { kind: "every", intervalSec, phaseSec };
|
|
1339
|
+
}
|
|
1340
|
+
if (this.checkIdentifier("atEach")) {
|
|
1341
|
+
this.advance();
|
|
1342
|
+
this.consume(TokenType.LParen, "Expected '(' after 'atEach'");
|
|
1343
|
+
this.consume(TokenType.LBracket, "Expected '[' to start atEach list");
|
|
1344
|
+
const timesSec = [];
|
|
1345
|
+
if (!this.check(TokenType.RBracket)) {
|
|
1346
|
+
timesSec.push(this.parseTimeSeconds("Expected atEach time", true));
|
|
1347
|
+
while (this.match(TokenType.Comma)) {
|
|
1348
|
+
timesSec.push(this.parseTimeSeconds("Expected atEach time", true));
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
this.consume(TokenType.RBracket, "Expected ']' after atEach list");
|
|
1352
|
+
this.consume(TokenType.RParen, "Expected ')' after atEach(...)");
|
|
1353
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
1354
|
+
return { kind: "atEach", timesSec };
|
|
1355
|
+
}
|
|
1356
|
+
if (this.checkIdentifier("at")) {
|
|
1357
|
+
this.advance();
|
|
1358
|
+
this.consume(TokenType.LParen, "Expected '(' after 'at'");
|
|
1359
|
+
const timeSec = this.parseTimeSeconds("Expected at(...) time", true);
|
|
1360
|
+
this.consume(TokenType.RParen, "Expected ')' after at(...)");
|
|
1361
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
1362
|
+
return { kind: "at", timeSec };
|
|
1363
|
+
}
|
|
1364
|
+
if (this.checkIdentifier("poisson")) {
|
|
1365
|
+
this.advance();
|
|
1366
|
+
this.consume(TokenType.LParen, "Expected '(' after 'poisson'");
|
|
1367
|
+
let ratePerSec = null;
|
|
1368
|
+
while (!this.check(TokenType.RParen) && !this.isAtEnd()) {
|
|
1369
|
+
if (this.check(TokenType.Identifier) && this.peek(1).type === TokenType.Equals) {
|
|
1370
|
+
const nameTok = this.advance();
|
|
1371
|
+
this.consume(TokenType.Equals, "Expected '=' after argument name");
|
|
1372
|
+
if (nameTok.lexeme !== "ratePerSec") {
|
|
1373
|
+
throw this.errorAtToken(nameTok, `Unknown poisson argument '${nameTok.lexeme}'`);
|
|
1374
|
+
}
|
|
1375
|
+
const valueTok = this.consumeNumber("Expected numeric ratePerSec");
|
|
1376
|
+
ratePerSec = Number(valueTok.value);
|
|
1377
|
+
}
|
|
1378
|
+
else {
|
|
1379
|
+
const valueTok = this.consumeNumber("Expected numeric ratePerSec");
|
|
1380
|
+
ratePerSec = Number(valueTok.value);
|
|
1381
|
+
}
|
|
1382
|
+
if (!this.match(TokenType.Comma))
|
|
1383
|
+
break;
|
|
1384
|
+
}
|
|
1385
|
+
this.consume(TokenType.RParen, "Expected ')' after poisson(...)");
|
|
1386
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
1387
|
+
if (ratePerSec == null) {
|
|
1388
|
+
throw this.errorAtToken(this.peek(), "poisson(ratePerSec=...) requires ratePerSec");
|
|
1389
|
+
}
|
|
1390
|
+
return { kind: "poisson", ratePerSec };
|
|
1391
|
+
}
|
|
1392
|
+
if (this.checkIdentifier("chance")) {
|
|
1393
|
+
this.advance();
|
|
1394
|
+
this.consume(TokenType.LParen, "Expected '(' after 'chance'");
|
|
1395
|
+
let p = null;
|
|
1396
|
+
let every = null;
|
|
1397
|
+
while (!this.check(TokenType.RParen) && !this.isAtEnd()) {
|
|
1398
|
+
if (this.check(TokenType.Identifier) && this.peek(1).type === TokenType.Equals) {
|
|
1399
|
+
const nameTok = this.advance();
|
|
1400
|
+
this.consume(TokenType.Equals, "Expected '=' after argument name");
|
|
1401
|
+
if (nameTok.lexeme === "p") {
|
|
1402
|
+
const valueTok = this.consumeNumber("Expected numeric p");
|
|
1403
|
+
p = Number(valueTok.value);
|
|
1404
|
+
}
|
|
1405
|
+
else if (nameTok.lexeme === "every") {
|
|
1406
|
+
every = this.parseChanceEvery();
|
|
1407
|
+
}
|
|
1408
|
+
else {
|
|
1409
|
+
throw this.errorAtToken(nameTok, `Unknown chance argument '${nameTok.lexeme}'`);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
else if (p == null) {
|
|
1413
|
+
const valueTok = this.consumeNumber("Expected numeric p");
|
|
1414
|
+
p = Number(valueTok.value);
|
|
1415
|
+
}
|
|
1416
|
+
else {
|
|
1417
|
+
throw this.errorAtToken(this.peek(), "Unexpected chance(...) argument");
|
|
1418
|
+
}
|
|
1419
|
+
if (!this.match(TokenType.Comma))
|
|
1420
|
+
break;
|
|
1421
|
+
}
|
|
1422
|
+
this.consume(TokenType.RParen, "Expected ')' after chance(...)");
|
|
1423
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
1424
|
+
if (p == null) {
|
|
1425
|
+
throw this.errorAtToken(this.peek(), "chance(p=..., every=...) requires p");
|
|
1426
|
+
}
|
|
1427
|
+
if (!every) {
|
|
1428
|
+
every = { kind: "docstep" };
|
|
1429
|
+
}
|
|
1430
|
+
return { kind: "chance", p, every };
|
|
1431
|
+
}
|
|
1432
|
+
if (this.checkIdentifier("beat")) {
|
|
1433
|
+
throw this.errorAtToken(this.peek(), "beat(...) refresh is not supported yet");
|
|
1434
|
+
}
|
|
1435
|
+
throw this.errorAtToken(this.peek(), "Invalid refresh policy");
|
|
1436
|
+
}
|
|
1437
|
+
parseTransitionSpec() {
|
|
1438
|
+
this.expectIdentifier("transition", "Expected 'transition'");
|
|
1439
|
+
this.consume(TokenType.Equals, "Expected '=' after 'transition'");
|
|
1440
|
+
if (this.checkIdentifier("none")) {
|
|
1441
|
+
this.advance();
|
|
1442
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
1443
|
+
return { kind: "none" };
|
|
1444
|
+
}
|
|
1445
|
+
if (this.checkIdentifier("appear")) {
|
|
1446
|
+
this.advance();
|
|
1447
|
+
if (this.match(TokenType.LParen)) {
|
|
1448
|
+
this.consume(TokenType.RParen, "Expected ')' after appear(");
|
|
1449
|
+
}
|
|
1450
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
1451
|
+
return { kind: "appear" };
|
|
1452
|
+
}
|
|
1453
|
+
if (this.checkIdentifier("fade")) {
|
|
1454
|
+
this.advance();
|
|
1455
|
+
const args = this.parseTransitionArgs(["duration", "ease"]);
|
|
1456
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
1457
|
+
return { kind: "fade", durationMs: args.durationMs, ease: args.ease };
|
|
1458
|
+
}
|
|
1459
|
+
if (this.checkIdentifier("wipe")) {
|
|
1460
|
+
this.advance();
|
|
1461
|
+
const args = this.parseTransitionArgs(["duration", "ease", "direction"]);
|
|
1462
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
1463
|
+
return { kind: "wipe", durationMs: args.durationMs, ease: args.ease, direction: args.direction };
|
|
1464
|
+
}
|
|
1465
|
+
if (this.checkIdentifier("flash")) {
|
|
1466
|
+
this.advance();
|
|
1467
|
+
const args = this.parseTransitionArgs(["duration"]);
|
|
1468
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
1469
|
+
return { kind: "flash", durationMs: args.durationMs };
|
|
1470
|
+
}
|
|
1471
|
+
throw this.errorAtToken(this.peek(), "Invalid transition spec");
|
|
1472
|
+
}
|
|
1473
|
+
parseTransitionArgs(allowed) {
|
|
1474
|
+
const args = {};
|
|
1475
|
+
this.consume(TokenType.LParen, "Expected '(' after transition");
|
|
1476
|
+
while (!this.check(TokenType.RParen) && !this.isAtEnd()) {
|
|
1477
|
+
if (this.check(TokenType.Identifier) && this.peek(1).type === TokenType.Equals) {
|
|
1478
|
+
const nameTok = this.advance();
|
|
1479
|
+
const name = nameTok.lexeme;
|
|
1480
|
+
this.consume(TokenType.Equals, "Expected '=' after argument name");
|
|
1481
|
+
if (!allowed.includes(name)) {
|
|
1482
|
+
throw this.errorAtToken(nameTok, `Unknown transition argument '${name}'`);
|
|
1483
|
+
}
|
|
1484
|
+
if (name === "duration") {
|
|
1485
|
+
args.durationMs = this.parseDurationMs("Expected transition duration");
|
|
1486
|
+
}
|
|
1487
|
+
else if (name === "ease") {
|
|
1488
|
+
const ease = this.parseTransitionString("Expected transition ease");
|
|
1489
|
+
if (!isTransitionEase(ease)) {
|
|
1490
|
+
throw this.errorAtToken(nameTok, `Unknown transition ease '${ease}'`);
|
|
1491
|
+
}
|
|
1492
|
+
args.ease = ease;
|
|
1493
|
+
}
|
|
1494
|
+
else if (name === "direction") {
|
|
1495
|
+
const direction = this.parseTransitionString("Expected wipe direction");
|
|
1496
|
+
if (!isTransitionDirection(direction)) {
|
|
1497
|
+
throw this.errorAtToken(nameTok, `Unknown wipe direction '${direction}'`);
|
|
1498
|
+
}
|
|
1499
|
+
args.direction = direction;
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
else {
|
|
1503
|
+
throw this.errorAtToken(this.peek(), "Expected named transition argument");
|
|
1504
|
+
}
|
|
1505
|
+
if (!this.match(TokenType.Comma))
|
|
1506
|
+
break;
|
|
1507
|
+
}
|
|
1508
|
+
this.consume(TokenType.RParen, "Expected ')' after transition arguments");
|
|
1509
|
+
return args;
|
|
1510
|
+
}
|
|
1511
|
+
parseTransitionString(message) {
|
|
1512
|
+
const tok = this.peek();
|
|
1513
|
+
if (tok.type === TokenType.String || tok.type === TokenType.Identifier) {
|
|
1514
|
+
this.advance();
|
|
1515
|
+
return String(tok.value ?? tok.lexeme);
|
|
1516
|
+
}
|
|
1517
|
+
throw this.errorAtToken(tok, message);
|
|
1518
|
+
}
|
|
1519
|
+
parseChanceEvery() {
|
|
1520
|
+
if (this.check(TokenType.Identifier)) {
|
|
1521
|
+
const tok = this.peek();
|
|
1522
|
+
if (tok.lexeme === "docstep") {
|
|
1523
|
+
this.advance();
|
|
1524
|
+
return { kind: "docstep" };
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
if (this.check(TokenType.String)) {
|
|
1528
|
+
const tok = this.advance();
|
|
1529
|
+
const raw = String(tok.value ?? tok.lexeme);
|
|
1530
|
+
if (raw === "docstep") {
|
|
1531
|
+
return { kind: "docstep" };
|
|
1532
|
+
}
|
|
1533
|
+
const seconds = parseTimeString(raw);
|
|
1534
|
+
if (seconds == null) {
|
|
1535
|
+
throw this.errorAtToken(tok, `Invalid duration '${raw}'`);
|
|
1536
|
+
}
|
|
1537
|
+
if (seconds <= 0) {
|
|
1538
|
+
throw this.errorAtToken(tok, "Duration must be positive");
|
|
1539
|
+
}
|
|
1540
|
+
return { kind: "time", intervalSec: seconds };
|
|
1541
|
+
}
|
|
1542
|
+
if (this.check(TokenType.Int) || this.check(TokenType.Float)) {
|
|
1543
|
+
const seconds = this.parseTimeSeconds("Expected duration");
|
|
1544
|
+
return { kind: "time", intervalSec: seconds };
|
|
1545
|
+
}
|
|
1546
|
+
throw this.errorAtToken(this.peek(), "Expected 'docstep' or duration for chance every");
|
|
1547
|
+
}
|
|
1548
|
+
parseTimeSeconds(message, allowZero = false) {
|
|
1549
|
+
const tok = this.peek();
|
|
1550
|
+
if (tok.type === TokenType.String) {
|
|
1551
|
+
this.advance();
|
|
1552
|
+
const raw = String(tok.value ?? tok.lexeme);
|
|
1553
|
+
const seconds = parseTimeString(raw);
|
|
1554
|
+
if (seconds == null) {
|
|
1555
|
+
throw this.errorAtToken(tok, `Invalid duration '${raw}'`);
|
|
1556
|
+
}
|
|
1557
|
+
if (seconds < 0 || (!allowZero && seconds === 0)) {
|
|
1558
|
+
throw this.errorAtToken(tok, allowZero ? "Duration must be non-negative" : "Duration must be positive");
|
|
1559
|
+
}
|
|
1560
|
+
return seconds;
|
|
1561
|
+
}
|
|
1562
|
+
if (tok.type === TokenType.Int || tok.type === TokenType.Float) {
|
|
1563
|
+
const { amount, unit } = this.parseDurationSpec();
|
|
1564
|
+
const seconds = durationToSeconds(amount, unit);
|
|
1565
|
+
if (seconds == null) {
|
|
1566
|
+
throw this.errorAtToken(tok, `Unsupported duration unit '${unit}'`);
|
|
1567
|
+
}
|
|
1568
|
+
if (seconds < 0 || (!allowZero && seconds === 0)) {
|
|
1569
|
+
throw this.errorAtToken(tok, allowZero ? "Duration must be non-negative" : "Duration must be positive");
|
|
1570
|
+
}
|
|
1571
|
+
return seconds;
|
|
1572
|
+
}
|
|
1573
|
+
throw this.errorAtToken(tok, message);
|
|
1574
|
+
}
|
|
1575
|
+
parseDurationMs(message) {
|
|
1576
|
+
const tok = this.peek();
|
|
1577
|
+
if (tok.type === TokenType.String) {
|
|
1578
|
+
this.advance();
|
|
1579
|
+
const raw = String(tok.value ?? tok.lexeme);
|
|
1580
|
+
const seconds = parseTimeString(raw);
|
|
1581
|
+
if (seconds == null) {
|
|
1582
|
+
throw this.errorAtToken(tok, `Invalid duration '${raw}'`);
|
|
1583
|
+
}
|
|
1584
|
+
if (seconds < 0) {
|
|
1585
|
+
throw this.errorAtToken(tok, "Duration must be non-negative");
|
|
1586
|
+
}
|
|
1587
|
+
return seconds * 1000;
|
|
1588
|
+
}
|
|
1589
|
+
if (tok.type === TokenType.Int || tok.type === TokenType.Float) {
|
|
1590
|
+
if (this.peek(1).type === TokenType.Identifier) {
|
|
1591
|
+
const { amount, unit } = this.parseDurationSpec();
|
|
1592
|
+
const seconds = durationToSeconds(amount, unit);
|
|
1593
|
+
if (seconds == null) {
|
|
1594
|
+
throw this.errorAtToken(tok, `Unsupported duration unit '${unit}'`);
|
|
1595
|
+
}
|
|
1596
|
+
if (seconds < 0) {
|
|
1597
|
+
throw this.errorAtToken(tok, "Duration must be non-negative");
|
|
1598
|
+
}
|
|
1599
|
+
return seconds * 1000;
|
|
1600
|
+
}
|
|
1601
|
+
this.advance();
|
|
1602
|
+
const amount = Number(tok.value);
|
|
1603
|
+
if (!Number.isFinite(amount)) {
|
|
1604
|
+
throw this.errorAtToken(tok, message);
|
|
1605
|
+
}
|
|
1606
|
+
if (amount < 0) {
|
|
1607
|
+
throw this.errorAtToken(tok, "Duration must be non-negative");
|
|
1608
|
+
}
|
|
1609
|
+
return amount;
|
|
1610
|
+
}
|
|
1611
|
+
throw this.errorAtToken(tok, message);
|
|
1612
|
+
}
|
|
1613
|
+
looksLikeNodeDecl() {
|
|
1614
|
+
return (this.check(TokenType.Identifier) &&
|
|
1615
|
+
this.peek(1).type === TokenType.Identifier &&
|
|
1616
|
+
this.peek(2).type === TokenType.LBrace);
|
|
1617
|
+
}
|
|
997
1618
|
parseIdentifierList() {
|
|
998
1619
|
this.advance(); // identifier keyword already checked
|
|
999
1620
|
this.consume(TokenType.Equals, "Expected '=' after identifier list key");
|
|
@@ -1011,6 +1632,32 @@ class Parser {
|
|
|
1011
1632
|
this.consumeOptional(TokenType.Semicolon);
|
|
1012
1633
|
return values;
|
|
1013
1634
|
}
|
|
1635
|
+
parseKeyPath(message) {
|
|
1636
|
+
const first = this.consume(TokenType.Identifier, message);
|
|
1637
|
+
const parts = [String(first.value)];
|
|
1638
|
+
while (this.match(TokenType.Dot)) {
|
|
1639
|
+
const next = this.consume(TokenType.Identifier, "Expected identifier after '.'");
|
|
1640
|
+
parts.push(String(next.value));
|
|
1641
|
+
}
|
|
1642
|
+
return parts.join(".");
|
|
1643
|
+
}
|
|
1644
|
+
parsePropertyMap() {
|
|
1645
|
+
const props = {};
|
|
1646
|
+
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
1647
|
+
const key = this.parseKeyPath("Expected property name");
|
|
1648
|
+
this.consume(TokenType.Equals, "Expected '=' after property name");
|
|
1649
|
+
if (this.match(TokenType.At)) {
|
|
1650
|
+
const expr = this.parseExpr();
|
|
1651
|
+
props[key] = { kind: "DynamicValue", expr };
|
|
1652
|
+
}
|
|
1653
|
+
else {
|
|
1654
|
+
const value = this.parseValueLiteral();
|
|
1655
|
+
props[key] = { kind: "LiteralValue", value };
|
|
1656
|
+
}
|
|
1657
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
1658
|
+
}
|
|
1659
|
+
return props;
|
|
1660
|
+
}
|
|
1014
1661
|
// --- Expressions ---
|
|
1015
1662
|
parseExpr() {
|
|
1016
1663
|
return this.parseOr();
|
|
@@ -1133,6 +1780,7 @@ class Parser {
|
|
|
1133
1780
|
kind: "UnaryExpression",
|
|
1134
1781
|
op,
|
|
1135
1782
|
argument,
|
|
1783
|
+
loc: argument?.loc,
|
|
1136
1784
|
};
|
|
1137
1785
|
}
|
|
1138
1786
|
if (this.match(TokenType.Minus)) {
|
|
@@ -1142,6 +1790,7 @@ class Parser {
|
|
|
1142
1790
|
kind: "UnaryExpression",
|
|
1143
1791
|
op,
|
|
1144
1792
|
argument,
|
|
1793
|
+
loc: argument?.loc,
|
|
1145
1794
|
};
|
|
1146
1795
|
}
|
|
1147
1796
|
return this.parsePostfix();
|
|
@@ -1156,11 +1805,14 @@ class Parser {
|
|
|
1156
1805
|
kind: "MemberExpression",
|
|
1157
1806
|
object: expr,
|
|
1158
1807
|
property,
|
|
1808
|
+
loc: expr?.loc,
|
|
1159
1809
|
};
|
|
1160
1810
|
}
|
|
1161
1811
|
else if (this.match(TokenType.LParen)) {
|
|
1162
1812
|
const args = this.parseArgumentList();
|
|
1163
|
-
|
|
1813
|
+
const callExpr = this.maybeNeighborsCall(expr, args);
|
|
1814
|
+
callExpr.loc = expr?.loc;
|
|
1815
|
+
expr = callExpr;
|
|
1164
1816
|
}
|
|
1165
1817
|
else {
|
|
1166
1818
|
break;
|
|
@@ -1177,6 +1829,7 @@ class Parser {
|
|
|
1177
1829
|
return {
|
|
1178
1830
|
kind: "Literal",
|
|
1179
1831
|
value: tok.value,
|
|
1832
|
+
loc: { line: tok.line, column: tok.column },
|
|
1180
1833
|
};
|
|
1181
1834
|
}
|
|
1182
1835
|
case TokenType.String: {
|
|
@@ -1184,6 +1837,7 @@ class Parser {
|
|
|
1184
1837
|
return {
|
|
1185
1838
|
kind: "Literal",
|
|
1186
1839
|
value: tok.value,
|
|
1840
|
+
loc: { line: tok.line, column: tok.column },
|
|
1187
1841
|
};
|
|
1188
1842
|
}
|
|
1189
1843
|
case TokenType.Bool: {
|
|
@@ -1191,6 +1845,23 @@ class Parser {
|
|
|
1191
1845
|
return {
|
|
1192
1846
|
kind: "Literal",
|
|
1193
1847
|
value: tok.value,
|
|
1848
|
+
loc: { line: tok.line, column: tok.column },
|
|
1849
|
+
};
|
|
1850
|
+
}
|
|
1851
|
+
case TokenType.LBracket: {
|
|
1852
|
+
this.advance(); // '['
|
|
1853
|
+
const items = [];
|
|
1854
|
+
if (!this.check(TokenType.RBracket)) {
|
|
1855
|
+
items.push(this.parseExpr());
|
|
1856
|
+
while (this.match(TokenType.Comma)) {
|
|
1857
|
+
items.push(this.parseExpr());
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
this.consume(TokenType.RBracket, "Expected ']' after list literal");
|
|
1861
|
+
return {
|
|
1862
|
+
kind: "ListExpression",
|
|
1863
|
+
items,
|
|
1864
|
+
loc: { line: tok.line, column: tok.column },
|
|
1194
1865
|
};
|
|
1195
1866
|
}
|
|
1196
1867
|
case TokenType.Identifier: {
|
|
@@ -1198,6 +1869,7 @@ class Parser {
|
|
|
1198
1869
|
return {
|
|
1199
1870
|
kind: "Identifier",
|
|
1200
1871
|
name: tok.lexeme,
|
|
1872
|
+
loc: { line: tok.line, column: tok.column },
|
|
1201
1873
|
};
|
|
1202
1874
|
}
|
|
1203
1875
|
case TokenType.LBrace: {
|
|
@@ -1224,13 +1896,23 @@ class Parser {
|
|
|
1224
1896
|
this.consume(TokenType.RParen, "Expected ')' after argument list");
|
|
1225
1897
|
return args;
|
|
1226
1898
|
}
|
|
1227
|
-
args.push(this.
|
|
1899
|
+
args.push(this.parseCallArg());
|
|
1228
1900
|
while (this.match(TokenType.Comma)) {
|
|
1229
|
-
args.push(this.
|
|
1901
|
+
args.push(this.parseCallArg());
|
|
1230
1902
|
}
|
|
1231
1903
|
this.consume(TokenType.RParen, "Expected ')' after argument list");
|
|
1232
1904
|
return args;
|
|
1233
1905
|
}
|
|
1906
|
+
parseCallArg() {
|
|
1907
|
+
if (this.check(TokenType.Identifier) && this.peek(1).type === TokenType.Equals) {
|
|
1908
|
+
const nameTok = this.advance();
|
|
1909
|
+
const name = String(nameTok.value);
|
|
1910
|
+
this.consume(TokenType.Equals, "Expected '=' after argument name");
|
|
1911
|
+
const value = this.parseExpr();
|
|
1912
|
+
return { kind: "NamedArg", name, value };
|
|
1913
|
+
}
|
|
1914
|
+
return this.parseExpr();
|
|
1915
|
+
}
|
|
1234
1916
|
maybeNeighborsCall(callee, args) {
|
|
1235
1917
|
if (callee.kind === "MemberExpression" &&
|
|
1236
1918
|
callee.object.kind === "Identifier" &&
|
|
@@ -1254,6 +1936,7 @@ class Parser {
|
|
|
1254
1936
|
op,
|
|
1255
1937
|
left,
|
|
1256
1938
|
right,
|
|
1939
|
+
loc: left?.loc,
|
|
1257
1940
|
};
|
|
1258
1941
|
}
|
|
1259
1942
|
// --- Rule & Runtime (placeholders for now) ---
|
|
@@ -1465,27 +2148,43 @@ class Parser {
|
|
|
1465
2148
|
const unitTok = this.advance();
|
|
1466
2149
|
const raw = String(unitTok.value);
|
|
1467
2150
|
const lowered = raw.toLowerCase();
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
2151
|
+
const unitMap = {
|
|
2152
|
+
s: "s",
|
|
2153
|
+
sec: "s",
|
|
2154
|
+
secs: "s",
|
|
2155
|
+
second: "s",
|
|
2156
|
+
seconds: "s",
|
|
2157
|
+
ms: "ms",
|
|
2158
|
+
millisecond: "ms",
|
|
2159
|
+
milliseconds: "ms",
|
|
2160
|
+
m: "m",
|
|
2161
|
+
min: "m",
|
|
2162
|
+
mins: "m",
|
|
2163
|
+
minute: "m",
|
|
2164
|
+
minutes: "m",
|
|
2165
|
+
h: "h",
|
|
2166
|
+
hr: "h",
|
|
2167
|
+
hrs: "h",
|
|
2168
|
+
hour: "h",
|
|
2169
|
+
hours: "h",
|
|
2170
|
+
beat: "beats",
|
|
2171
|
+
beats: "beats",
|
|
2172
|
+
bar: "bars",
|
|
2173
|
+
bars: "bars",
|
|
2174
|
+
measure: "bars",
|
|
2175
|
+
measures: "bars",
|
|
2176
|
+
sub: "subs",
|
|
2177
|
+
subs: "subs",
|
|
2178
|
+
subdivision: "subs",
|
|
2179
|
+
subdivisions: "subs",
|
|
2180
|
+
tick: "ticks",
|
|
2181
|
+
ticks: "ticks",
|
|
2182
|
+
};
|
|
2183
|
+
const mapped = unitMap[lowered];
|
|
2184
|
+
if (!mapped) {
|
|
1487
2185
|
throw this.errorAtToken(unitTok, `Unknown duration unit '${unitTok.lexeme}'`);
|
|
1488
2186
|
}
|
|
2187
|
+
unit = mapped;
|
|
1489
2188
|
}
|
|
1490
2189
|
return { amount, unit };
|
|
1491
2190
|
}
|
|
@@ -1568,6 +2267,38 @@ class Parser {
|
|
|
1568
2267
|
throw this.errorAtToken(tok, "Expected literal");
|
|
1569
2268
|
}
|
|
1570
2269
|
}
|
|
2270
|
+
parseValueLiteral() {
|
|
2271
|
+
const tok = this.peek();
|
|
2272
|
+
switch (tok.type) {
|
|
2273
|
+
case TokenType.Int:
|
|
2274
|
+
case TokenType.Float:
|
|
2275
|
+
this.advance();
|
|
2276
|
+
return tok.value;
|
|
2277
|
+
case TokenType.String:
|
|
2278
|
+
this.advance();
|
|
2279
|
+
return tok.value;
|
|
2280
|
+
case TokenType.Bool:
|
|
2281
|
+
this.advance();
|
|
2282
|
+
return tok.value;
|
|
2283
|
+
case TokenType.Identifier:
|
|
2284
|
+
this.advance();
|
|
2285
|
+
return tok.lexeme;
|
|
2286
|
+
case TokenType.LBracket: {
|
|
2287
|
+
this.advance();
|
|
2288
|
+
const items = [];
|
|
2289
|
+
if (!this.check(TokenType.RBracket)) {
|
|
2290
|
+
items.push(this.parseValueLiteral());
|
|
2291
|
+
while (this.match(TokenType.Comma)) {
|
|
2292
|
+
items.push(this.parseValueLiteral());
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
this.consume(TokenType.RBracket, "Expected ']' after list literal");
|
|
2296
|
+
return items;
|
|
2297
|
+
}
|
|
2298
|
+
default:
|
|
2299
|
+
throw this.errorAtToken(tok, "Expected literal value");
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
1571
2302
|
consumeNumber(message) {
|
|
1572
2303
|
const tok = this.peek();
|
|
1573
2304
|
if (tok.type === TokenType.Int || tok.type === TokenType.Float) {
|
|
@@ -1640,11 +2371,190 @@ class Parser {
|
|
|
1640
2371
|
return new Error(`Parse error at ${token.line}:${token.column} near '${token.lexeme}': ${message}`);
|
|
1641
2372
|
}
|
|
1642
2373
|
}
|
|
1643
|
-
|
|
1644
|
-
|
|
2374
|
+
function isTransitionEase(value) {
|
|
2375
|
+
return value === "linear" || value === "inOut" || value === "in" || value === "out";
|
|
2376
|
+
}
|
|
2377
|
+
function isTransitionDirection(value) {
|
|
2378
|
+
return value === "left" || value === "right" || value === "up" || value === "down";
|
|
2379
|
+
}
|
|
2380
|
+
const DEFAULT_INCLUDE_BYTES = 1024 * 1024;
|
|
2381
|
+
const DEFAULT_INCLUDE_DEPTH = 8;
|
|
2382
|
+
export function parseDocument(source, options = {}) {
|
|
1645
2383
|
const lexer = new Lexer(source);
|
|
1646
2384
|
const tokens = lexer.tokenize();
|
|
1647
|
-
const parser = new Parser(tokens);
|
|
1648
|
-
|
|
2385
|
+
const parser = new Parser(tokens, { allowBodyFragments: options.allowBodyFragments });
|
|
2386
|
+
const doc = parser.parseDocument();
|
|
2387
|
+
const shouldResolve = options.resolveIncludes || options.sourcePath || options.docRoot;
|
|
2388
|
+
if (!shouldResolve)
|
|
2389
|
+
return doc;
|
|
2390
|
+
const root = options.docRoot ?? (options.sourcePath ? path.dirname(options.sourcePath) : null);
|
|
2391
|
+
if (!root)
|
|
2392
|
+
return doc;
|
|
2393
|
+
return resolveIncludes(doc, {
|
|
2394
|
+
docRoot: root,
|
|
2395
|
+
sourcePath: options.sourcePath ?? "<buffer>",
|
|
2396
|
+
maxIncludeBytes: options.maxIncludeBytes ?? DEFAULT_INCLUDE_BYTES,
|
|
2397
|
+
includeDepthLimit: options.includeDepthLimit ?? DEFAULT_INCLUDE_DEPTH,
|
|
2398
|
+
});
|
|
2399
|
+
}
|
|
2400
|
+
function resolveIncludes(doc, options) {
|
|
2401
|
+
const context = { ...options, seen: new Set() };
|
|
2402
|
+
const body = doc.body;
|
|
2403
|
+
if (!body?.nodes?.length)
|
|
2404
|
+
return doc;
|
|
2405
|
+
const nodes = resolveIncludeNodes(body.nodes, context, "root", 0);
|
|
2406
|
+
return { ...doc, body: { nodes } };
|
|
2407
|
+
}
|
|
2408
|
+
function resolveIncludeNodes(nodes, ctx, parentPath, depth) {
|
|
2409
|
+
if (depth > ctx.includeDepthLimit) {
|
|
2410
|
+
throw new Error(`Include depth exceeds limit (${ctx.includeDepthLimit}) at ${parentPath}`);
|
|
2411
|
+
}
|
|
2412
|
+
const result = [];
|
|
2413
|
+
for (let index = 0; index < nodes.length; index += 1) {
|
|
2414
|
+
const node = nodes[index];
|
|
2415
|
+
const nodePath = `${parentPath}/${node.kind}:${node.id}:${index}`;
|
|
2416
|
+
if (node.kind === "include") {
|
|
2417
|
+
const includePath = resolveIncludePath(node, ctx, nodePath);
|
|
2418
|
+
const included = loadIncludedDocument(includePath, ctx, depth + 1);
|
|
2419
|
+
const includeNodes = included.body?.nodes ?? [];
|
|
2420
|
+
const rewritten = rewriteNodeIds(includeNodes, includePath, nodePath);
|
|
2421
|
+
result.push(...rewritten);
|
|
2422
|
+
continue;
|
|
2423
|
+
}
|
|
2424
|
+
const children = node.children?.length
|
|
2425
|
+
? resolveIncludeNodes(node.children, ctx, nodePath, depth)
|
|
2426
|
+
: [];
|
|
2427
|
+
result.push({ ...node, children });
|
|
2428
|
+
}
|
|
2429
|
+
return result;
|
|
2430
|
+
}
|
|
2431
|
+
function resolveIncludePath(node, ctx, nodePath) {
|
|
2432
|
+
const raw = node.props?.path?.kind === "LiteralValue"
|
|
2433
|
+
? String(node.props.path.value)
|
|
2434
|
+
: node.props?.src?.kind === "LiteralValue"
|
|
2435
|
+
? String(node.props.src.value)
|
|
2436
|
+
: null;
|
|
2437
|
+
if (!raw) {
|
|
2438
|
+
throw new Error(`Include at ${nodePath} requires a literal 'path' (or 'src') string`);
|
|
2439
|
+
}
|
|
2440
|
+
if (path.isAbsolute(raw)) {
|
|
2441
|
+
throw new Error(`Include path must be relative: '${raw}'`);
|
|
2442
|
+
}
|
|
2443
|
+
if (raw.split(/[\\/]+/).some((part) => part === "..")) {
|
|
2444
|
+
throw new Error(`Include path cannot contain '..': '${raw}'`);
|
|
2445
|
+
}
|
|
2446
|
+
const resolved = path.resolve(ctx.docRoot, raw);
|
|
2447
|
+
if (!resolved.startsWith(ctx.docRoot)) {
|
|
2448
|
+
throw new Error(`Include path escapes doc root: '${raw}'`);
|
|
2449
|
+
}
|
|
2450
|
+
if (path.extname(resolved) !== ".flux") {
|
|
2451
|
+
throw new Error(`Include path must target a .flux file: '${raw}'`);
|
|
2452
|
+
}
|
|
2453
|
+
return resolved;
|
|
2454
|
+
}
|
|
2455
|
+
function parseTimeString(raw) {
|
|
2456
|
+
const trimmed = raw.trim();
|
|
2457
|
+
const match = trimmed.match(/^([0-9]*\.?[0-9]+)\s*(ms|s|m)$/i);
|
|
2458
|
+
if (!match)
|
|
2459
|
+
return null;
|
|
2460
|
+
const value = Number(match[1]);
|
|
2461
|
+
if (!Number.isFinite(value) || value < 0)
|
|
2462
|
+
return null;
|
|
2463
|
+
const unit = match[2].toLowerCase();
|
|
2464
|
+
switch (unit) {
|
|
2465
|
+
case "ms":
|
|
2466
|
+
return value / 1000;
|
|
2467
|
+
case "s":
|
|
2468
|
+
return value;
|
|
2469
|
+
case "m":
|
|
2470
|
+
return value * 60;
|
|
2471
|
+
default:
|
|
2472
|
+
return null;
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
function durationToSeconds(amount, unit) {
|
|
2476
|
+
switch (unit) {
|
|
2477
|
+
case "ms":
|
|
2478
|
+
return amount / 1000;
|
|
2479
|
+
case "s":
|
|
2480
|
+
case "sec":
|
|
2481
|
+
case "secs":
|
|
2482
|
+
case "second":
|
|
2483
|
+
case "seconds":
|
|
2484
|
+
return amount;
|
|
2485
|
+
case "m":
|
|
2486
|
+
case "min":
|
|
2487
|
+
case "mins":
|
|
2488
|
+
case "minute":
|
|
2489
|
+
case "minutes":
|
|
2490
|
+
return amount * 60;
|
|
2491
|
+
case "h":
|
|
2492
|
+
case "hr":
|
|
2493
|
+
case "hrs":
|
|
2494
|
+
case "hour":
|
|
2495
|
+
case "hours":
|
|
2496
|
+
return amount * 3600;
|
|
2497
|
+
default:
|
|
2498
|
+
return null;
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
function loadIncludedDocument(filePath, ctx, depth) {
|
|
2502
|
+
if (ctx.seen.has(filePath)) {
|
|
2503
|
+
throw new Error(`Include cycle detected at '${filePath}'`);
|
|
2504
|
+
}
|
|
2505
|
+
ctx.seen.add(filePath);
|
|
2506
|
+
const stat = fs.statSync(filePath);
|
|
2507
|
+
if (stat.size > ctx.maxIncludeBytes) {
|
|
2508
|
+
throw new Error(`Include file too large (${stat.size} bytes): '${filePath}'`);
|
|
2509
|
+
}
|
|
2510
|
+
const source = fs.readFileSync(filePath, "utf8");
|
|
2511
|
+
const doc = parseDocument(source, {
|
|
2512
|
+
sourcePath: filePath,
|
|
2513
|
+
docRoot: ctx.docRoot,
|
|
2514
|
+
resolveIncludes: true,
|
|
2515
|
+
maxIncludeBytes: ctx.maxIncludeBytes,
|
|
2516
|
+
includeDepthLimit: ctx.includeDepthLimit - depth,
|
|
2517
|
+
allowBodyFragments: true,
|
|
2518
|
+
});
|
|
2519
|
+
ctx.seen.delete(filePath);
|
|
2520
|
+
return doc;
|
|
2521
|
+
}
|
|
2522
|
+
function rewriteNodeIds(nodes, includePath, parentPath) {
|
|
2523
|
+
return nodes.map((node, index) => {
|
|
2524
|
+
const nodePath = `${parentPath}/${node.kind}:${node.id}:${index}`;
|
|
2525
|
+
const hash = stableHash(includePath, nodePath).toString(16).padStart(6, "0");
|
|
2526
|
+
const nextId = `${node.id}_${hash}`;
|
|
2527
|
+
const children = rewriteNodeIds(node.children ?? [], includePath, nodePath);
|
|
2528
|
+
return { ...node, id: nextId, children };
|
|
2529
|
+
});
|
|
2530
|
+
}
|
|
2531
|
+
function stableHash(...values) {
|
|
2532
|
+
const serialized = values.map((value) => stableSerialize(value)).join("|");
|
|
2533
|
+
let hash = 0x811c9dc5;
|
|
2534
|
+
for (let i = 0; i < serialized.length; i += 1) {
|
|
2535
|
+
hash ^= serialized.charCodeAt(i);
|
|
2536
|
+
hash = Math.imul(hash, 0x01000193);
|
|
2537
|
+
}
|
|
2538
|
+
return hash >>> 0;
|
|
2539
|
+
}
|
|
2540
|
+
function stableSerialize(value) {
|
|
2541
|
+
if (value == null)
|
|
2542
|
+
return "null";
|
|
2543
|
+
if (typeof value === "number")
|
|
2544
|
+
return `n:${String(value)}`;
|
|
2545
|
+
if (typeof value === "string")
|
|
2546
|
+
return `s:${value}`;
|
|
2547
|
+
if (typeof value === "boolean")
|
|
2548
|
+
return `b:${value}`;
|
|
2549
|
+
if (Array.isArray(value)) {
|
|
2550
|
+
return `a:[${value.map((item) => stableSerialize(item)).join(",")}]`;
|
|
2551
|
+
}
|
|
2552
|
+
if (typeof value === "object") {
|
|
2553
|
+
const entries = Object.entries(value).sort(([a], [b]) => a.localeCompare(b));
|
|
2554
|
+
return `o:{${entries
|
|
2555
|
+
.map(([key, val]) => `${key}:${stableSerialize(val)}`)
|
|
2556
|
+
.join(",")}}`;
|
|
2557
|
+
}
|
|
2558
|
+
return `u:${String(value)}`;
|
|
1649
2559
|
}
|
|
1650
2560
|
//# sourceMappingURL=parser.js.map
|