yini-parser 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +183 -37
  3. package/dist/YINI.d.ts +22 -7
  4. package/dist/YINI.js +101 -0
  5. package/dist/core/astBuilder.d.ts +94 -15
  6. package/dist/core/astBuilder.js +394 -362
  7. package/dist/core/errorDataHandler.d.ts +6 -1
  8. package/dist/core/errorDataHandler.js +30 -43
  9. package/dist/core/internalTypes.d.ts +10 -1
  10. package/dist/core/objectBuilder.js +21 -6
  11. package/dist/core/options/defaultParserOptions.d.ts +3 -2
  12. package/dist/core/options/defaultParserOptions.js +2 -1
  13. package/dist/core/options/optionsFunctions.js +5 -1
  14. package/dist/core/pipeline/pipeline.js +31 -10
  15. package/dist/core/runtime.js +28 -19
  16. package/dist/grammar/generated/YiniLexer.d.ts +28 -35
  17. package/dist/grammar/generated/YiniLexer.js +323 -310
  18. package/dist/grammar/generated/YiniParser.d.ts +158 -80
  19. package/dist/grammar/generated/YiniParser.js +1141 -620
  20. package/dist/grammar/generated/YiniParserVisitor.d.ts +77 -14
  21. package/dist/grammar/generated/YiniParserVisitor.js +1 -1
  22. package/dist/index.d.ts +2 -1
  23. package/dist/index.js +4 -3
  24. package/dist/parsers/extractHeaderParts.d.ts +12 -19
  25. package/dist/parsers/extractHeaderParts.js +57 -46
  26. package/dist/parsers/parseNumber.d.ts +24 -6
  27. package/dist/parsers/parseNumber.js +114 -49
  28. package/dist/parsers/parseSectionHeader.d.ts +11 -3
  29. package/dist/parsers/parseSectionHeader.js +55 -43
  30. package/dist/parsers/parseString.js +39 -20
  31. package/dist/parsers/validateShebangPlacement.d.ts +3 -0
  32. package/dist/parsers/validateShebangPlacement.js +52 -0
  33. package/dist/types/index.d.ts +19 -2
  34. package/dist/utils/print.d.ts +1 -0
  35. package/dist/utils/print.js +5 -1
  36. package/dist/utils/string.d.ts +1 -0
  37. package/dist/utils/string.js +17 -1
  38. package/dist/utils/system.d.ts +1 -0
  39. package/dist/utils/system.js +6 -1
  40. package/dist/utils/yiniHelpers.d.ts +44 -2
  41. package/dist/utils/yiniHelpers.js +134 -46
  42. package/examples/compare-formats.md +1 -1
  43. package/examples/nested.yini +1 -1
  44. package/package.json +11 -3
  45. package/dist/YINI.js.map +0 -1
  46. package/dist/config/env.js.map +0 -1
  47. package/dist/core/astBuilder.js.map +0 -1
  48. package/dist/core/errorDataHandler.js.map +0 -1
  49. package/dist/core/internalTypes.js.map +0 -1
  50. package/dist/core/objectBuilder.js.map +0 -1
  51. package/dist/core/options/defaultParserOptions.js.map +0 -1
  52. package/dist/core/options/failLevel.js.map +0 -1
  53. package/dist/core/options/optionsFunctions.js.map +0 -1
  54. package/dist/core/parsingRules/modeFromRulesMatcher.js.map +0 -1
  55. package/dist/core/parsingRules/rulesConstAndGuards.js.map +0 -1
  56. package/dist/core/pipeline/errorListeners.js.map +0 -1
  57. package/dist/core/pipeline/pipeline.js.map +0 -1
  58. package/dist/core/resultMetadataBuilder.js.map +0 -1
  59. package/dist/core/runtime.js.map +0 -1
  60. package/dist/dev/main.d.ts +0 -1
  61. package/dist/dev/main.js +0 -139
  62. package/dist/dev/main.js.map +0 -1
  63. package/dist/dev/quick-test-samples/defect-inputs.d.ts +0 -37
  64. package/dist/dev/quick-test-samples/defect-inputs.js +0 -106
  65. package/dist/dev/quick-test-samples/defect-inputs.js.map +0 -1
  66. package/dist/dev/quick-test-samples/valid-inputs.d.ts +0 -21
  67. package/dist/dev/quick-test-samples/valid-inputs.js +0 -422
  68. package/dist/dev/quick-test-samples/valid-inputs.js.map +0 -1
  69. package/dist/grammar/generated/YiniLexer.js.map +0 -1
  70. package/dist/grammar/generated/YiniParser.js.map +0 -1
  71. package/dist/grammar/generated/YiniParserVisitor.js.map +0 -1
  72. package/dist/index.js.map +0 -1
  73. package/dist/parsers/extractHeaderParts.js.map +0 -1
  74. package/dist/parsers/extractSignificantYiniLine.js.map +0 -1
  75. package/dist/parsers/parseBoolean.js.map +0 -1
  76. package/dist/parsers/parseNull.js.map +0 -1
  77. package/dist/parsers/parseNumber.js.map +0 -1
  78. package/dist/parsers/parseSectionHeader.js.map +0 -1
  79. package/dist/parsers/parseString.js.map +0 -1
  80. package/dist/types/index.js.map +0 -1
  81. package/dist/utils/number.js.map +0 -1
  82. package/dist/utils/object.js.map +0 -1
  83. package/dist/utils/pathAndFileName.js.map +0 -1
  84. package/dist/utils/print.js.map +0 -1
  85. package/dist/utils/string.js.map +0 -1
  86. package/dist/utils/system.js.map +0 -1
  87. package/dist/utils/yiniHelpers.js.map +0 -1
@@ -36,13 +36,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
+ /**
40
+ * Here is where we walk the parse tree (CST) and build/validate the AST.
41
+ */
42
+ // src/core/astBuilder.ts
39
43
  const env_1 = require("../config/env");
40
44
  const YiniParserVisitor_1 = __importDefault(require("../grammar/generated/YiniParserVisitor"));
41
45
  const extractSignificantYiniLine_1 = require("../parsers/extractSignificantYiniLine");
42
46
  const parseBoolean_1 = __importDefault(require("../parsers/parseBoolean"));
43
47
  const parseNull_1 = __importDefault(require("../parsers/parseNull"));
44
48
  const parseNumber_1 = __importDefault(require("../parsers/parseNumber"));
45
- // import parseNumber from '../parsers/parseNumber'
46
49
  const parseSectionHeader_1 = __importDefault(require("../parsers/parseSectionHeader"));
47
50
  const parseString_1 = __importStar(require("../parsers/parseString"));
48
51
  const number_1 = require("../utils/number");
