yini-parser 1.4.3 → 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 (90) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +187 -35
  3. package/dist/YINI.d.ts +29 -18
  4. package/dist/YINI.js +104 -3
  5. package/dist/core/astBuilder.d.ts +94 -18
  6. package/dist/core/astBuilder.js +439 -376
  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.d.ts +8 -4
  11. package/dist/core/objectBuilder.js +47 -62
  12. package/dist/core/options/defaultParserOptions.d.ts +3 -2
  13. package/dist/core/options/defaultParserOptions.js +11 -2
  14. package/dist/core/options/optionsFunctions.js +6 -4
  15. package/dist/core/parsingRules/modeFromRulesMatcher.d.ts +1 -1
  16. package/dist/core/parsingRules/modeFromRulesMatcher.js +22 -13
  17. package/dist/core/pipeline/pipeline.js +35 -10
  18. package/dist/core/runtime.js +28 -19
  19. package/dist/grammar/generated/YiniLexer.d.ts +40 -53
  20. package/dist/grammar/generated/YiniLexer.js +357 -356
  21. package/dist/grammar/generated/YiniParser.d.ts +174 -118
  22. package/dist/grammar/generated/YiniParser.js +1185 -929
  23. package/dist/grammar/generated/YiniParserVisitor.d.ts +82 -19
  24. package/dist/grammar/generated/YiniParserVisitor.js +1 -1
  25. package/dist/index.d.ts +2 -1
  26. package/dist/index.js +4 -3
  27. package/dist/parsers/extractHeaderParts.d.ts +12 -19
  28. package/dist/parsers/extractHeaderParts.js +57 -46
  29. package/dist/parsers/parseNumber.d.ts +24 -6
  30. package/dist/parsers/parseNumber.js +114 -49
  31. package/dist/parsers/parseSectionHeader.d.ts +11 -3
  32. package/dist/parsers/parseSectionHeader.js +55 -43
  33. package/dist/parsers/parseString.js +39 -20
  34. package/dist/parsers/validateShebangPlacement.d.ts +3 -0
  35. package/dist/parsers/validateShebangPlacement.js +52 -0
  36. package/dist/types/index.d.ts +20 -3
  37. package/dist/utils/print.d.ts +1 -0
  38. package/dist/utils/print.js +5 -1
  39. package/dist/utils/string.d.ts +1 -0
  40. package/dist/utils/string.js +17 -1
  41. package/dist/utils/system.d.ts +1 -0
  42. package/dist/utils/system.js +6 -1
  43. package/dist/utils/yiniHelpers.d.ts +44 -2
  44. package/dist/utils/yiniHelpers.js +134 -46
  45. package/examples/compare-formats.md +1 -1
  46. package/examples/nested.yini +1 -1
  47. package/package.json +11 -3
  48. package/dist/YINI.js.map +0 -1
  49. package/dist/config/env.js.map +0 -1
  50. package/dist/core/astBuilder.js.map +0 -1
  51. package/dist/core/errorDataHandler.js.map +0 -1
  52. package/dist/core/internalTypes.js.map +0 -1
  53. package/dist/core/objectBuilder.js.map +0 -1
  54. package/dist/core/options/defaultParserOptions.js.map +0 -1
  55. package/dist/core/options/failLevel.js.map +0 -1
  56. package/dist/core/options/optionsFunctions.js.map +0 -1
  57. package/dist/core/parsingRules/modeFromRulesMatcher.js.map +0 -1
  58. package/dist/core/parsingRules/rulesConstAndGuards.js.map +0 -1
  59. package/dist/core/pipeline/errorListeners.js.map +0 -1
  60. package/dist/core/pipeline/pipeline.js.map +0 -1
  61. package/dist/core/resultMetadataBuilder.js.map +0 -1
  62. package/dist/core/runtime.js.map +0 -1
  63. package/dist/dev/main.d.ts +0 -1
  64. package/dist/dev/main.js +0 -168
  65. package/dist/dev/main.js.map +0 -1
  66. package/dist/dev/quick-test-samples/defect-inputs.d.ts +0 -37
  67. package/dist/dev/quick-test-samples/defect-inputs.js +0 -106
  68. package/dist/dev/quick-test-samples/defect-inputs.js.map +0 -1
  69. package/dist/dev/quick-test-samples/valid-inputs.d.ts +0 -21
  70. package/dist/dev/quick-test-samples/valid-inputs.js +0 -422
  71. package/dist/dev/quick-test-samples/valid-inputs.js.map +0 -1
  72. package/dist/grammar/generated/YiniLexer.js.map +0 -1
  73. package/dist/grammar/generated/YiniParser.js.map +0 -1
  74. package/dist/grammar/generated/YiniParserVisitor.js.map +0 -1
  75. package/dist/index.js.map +0 -1
  76. package/dist/parsers/extractHeaderParts.js.map +0 -1
  77. package/dist/parsers/extractSignificantYiniLine.js.map +0 -1
  78. package/dist/parsers/parseBoolean.js.map +0 -1
  79. package/dist/parsers/parseNull.js.map +0 -1
  80. package/dist/parsers/parseNumber.js.map +0 -1
  81. package/dist/parsers/parseSectionHeader.js.map +0 -1
  82. package/dist/parsers/parseString.js.map +0 -1
  83. package/dist/types/index.js.map +0 -1
  84. package/dist/utils/number.js.map +0 -1
  85. package/dist/utils/object.js.map +0 -1
  86. package/dist/utils/pathAndFileName.js.map +0 -1
  87. package/dist/utils/print.js.map +0 -1
  88. package/dist/utils/string.js.map +0 -1
  89. package/dist/utils/system.js.map +0 -1
  90. package/dist/utils/yiniHelpers.js.map +0 -1
@@ -36,12 +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"));
47
+ const parseNull_1 = __importDefault(require("../parsers/parseNull"));
43
48
  const parseNumber_1 = __importDefault(require("../parsers/parseNumber"));
