@flux-lang/core 0.1.2 → 0.1.4-canary.078fd1eea
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 +180 -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 +11 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- 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 +1137 -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 +1959 -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 +100 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +145 -0
- package/dist/runtime.js.map +1 -0
- 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 +3 -3
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 { ... }
|
|
@@ -519,6 +523,12 @@ class Parser {
|
|
|
519
523
|
const grids = [];
|
|
520
524
|
const rules = [];
|
|
521
525
|
let runtime;
|
|
526
|
+
let materials;
|
|
527
|
+
let assets;
|
|
528
|
+
let tokens;
|
|
529
|
+
let styles;
|
|
530
|
+
const themes = [];
|
|
531
|
+
let body;
|
|
522
532
|
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
523
533
|
if (this.checkIdentifier("meta")) {
|
|
524
534
|
const blockMeta = this.parseMetaBlock();
|
|
@@ -540,6 +550,24 @@ class Parser {
|
|
|
540
550
|
else if (this.checkIdentifier("runtime")) {
|
|
541
551
|
runtime = this.parseRuntimeBlock();
|
|
542
552
|
}
|
|
553
|
+
else if (this.checkIdentifier("assets")) {
|
|
554
|
+
assets = this.parseAssetsBlock();
|
|
555
|
+
}
|
|
556
|
+
else if (this.checkIdentifier("materials")) {
|
|
557
|
+
materials = this.parseMaterialsBlock();
|
|
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
|
+
}
|
|
543
571
|
else {
|
|
544
572
|
const tok = this.peek();
|
|
545
573
|
throw this.errorAtToken(tok, `Unexpected top-level construct '${tok.lexeme}'`);
|
|
@@ -553,6 +581,12 @@ class Parser {
|
|
|
553
581
|
grids,
|
|
554
582
|
rules,
|
|
555
583
|
runtime,
|
|
584
|
+
materials,
|
|
585
|
+
assets,
|
|
586
|
+
tokens,
|
|
587
|
+
styles,
|
|
588
|
+
themes,
|
|
589
|
+
body,
|
|
556
590
|
};
|
|
557
591
|
return doc;
|
|
558
592
|
}
|
|
@@ -562,8 +596,7 @@ class Parser {
|
|
|
562
596
|
this.consume(TokenType.LBrace, "Expected '{' after 'meta'");
|
|
563
597
|
const meta = { version: "0.1.0" };
|
|
564
598
|
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
565
|
-
const
|
|
566
|
-
const key = String(keyTok.value);
|
|
599
|
+
const key = this.parseKeyPath("Expected meta field name");
|
|
567
600
|
this.consume(TokenType.Equals, "Expected '=' after meta field name");
|
|
568
601
|
const valueTok = this.consume(TokenType.String, "Expected string value for meta field");
|
|
569
602
|
meta[key] = String(valueTok.value);
|
|
@@ -813,6 +846,818 @@ class Parser {
|
|
|
813
846
|
this.consume(TokenType.RBrace, "Expected '}' after cell block");
|
|
814
847
|
return cell;
|
|
815
848
|
}
|
|
849
|
+
// --- Materials ---
|
|
850
|
+
parseMaterialsBlock() {
|
|
851
|
+
this.expectIdentifier("materials", "Expected 'materials'");
|
|
852
|
+
this.consume(TokenType.LBrace, "Expected '{' after 'materials'");
|
|
853
|
+
const materials = [];
|
|
854
|
+
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
855
|
+
if (this.checkIdentifier("material")) {
|
|
856
|
+
materials.push(this.parseMaterialDecl());
|
|
857
|
+
}
|
|
858
|
+
else {
|
|
859
|
+
this.skipStatement();
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
this.consume(TokenType.RBrace, "Expected '}' after materials block");
|
|
863
|
+
return { materials };
|
|
864
|
+
}
|
|
865
|
+
parseMaterialDecl() {
|
|
866
|
+
this.expectIdentifier("material", "Expected 'material'");
|
|
867
|
+
const nameTok = this.consume(TokenType.Identifier, "Expected material name");
|
|
868
|
+
const name = String(nameTok.value);
|
|
869
|
+
this.consume(TokenType.LBrace, "Expected '{' after material name");
|
|
870
|
+
const material = { name, tags: [] };
|
|
871
|
+
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
872
|
+
if (this.checkIdentifier("tags")) {
|
|
873
|
+
material.tags = this.parseIdentifierList();
|
|
874
|
+
}
|
|
875
|
+
else if (this.checkIdentifier("label")) {
|
|
876
|
+
this.advance();
|
|
877
|
+
this.consume(TokenType.Equals, "Expected '=' after 'label'");
|
|
878
|
+
const tok = this.consume(TokenType.String, "Expected string for label");
|
|
879
|
+
material.label = String(tok.value);
|
|
880
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
881
|
+
}
|
|
882
|
+
else if (this.checkIdentifier("description")) {
|
|
883
|
+
this.advance();
|
|
884
|
+
this.consume(TokenType.Equals, "Expected '=' after 'description'");
|
|
885
|
+
const tok = this.consume(TokenType.String, "Expected string for description");
|
|
886
|
+
material.description = String(tok.value);
|
|
887
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
888
|
+
}
|
|
889
|
+
else if (this.checkIdentifier("color")) {
|
|
890
|
+
this.advance();
|
|
891
|
+
this.consume(TokenType.Equals, "Expected '=' after 'color'");
|
|
892
|
+
const tok = this.consume(TokenType.String, "Expected string for color");
|
|
893
|
+
material.color = String(tok.value);
|
|
894
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
895
|
+
}
|
|
896
|
+
else if (this.checkIdentifier("score")) {
|
|
897
|
+
material.score = this.parseMaterialScoreBlock();
|
|
898
|
+
}
|
|
899
|
+
else if (this.checkIdentifier("midi")) {
|
|
900
|
+
material.midi = this.parseMaterialMidiBlock();
|
|
901
|
+
}
|
|
902
|
+
else if (this.checkIdentifier("video")) {
|
|
903
|
+
material.video = this.parseMaterialVideoBlock();
|
|
904
|
+
}
|
|
905
|
+
else {
|
|
906
|
+
this.skipStatement();
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
this.consume(TokenType.RBrace, "Expected '}' after material block");
|
|
910
|
+
return material;
|
|
911
|
+
}
|
|
912
|
+
parseMaterialScoreBlock() {
|
|
913
|
+
this.expectIdentifier("score", "Expected 'score'");
|
|
914
|
+
this.consume(TokenType.LBrace, "Expected '{' after 'score'");
|
|
915
|
+
const score = {};
|
|
916
|
+
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
917
|
+
if (this.checkIdentifier("text")) {
|
|
918
|
+
this.advance();
|
|
919
|
+
this.consume(TokenType.Equals, "Expected '=' after 'text'");
|
|
920
|
+
const tok = this.consume(TokenType.String, "Expected string for text");
|
|
921
|
+
score.text = String(tok.value);
|
|
922
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
923
|
+
}
|
|
924
|
+
else if (this.checkIdentifier("staff")) {
|
|
925
|
+
this.advance();
|
|
926
|
+
this.consume(TokenType.Equals, "Expected '=' after 'staff'");
|
|
927
|
+
const tok = this.consume(TokenType.String, "Expected string for staff");
|
|
928
|
+
score.staff = String(tok.value);
|
|
929
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
930
|
+
}
|
|
931
|
+
else if (this.checkIdentifier("clef")) {
|
|
932
|
+
this.advance();
|
|
933
|
+
this.consume(TokenType.Equals, "Expected '=' after 'clef'");
|
|
934
|
+
const tok = this.consume(TokenType.String, "Expected string for clef");
|
|
935
|
+
score.clef = String(tok.value);
|
|
936
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
937
|
+
}
|
|
938
|
+
else {
|
|
939
|
+
this.skipStatement();
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
this.consume(TokenType.RBrace, "Expected '}' after score block");
|
|
943
|
+
return score;
|
|
944
|
+
}
|
|
945
|
+
parseMaterialMidiBlock() {
|
|
946
|
+
this.expectIdentifier("midi", "Expected 'midi'");
|
|
947
|
+
this.consume(TokenType.LBrace, "Expected '{' after 'midi'");
|
|
948
|
+
const midi = {};
|
|
949
|
+
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
950
|
+
if (this.checkIdentifier("channel")) {
|
|
951
|
+
this.advance();
|
|
952
|
+
this.consume(TokenType.Equals, "Expected '=' after 'channel'");
|
|
953
|
+
const tok = this.consumeNumber("Expected numeric channel");
|
|
954
|
+
midi.channel = Number(tok.value);
|
|
955
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
956
|
+
}
|
|
957
|
+
else if (this.checkIdentifier("pitch")) {
|
|
958
|
+
this.advance();
|
|
959
|
+
this.consume(TokenType.Equals, "Expected '=' after 'pitch'");
|
|
960
|
+
const tok = this.consumeNumber("Expected numeric pitch");
|
|
961
|
+
midi.pitch = Number(tok.value);
|
|
962
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
963
|
+
}
|
|
964
|
+
else if (this.checkIdentifier("velocity")) {
|
|
965
|
+
this.advance();
|
|
966
|
+
this.consume(TokenType.Equals, "Expected '=' after 'velocity'");
|
|
967
|
+
const tok = this.consumeNumber("Expected numeric velocity");
|
|
968
|
+
midi.velocity = Number(tok.value);
|
|
969
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
970
|
+
}
|
|
971
|
+
else if (this.checkIdentifier("durationSeconds")) {
|
|
972
|
+
this.advance();
|
|
973
|
+
this.consume(TokenType.Equals, "Expected '=' after 'durationSeconds'");
|
|
974
|
+
const tok = this.consumeNumber("Expected numeric durationSeconds");
|
|
975
|
+
midi.durationSeconds = Number(tok.value);
|
|
976
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
977
|
+
}
|
|
978
|
+
else {
|
|
979
|
+
this.skipStatement();
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
this.consume(TokenType.RBrace, "Expected '}' after midi block");
|
|
983
|
+
return midi;
|
|
984
|
+
}
|
|
985
|
+
parseMaterialVideoBlock() {
|
|
986
|
+
this.expectIdentifier("video", "Expected 'video'");
|
|
987
|
+
this.consume(TokenType.LBrace, "Expected '{' after 'video'");
|
|
988
|
+
const video = { clip: "" };
|
|
989
|
+
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
990
|
+
if (this.checkIdentifier("clip")) {
|
|
991
|
+
this.advance();
|
|
992
|
+
this.consume(TokenType.Equals, "Expected '=' after 'clip'");
|
|
993
|
+
const tok = this.consume(TokenType.String, "Expected string for clip");
|
|
994
|
+
video.clip = String(tok.value);
|
|
995
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
996
|
+
}
|
|
997
|
+
else if (this.checkIdentifier("inSeconds")) {
|
|
998
|
+
this.advance();
|
|
999
|
+
this.consume(TokenType.Equals, "Expected '=' after 'inSeconds'");
|
|
1000
|
+
const tok = this.consumeNumber("Expected numeric inSeconds");
|
|
1001
|
+
video.inSeconds = Number(tok.value);
|
|
1002
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
1003
|
+
}
|
|
1004
|
+
else if (this.checkIdentifier("outSeconds")) {
|
|
1005
|
+
this.advance();
|
|
1006
|
+
this.consume(TokenType.Equals, "Expected '=' after 'outSeconds'");
|
|
1007
|
+
const tok = this.consumeNumber("Expected numeric outSeconds");
|
|
1008
|
+
video.outSeconds = Number(tok.value);
|
|
1009
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
1010
|
+
}
|
|
1011
|
+
else if (this.checkIdentifier("layer")) {
|
|
1012
|
+
this.advance();
|
|
1013
|
+
this.consume(TokenType.Equals, "Expected '=' after 'layer'");
|
|
1014
|
+
const tok = this.consume(TokenType.String, "Expected string for layer");
|
|
1015
|
+
video.layer = String(tok.value);
|
|
1016
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
1017
|
+
}
|
|
1018
|
+
else {
|
|
1019
|
+
this.skipStatement();
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
this.consume(TokenType.RBrace, "Expected '}' after video block");
|
|
1023
|
+
return video;
|
|
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
|
+
}
|
|
1618
|
+
parseIdentifierList() {
|
|
1619
|
+
this.advance(); // identifier keyword already checked
|
|
1620
|
+
this.consume(TokenType.Equals, "Expected '=' after identifier list key");
|
|
1621
|
+
this.consume(TokenType.LBracket, "Expected '[' to start identifier list");
|
|
1622
|
+
const values = [];
|
|
1623
|
+
if (!this.check(TokenType.RBracket)) {
|
|
1624
|
+
const first = this.consume(TokenType.Identifier, "Expected identifier");
|
|
1625
|
+
values.push(String(first.value));
|
|
1626
|
+
while (this.match(TokenType.Comma)) {
|
|
1627
|
+
const t = this.consume(TokenType.Identifier, "Expected identifier");
|
|
1628
|
+
values.push(String(t.value));
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
this.consume(TokenType.RBracket, "Expected ']' after identifier list");
|
|
1632
|
+
this.consumeOptional(TokenType.Semicolon);
|
|
1633
|
+
return values;
|
|
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
|
+
}
|
|
816
1661
|
// --- Expressions ---
|
|
817
1662
|
parseExpr() {
|
|
818
1663
|
return this.parseOr();
|
|
@@ -935,6 +1780,7 @@ class Parser {
|
|
|
935
1780
|
kind: "UnaryExpression",
|
|
936
1781
|
op,
|
|
937
1782
|
argument,
|
|
1783
|
+
loc: argument?.loc,
|
|
938
1784
|
};
|
|
939
1785
|
}
|
|
940
1786
|
if (this.match(TokenType.Minus)) {
|
|
@@ -944,6 +1790,7 @@ class Parser {
|
|
|
944
1790
|
kind: "UnaryExpression",
|
|
945
1791
|
op,
|
|
946
1792
|
argument,
|
|
1793
|
+
loc: argument?.loc,
|
|
947
1794
|
};
|
|
948
1795
|
}
|
|
949
1796
|
return this.parsePostfix();
|
|
@@ -958,11 +1805,14 @@ class Parser {
|
|
|
958
1805
|
kind: "MemberExpression",
|
|
959
1806
|
object: expr,
|
|
960
1807
|
property,
|
|
1808
|
+
loc: expr?.loc,
|
|
961
1809
|
};
|
|
962
1810
|
}
|
|
963
1811
|
else if (this.match(TokenType.LParen)) {
|
|
964
1812
|
const args = this.parseArgumentList();
|
|
965
|
-
|
|
1813
|
+
const callExpr = this.maybeNeighborsCall(expr, args);
|
|
1814
|
+
callExpr.loc = expr?.loc;
|
|
1815
|
+
expr = callExpr;
|
|
966
1816
|
}
|
|
967
1817
|
else {
|
|
968
1818
|
break;
|
|
@@ -979,6 +1829,7 @@ class Parser {
|
|
|
979
1829
|
return {
|
|
980
1830
|
kind: "Literal",
|
|
981
1831
|
value: tok.value,
|
|
1832
|
+
loc: { line: tok.line, column: tok.column },
|
|
982
1833
|
};
|
|
983
1834
|
}
|
|
984
1835
|
case TokenType.String: {
|
|
@@ -986,6 +1837,7 @@ class Parser {
|
|
|
986
1837
|
return {
|
|
987
1838
|
kind: "Literal",
|
|
988
1839
|
value: tok.value,
|
|
1840
|
+
loc: { line: tok.line, column: tok.column },
|
|
989
1841
|
};
|
|
990
1842
|
}
|
|
991
1843
|
case TokenType.Bool: {
|
|
@@ -993,6 +1845,23 @@ class Parser {
|
|
|
993
1845
|
return {
|
|
994
1846
|
kind: "Literal",
|
|
995
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 },
|
|
996
1865
|
};
|
|
997
1866
|
}
|
|
998
1867
|
case TokenType.Identifier: {
|
|
@@ -1000,6 +1869,7 @@ class Parser {
|
|
|
1000
1869
|
return {
|
|
1001
1870
|
kind: "Identifier",
|
|
1002
1871
|
name: tok.lexeme,
|
|
1872
|
+
loc: { line: tok.line, column: tok.column },
|
|
1003
1873
|
};
|
|
1004
1874
|
}
|
|
1005
1875
|
case TokenType.LBrace: {
|
|
@@ -1026,13 +1896,23 @@ class Parser {
|
|
|
1026
1896
|
this.consume(TokenType.RParen, "Expected ')' after argument list");
|
|
1027
1897
|
return args;
|
|
1028
1898
|
}
|
|
1029
|
-
args.push(this.
|
|
1899
|
+
args.push(this.parseCallArg());
|
|
1030
1900
|
while (this.match(TokenType.Comma)) {
|
|
1031
|
-
args.push(this.
|
|
1901
|
+
args.push(this.parseCallArg());
|
|
1032
1902
|
}
|
|
1033
1903
|
this.consume(TokenType.RParen, "Expected ')' after argument list");
|
|
1034
1904
|
return args;
|
|
1035
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
|
+
}
|
|
1036
1916
|
maybeNeighborsCall(callee, args) {
|
|
1037
1917
|
if (callee.kind === "MemberExpression" &&
|
|
1038
1918
|
callee.object.kind === "Identifier" &&
|
|
@@ -1056,6 +1936,7 @@ class Parser {
|
|
|
1056
1936
|
op,
|
|
1057
1937
|
left,
|
|
1058
1938
|
right,
|
|
1939
|
+
loc: left?.loc,
|
|
1059
1940
|
};
|
|
1060
1941
|
}
|
|
1061
1942
|
// --- Rule & Runtime (placeholders for now) ---
|
|
@@ -1267,27 +2148,43 @@ class Parser {
|
|
|
1267
2148
|
const unitTok = this.advance();
|
|
1268
2149
|
const raw = String(unitTok.value);
|
|
1269
2150
|
const lowered = raw.toLowerCase();
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
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) {
|
|
1289
2185
|
throw this.errorAtToken(unitTok, `Unknown duration unit '${unitTok.lexeme}'`);
|
|
1290
2186
|
}
|
|
2187
|
+
unit = mapped;
|
|
1291
2188
|
}
|
|
1292
2189
|
return { amount, unit };
|
|
1293
2190
|
}
|
|
@@ -1370,6 +2267,38 @@ class Parser {
|
|
|
1370
2267
|
throw this.errorAtToken(tok, "Expected literal");
|
|
1371
2268
|
}
|
|
1372
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
|
+
}
|
|
1373
2302
|
consumeNumber(message) {
|
|
1374
2303
|
const tok = this.peek();
|
|
1375
2304
|
if (tok.type === TokenType.Int || tok.type === TokenType.Float) {
|
|
@@ -1442,11 +2371,190 @@ class Parser {
|
|
|
1442
2371
|
return new Error(`Parse error at ${token.line}:${token.column} near '${token.lexeme}': ${message}`);
|
|
1443
2372
|
}
|
|
1444
2373
|
}
|
|
1445
|
-
|
|
1446
|
-
|
|
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 = {}) {
|
|
1447
2383
|
const lexer = new Lexer(source);
|
|
1448
2384
|
const tokens = lexer.tokenize();
|
|
1449
|
-
const parser = new Parser(tokens);
|
|
1450
|
-
|
|
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)}`;
|
|
1451
2559
|
}
|
|
1452
2560
|
//# sourceMappingURL=parser.js.map
|