@@ -93,54 +96,9 @@ const makeListValue = (elems = [], tag = undefined) => {
93
96
  const makeObjectValue = (entries = {}, tag = undefined) => {
94
97
  return { type: 'Object', entries, tag };
95
98
  };
96
- function trimQuotes(text) {
97
- // STRING token already excludes quotes; the rule returns the literal with quotes present.
98
- // We’ll reliably strip the outer quote(s) and leave contents as-is (concat pieces handled below).
99
- const q = text[0];
100
- if ((q === '"' || q === "'") &&
101
- text.length >= 2 &&
102
- text[text.length - 1] === q) {
103
- return text.slice(1, -1);
104
- }
105
- // Triple-quoted cases are handled by the lexer too; same stripping works since token text begins with quotes.
106
- if (text.startsWith('"""') && text.endsWith('"""') && text.length >= 6) {
107
- return text.slice(3, -3);
108
- }
109
- return text;
110
- }
111
99
  function makeSection(name, level) {
112
100
  return { sectionName: name, level, members: new Map(), children: [] };
113
101
  }
114
- /** Parse SECTION_HEAD token text → {level, name}.
115
- * Supports repeated markers (^^^^) and shorthand (^7) (Spec 5.2–5.3.1). :contentReference[oaicite:5]{index=5}:contentReference[oaicite:6]{index=6}
116
- */
117
- // function parseSectionHeadToken(raw: string): { level: number; name: string } {
118
- // // SECTION_HEAD token text includes: optional WS, marker(s) or shorthand, WS, IDENT (possibly backticked), NL+
119
- // // We only need the visible line content up to NL.
120
- // const line = raw.split(/\r?\n/)[0]
121
- // // Extract marker block and name
122
- // // Examples: "^^ Section", "^7 `Section name`", "< MySection"
123
- // const m = line.match(/^\s*([\^<§€]+|\^|\<|§|€)(\d+)?[ \t]+(.+?)\s*$/)
124
- // if (m) {
125
- // const markerRun = m[1]
126
- // const numeric = m[2]
127
- // let level: number
128
- // if (numeric) {
129
- // level = parseInt(numeric, 10)
130
- // } else {
131
- // // count repeated marker chars (^^^^)
132
- // level = markerRun.length
133
- // }
134
- // // Section name may be backticked: `Name with spaces`
135
- // let name = m[3]
136
- // if (name.startsWith('`') && name.endsWith('`')) {
137
- // name = name.slice(1, -1)
138
- // }
139
- // return { level, name }
140
- // }
141
- // // Fallback: be defensive
142
- // return { level: 1, name: line.trim() }
143
- // }
144
102
  // --- Builder Visitor -----------------------------------------------------
145
103
  /**
146
104
  * This interface defines a complete generic visitor for a parse tree produced
@@ -150,6 +108,7 @@ function makeSection(name, level) {
150
108
  * operations with no return type.
151
109
  */
152
110
  // export default class YINIVisitor<IResult> extends YiniParserVisitor<IResult> {
111
+ // export default class ASTBuilder<Result> extends YiniParserVisitor<Result> {
153
112
  class ASTBuilder extends YiniParserVisitor_1.default {
154
113
  /**
155
114
  * @param metaFileName If parsing from a file, provide the file name here so the meta information can be updated accordingly.
@@ -158,6 +117,7 @@ class ASTBuilder extends YiniParserVisitor_1.default {
158
117
  constructor(errorHandler, options, sourceType, metaFileName) {
159
118
  super();
160
119
  this.errorHandler = null;
120
+ this.ignoredSectionLevel = null;
161
121
  this.meta_hasYiniMarker = false; // For stats.
162
122
  // private meta_numOfSections = 0 // For stats.
163
123
  this._numOfMembers = 0; // For error checking and stats.
@@ -175,8 +135,6 @@ class ASTBuilder extends YiniParserVisitor_1.default {
175
135
  * @param ctx the parse tree
176
136
  * @return the visitor result
177
137
  */
178
- // visitYini?: (ctx: YiniContext) => Result
179
- // visitYini?: (ctx: YiniContext) => any
180
138
  this.visitYini = (ctx) => {
181
139
  // children: prolog?, stmt*, terminal?, EOF
182
140
  ctx.children?.forEach((c) => this.visit?.(c));
@@ -197,36 +155,63 @@ class ASTBuilder extends YiniParserVisitor_1.default {
197
155
  * @param ctx the parse tree
198
156
  * @return the visitor result
199
157
  */
200
- // visitTerminal_stmt?: (ctx: Terminal_stmtContext) => Result;
201
158
  this.visitTerminal_stmt = (ctx) => {
202
159
  (0, print_1.debugPrint)('-> Entered visitTerminal_stmt(..)');
203
160
  let rawText = ctx.getText().trim();
204
161
  (0, print_1.debugPrint)('rawText = "' + rawText + '"');
205
- // rawText = extractYiniLine(rawText) // Remove possible comments.
206
- // rawText = stripCommentsAndAfter(rawText.split('\n', 1)[0]).trim() // Remove possible comments.
207
162
  rawText = (0, yiniHelpers_1.stripCommentsAndAfter)(rawText); // Remove possible comments.
208
163
  (0, print_1.debugPrint)('rawText2 = "' + rawText + '"');
209
164
  if (rawText.toLowerCase() === '/end') {
210
165
  // NOTE: Below, maybe not reached at all.
211
166
  if (this.ast.terminatorSeen) {
212
167
  // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
213
- this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Warning', 'Hit a duplicate terminator in document', `'${rawText}' already exists in this file, there must only be one terminator at the end of file ('/END'). Also note that the terminator is optional in both lenient and strict mode, unless the option 'isRequireDocTerminator' is enabled.`);
168
+ this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Warning', 'Hit a duplicate terminator in document', `'${rawText}' already exists in this file. A YINI document may contain only one document terminator ('/END'). The terminator is optional in lenient mode and required in strict mode unless parser options explicitly override that policy.`);
214
169
  }
215
170
  }
216
171
  else {
217
172
  // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
218
- this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', 'Encountered unknow syntax for terminator', `Got '${rawText}', but expected '/END' (case insensitive).`);
173
+ this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', 'Encountered unknown syntax for terminator', `Got '${rawText}', but expected '/END' (case insensitive).`);
219
174
  }
220
175
  this.ast.terminatorSeen = true;
221
176
  return null;
222
177
  };
178
+ /**
179
+ * Visit a parse tree produced by `YiniParser.terminal_trivia`.
180
+ * @param ctx the parse tree
181
+ * @return the visitor result
182
+ */
183
+ this.visitTerminal_trivia = (ctx) => {
184
+ ctx.children?.forEach((c) => this.visit?.(c));
185
+ return null;
186
+ };
223
187
  /**
224
188
  * Visit a parse tree produced by `YiniParser.stmt`.
225
189
  * @param ctx the parse tree
226
- * @grammarRule eol | SECTION_HEAD | assignment | colon_list_decl | marker_stmt | bad_member
227
190
  * @return the visitor result
228
191
  */
229
192
  // visitStmt?: (ctx: StmtContext) => Result
193
+ /**
194
+ * Visit a parse tree produced by `YiniParser.full_line_comment_stmt`.
195
+ * @param ctx the parse tree
196
+ * @return the visitor result
197
+ */
198
+ this.visitFull_line_comment_stmt = (_ctx) => {
199
+ return null;
200
+ };
201
+ /**
202
+ * Visit a parse tree produced by `YiniParser.disabled_line_stmt`.
203
+ * @param ctx the parse tree
204
+ * @return the visitor result
205
+ */
206
+ this.visitDisabled_line_stmt = (_ctx) => {
207
+ return null;
208
+ };
209
+ /**
210
+ * Visit a parse tree produced by `YiniParser.stmt`.
211
+ * @param ctx the parse tree
212
+ * @grammarRule eol | full_line_comment_stmt | disabled_line_stmt | SECTION_HEAD | invalid_section_stmt | assignment | meta_stmt | bad_member
213
+ * @return the visitor result
214
+ */
230
215
  this.visitStmt = (ctx) => {
231
216
  const child = ctx.getChild(0);
232
217
  const ruleName = child?.constructor?.name ?? '';
@@ -237,6 +222,15 @@ class ASTBuilder extends YiniParserVisitor_1.default {
237
222
  // }
238
223
  if (ruleName.includes('EolContext'))
239
224
  return this.visitEol?.(child);
225
+ if (ruleName.includes('Full_line_comment_stmtContext'))
226
+ return this.visitFull_line_comment_stmt?.(child);
227
+ if (ruleName.includes('Disabled_line_stmtContext'))
228
+ return this.visitDisabled_line_stmt?.(child);
229
+ if (this.isIgnoringDuplicateSection() && !ctx.SECTION_HEAD()) {
230
+ return null;
231
+ }
232
+ if (ruleName.includes('Invalid_section_stmtContext'))
233
+ return this.visitInvalid_section_stmt?.(child);
240
234
  if (ruleName.includes('AssignmentContext'))
241
235
  return this.visitAssignment?.(child);
242
236
  if (ruleName.includes('Meta_stmtContext'))
@@ -249,6 +243,9 @@ class ASTBuilder extends YiniParserVisitor_1.default {
249
243
  (0, print_1.debugPrint)('S3, header: >>>' + header + '<<<');
250
244
  if (!!header) {
251
245
  const { sectionName, sectionLevel } = (0, parseSectionHeader_1.default)(header, this.errorHandler, ctx);
246
+ if (this.shouldSkipSectionAtLevel(sectionLevel)) {
247
+ return null;
248
+ }
252
249
  // Validate level sequencing per spec 5.3 (no skipping upward)
253
250
  const currentLevel = this.sectionStack[this.sectionStack.length - 1].level;
254
251
  if (sectionLevel > currentLevel + 1) {
@@ -262,6 +259,10 @@ class ASTBuilder extends YiniParserVisitor_1.default {
262
259
  // bad_member fallback
263
260
  return this.visitBad_member?.(ctx.getChild(0));
264
261
  };
262
+ this.visitInvalid_section_stmt = (ctx) => {
263
+ this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', 'Invalid section header', `Offending section header: '${ctx.getText().trim()}'`, 'Section headers must use a valid marker sequence followed by a valid section name.');
264
+ return null;
265
+ };
265
266
  /**
266
267
  * Visit a parse tree produced by `YiniParser.meta_stmt`.
267
268
  * @param ctx the parse tree
@@ -286,31 +287,19 @@ class ASTBuilder extends YiniParserVisitor_1.default {
286
287
  */
287
288
  this.visitDirective = (ctx) => {
288
289
  (0, print_1.debugPrint)('-> Entered visitDirective(..)');
289
- let rawText = ctx.getText().trim();
290
- (0, print_1.debugPrint)('rawText = "' + rawText + '"');
291
- // NOTE: Important to strip any possible comments on the same line.
292
- rawText = (0, yiniHelpers_1.stripCommentsAndAfter)(rawText); // Remove possible comments.
293
- (0, print_1.debugPrint)('rawText2 = "' + rawText + '"');
290
+ const rawText = (0, yiniHelpers_1.stripCommentsAndAfter)(ctx.getText().trim());
294
291
  if (this.mapSectionNamePaths.size || this._numOfMembers) {
295
- // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
296
- this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), this.isStrict ? 'Syntax-Error' : 'Syntax-Warning', `Found a directive statement in the wrong place ${this.isStrict ? '(strict mode)' : '(lenient mode)'}`, `Directive '${rawText}' must appear only at the beginning of the document, before any sections or members.`, `Tip: Move the line with '${rawText}' to the very top of the file (but after a possible #! line or comments).`);
292
+ this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), this.isStrict ? 'Syntax-Error' : 'Syntax-Warning', `Found a directive statement in the wrong place ${this.isStrict ? '(strict mode)' : '(lenient mode)'}`, `Directive '${rawText}' must appear only at the beginning of the document, before any sections or members.`, 'Move the directive to the top of the file, after a possible shebang, comments, or whitespace.');
297
293
  }
298
- if (rawText.toLowerCase().startsWith('@include')) {
299
- // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
300
- this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Notice', `Detected unsupported directive '@include'`, `This directive is not currently supported by the parser.`);
301
- }
302
- else if (rawText.toLowerCase() === '@yini') {
303
- if (this.ast.yiniMarkerSeen) {
304
- // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
305
- this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), this.isStrict ? 'Syntax-Error' : 'Syntax-Warning', `Hit a duplicate YINI Marker in document`, `'${rawText}' already exists in this file, it's enough with only one YINI Marker ('@YINI').`);
306
- }
294
+ const yiniDirective = ctx.yini_directive?.();
295
+ if (yiniDirective) {
296
+ return this.visitYini_directive(yiniDirective);
307
297
  }
308
- else {
309
- // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
310
- this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', 'Encountered unknow directive statement', `Got '${rawText}', but expected '@YINI' (case insensitive).`);
298
+ if (ctx.INCLUDE_TOKEN?.()) {
299
+ this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', "Unsupported directive '@include'", "'@include' is reserved for possible future use, but is not currently supported by this parser.", 'Remove the directive or preprocess included files before parsing.');
300
+ return null;
311
301
  }
312
- // @yini marker is advisory (no semantic value) per spec. We ignore it. (Spec 2.4) :contentReference[oaicite:9]{index=9}
313
- this.ast.yiniMarkerSeen = true;
302
+ this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', 'Encountered unknown directive statement', `Got '${rawText}'.`, "Expected '@yini', '@yini strict', '@yini lenient', or a supported directive.");
314
303
  return null;
315
304
  };
316
305
  /**
@@ -406,10 +395,6 @@ class ASTBuilder extends YiniParserVisitor_1.default {
406
395
  */
407
396
  let valueContext = ctx.value?.();
408
397
  let valueNode;
409
- const hasEquals = rawMemberText.includes('=');
410
- // const hasTextAfterEquals = hasEquals
411
- // ? rawMemberText.split('=').slice(1).join('=').trim().length > 0
412
- // : false
413
398
  const eqIndex = rawMemberText.indexOf('=');
414
399
  const hasTextAfterEquals = eqIndex >= 0 && rawMemberText.slice(eqIndex + 1).trim().length > 0;
415
400
  if (!valueContext || !rawValue) {
@@ -441,27 +426,6 @@ class ASTBuilder extends YiniParserVisitor_1.default {
441
426
  if ((0, env_1.isDebug)()) {
442
427
  (0, print_1.printObject)(valueNode);
443
428
  }
444
- /*
445
- if (!valueNode) {
446
- this.errorHandler!.pushOrBail(
447
- toErrorLocation(ctx),
448
- 'Syntax-Error',
449
- 'Invalid value',
450
- `Invalid value for key '${resultKey}' in member (<key> = <value> pair).`,
451
- `Got '${rawValue}', but expected a valid value/literal (string, number, boolean, null, list, or object). Optionally with a single leading minus sign '-'.`,
452
- )
453
- } else if (
454
- valueNode.type === 'Undefined' &&
455
- valueNode.tag !== 'Invalid string literal already reported'
456
- ) {
457
- this.errorHandler!.pushOrBail(
458
- toErrorLocation(ctx),
459
- 'Syntax-Error',
460
- 'Invalid value',
461
- `Invalid value for key '${resultKey}' in member (<key> = <value> pair).`,
462
- `Got '${rawValue}', but expected a valid value/literal (string, number, boolean, null, list, or object). Optionally with a single leading minus sign '-'.`,
463
- )
464
- }*/
465
429
  if (!valueNode) {
466
430
  // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
467
431
  this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', 'Invalid value', `Invalid value for key '${resultKey}' in member (<key> = <value> pair).`, `Got '${rawValue}', but expected a valid value/literal (string, number, boolean, null, list, or object). Optionally with a single leading minus sign '-'.`);
@@ -490,26 +454,17 @@ class ASTBuilder extends YiniParserVisitor_1.default {
490
454
  * @param ctx the parse tree
491
455
  * @return the visitor result
492
456
  */
493
- // visitValue?: (ctx: ValueContext) => Result
494
457
  this.visitValue = (ctx) => {
495
458
  (0, print_1.debugPrint)('----------------------------');
496
459
  (0, print_1.debugPrint)('-> Entered visitValue(..)');
497
460
  let valueNode = undefined;
498
- if (ctx.null_literal()) {
499
- (0, print_1.debugPrint)(' visiting visitNull_literal(..)');
500
- valueNode = this.visitNull_literal(ctx.null_literal());
501
- }
502
- else if (ctx.string_literal()) {
503
- (0, print_1.debugPrint)(' visiting visitString_literal(..)');
504
- valueNode = this.visitString_literal(ctx.string_literal());
505
- }
506
- else if (ctx.number_literal()) {
507
- (0, print_1.debugPrint)(' visiting visitNumber_literal(..)');
508
- valueNode = this.visitNumber_literal(ctx.number_literal());
461
+ if (ctx.concat_expression()) {
462
+ (0, print_1.debugPrint)(' visiting visitConcat_expression(..)');
463
+ valueNode = this.visitConcat_expression(ctx.concat_expression());
509
464
  }
510
- else if (ctx.boolean_literal()) {
511
- (0, print_1.debugPrint)(' visiting visitBoolean_literal(..)');
512
- valueNode = this.visitBoolean_literal(ctx.boolean_literal());
465
+ else if (ctx.scalar_value()) {
466
+ (0, print_1.debugPrint)(' visiting visitScalar_value(..)');
467
+ valueNode = this.visitScalar_value(ctx.scalar_value());
513
468
  }
514
469
  else if (ctx.list_literal()) {
515
470
  (0, print_1.debugPrint)(' visiting visitList_literal(..)');
@@ -531,109 +486,164 @@ class ASTBuilder extends YiniParserVisitor_1.default {
531
486
  return valueNode;
532
487
  };
533
488
  /**
534
- * Visit a parse tree produced by `YiniParser.string_literal`.
489
+ * Visit a parse tree produced by `YiniParser.scalar_value`.
535
490
  * @param ctx the parse tree
536
491
  * @return the visitor result
537
492
  */
538
- /*
539
- visitString_literal = (ctx: String_literalContext): any => {
540
- let text = ''
541
-
542
- const pieces = [
543
- ctx.STRING(),
544
- ...(ctx.string_concat_list()?.map((c) => c.STRING()) ?? []),
545
- ]
546
-
547
- try {
548
- for (const token of pieces) {
549
- const tokenText = token.getText()
550
- const parsed = this.extractStringKindAndValue(tokenText)
551
-
552
- try {
553
- text += parseStringLiteral(parsed)
554
- } catch (err: unknown) {
555
- const msg = '' + (<any>err)?.message
556
- this.errorHandler!.pushOrBail(
557
- toErrorLocation(ctx),
558
- 'Syntax-Error',
559
- 'Parse error in string',
560
- `${msg}`,
561
- )
562
- }
563
- }
564
-
565
- return makeScalarValue('String', text)
566
- } catch (err: unknown) {
567
- const msg = err instanceof Error ? err.message : String(err)
568
-
569
- let msgWhat = 'Invalid string literal'
570
- let msgWhy = msg
571
- let msgHint = ''
572
-
573
- if (err instanceof CYiniStringParseError) {
574
- msgWhat = 'Invalid escape sequence in string'
575
- msgWhy = msg
576
-
577
- if (/Invalid escape sequence \\\\/.test(msg)) {
578
- msgHint =
579
- 'Use double backslashes (\\\\) in C-strings, or use a raw string for file paths.'
580
- } else if (/end of string/i.test(msg)) {
581
- msgHint =
582
- 'Check that all escape sequences in the C-string are complete and valid.'
583
- }
584
- }
585
-
586
- this.errorHandler!.pushOrBail(
587
- toErrorLocation(ctx),
588
- 'Syntax-Error',
589
- msgWhat,
590
- msgWhy,
591
- msgHint,
592
- )
593
-
594
- return makeScalarValue(
595
- 'Undefined',
596
- undefined,
597
- 'Invalid string literal',
598
- )
493
+ this.visitScalar_value = (ctx) => {
494
+ (0, print_1.debugPrint)('-> Entered visitScalar_value(..)');
495
+ if (ctx.string_literal()) {
496
+ return this.visitString_literal(ctx.string_literal());
599
497
  }
600
- }
601
- */
602
- this.visitString_literal = (ctx) => {
603
- let text = '';
604
- const pieces = [
605
- ctx.STRING(),
606
- ...(ctx.string_concat_list()?.map((c) => c.STRING()) ?? []),
607
- ];
608
- for (const token of pieces) {
609
- const tokenText = token.getText();
610
- const parsed = this.extractStringKindAndValue(tokenText);
611
- try {
612
- text += (0, parseString_1.default)(parsed);
498
+ if (ctx.number_literal()) {
499
+ return this.visitNumber_literal(ctx.number_literal());
500
+ }
501
+ if (ctx.boolean_literal()) {
502
+ return this.visitBoolean_literal(ctx.boolean_literal());
503
+ }
504
+ if (ctx.null_literal()) {
505
+ return this.visitNull_literal(ctx.null_literal());
506
+ }
507
+ return makeScalarValue('Undefined', undefined, 'Invalid scalar value');
508
+ };
509
+ /**
510
+ * Visit a parse tree produced by `YiniParser.concat_expression`.
511
+ *
512
+ * Grammar:
513
+ * concat_expression : STRING concat_tail+ ;
514
+ *
515
+ * Spec:
516
+ * - Concatenation must begin with a string literal.
517
+ * - In strict mode, every operand must be a string literal.
518
+ * - In lenient mode, later operands may be string, number, boolean, or null.
519
+ * - YINI does not define numeric addition.
520
+ *
521
+ * Examples:
522
+ * "Port: " + 8080 // lenient valid
523
+ * "1" + 2 + 3 // lenient valid
524
+ * 8080 + " is the port" // invalid: must begin with string literal
525
+ * 1 + 2 + "3" // invalid: must begin with string literal
526
+ * 1 + 2 + 3 // invalid: YINI does not define numeric addition
527
+ */
528
+ this.visitConcat_expression = (ctx) => {
529
+ (0, print_1.debugPrint)('-> Entered visitConcat_expression(..)');
530
+ const operands = [];
531
+ const firstStringToken = ctx.STRING();
532
+ if (!firstStringToken) {
533
+ this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', 'Invalid concatenation expression', 'A concatenation expression must begin with a string literal.', 'Start the expression with a quoted string, for example: "value: " + 123.');
534
+ return makeScalarValue('Undefined', undefined, 'Invalid concatenation start already reported');
535
+ }
536
+ operands.push(this.parseStringToken(firstStringToken.getText(), ctx));
537
+ for (const tail of ctx.concat_tail_list()) {
538
+ const operand = this.visitConcat_tail(tail);
539
+ operands.push(operand);
540
+ }
541
+ if (operands.some((operand) => !operand || operand.type === 'Undefined')) {
542
+ return makeScalarValue('Undefined', undefined, 'Invalid concatenation operand already reported');
543
+ }
544
+ if (this.isStrict) {
545
+ const firstNonString = operands.find((operand) => operand.type !== 'String');
546
+ if (firstNonString) {
547
+ this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', 'Invalid strict-mode concatenation expression', 'In strict mode, all concatenation operands must be string literals.', 'Convert non-string operands to explicit string literals.');
548
+ return makeScalarValue('Undefined', undefined, 'Invalid strict concatenation already reported');
613
549
  }
614
- catch (err) {
615
- const msg = err instanceof Error ? err.message : String(err);
616
- let msgWhat = 'Parse error in string';
617
- let msgWhy = msg;
618
- let msgHint = '';
619
- if (err instanceof parseString_1.CYiniStringParseError) {
620
- if (/Invalid escape sequence/i.test(msg)) {
621
- msgWhat = 'Invalid escape sequence in string';
622
- msgHint =
623
- 'Use double backslashes (\\\\) in C-strings, or use a raw string if escapes are not needed.';
624
- }
625
- else if (/end of string/i.test(msg)) {
626
- msgWhat = 'Incomplete escape sequence in string';
627
- msgHint =
628
- 'Check that all escape sequences in the C-string are complete and valid.';
629
- }
630
- }
631
- // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
632
- this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', msgWhat, msgWhy, msgHint);
633
- return makeScalarValue('Undefined', undefined, 'Invalid string literal already reported');
550
+ }
551
+ const result = operands
552
+ .map((operand) => this.stringifyConcatOperand(operand))
553
+ .join('');
554
+ return makeScalarValue('String', result, 'Concatenated string');
555
+ };
556
+ /**
557
+ * Visit a parse tree produced by `YiniParser.concat_tail`.
558
+ * @param ctx the parse tree
559
+ * @return the visitor result
560
+ */
561
+ this.visitConcat_tail = (ctx) => {
562
+ (0, print_1.debugPrint)('-> Entered visitConcat_tail(..)');
563
+ const operandCtx = ctx.concat_operand();
564
+ if (!operandCtx) {
565
+ this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', 'Missing concatenation operand', 'Expected a value after the + operator.', 'Add a string literal, number, boolean, or null after +.');
566
+ return makeScalarValue('Undefined', undefined, 'Missing concatenation operand already reported');
567
+ }
568
+ return this.visitConcat_operand(operandCtx);
569
+ };
570
+ /**
571
+ * Visit a parse tree produced by `YiniParser.concat_operand`.
572
+ *
573
+ * @note Must use token accessors, not literal visitors.
574
+ * Use STRING() instead of string_literal(), etc.
575
+ *
576
+ * @param ctx the parse tree
577
+ * @return the visitor result
578
+ */
579
+ this.visitConcat_operand = (ctx) => {
580
+ (0, print_1.debugPrint)('-> Entered visitConcat_operand(..)');
581
+ if (ctx.STRING()) {
582
+ return this.parseStringToken(ctx.STRING().getText(), ctx);
583
+ }
584
+ if (ctx.NUMBER()) {
585
+ const parsedNum = (0, parseNumber_1.default)(ctx.NUMBER().getText());
586
+ if (parsedNum?.value !== 0 &&
587
+ (!parsedNum?.value ||
588
+ (0, number_1.isNaNValue)(parsedNum.value) ||
589
+ (0, number_1.isInfinityValue)(parsedNum.value))) {
590
+ return makeScalarValue('Undefined', undefined, parsedNum?.tag ?? 'Invalid number literal');
634
591
  }
592
+ return makeScalarValue('Number', parsedNum.value, parsedNum.tag);
593
+ }
594
+ if (ctx.BOOLEAN_TRUE() || ctx.BOOLEAN_FALSE()) {
595
+ return makeScalarValue('Boolean', (0, parseBoolean_1.default)(ctx.getText()));
635
596
  }
636
- return makeScalarValue('String', text);
597
+ if (ctx.NULL()) {
598
+ return makeScalarValue('Null', null, 'Explicit Null');
599
+ }
600
+ this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', 'Invalid concatenation operand', `Got '${ctx.getText()}', but expected a string literal, number literal, boolean literal, or null literal.`, 'Lists and inline objects cannot be used as concatenation operands.');
601
+ return makeScalarValue('Undefined', undefined, 'Invalid concatenation operand already reported');
602
+ };
603
+ /*
604
+ * Visit a parse tree produced by `YiniParser.visitString_literal`.
605
+ * @note Should parse exactly one string literal. No concatenation logic here.
606
+ *
607
+ * @param ctx the parse tree
608
+ * @return the visitor result
609
+ */
610
+ this.visitString_literal = (ctx) => {
611
+ // const rawText = ctx.getText()
612
+ // const parsed = this.extractStringKindAndValue(rawText)
613
+ // try {
614
+ // const value = parseStringLiteral(parsed)
615
+ // return makeScalarValue('String', value)
616
+ // } catch (err: unknown) {
617
+ // const msg = err instanceof Error ? err.message : String(err)
618
+ // let msgWhat = 'Parse error in string'
619
+ // let msgWhy = msg
620
+ // let msgHint = ''
621
+ // if (err instanceof CYiniStringParseError) {
622
+ // if (/Invalid escape sequence/i.test(msg)) {
623
+ // msgWhat = 'Invalid escape sequence in string'
624
+ // msgHint =
625
+ // 'Use double backslashes (\\\\) in C-strings, or use a raw string if escapes are not needed.'
626
+ // } else if (/end of string/i.test(msg)) {
627
+ // msgWhat = 'Incomplete escape sequence in string'
628
+ // msgHint =
629
+ // 'Check that all escape sequences in the C-string are complete and valid.'
630
+ // }
631
+ // }
632
+ // this.errorHandler!.pushOrBail(
633
+ // toErrorLocation(ctx),
634
+ // 'Syntax-Error',
635
+ // msgWhat,
636
+ // msgWhy,
637
+ // msgHint,
638
+ // )
639
+ // return makeScalarValue(
640
+ // 'Undefined',
641
+ // undefined,
642
+ // 'Invalid string literal already reported',
643
+ // )
644
+ // }
645
+ // }
646
+ return this.parseStringToken(ctx.STRING().getText(), ctx);
637
647
  };
638
648
  /**
639
649
  * Visit a parse tree produced by `YiniParser.number_literal`.
@@ -712,8 +722,7 @@ class ASTBuilder extends YiniParserVisitor_1.default {
712
722
  // visitList_literal?: (ctx: List_literalContext) => Result
713
723
  this.visitList_literal = (ctx) => {
714
724
  (0, print_1.debugPrint)('-> Entered visitList_literal(..)');
715
- // '[' elements? ']' ; empty_list handled by lexer (Spec section, 10.1). :contentReference[oaicite:14]{index=14}
716
- const elems = this.visitElements(ctx.elements());
725
+ const elems = ctx.elements() ? this.visitElements(ctx.elements()) : [];
717
726
  const value = makeListValue(elems, 'From bracketed list');
718
727
  (0, print_1.debugPrint)('<- About to exit visitList_literal(..)...');
719
728
  if ((0, env_1.isDebug)()) {
@@ -731,12 +740,14 @@ class ASTBuilder extends YiniParserVisitor_1.default {
731
740
  this.visitElements = (ctx) => {
732
741
  (0, print_1.debugPrint)('-> Entered visitElements(..)');
733
742
  (0, print_1.debugPrint)(' elements.length = ' + ctx?.value_list().length);
743
+ if (this.isStrict && this.hasTrailingComma(ctx)) {
744
+ this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', 'Trailing comma is not allowed in strict mode', 'A list literal ended with a trailing comma.', 'Remove the final comma, or parse the document in lenient mode.');
745
+ }
734
746
  const elems = !ctx?.value_list()
735
747
  ? []
736
748
  : ctx.value_list().map((elem) => {
737
749
  const valueNode = this.visitValue(elem);
738
750
  if (!valueNode) {
739
- // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
740
751
  this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', 'Invalid list element', `Invalid list element: '${elem?.getText()}'`, `Expected a valid value/literal (string, number, boolean, null, list, or object). Optionally with a single leading minus sign '-'.`);
741
752
  }
742
753
  return valueNode;
@@ -756,10 +767,6 @@ class ASTBuilder extends YiniParserVisitor_1.default {
756
767
  */
757
768
  this.visitObject_literal = (ctx) => {
758
769
  (0, print_1.debugPrint)('-> Entered visitObject_literal(..)');
759
- // debugPrint('entries.EMPTY_OBJECT = ' + ctx?.EMPTY_OBJECT())
760
- // debugPrint('entries.length = ' + ctx?.object_members())
761
- // printObject(ctx)
762
- // const entries = this.visitObject_members(ctx?.object_members())
763
770
  const entries = ctx.object_members()
764
771
  ? this.visitObject_members(ctx.object_members())
765
772
  : {};
@@ -776,114 +783,127 @@ class ASTBuilder extends YiniParserVisitor_1.default {
776
783
  * @param ctx the parse tree
777
784
  * @grammarRule object_member (COMMA NL* object_member)* COMMA?
778
785
  * @return the visitor result
786
+ *
787
+ * @note
788
+ * SPEC says:
789
+ * - Lenient: first wins + warning
790
+ * - Strict: error
791
+ *
792
+ * NEVER silently overwrite.
779
793
  */
780
794
  this.visitObject_members = (ctx) => {
781
795
  (0, print_1.debugPrint)('-> Entered visitObject_members(..)');
782
- (0, print_1.debugPrint)('entries.length = ' + ctx?.object_member_list().length);
783
- // const entries: Array<{ k: string; v: TValueLiteral }> = []
796
+ if (this.isStrict && this.hasTrailingComma(ctx)) {
797
+ this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', 'Trailing comma is not allowed in strict mode', 'An inline object literal ended with a trailing comma.', 'Remove the final comma, or parse the document in lenient mode.');
798
+ }
784
799
  const entries = {};
785
800
  ctx.object_member_list().forEach((member) => {
786
801
  const { key, value } = this.visitObject_member(member);
787
- (0, print_1.debugPrint)(' key = ' + key);
802
+ if (!value || value.type === 'Undefined') {
803
+ (0, print_1.debugPrint)('Skip inserting Undefined');
804
+ return;
805
+ }
806
+ if (Object.prototype.hasOwnProperty.call(entries, key)) {
807
+ this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(member), this.isStrict ? 'Syntax-Error' : 'Syntax-Warning', 'Duplicate inline object member', `Object member '${key}' is already defined in this inline object.`, this.isStrict
808
+ ? 'Inline object member keys must be unique in strict mode.'
809
+ : 'The first object member value is kept; later duplicates are ignored.');
810
+ return;
811
+ }
788
812
  entries[key] = value;
789
813
  });
790
- (0, print_1.debugPrint)('<- About to exit visitObject_members(..)');
791
814
  return entries;
792
815
  };
793
816
  /**
794
817
  * Visit a parse tree produced by `YiniParser.object_member`.
795
818
  * @param ctx the parse tree
796
- * @grammarRule KEY WS? COLON NL* value
797
- * @return the visitor result
819
+ * @grammarRule KEY object_member_separator NL* value
820
+ * @return the object member key and value
798
821
  */
799
- // visitObject_member?: (ctx: Object_memberContext) => Result
800
822
  this.visitObject_member = (ctx) => {
801
823
  (0, print_1.debugPrint)('-> Entered visitObject_member(..)');
802
824
  const rawKey = ctx.KEY().getText();
803
825
  const key = (0, string_1.trimBackticks)(rawKey);
804
- const rawValue = ctx.value().getText();
805
- const valueNode = ctx.value()
806
- ? this.visitValue(ctx.value())
807
- : makeScalarValue('Null', null, 'Implicit Null');
826
+ const separator = this.visitObject_member_separator(ctx.object_member_separator());
827
+ if (separator === '=' && this.isStrict) {
828
+ this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx.object_member_separator()), 'Syntax-Error', "Invalid inline object member separator '=' in strict mode", "Inside inline objects, members must use ':' in strict mode.", `Use '${key}: <value>' instead of '${key} = <value>'.`);
829
+ return {
830
+ key,
831
+ value: makeScalarValue('Undefined', undefined, 'Invalid object member separator already reported'),
832
+ separator,
833
+ };
834
+ }
835
+ const valueCtx = ctx.value();
836
+ const rawValue = valueCtx?.getText() ?? '';
837
+ const valueNode = valueCtx
838
+ ? this.visitValue(valueCtx)
839
+ : makeScalarValue('Undefined', undefined, 'Missing object member value');
808
840
  (0, print_1.debugPrint)(' rawKey = ' + rawKey);
809
841
  (0, print_1.debugPrint)(' key = ' + key);
842
+ (0, print_1.debugPrint)('separator = ' + separator);
810
843
  (0, print_1.debugPrint)('rawValue = ' + rawValue);
811
844
  if (!valueNode) {
812
- // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
813
845
  this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', 'Invalid object entry', `Invalid object entry for key '${key}'.`, `Got '${rawValue}', but expected a valid value/literal (string, number, boolean, null, list, or object). Optionally with a single leading minus sign '-'.`);
814
846
  }
815
847
  (0, print_1.debugPrint)('<- About to exit visitObject_member(..)');
816
848
  if ((0, env_1.isDebug)()) {
817
849
  console.log('Returning:');
818
- (0, print_1.printObject)({ key, value: valueNode });
850
+ (0, print_1.printObject)({ key, value: valueNode, separator });
819
851
  }
820
- return { key, value: valueNode };
852
+ return { key, value: valueNode, separator };
821
853
  };
822
854
  /**
823
- * @note Colon list not supported any more since YINI Spec Package v1.0.0.rc4
855
+ * Visit a parse tree produced by `YiniParser.object_member_separator`.
856
+ * @param ctx the parse tree
857
+ * @grammarRule COLON | EQ
858
+ * @return ':' or '='
824
859
  */
825
- // visitColon_list_decl = (ctx: Colon_list_declContext): any => {
826
- // debugPrint('-> Entered visitColon_list_decl(..)')
827
- // const key = ctx.getChild(0).getText()
828
- // debugPrint(`visitColon_list_decl(..): key = '${key}'`)
829
- // const elems = this.visitElements(ctx.elements())
830
- // const value = makeListValue(elems, 'From colon-list')
831
- // const current = this.sectionStack[this.sectionStack.length - 1]
832
- // // putMember(current, key, list, this.ast, this.onDuplicateKey)
833
- // this.putMember(
834
- // this.errorHandler!,
835
- // ctx,
836
- // current,
837
- // key,
838
- // value,
839
- // // this.ast,
840
- // this.onDuplicateKey,
841
- // )
842
- // debugPrint('<- About to exit visitColon_list_decl(..)...')
843
- // if (isDebug()) {
844
- // console.log('List literal: (from a Colon-list)')
845
- // printObject(value)
846
- // }
847
- // return value
848
- // }
860
+ this.visitObject_member_separator = (ctx) => {
861
+ const sep = ctx.getText();
862
+ if (sep === ':' || sep === '=') {
863
+ return sep;
864
+ }
865
+ this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', 'Invalid inline object member separator', `Got '${sep}', but expected ':' or '='.`, "Use ':' for canonical inline object members. In lenient mode only, '=' may also be accepted.");
866
+ return ':';
867
+ };
849
868
  /**
850
- * Visit a parse tree produced by `YiniParser.string_concat`.
851
- * @param ctx the parse tree
852
- * @return the visitor result
869
+ *
870
+ * @note
871
+ * @yini strict + active lenient => error
872
+ * @yini lenient + active strict => warning
873
+ * @yini strict + active strict => ok
874
+ * @yini lenient + active lenient => ok
875
+ * @yini + any mode => ok
853
876
  */
854
- /*
855
- visitString_concat = (ctx: String_concatContext): any => {
856
- const rawText = ctx.STRING().getText() // The token text.
857
- const parsedInput = this.extractStringKindAndValue(rawText)
858
- // return parseStringLiteral(parsedInput)
859
-
860
- let txt = ''
861
- try {
862
- txt = parseStringLiteral(parsedInput)
863
- } catch (err) {
864
- const msg = '' + (<any>err)?.message
865
- this.errorHandler!.pushOrBail(
866
- toErrorLocation(ctx),
867
- 'Syntax-Error',
868
- 'Parse error in string',
869
- `${msg}`,
870
- )
877
+ this.visitYini_directive = (ctx) => {
878
+ (0, print_1.debugPrint)('-> Entered visitYini_directive(..)');
879
+ const modeText = this.visitYini_mode_declaration(ctx.yini_mode_declaration?.());
880
+ const displayText = modeText ? `@yini ${modeText}` : '@yini';
881
+ if (this.ast.yiniMarkerSeen) {
882
+ this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), this.isStrict ? 'Syntax-Error' : 'Syntax-Warning', 'Duplicate YINI marker in document', `'${displayText}' appears after an earlier @yini marker. Only one YINI marker should be used.`);
871
883
  }
872
- }
873
- */
874
- //@ todo (?) Check that this function actually works, not sure this function is finished.
875
- this.visitString_concat = (ctx) => {
876
- const rawText = ctx.STRING().getText();
877
- const parsedInput = this.extractStringKindAndValue(rawText);
878
- try {
879
- return (0, parseString_1.default)(parsedInput);
884
+ this.ast.yiniMarkerSeen = true;
885
+ this.meta_hasYiniMarker = true;
886
+ if (!modeText) {
887
+ return null;
880
888
  }
881
- catch (err) {
882
- const msg = err instanceof Error ? err.message : String(err);
883
- // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
884
- this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', 'Parse error in string', msg);
885
- return undefined;
889
+ const activeMode = this.isStrict ? 'strict' : 'lenient';
890
+ if (modeText !== 'strict' && modeText !== 'lenient') {
891
+ this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx.yini_mode_declaration?.() ?? ctx), 'Syntax-Error', 'Invalid @yini mode declaration', `Got '${modeText}', but expected 'strict' or 'lenient'.`, "Use '@yini strict', '@yini lenient', or just '@yini'.");
892
+ return null;
893
+ }
894
+ if (modeText === 'strict' && activeMode === 'lenient') {
895
+ this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', 'YINI mode declaration does not match active parser mode', 'Document declares strict mode but parser is running in lenient mode.', "Parse this document in strict mode, or change/remove the '@yini strict' declaration.");
896
+ return null;
886
897
  }
898
+ if (modeText === 'lenient' && activeMode === 'strict') {
899
+ this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Warning', 'YINI mode declaration does not match active parser mode', 'Document declares lenient mode but parser is running in strict mode.', 'The document remains valid if it satisfies strict-mode rules, but the declared parsing intent is lenient.');
900
+ }
901
+ return null;
902
+ };
903
+ this.visitYini_mode_declaration = (ctx) => {
904
+ if (!ctx)
905
+ return undefined;
906
+ return ctx.getText().trim().toLowerCase();
887
907
  };
888
908
  /**
889
909
  * Visit a parse tree produced by `YiniParser.bad_member`.
@@ -916,14 +936,10 @@ class ASTBuilder extends YiniParserVisitor_1.default {
916
936
  this.options = options;
917
937
  this.errorHandler = errorHandler;
918
938
  this.isStrict = options?.rules?.initialMode === 'strict';
919
- this.onDuplicateKey = options?.rules?.onDuplicateKey ?? 'error'; // Different setting depending on mode.
920
- // if (options.isStrict) {
921
- // this.onDuplicateKey = 'error'
922
- // } else {
923
- // this.onDuplicateKey = 'warn'
924
- // }
939
+ this.onDuplicateKey =
940
+ options?.rules?.onDuplicateKey ??
941
+ (this.isStrict ? 'error' : 'warn-and-keep-first');
925
942
  const root = makeSection('(root)', 0);
926
- // this.mapSectionNamePaths.set('(root)', 0)
927
943
  this.ast = {
928
944
  root,
929
945
  isStrict: this.isStrict,
@@ -954,69 +970,21 @@ class ASTBuilder extends YiniParserVisitor_1.default {
954
970
  this.errorHandler.pushOrBail(undefined, 'Syntax-Error', 'Strict mode requires exactly one explicit top-level section.', `Found ${numTopLevelSections} explicit top-level section${numTopLevelSections === 1 ? '' : 's'}.`, 'Wrap the document in exactly one explicit top-level section and nest any additional sections beneath it.');
955
971
  }
956
972
  }
957
- extractStringParts(tokenText) {
958
- // Detect prefix
959
- let prefix = '';
960
- let rest = tokenText;
961
- const prefixMatch = tokenText.match(/^(C|c|H|h|R|r)/);
962
- if (prefixMatch) {
963
- prefix = prefixMatch[1].toUpperCase();
964
- rest = tokenText.slice(1);
965
- }
966
- // Triple quoted
967
- if (rest.startsWith('"""')) {
968
- const inner = rest.slice(3, -3);
969
- if (prefix === 'C') {
970
- return { strKind: 'triple-classic', value: inner };
971
- }
972
- return { strKind: 'triple-raw', value: inner };
973
- }
974
- // Single quoted or double quoted
975
- const quote = rest[0];
976
- const inner = rest.slice(1, -1);
977
- switch (prefix) {
978
- case 'C':
979
- return { strKind: 'classic', value: inner };
980
- case 'H':
981
- return { strKind: 'hyper', value: inner };
982
- case 'R':
983
- case '':
984
- return { strKind: 'raw', value: inner };
985
- default:
986
- return { strKind: 'raw', value: inner };
987
- }
988
- }
989
973
  extractStringKindAndValue(raw) {
990
- const triple = raw.startsWith('C"""') ||
991
- raw.startsWith('c"""') ||
992
- raw.startsWith('R"""') ||
993
- raw.startsWith('r"""') ||
994
- raw.startsWith('"""');
995
- let prefix = '';
996
- let value = '';
997
- let strKind;
998
- if (/^[Cc]/.test(raw))
999
- prefix = 'C';
1000
- else if (/^[Hh]/.test(raw))
1001
- prefix = 'H';
1002
- else if (/^[Rr]/.test(raw))
1003
- prefix = 'R';
1004
- if (raw.startsWith('"""') ||
1005
- raw.startsWith('C"""') ||
1006
- raw.startsWith('c"""')) {
1007
- value = raw.replace(/^[CRcr]?"""/, '').replace(/"""$/, '');
1008
- strKind = prefix === 'C' ? 'triple-classic' : 'triple-raw';
974
+ const hasClassicPrefix = /^[Cc]/.test(raw);
975
+ const hasRawPrefix = /^[Rr]/.test(raw);
976
+ const hasPrefix = hasClassicPrefix || hasRawPrefix;
977
+ const body = hasPrefix ? raw.slice(1) : raw;
978
+ if (body.startsWith('"""')) {
979
+ return {
980
+ strKind: hasClassicPrefix ? 'triple-classic' : 'triple-raw',
981
+ value: body.slice(3, -3),
982
+ };
1009
983
  }
1010
- else {
1011
- value = raw.replace(/^[CHRchr]?['"]/, '').replace(/['"]$/, '');
1012
- if (prefix === 'C')
1013
- strKind = 'classic';
1014
- else if (prefix === 'H')
1015
- strKind = 'hyper';
1016
- else
1017
- strKind = 'raw';
1018
- }
1019
- return { strKind, value };
984
+ return {
985
+ strKind: hasClassicPrefix ? 'classic' : 'raw',
986
+ value: body.slice(1, -1),
987
+ };
1020
988
  }
1021
989
  /** Attach a section to the stack respecting up/down moves (Spec 5.3). :contentReference[oaicite:7]{index=7} */
1022
990
  attachSection(ctx, stack, section, ast) {
@@ -1025,7 +993,7 @@ class ASTBuilder extends YiniParserVisitor_1.default {
1025
993
  if (targetLevel <= 0) {
1026
994
  // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
1027
995
  this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Warning', `Invalid section level: ${targetLevel}`);
1028
- return;
996
+ return false;
1029
997
  }
1030
998
  // ------------------------------
1031
999
  // Construct section name path.
@@ -1041,12 +1009,15 @@ class ASTBuilder extends YiniParserVisitor_1.default {
1041
1009
  (0, print_1.debugPrint)('section full path: keyPath = ' + keyPath);
1042
1010
  // ------------------------------
1043
1011
  if (this.hasDefinedSectionTitle(keyPath)) {
1044
- // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
1045
- this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', 'Duplicate section name', `Section name: '${sectionName}' at level ${targetLevel} is already defined and cannot be redefined.`);
1012
+ this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), this.isStrict ? 'Syntax-Error' : 'Syntax-Warning', 'Duplicate section name', `Section name: '${sectionName}' at level ${targetLevel} is already defined and cannot be redefined.`, this.isStrict
1013
+ ? 'Duplicate sections are not allowed in strict mode.'
1014
+ : 'The first section definition is kept; later duplicate sections are ignored.');
1015
+ this.ignoredSectionLevel = targetLevel;
1016
+ return false;
1046
1017
  }
1047
1018
  else {
1048
1019
  if (section.members === undefined) {
1049
- (0, print_1.debugPrint)('This sReslult does not hold any valid members (=undefined)');
1020
+ (0, print_1.debugPrint)('This section result does not hold any valid members (=undefined)');
1050
1021
  }
1051
1022
  else {
1052
1023
  // this.existingSectionTitles.set(key, true)
@@ -1066,6 +1037,20 @@ class ASTBuilder extends YiniParserVisitor_1.default {
1066
1037
  if (targetLevel > this.meta_maxLevel) {
1067
1038
  this.meta_maxLevel = targetLevel;
1068
1039
  }
1040
+ return true;
1041
+ }
1042
+ isIgnoringDuplicateSection() {
1043
+ return this.ignoredSectionLevel !== null;
1044
+ }
1045
+ shouldSkipSectionAtLevel(sectionLevel) {
1046
+ if (this.ignoredSectionLevel === null) {
1047
+ return false;
1048
+ }
1049
+ if (sectionLevel > this.ignoredSectionLevel) {
1050
+ return true;
1051
+ }
1052
+ this.ignoredSectionLevel = null;
1053
+ return false;
1069
1054
  }
1070
1055
  /** Insert a key/value into current section (duplicate handling per options). */
1071
1056
  putMember(errorHandler, ctx, sec, key, value, mode = 'warn-and-keep-first') {
@@ -1097,6 +1082,53 @@ class ASTBuilder extends YiniParserVisitor_1.default {
1097
1082
  }
1098
1083
  sec.members.set(key, value);
1099
1084
  }
1085
+ stringifyConcatOperand(value) {
1086
+ switch (value.type) {
1087
+ case 'String':
1088
+ return value.value;
1089
+ case 'Number':
1090
+ return String(value.value);
1091
+ case 'Boolean':
1092
+ return value.value ? 'true' : 'false';
1093
+ case 'Null':
1094
+ return 'null';
1095
+ default:
1096
+ return '';
1097
+ }
1098
+ }
1099
+ parseStringToken(tokenText, ctx) {
1100
+ const parsed = this.extractStringKindAndValue(tokenText);
1101
+ try {
1102
+ const value = (0, parseString_1.default)(parsed);
1103
+ return makeScalarValue('String', value);
1104
+ }
1105
+ catch (err) {
1106
+ const msg = err instanceof Error ? err.message : String(err);
1107
+ let msgWhat = 'Parse error in string';
1108
+ let msgWhy = msg;
1109
+ let msgHint = '';
1110
+ if (err instanceof parseString_1.CYiniStringParseError) {
1111
+ if (/Invalid escape sequence/i.test(msg)) {
1112
+ msgWhat = 'Invalid escape sequence in string';
1113
+ msgHint =
1114
+ 'Use double backslashes (\\\\) in C-strings, or use a raw string if escapes are not needed.';
1115
+ }
1116
+ else if (/end of string/i.test(msg)) {
1117
+ msgWhat = 'Incomplete escape sequence in string';
1118
+ msgHint =
1119
+ 'Check that all escape sequences in the C-string are complete and valid.';
1120
+ }
1121
+ }
1122
+ this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', msgWhat, msgWhy, msgHint);
1123
+ return makeScalarValue('Undefined', undefined, 'Invalid string literal already reported');
1124
+ }
1125
+ }
1126
+ hasTrailingComma(ctx) {
1127
+ const childCount = ctx.getChildCount?.() ?? 0;
1128
+ if (childCount <= 0)
1129
+ return false;
1130
+ return ctx.getChild(childCount - 1)?.getText?.() === ',';
1131
+ }
1100
1132
  // --------------------------------
1101
1133
  // Public entry
1102
1134
  buildAST(ctx) {