44
- // import parseNumber from '../parsers/parseNumber'
45
49
  const parseSectionHeader_1 = __importDefault(require("../parsers/parseSectionHeader"));
46
50
  const parseString_1 = __importStar(require("../parsers/parseString"));
47
51
  const number_1 = require("../utils/number");
@@ -71,6 +75,7 @@ const makeScalarValue = (type, value = null, tag = undefined) => {
71
75
  case 'Undefined':
72
76
  return { type: 'Undefined', value: undefined, tag };
73
77
  default:
78
+ // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
74
79
  new errorDataHandler_1.ErrorDataHandler(_sourceType).pushOrBail(undefined, 'Fatal-Error', `No such type in makeValue(..), type: ${type}, value: ${value}`, 'Something in the code is done incorrectly in order for this to happen... :S');
75
80
  }
76
81
  return { type: 'Null', value: null, tag };
@@ -91,54 +96,9 @@ const makeListValue = (elems = [], tag = undefined) => {
91
96
  const makeObjectValue = (entries = {}, tag = undefined) => {
92
97
  return { type: 'Object', entries, tag };
93
98
  };
94
- function trimQuotes(text) {
95
- // STRING token already excludes quotes; the rule returns the literal with quotes present.
96
- // We’ll reliably strip the outer quote(s) and leave contents as-is (concat pieces handled below).
97
- const q = text[0];
98
- if ((q === '"' || q === "'") &&
99
- text.length >= 2 &&
100
- text[text.length - 1] === q) {
101
- return text.slice(1, -1);
102
- }
103
- // Triple-quoted cases are handled by the lexer too; same stripping works since token text begins with quotes.
104
- if (text.startsWith('"""') && text.endsWith('"""') && text.length >= 6) {
105
- return text.slice(3, -3);
106
- }
107
- return text;
108
- }
109
99
  function makeSection(name, level) {
110
100
  return { sectionName: name, level, members: new Map(), children: [] };
111
101
  }
112
- /** Parse SECTION_HEAD token text → {level, name}.
113
- * Supports repeated markers (^^^^) and shorthand (^7) (Spec 5.2–5.3.1). :contentReference[oaicite:5]{index=5}:contentReference[oaicite:6]{index=6}
114
- */
115
- // function parseSectionHeadToken(raw: string): { level: number; name: string } {
116
- // // SECTION_HEAD token text includes: optional WS, marker(s) or shorthand, WS, IDENT (possibly backticked), NL+
117
- // // We only need the visible line content up to NL.
118
- // const line = raw.split(/\r?\n/)[0]
119
- // // Extract marker block and name
120
- // // Examples: "^^ Section", "^7 `Section name`", "< MySection"
121
- // const m = line.match(/^\s*([\^<§€]+|\^|\<|§|€)(\d+)?[ \t]+(.+?)\s*$/)
122
- // if (m) {
123
- // const markerRun = m[1]
124
- // const numeric = m[2]
125
- // let level: number
126
- // if (numeric) {
127
- // level = parseInt(numeric, 10)
128
- // } else {
129
- // // count repeated marker chars (^^^^)
130
- // level = markerRun.length
131
- // }
132
- // // Section name may be backticked: `Name with spaces`
133
- // let name = m[3]
134
- // if (name.startsWith('`') && name.endsWith('`')) {
135
- // name = name.slice(1, -1)
136
- // }
137
- // return { level, name }
138
- // }
139
- // // Fallback: be defensive
140
- // return { level: 1, name: line.trim() }
141
- // }
142
102
  // --- Builder Visitor -----------------------------------------------------
143
103
  /**
144
104
  * This interface defines a complete generic visitor for a parse tree produced
@@ -148,6 +108,7 @@ function makeSection(name, level) {
148
108
  * operations with no return type.
149
109
  */
150
110
  // export default class YINIVisitor<IResult> extends YiniParserVisitor<IResult> {
111
+ // export default class ASTBuilder<Result> extends YiniParserVisitor<Result> {
151
112
  class ASTBuilder extends YiniParserVisitor_1.default {
152
113
  /**
153
114
  * @param metaFileName If parsing from a file, provide the file name here so the meta information can be updated accordingly.
@@ -156,13 +117,13 @@ class ASTBuilder extends YiniParserVisitor_1.default {
156
117
  constructor(errorHandler, options, sourceType, metaFileName) {
157
118
  super();
158
119
  this.errorHandler = null;
120
+ this.ignoredSectionLevel = null;
159
121
  this.meta_hasYiniMarker = false; // For stats.
160
122
  // private meta_numOfSections = 0 // For stats.
161
123
  this._numOfMembers = 0; // For error checking and stats.
162
124
  // private meta_numOfChains = 0 // For stats.
163
125
  this.meta_maxLevel = 0; // For stats.
164
126
  this.mapSectionNamePaths = new Map();
165
- // --- Private helper methods --------------------------------
166
127
  this.hasDefinedSectionTitle = (keyPath) => {
167
128
  return this.mapSectionNamePaths?.has(keyPath);
168
129
  };
@@ -174,8 +135,6 @@ class ASTBuilder extends YiniParserVisitor_1.default {
174
135
  * @param ctx the parse tree
175
136
  * @return the visitor result
176
137
  */
177
- // visitYini?: (ctx: YiniContext) => Result
178
- // visitYini?: (ctx: YiniContext) => any
179
138
  this.visitYini = (ctx) => {
180
139
  // children: prolog?, stmt*, terminal?, EOF
181
140
  ctx.children?.forEach((c) => this.visit?.(c));
@@ -196,50 +155,87 @@ class ASTBuilder extends YiniParserVisitor_1.default {
196
155
  * @param ctx the parse tree
197
156
  * @return the visitor result
198
157
  */
199
- // visitTerminal_stmt?: (ctx: Terminal_stmtContext) => Result;
200
158
  this.visitTerminal_stmt = (ctx) => {
201
159
  (0, print_1.debugPrint)('-> Entered visitTerminal_stmt(..)');
202
160
  let rawText = ctx.getText().trim();
203
161
  (0, print_1.debugPrint)('rawText = "' + rawText + '"');
204
- // rawText = extractYiniLine(rawText) // Remove possible comments.
205
- // rawText = stripCommentsAndAfter(rawText.split('\n', 1)[0]).trim() // Remove possible comments.
206
162
  rawText = (0, yiniHelpers_1.stripCommentsAndAfter)(rawText); // Remove possible comments.
207
163
  (0, print_1.debugPrint)('rawText2 = "' + rawText + '"');
208
164
  if (rawText.toLowerCase() === '/end') {
209
165
  // NOTE: Below, maybe not reached at all.
210
166
  if (this.ast.terminatorSeen) {
211
167
  // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
212
- 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.`);
213
169
  }
214
170
  }
215
171
  else {
216
172
  // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
217
- 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).`);
218
174
  }
219
175
  this.ast.terminatorSeen = true;
220
176
  return null;
221
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
+ };
222
187
  /**
223
188
  * Visit a parse tree produced by `YiniParser.stmt`.
224
189
  * @param ctx the parse tree
225
- * @grammarRule eol | SECTION_HEAD | assignment | colon_list_decl | marker_stmt | bad_member
226
190
  * @return the visitor result
227
191
  */
228
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
+ */
229
215
  this.visitStmt = (ctx) => {
230
216
  const child = ctx.getChild(0);
231
217
  const ruleName = child?.constructor?.name ?? '';
218
+ // debugPrint('S0')
219
+ // const badHeaderWDotName = ctx.BAD_SECTION_HEAD_W_DOT_NAME()?.getText()
220
+ // if (badHeaderWDotName) {
221
+ // console.log('QQQQQQQQ = ' + badHeaderWDotName)
222
+ // }
232
223
  if (ruleName.includes('EolContext'))
233
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);
234
234
  if (ruleName.includes('AssignmentContext'))
235
235
  return this.visitAssignment?.(child);
236
- if (ruleName.includes('Colon_list_declContext'))
237
- return this.visitColon_list_decl?.(child);
238
236
  if (ruleName.includes('Meta_stmtContext'))
239
237
  return this.visitMeta_stmt?.(child);
240
238
  (0, print_1.debugPrint)('S1');
241
- // let headerAlt = child.getText?.() ?? ''
242
- // let header = ctx.SECTION_HEAD()?.getText().trim() || ''
243
239
  let header = ctx.SECTION_HEAD()?.getText().trim() || '';
244
240
  // debugPrint('S2, lineAlt: >>>' + lineAlt + '<<<')
245
241
  (0, print_1.debugPrint)('S2, header: >>>' + header + '<<<');
@@ -247,6 +243,9 @@ class ASTBuilder extends YiniParserVisitor_1.default {
247
243
  (0, print_1.debugPrint)('S3, header: >>>' + header + '<<<');
248
244
  if (!!header) {
249
245
  const { sectionName, sectionLevel } = (0, parseSectionHeader_1.default)(header, this.errorHandler, ctx);
246
+ if (this.shouldSkipSectionAtLevel(sectionLevel)) {
247
+ return null;
248
+ }
250
249
  // Validate level sequencing per spec 5.3 (no skipping upward)
251
250
  const currentLevel = this.sectionStack[this.sectionStack.length - 1].level;
252
251
  if (sectionLevel > currentLevel + 1) {
@@ -260,6 +259,10 @@ class ASTBuilder extends YiniParserVisitor_1.default {
260
259
  // bad_member fallback
261
260
  return this.visitBad_member?.(ctx.getChild(0));
262
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
+ };
263
266
  /**
264
267
  * Visit a parse tree produced by `YiniParser.meta_stmt`.
265
268
  * @param ctx the parse tree
@@ -284,31 +287,19 @@ class ASTBuilder extends YiniParserVisitor_1.default {
284
287
  */
285
288
  this.visitDirective = (ctx) => {
286
289
  (0, print_1.debugPrint)('-> Entered visitDirective(..)');
287
- let rawText = ctx.getText().trim();
288
- (0, print_1.debugPrint)('rawText = "' + rawText + '"');
289
- // NOTE: Important to strip any possible comments on the same line.
290
- rawText = (0, yiniHelpers_1.stripCommentsAndAfter)(rawText); // Remove possible comments.
291
- (0, print_1.debugPrint)('rawText2 = "' + rawText + '"');
290
+ const rawText = (0, yiniHelpers_1.stripCommentsAndAfter)(ctx.getText().trim());
292
291
  if (this.mapSectionNamePaths.size || this._numOfMembers) {
293
- // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
294
- 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.');
295
293
  }
296
- if (rawText.toLowerCase().startsWith('@include')) {
297
- // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
298
- this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Notice', `Detected unsupported directive '@include'`, `This directive is not currently supported by the parser.`);
299
- }
300
- else if (rawText.toLowerCase() === '@yini') {
301
- if (this.ast.yiniMarkerSeen) {
302
- // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
303
- 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').`);
304
- }
294
+ const yiniDirective = ctx.yini_directive?.();
295
+ if (yiniDirective) {
296
+ return this.visitYini_directive(yiniDirective);
305
297
  }
306
- else {
307
- // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
308
- 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;
309
301
  }
310
- // @yini marker is advisory (no semantic value) per spec. We ignore it. (Spec 2.4) :contentReference[oaicite:9]{index=9}
311
- 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.");
312
303
  return null;
313
304
  };
314
305
  /**
@@ -404,10 +395,6 @@ class ASTBuilder extends YiniParserVisitor_1.default {
404
395
  */
405
396
  let valueContext = ctx.value?.();
406
397
  let valueNode;
407
- const hasEquals = rawMemberText.includes('=');
408
- // const hasTextAfterEquals = hasEquals
409
- // ? rawMemberText.split('=').slice(1).join('=').trim().length > 0
410
- // : false
411
398
  const eqIndex = rawMemberText.indexOf('=');
412
399
  const hasTextAfterEquals = eqIndex >= 0 && rawMemberText.slice(eqIndex + 1).trim().length > 0;
413
400
  if (!valueContext || !rawValue) {
@@ -423,9 +410,11 @@ class ASTBuilder extends YiniParserVisitor_1.default {
423
410
  break;
424
411
  case 'allow-with-warning':
425
412
  valueNode = makeScalarValue('Null', null, 'Implicit null (empty value)');
413
+ // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
426
414
  this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Warning', `Empty value treated as null for key '${resultKey}'.`, `An empty value after '=' was encountered. Per 'treatEmptyValueAsNull = allow-with-warning', interpreted as null.`, `If you intended null, write it explicitly: ${resultKey} = null. Otherwise provide a non-empty value or set 'treatEmptyValueAsNull' to 'disallow'.`);
427
415
  break;
428
416
  case 'disallow':
417
+ // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
429
418
  this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', `Missing value for key '${resultKey}'`, `Expected a value after '=' but found none. Implicit nulls are disallowed by 'treatEmptyValueAsNull = disallow'.`, `Write 'null' explicitly (${resultKey} = null) if that is intended, or provide a concrete value.`);
430
419
  return makeScalarValue('Undefined', undefined, 'Missing value already reported');
431
420
  }
@@ -437,34 +426,15 @@ class ASTBuilder extends YiniParserVisitor_1.default {
437
426
  if ((0, env_1.isDebug)()) {
438
427
  (0, print_1.printObject)(valueNode);
439
428
  }
440
- /*
441
- if (!valueNode) {
442
- this.errorHandler!.pushOrBail(
443
- toErrorLocation(ctx),
444
- 'Syntax-Error',
445
- 'Invalid value',
446
- `Invalid value for key '${resultKey}' in member (<key> = <value> pair).`,
447
- `Got '${rawValue}', but expected a valid value/literal (string, number, boolean, null, list, or object). Optionally with a single leading minus sign '-'.`,
448
- )
449
- } else if (
450
- valueNode.type === 'Undefined' &&
451
- valueNode.tag !== 'Invalid string literal already reported'
452
- ) {
453
- this.errorHandler!.pushOrBail(
454
- toErrorLocation(ctx),
455
- 'Syntax-Error',
456
- 'Invalid value',
457
- `Invalid value for key '${resultKey}' in member (<key> = <value> pair).`,
458
- `Got '${rawValue}', but expected a valid value/literal (string, number, boolean, null, list, or object). Optionally with a single leading minus sign '-'.`,
459
- )
460
- }*/
461
429
  if (!valueNode) {
430
+ // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
462
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 '-'.`);
463
432
  }
464
433
  else if (valueNode.type === 'Undefined' &&
465
434
  valueNode.tag !== 'Invalid string literal already reported' &&
466
435
  valueNode.tag !== 'Missing value already reported' &&
467
436
  valueNode.tag !== 'Parser syntax error already reported') {
437
+ // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
468
438
  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 '-'.`);
469
439
  }
470
440
  // It keeps the sentinel tags useful for control flow inside visitMember(..),
@@ -484,26 +454,17 @@ class ASTBuilder extends YiniParserVisitor_1.default {
484
454
  * @param ctx the parse tree
485
455
  * @return the visitor result
486
456
  */
487
- // visitValue?: (ctx: ValueContext) => Result
488
457
  this.visitValue = (ctx) => {
489
458
  (0, print_1.debugPrint)('----------------------------');
490
459
  (0, print_1.debugPrint)('-> Entered visitValue(..)');
491
460
  let valueNode = undefined;
492
- if (ctx.null_literal()) {
493
- (0, print_1.debugPrint)(' visiting visitNull_literal(..)');
494
- valueNode = this.visitNull_literal(ctx.null_literal());
495
- }
496
- else if (ctx.string_literal()) {
497
- (0, print_1.debugPrint)(' visiting visitString_literal(..)');
498
- valueNode = this.visitString_literal(ctx.string_literal());
461
+ if (ctx.concat_expression()) {
462
+ (0, print_1.debugPrint)(' visiting visitConcat_expression(..)');
463
+ valueNode = this.visitConcat_expression(ctx.concat_expression());
499
464
  }
500
- else if (ctx.number_literal()) {
501
- (0, print_1.debugPrint)(' visiting visitNumber_literal(..)');
502
- valueNode = this.visitNumber_literal(ctx.number_literal());
503
- }
504
- else if (ctx.boolean_literal()) {
505
- (0, print_1.debugPrint)(' visiting visitBoolean_literal(..)');
506
- 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());
507
468
  }
508
469
  else if (ctx.list_literal()) {
509
470
  (0, print_1.debugPrint)(' visiting visitList_literal(..)');
@@ -525,108 +486,164 @@ class ASTBuilder extends YiniParserVisitor_1.default {
525
486
  return valueNode;
526
487
  };
527
488
  /**
528
- * Visit a parse tree produced by `YiniParser.string_literal`.
489
+ * Visit a parse tree produced by `YiniParser.scalar_value`.
529
490
  * @param ctx the parse tree
530
491
  * @return the visitor result
531
492
  */
532
- /*
533
- visitString_literal = (ctx: String_literalContext): any => {
534
- let text = ''
535
-
536
- const pieces = [
537
- ctx.STRING(),
538
- ...(ctx.string_concat_list()?.map((c) => c.STRING()) ?? []),
539
- ]
540
-
541
- try {
542
- for (const token of pieces) {
543
- const tokenText = token.getText()
544
- const parsed = this.extractStringKindAndValue(tokenText)
545
-
546
- try {
547
- text += parseStringLiteral(parsed)
548
- } catch (err: unknown) {
549
- const msg = '' + (<any>err)?.message
550
- this.errorHandler!.pushOrBail(
551
- toErrorLocation(ctx),
552
- 'Syntax-Error',
553
- 'Parse error in string',
554
- `${msg}`,
555
- )
556
- }
557
- }
558
-
559
- return makeScalarValue('String', text)
560
- } catch (err: unknown) {
561
- const msg = err instanceof Error ? err.message : String(err)
562
-
563
- let msgWhat = 'Invalid string literal'
564
- let msgWhy = msg
565
- let msgHint = ''
566
-
567
- if (err instanceof CYiniStringParseError) {
568
- msgWhat = 'Invalid escape sequence in string'
569
- msgWhy = msg
570
-
571
- if (/Invalid escape sequence \\\\/.test(msg)) {
572
- msgHint =
573
- 'Use double backslashes (\\\\) in C-strings, or use a raw string for file paths.'
574
- } else if (/end of string/i.test(msg)) {
575
- msgHint =
576
- 'Check that all escape sequences in the C-string are complete and valid.'
577
- }
578
- }
579
-
580
- this.errorHandler!.pushOrBail(
581
- toErrorLocation(ctx),
582
- 'Syntax-Error',
583
- msgWhat,
584
- msgWhy,
585
- msgHint,
586
- )
587
-
588
- return makeScalarValue(
589
- 'Undefined',
590
- undefined,
591
- 'Invalid string literal',
592
- )
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());
593
497
  }
594
- }
595
- */
596
- this.visitString_literal = (ctx) => {
597
- let text = '';
598
- const pieces = [
599
- ctx.STRING(),
600
- ...(ctx.string_concat_list()?.map((c) => c.STRING()) ?? []),
601
- ];
602
- for (const token of pieces) {
603
- const tokenText = token.getText();
604
- const parsed = this.extractStringKindAndValue(tokenText);
605
- try {
606
- 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');
607
549
  }
608
- catch (err) {
609
- const msg = err instanceof Error ? err.message : String(err);
610
- let msgWhat = 'Parse error in string';
611
- let msgWhy = msg;
612
- let msgHint = '';
613
- if (err instanceof parseString_1.CYiniStringParseError) {
614
- if (/Invalid escape sequence/i.test(msg)) {
615
- msgWhat = 'Invalid escape sequence in string';
616
- msgHint =
617
- 'Use double backslashes (\\\\) in C-strings, or use a raw string if escapes are not needed.';
618
- }
619
- else if (/end of string/i.test(msg)) {
620
- msgWhat = 'Incomplete escape sequence in string';
621
- msgHint =
622
- 'Check that all escape sequences in the C-string are complete and valid.';
623
- }
624
- }
625
- this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', msgWhat, msgWhy, msgHint);
626
- 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');
627
591
  }
592
+ return makeScalarValue('Number', parsedNum.value, parsedNum.tag);
628
593
  }
629
- return makeScalarValue('String', text);
594
+ if (ctx.BOOLEAN_TRUE() || ctx.BOOLEAN_FALSE()) {
595
+ return makeScalarValue('Boolean', (0, parseBoolean_1.default)(ctx.getText()));
596
+ }
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);
630
647
  };
631
648
  /**
632
649
  * Visit a parse tree produced by `YiniParser.number_literal`.
@@ -687,8 +704,14 @@ class ASTBuilder extends YiniParserVisitor_1.default {
687
704
  // visitNull_literal?: (ctx: Null_literalContext) => Result
688
705
  this.visitNull_literal = (ctx) => {
689
706
  (0, print_1.debugPrint)('-> Entered visitNull_literal(..)');
690
- (0, print_1.debugPrint)('raw = ' + ctx.getText());
691
- return makeScalarValue('Null', null, 'Explicit Null');
707
+ // debugPrint('raw = ' + ctx.getText())
708
+ // return makeScalarValue('Null', null, 'Explicit Null')
709
+ const raw = ctx.getText();
710
+ (0, print_1.debugPrint)('raw: "' + raw + '"');
711
+ const parsed = (0, parseNull_1.default)(raw);
712
+ (0, print_1.debugPrint)('parsed: "' + parsed + '"');
713
+ // Case-insensitive true/false/on/off/yes/no (Spec section, 8.1).
714
+ return makeScalarValue('Null', parsed, 'Explicit Null');
692
715
  };
693
716
  /**
694
717
  * Visit a parse tree produced by `YiniParser.list_literal`.
@@ -699,8 +722,7 @@ class ASTBuilder extends YiniParserVisitor_1.default {
699
722
  // visitList_literal?: (ctx: List_literalContext) => Result
700
723
  this.visitList_literal = (ctx) => {
701
724
  (0, print_1.debugPrint)('-> Entered visitList_literal(..)');
702
- // '[' elements? ']' ; empty_list handled by lexer (Spec section, 10.1). :contentReference[oaicite:14]{index=14}
703
- const elems = this.visitElements(ctx.elements());
725
+ const elems = ctx.elements() ? this.visitElements(ctx.elements()) : [];
704
726
  const value = makeListValue(elems, 'From bracketed list');
705
727
  (0, print_1.debugPrint)('<- About to exit visitList_literal(..)...');
706
728
  if ((0, env_1.isDebug)()) {
@@ -718,12 +740,14 @@ class ASTBuilder extends YiniParserVisitor_1.default {
718
740
  this.visitElements = (ctx) => {
719
741
  (0, print_1.debugPrint)('-> Entered visitElements(..)');
720
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
+ }
721
746
  const elems = !ctx?.value_list()
722
747
  ? []
723
748
  : ctx.value_list().map((elem) => {
724
749
  const valueNode = this.visitValue(elem);
725
750
  if (!valueNode) {
726
- // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
727
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 '-'.`);
728
752
  }
729
753
  return valueNode;
@@ -743,10 +767,6 @@ class ASTBuilder extends YiniParserVisitor_1.default {
743
767
  */
744
768
  this.visitObject_literal = (ctx) => {
745
769
  (0, print_1.debugPrint)('-> Entered visitObject_literal(..)');
746
- // debugPrint('entries.EMPTY_OBJECT = ' + ctx?.EMPTY_OBJECT())
747
- // debugPrint('entries.length = ' + ctx?.object_members())
748
- // printObject(ctx)
749
- // const entries = this.visitObject_members(ctx?.object_members())
750
770
  const entries = ctx.object_members()
751
771
  ? this.visitObject_members(ctx.object_members())
752
772
  : {};
@@ -763,110 +783,127 @@ class ASTBuilder extends YiniParserVisitor_1.default {
763
783
  * @param ctx the parse tree
764
784
  * @grammarRule object_member (COMMA NL* object_member)* COMMA?
765
785
  * @return the visitor result
786
+ *
787
+ * @note
788
+ * SPEC says:
789
+ * - Lenient: first wins + warning
790
+ * - Strict: error
791
+ *
792
+ * NEVER silently overwrite.
766
793
  */
767
794
  this.visitObject_members = (ctx) => {
768
795
  (0, print_1.debugPrint)('-> Entered visitObject_members(..)');
769
- (0, print_1.debugPrint)('entries.length = ' + ctx?.object_member_list().length);
770
- const entries = [];
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
+ }
799
+ const entries = {};
771
800
  ctx.object_member_list().forEach((member) => {
772
801
  const { key, value } = this.visitObject_member(member);
773
- (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
+ }
774
812
  entries[key] = value;
775
813
  });
776
- (0, print_1.debugPrint)('<- About to exit visitObject_members(..)');
777
814
  return entries;
778
815
  };
779
816
  /**
780
817
  * Visit a parse tree produced by `YiniParser.object_member`.
781
818
  * @param ctx the parse tree
782
- * @grammarRule KEY WS? COLON NL* value
783
- * @return the visitor result
819
+ * @grammarRule KEY object_member_separator NL* value
820
+ * @return the object member key and value
784
821
  */
785
- // visitObject_member?: (ctx: Object_memberContext) => Result
786
822
  this.visitObject_member = (ctx) => {
787
823
  (0, print_1.debugPrint)('-> Entered visitObject_member(..)');
788
824
  const rawKey = ctx.KEY().getText();
789
825
  const key = (0, string_1.trimBackticks)(rawKey);
790
- const rawValue = ctx.value().getText();
791
- const valueNode = ctx.value()
792
- ? this.visitValue(ctx.value())
793
- : makeScalarValue('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');
794
840
  (0, print_1.debugPrint)(' rawKey = ' + rawKey);
795
841
  (0, print_1.debugPrint)(' key = ' + key);
842
+ (0, print_1.debugPrint)('separator = ' + separator);
796
843
  (0, print_1.debugPrint)('rawValue = ' + rawValue);
797
844
  if (!valueNode) {
798
- // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
799
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 '-'.`);
800
846
  }
801
847
  (0, print_1.debugPrint)('<- About to exit visitObject_member(..)');
802
848
  if ((0, env_1.isDebug)()) {
803
849
  console.log('Returning:');
804
- (0, print_1.printObject)({ key, value: valueNode });
850
+ (0, print_1.printObject)({ key, value: valueNode, separator });
805
851
  }
806
- return { key, value: valueNode };
852
+ return { key, value: valueNode, separator };
807
853
  };
808
854
  /**
809
- * Visit a parse tree produced by `YiniParser.colon_list_decl`.
855
+ * Visit a parse tree produced by `YiniParser.object_member_separator`.
810
856
  * @param ctx the parse tree
811
- * @grammarRule KEY WS? COLON (eol | WS+)* elements (eol | WS+)* eol
812
- * @return the visitor result
857
+ * @grammarRule COLON | EQ
858
+ * @return ':' or '='
813
859
  */
814
- // visitColon_list_decl?: (ctx: ListAfterColonContext) => Result
815
- this.visitColon_list_decl = (ctx) => {
816
- (0, print_1.debugPrint)('-> Entered visitColon_list_decl(..)');
817
- const key = ctx.getChild(0).getText();
818
- (0, print_1.debugPrint)(`visitColon_list_decl(..): key = '${key}'`);
819
- const elems = this.visitElements(ctx.elements());
820
- const value = makeListValue(elems, 'From colon-list');
821
- const current = this.sectionStack[this.sectionStack.length - 1];
822
- // putMember(current, key, list, this.ast, this.onDuplicateKey)
823
- this.putMember(this.errorHandler, ctx, current, key, value,
824
- // this.ast,
825
- this.onDuplicateKey);
826
- (0, print_1.debugPrint)('<- About to exit visitColon_list_decl(..)...');
827
- if ((0, env_1.isDebug)()) {
828
- console.log('List literal: (from a Colon-list)');
829
- (0, print_1.printObject)(value);
860
+ this.visitObject_member_separator = (ctx) => {
861
+ const sep = ctx.getText();
862
+ if (sep === ':' || sep === '=') {
863
+ return sep;
830
864
  }
831
- return value;
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 ':';
832
867
  };
833
868
  /**
834
- * Visit a parse tree produced by `YiniParser.string_concat`.
835
- * @param ctx the parse tree
836
- * @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
837
876
  */
838
- /*
839
- visitString_concat = (ctx: String_concatContext): any => {
840
- const rawText = ctx.STRING().getText() // The token text.
841
- const parsedInput = this.extractStringKindAndValue(rawText)
842
- // return parseStringLiteral(parsedInput)
843
-
844
- let txt = ''
845
- try {
846
- txt = parseStringLiteral(parsedInput)
847
- } catch (err) {
848
- const msg = '' + (<any>err)?.message
849
- this.errorHandler!.pushOrBail(
850
- toErrorLocation(ctx),
851
- 'Syntax-Error',
852
- 'Parse error in string',
853
- `${msg}`,
854
- )
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.`);
855
883
  }
856
- }
857
- */
858
- //@ todo (?) Check that this function actually works, not sure this function is finished.
859
- this.visitString_concat = (ctx) => {
860
- const rawText = ctx.STRING().getText();
861
- const parsedInput = this.extractStringKindAndValue(rawText);
862
- try {
863
- return (0, parseString_1.default)(parsedInput);
884
+ this.ast.yiniMarkerSeen = true;
885
+ this.meta_hasYiniMarker = true;
886
+ if (!modeText) {
887
+ return null;
864
888
  }
865
- catch (err) {
866
- const msg = err instanceof Error ? err.message : String(err);
867
- this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', 'Parse error in string', msg);
868
- 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;
869
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();
870
907
  };
871
908
  /**
872
909
  * Visit a parse tree produced by `YiniParser.bad_member`.
@@ -899,14 +936,10 @@ class ASTBuilder extends YiniParserVisitor_1.default {
899
936
  this.options = options;
900
937
  this.errorHandler = errorHandler;
901
938
  this.isStrict = options?.rules?.initialMode === 'strict';
902
- this.onDuplicateKey = options?.rules?.onDuplicateKey ?? 'error'; // Different setting depending on mode.
903
- // if (options.isStrict) {
904
- // this.onDuplicateKey = 'error'
905
- // } else {
906
- // this.onDuplicateKey = 'warn'
907
- // }
939
+ this.onDuplicateKey =
940
+ options?.rules?.onDuplicateKey ??
941
+ (this.isStrict ? 'error' : 'warn-and-keep-first');
908
942
  const root = makeSection('(root)', 0);
909
- // this.mapSectionNamePaths.set('(root)', 0)
910
943
  this.ast = {
911
944
  root,
912
945
  isStrict: this.isStrict,
@@ -921,69 +954,37 @@ class ASTBuilder extends YiniParserVisitor_1.default {
921
954
  };
922
955
  this.sectionStack = [root];
923
956
  }
924
- extractStringParts(tokenText) {
925
- // Detect prefix
926
- let prefix = '';
927
- let rest = tokenText;
928
- const prefixMatch = tokenText.match(/^(C|c|H|h|R|r)/);
929
- if (prefixMatch) {
930
- prefix = prefixMatch[1].toUpperCase();
931
- rest = tokenText.slice(1);
932
- }
933
- // Triple quoted
934
- if (rest.startsWith('"""')) {
935
- const inner = rest.slice(3, -3);
936
- if (prefix === 'C') {
937
- return { strKind: 'triple-classic', value: inner };
938
- }
939
- return { strKind: 'triple-raw', value: inner };
957
+ // --- Private helper methods --------------------------------
958
+ validateStrictTopLevelStructure() {
959
+ if (!this.isStrict)
960
+ return;
961
+ const numTopLevelSections = this.ast.root.children.length;
962
+ const numTopLevelMembers = this.ast.root.members.size;
963
+ if (numTopLevelMembers > 0) {
964
+ // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
965
+ this.errorHandler.pushOrBail(undefined, 'Syntax-Error', 'Top-level members are not allowed in strict mode.', 'Members were found outside the single required explicit top-level section.', 'Move all top-level members into the explicit top-level section, or parse the document in lenient mode.');
940
966
  }
941
- // Single quoted or double quoted
942
- const quote = rest[0];
943
- const inner = rest.slice(1, -1);
944
- switch (prefix) {
945
- case 'C':
946
- return { strKind: 'classic', value: inner };
947
- case 'H':
948
- return { strKind: 'hyper', value: inner };
949
- case 'R':
950
- case '':
951
- return { strKind: 'raw', value: inner };
952
- default:
953
- return { strKind: 'raw', value: inner };
967
+ // Exactly one explicit top-level section in strict mode.
968
+ if (numTopLevelSections !== 1) {
969
+ // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
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.');
954
971
  }
955
972
  }
956
973
  extractStringKindAndValue(raw) {
957
- const triple = raw.startsWith('C"""') ||
958
- raw.startsWith('c"""') ||
959
- raw.startsWith('R"""') ||
960
- raw.startsWith('r"""') ||
961
- raw.startsWith('"""');
962
- let prefix = '';
963
- let value = '';
964
- let strKind;
965
- if (/^[Cc]/.test(raw))
966
- prefix = 'C';
967
- else if (/^[Hh]/.test(raw))
968
- prefix = 'H';
969
- else if (/^[Rr]/.test(raw))
970
- prefix = 'R';
971
- if (raw.startsWith('"""') ||
972
- raw.startsWith('C"""') ||
973
- raw.startsWith('c"""')) {
974
- value = raw.replace(/^[CRcr]?"""/, '').replace(/"""$/, '');
975
- strKind = prefix === 'C' ? 'triple-classic' : 'triple-raw';
976
- }
977
- else {
978
- value = raw.replace(/^[CHRchr]?['"]/, '').replace(/['"]$/, '');
979
- if (prefix === 'C')
980
- strKind = 'classic';
981
- else if (prefix === 'H')
982
- strKind = 'hyper';
983
- else
984
- strKind = '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
+ };
985
983
  }
986
- return { strKind, value };
984
+ return {
985
+ strKind: hasClassicPrefix ? 'classic' : 'raw',
986
+ value: body.slice(1, -1),
987
+ };
987
988
  }
988
989
  /** Attach a section to the stack respecting up/down moves (Spec 5.3). :contentReference[oaicite:7]{index=7} */
989
990
  attachSection(ctx, stack, section, ast) {
@@ -992,7 +993,7 @@ class ASTBuilder extends YiniParserVisitor_1.default {
992
993
  if (targetLevel <= 0) {
993
994
  // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
994
995
  this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Warning', `Invalid section level: ${targetLevel}`);
995
- return;
996
+ return false;
996
997
  }
997
998
  // ------------------------------
998
999
  // Construct section name path.
@@ -1008,12 +1009,15 @@ class ASTBuilder extends YiniParserVisitor_1.default {
1008
1009
  (0, print_1.debugPrint)('section full path: keyPath = ' + keyPath);
1009
1010
  // ------------------------------
1010
1011
  if (this.hasDefinedSectionTitle(keyPath)) {
1011
- // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
1012
- 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;
1013
1017
  }
1014
1018
  else {
1015
1019
  if (section.members === undefined) {
1016
- (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)');
1017
1021
  }
1018
1022
  else {
1019
1023
  // this.existingSectionTitles.set(key, true)
@@ -1033,6 +1037,20 @@ class ASTBuilder extends YiniParserVisitor_1.default {
1033
1037
  if (targetLevel > this.meta_maxLevel) {
1034
1038
  this.meta_maxLevel = targetLevel;
1035
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;
1036
1054
  }
1037
1055
  /** Insert a key/value into current section (duplicate handling per options). */
1038
1056
  putMember(errorHandler, ctx, sec, key, value, mode = 'warn-and-keep-first') {
@@ -1042,12 +1060,15 @@ class ASTBuilder extends YiniParserVisitor_1.default {
1042
1060
  if (sec.members.has(key)) {
1043
1061
  switch (mode) {
1044
1062
  case 'error':
1063
+ // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
1045
1064
  errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', 'Hit a duplicate key in this section and scope', `Key '${key}' already exists in section '${sec.sectionName}' on level ${sec.level}.`);
1046
1065
  break;
1047
1066
  case 'warn-and-keep-first':
1067
+ // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
1048
1068
  errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Warning', `Hit a duplicate key (will keep first value) in this section and scope`, `Key '${key}' already exists in section '${sec.sectionName}' on level ${sec.level}.`);
1049
1069
  return; // Keep first, don't overwrite.
1050
1070
  case 'warn-and-overwrite':
1071
+ // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
1051
1072
  errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Warning', `Overwrote a duplicate key (will keep last value) in this section and scope`, `Key '${key}' was overwritten in section '${sec.sectionName}' on level ${sec.level}.`);
1052
1073
  break; // Overwrite, replace value.
1053
1074
  case 'keep-first':
@@ -1061,34 +1082,76 @@ class ASTBuilder extends YiniParserVisitor_1.default {
1061
1082
  }
1062
1083
  sec.members.set(key, value);
1063
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
+ }
1064
1132
  // --------------------------------
1065
1133
  // Public entry
1066
1134
  buildAST(ctx) {
1067
1135
  this.visitYini?.(ctx);
1068
- // The document terminator is optional by default.
1069
- // If the option `isRequireDocTerminator` is set to true,
1070
- // the '/END' terminator at the end of the document becomes required.
1071
- if (!this.ast.terminatorSeen &&
1072
- this.options.rules.requireDocTerminator === 'required') {
1073
- const msgWhat = `Missing '/END' at end of document (option requireDocTerminator is ${this.options.rules.requireDocTerminator}).`;
1074
- const msgWhy = `The terminator '/END' (case insensitive) is required and must appear at the end of the document.`;
1075
- const msgHint = `This is option can be overriden by the option requireDocTerminator.`;
1136
+ // Strict-mode structural validation.
1137
+ this.validateStrictTopLevelStructure();
1138
+ const isMissingTerminator = !this.ast.terminatorSeen;
1139
+ // In strict mode, the document terminator is required by default.
1140
+ // However, the parse option `requireDocTerminator` is authoritative
1141
+ // and may override that default behavior.
1142
+ const terminatorPolicy = this.options.rules.requireDocTerminator;
1143
+ if (isMissingTerminator && terminatorPolicy === 'required') {
1076
1144
  // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
1077
- this.errorHandler.pushOrBail(undefined, 'Syntax-Error', msgWhat, msgWhy, msgHint);
1145
+ this.errorHandler.pushOrBail(undefined, 'Syntax-Error', "Missing '/END' at end of document.", "The document terminator '/END' (case-insensitive) is required at the end of the document.", "Add '/END' as the final significant line, or change requireDocTerminator to 'optional' or 'warn-if-missing'.");
1078
1146
  }
1079
- else if (!this.ast.terminatorSeen &&
1080
- this.options.rules.requireDocTerminator === 'warn-if-missing') {
1081
- const msgWhat = `Missing '/END' at end of document (option requireDocTerminator is ${this.options.rules.requireDocTerminator}).`;
1082
- const msgWhy = `The terminator '/END' (case insensitive) might be missing at the end of the document.`;
1083
- const msgHint = `This is option can be overriden by the option requireDocTerminator.`;
1147
+ else if (isMissingTerminator &&
1148
+ terminatorPolicy === 'warn-if-missing') {
1084
1149
  // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
1085
- this.errorHandler.pushOrBail(undefined, 'Syntax-Warning', msgWhat, msgWhy, msgHint);
1150
+ this.errorHandler.pushOrBail(undefined, 'Syntax-Warning', "Missing '/END' at end of document.", "The document terminator '/END' (case-insensitive) appears to be missing at the end of the document.", "Add '/END' as the final significant line, or change requireDocTerminator to 'optional'.");
1086
1151
  }
1087
- // Note: Below is important for error checking as well as for meta data.
1088
1152
  this.ast.numOfSections = this.mapSectionNamePaths.size;
1089
1153
  this.ast.numOfMembers = this._numOfMembers;
1090
1154
  if (this.options.isIncludeMeta) {
1091
- // Attach collected meta information.
1092
1155
  this.ast.maxDepth = this.meta_maxLevel;
1093
1156
  this.ast.sectionNamePaths = [...this.mapSectionNamePaths.keys()];
1094
1157
  }