yini-parser 1.6.0 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 1.6.1 - 2026 June
4
+ - **Fixed:** Aligned parser behavior with the external `yini-test` conformance suite for shebang-like comment lines, misplaced `@yini` directives, and triple-quoted string line endings.
5
+ - **Fixed:** `#!` lines after an opening `@yini` marker are now treated as comment trivia, while `@yini` directives after document content are reported as syntax errors in both lenient and strict mode.
6
+ - **Fixed:** Triple-quoted raw and Classic strings now normalize physical `CRLF`/`CR` line endings to `LF`, producing stable output across platforms.
7
+ - **Fixed:** Diagnostic line numbers no longer shift by one after a valid shebang line; strict-mode trailing-comma errors now point to the actual physical source line.
8
+ - **Fixed:** Removed the outdated warning `Warning: Strict initialMode is not yet fully implemented.` Strict mode is now implemented against the latest YINI Specification RC 6.
9
+ - **Improved:** Parse errors now use a concise `YiniParseError`, and missing `/END` messages clearly explain that `/END` is required in strict mode but optional in the default lenient mode.
10
+
3
11
  ## 1.6.0 - 2026 May
4
12
  - **Improved:** Reduced the published npm package contents by excluding development-only build output, internal tool output, and duplicate `dist/src` declaration files from the package tarball.
5
13
  - **Added:** Implemented a `yini-test` adapter (`tools/yini-test-adapter.ts`) for testing this parser against an external conformance corpus. The `yini-test` corpus/test runner is planned to be made public and released separately in the future.
package/LICENSE CHANGED
@@ -186,7 +186,7 @@
186
186
  same "printed page" as the copyright notice for easier
187
187
  identification within third-party archives.
188
188
 
189
- Copyright 2024 Gothenburg, Marko K. S. (Sweden via Finland).
189
+ Copyright 2026 Marko K. Seppänen
190
190
 
191
191
  Licensed under the Apache License, Version 2.0 (the "License");
192
192
  you may not use this file except in compliance with the License.
package/README.md CHANGED
@@ -71,6 +71,20 @@ console.log(config.App.name) // My App
71
71
  console.log(config.App.Features.caching) // true
72
72
  ```
73
73
 
74
+ ### Modes and `/END`
75
+
76
+ `yini-parser` runs in lenient mode by default. In lenient mode, the document terminator `/END` is optional.
77
+
78
+ Strict mode requires `/END` unless you explicitly override `requireDocTerminator`:
79
+
80
+ ```ts
81
+ const config = YINI.parseFile('./config.yini', {
82
+ strictMode: true,
83
+ })
84
+ ```
85
+
86
+ If strict mode reports a missing `/END`, either add `/END` as the final significant line or parse with the default lenient mode.
87
+
74
88
  See the [YINI specification and documentation](https://yini-lang.org/refs/specification?utm_source=github&utm_medium=referral&utm_campaign=yini_parser_ts&utm_content=readme).
75
89
 
76
90
  ---
@@ -298,6 +312,8 @@ When reporting parser behavior, it is helpful to include:
298
312
 
299
313
  This parser is covered by smoke, integration, and regression tests across lenient, strict, and metadata-enabled modes.
300
314
 
315
+ It has also been run against `yini-test-suite` v0.3.0, the external [YINI conformance test suite](https://github.com/YINI-lang/yini-test-suite), with all TypeScript parser cases passing.
316
+
301
317
  ---
302
318
 
303
319
  ## Links
package/dist/YINI.js CHANGED
@@ -137,6 +137,9 @@ class YINI {
137
137
  text.includes('active parser mode')) {
138
138
  return 'YINI_MODE_MISMATCH';
139
139
  }
140
+ if (text.includes('directive') && text.includes('wrong place')) {
141
+ return 'misplaced-directive';
142
+ }
140
143
  if (text.includes('invalid escape sequence')) {
141
144
  return 'invalid-escape-sequence';
142
145
  }
@@ -289,7 +289,7 @@ class ASTBuilder extends YiniParserVisitor_1.default {
289
289
  (0, print_1.debugPrint)('-> Entered visitDirective(..)');
290
290
  const rawText = (0, yiniHelpers_1.stripCommentsAndAfter)(ctx.getText().trim());
291
291
  if (this.mapSectionNamePaths.size || this._numOfMembers) {
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.');
292
+ this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', 'Found a directive statement in the wrong place', `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.');
293
293
  }
294
294
  const yiniDirective = ctx.yini_directive?.();
295
295
  if (yiniDirective) {
@@ -1142,12 +1142,12 @@ class ASTBuilder extends YiniParserVisitor_1.default {
1142
1142
  const terminatorPolicy = this.options.rules.requireDocTerminator;
1143
1143
  if (isMissingTerminator && terminatorPolicy === 'required') {
1144
1144
  // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
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'.");
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, parse in lenient mode, or change requireDocTerminator to 'optional' or 'warn-if-missing'.");
1146
1146
  }
1147
1147
  else if (isMissingTerminator &&
1148
1148
  terminatorPolicy === 'warn-if-missing') {
1149
1149
  // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
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'.");
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, parse in lenient mode, or change requireDocTerminator to 'optional'.");
1151
1151
  }
1152
1152
  this.ast.numOfSections = this.mapSectionNamePaths.size;
1153
1153
  this.ast.numOfMembers = this._numOfMembers;
@@ -1,6 +1,28 @@
1
1
  import { IssuePayload } from '../types';
2
2
  import { IErrorLocationInput, TBailSensitivityLevel, TIssueType, TSubjectType } from './internalTypes';
3
3
  export declare const toErrorLocation: (ctx: any) => IErrorLocationInput | undefined;
4
+ export interface YiniParseErrorDetails {
5
+ type: TIssueType;
6
+ typeKey: string;
7
+ sourceType: TSubjectType;
8
+ fileName?: string;
9
+ line?: number;
10
+ column?: number;
11
+ message: string;
12
+ advice?: string;
13
+ hint?: string;
14
+ }
15
+ export declare class YiniParseError extends Error {
16
+ readonly type: TIssueType;
17
+ readonly typeKey: string;
18
+ readonly sourceType: TSubjectType;
19
+ readonly fileName?: string;
20
+ readonly line?: number;
21
+ readonly column?: number;
22
+ readonly advice?: string;
23
+ readonly hint?: string;
24
+ constructor(details: YiniParseErrorDetails);
25
+ }
4
26
  /**
5
27
  * This class handles all error/notice reporting and processes exit/throwing.
6
28
  */
@@ -36,6 +58,7 @@ export declare class ErrorDataHandler {
36
58
  isSilent?: boolean, // Suppress all output (even errors, exit code only).
37
59
  isThrowOnError?: boolean);
38
60
  private makeIssue;
61
+ private throwParseError;
39
62
  /**
40
63
  * After pushing processing may continue or exit, depending on the error
41
64
  * and/or the bail threshold (that can be optionally set by the user).
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ErrorDataHandler = exports.toErrorLocation = void 0;
3
+ exports.ErrorDataHandler = exports.YiniParseError = exports.toErrorLocation = void 0;
4
4
  // src/core/errorDataHandler.ts
5
5
  const env_1 = require("../config/env");
6
6
  const print_1 = require("../utils/print");
@@ -29,6 +29,66 @@ const issueTitle = [
29
29
  'Notice:',
30
30
  'Info:',
31
31
  ];
32
+ class YiniParseError extends Error {
33
+ constructor(details) {
34
+ super(formatThrownMessage(details));
35
+ Object.setPrototypeOf(this, new.target.prototype);
36
+ this.name = getErrorName(details.type);
37
+ this.type = details.type;
38
+ this.typeKey = details.typeKey;
39
+ this.sourceType = details.sourceType;
40
+ this.fileName = details.fileName;
41
+ this.line = details.line;
42
+ this.column = details.column;
43
+ this.advice = details.advice;
44
+ this.hint = details.hint;
45
+ // Node prints `error.stack` for uncaught errors. Keep parser failures
46
+ // focused on the user's document instead of internal parser frames.
47
+ this.stack = `${this.name}: ${this.message}`;
48
+ }
49
+ }
50
+ exports.YiniParseError = YiniParseError;
51
+ const getErrorName = (type) => {
52
+ switch (type) {
53
+ case 'Syntax-Error':
54
+ case 'Syntax-Warning':
55
+ return 'YiniSyntaxError';
56
+ case 'Internal-Error':
57
+ case 'Fatal-Error':
58
+ return 'YiniInternalError';
59
+ default:
60
+ return 'YiniParseError';
61
+ }
62
+ };
63
+ const formatThrownMessage = (details) => {
64
+ const lines = [];
65
+ const location = formatThrownLocation(details);
66
+ const title = details.type.replace(/-/g, ' ');
67
+ lines.push(location
68
+ ? `${title} in ${location}: ${details.message}`
69
+ : `${title}: ${details.message}`);
70
+ if (details.advice) {
71
+ lines.push(details.advice);
72
+ }
73
+ if (details.hint) {
74
+ lines.push(`Hint: ${details.hint}`);
75
+ }
76
+ return lines.join('\n');
77
+ };
78
+ const formatThrownLocation = (details) => {
79
+ if (details.sourceType === 'None/Ignore')
80
+ return '';
81
+ let location = details.sourceType === 'File'
82
+ ? details.fileName || 'YINI file'
83
+ : 'inline YINI content';
84
+ if (details.line !== undefined) {
85
+ location += `:${details.line}`;
86
+ if (details.column !== undefined) {
87
+ location += `:${details.column}`;
88
+ }
89
+ }
90
+ return location;
91
+ };
32
92
  /**
33
93
  * This class handles all error/notice reporting and processes exit/throwing.
34
94
  */
@@ -77,6 +137,19 @@ class ErrorDataHandler {
77
137
  (0, env_1.isDebug)() && console.log(issue);
78
138
  return issue;
79
139
  }
140
+ throwParseError(type, issue, msgWhatInclLineNum) {
141
+ throw new YiniParseError({
142
+ type,
143
+ typeKey: issue.typeKey,
144
+ sourceType: this.subjectType,
145
+ fileName: this.fileName,
146
+ line: issue.line,
147
+ column: issue.column,
148
+ message: msgWhatInclLineNum,
149
+ advice: issue.advice,
150
+ hint: issue.hint,
151
+ });
152
+ }
80
153
  /**
81
154
  * After pushing processing may continue or exit, depending on the error
82
155
  * and/or the bail threshold (that can be optionally set by the user).
@@ -126,7 +199,10 @@ class ErrorDataHandler {
126
199
  switch (type) {
127
200
  case 'Internal-Error':
128
201
  this.numInternalErrors++;
129
- this.errors.push(this.makeIssue(lineNum, colNum, type, msgWhat, msgWhy, msgHint));
202
+ {
203
+ const issue = this.makeIssue(lineNum, colNum, type, msgWhat, msgWhy, msgHint);
204
+ this.errors.push(issue);
205
+ }
130
206
  this.emitInternalError(loc, msgWhatInclLineNum, msgWhy, msgHint);
131
207
  if (this.persistThreshold === '1-Abort-on-Errors' ||
132
208
  this.persistThreshold === '2-Abort-Even-on-Warnings') {
@@ -134,16 +210,16 @@ class ErrorDataHandler {
134
210
  (0, print_1.debugPrint)('Skipped throwing');
135
211
  }
136
212
  else {
137
- const thrownMsg = msgWhy
138
- ? `Internal-Error: ${msgWhatInclLineNum}. ${msgWhy}`
139
- : `Internal-Error: ${msgWhatInclLineNum}`;
140
- throw new Error(thrownMsg);
213
+ this.throwParseError(type, this.errors[this.errors.length - 1], msgWhatInclLineNum);
141
214
  }
142
215
  }
143
216
  break;
144
217
  case 'Syntax-Error':
145
218
  this.numSyntaxErrors++;
146
- this.errors.push(this.makeIssue(lineNum, colNum, type, msgWhat, msgWhy, msgHint));
219
+ {
220
+ const issue = this.makeIssue(lineNum, colNum, type, msgWhat, msgWhy, msgHint);
221
+ this.errors.push(issue);
222
+ }
147
223
  this.emitSyntaxError(loc, msgWhatInclLineNum, msgWhy, msgHint);
148
224
  if (this.persistThreshold === '1-Abort-on-Errors' ||
149
225
  this.persistThreshold === '2-Abort-Even-on-Warnings') {
@@ -151,16 +227,16 @@ class ErrorDataHandler {
151
227
  (0, print_1.debugPrint)('Skipped throwing');
152
228
  }
153
229
  else {
154
- const thrownMsg = msgWhy
155
- ? `Syntax-Error: ${msgWhatInclLineNum}. ${msgWhy}`
156
- : `Syntax-Error: ${msgWhatInclLineNum}`;
157
- throw new Error(thrownMsg);
230
+ this.throwParseError(type, this.errors[this.errors.length - 1], msgWhatInclLineNum);
158
231
  }
159
232
  }
160
233
  break;
161
234
  case 'Syntax-Warning':
162
235
  this.numSyntaxWarnings++;
163
- this.warnings.push(this.makeIssue(lineNum, colNum, type, msgWhat, msgWhy, msgHint));
236
+ {
237
+ const issue = this.makeIssue(lineNum, colNum, type, msgWhat, msgWhy, msgHint);
238
+ this.warnings.push(issue);
239
+ }
164
240
  if (!this.isQuiet) {
165
241
  this.emitSyntaxWarning(loc, msgWhatInclLineNum, msgWhy, msgHint);
166
242
  }
@@ -169,10 +245,7 @@ class ErrorDataHandler {
169
245
  (0, print_1.debugPrint)('Skipped throwing');
170
246
  }
171
247
  else {
172
- const thrownMsg = msgWhy
173
- ? `Syntax-Error: ${msgWhatInclLineNum}. ${msgWhy}`
174
- : `Syntax-Error: ${msgWhatInclLineNum}`;
175
- throw new Error(thrownMsg);
248
+ this.throwParseError(type, this.warnings[this.warnings.length - 1], msgWhatInclLineNum);
176
249
  }
177
250
  }
178
251
  break;
@@ -200,10 +273,7 @@ class ErrorDataHandler {
200
273
  // In test, throw an error instead of bailing/exiting.
201
274
  // IMPORTANT: Never exit with exit code since this is a library!
202
275
  {
203
- const thrownMsg = msgWhy
204
- ? `Internal-Error: ${msgWhatInclLineNum}. ${msgWhy}`
205
- : `Internal-Error: ${msgWhatInclLineNum}`;
206
- throw new Error(thrownMsg);
276
+ this.throwParseError('Internal-Error', this.errors[this.errors.length - 1], msgWhatInclLineNum);
207
277
  }
208
278
  }
209
279
  }
@@ -196,8 +196,6 @@ const runPipeline = (yiniContent, coreOptions, runtimeInfo, _meta_userOpts) => {
196
196
  (0, env_1.isDebug)() && console.debug(finalJSResult);
197
197
  (0, print_1.debugPrint)();
198
198
  if (coreOptions.rules.initialMode === 'strict') {
199
- // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
200
- errorHandler.pushOrBail(undefined, 'Syntax-Warning', 'Warning: Strict initialMode is not yet fully implemented.', 'Some validation rules may still be missing or incomplete.');
201
199
  if (coreOptions.bailSensitivity === '0-Ignore-Errors') {
202
200
  if (coreOptions.isDiagnosticOutputEnabled &&
203
201
  !coreOptions.isQuiet &&
@@ -65,21 +65,6 @@ class YiniRuntime {
65
65
  runParse(yiniContent, arg2, // strictMode | options
66
66
  failLevel = 'auto', includeMetadata = false) {
67
67
  (0, print_1.debugPrint)('-> Entered runParse(..) in YiniRuntime class\n');
68
- // Handle optional UTF-8 BOM content of file.
69
- if (yiniContent.startsWith('\uFEFF')) {
70
- // (!) NOTE: slice(1) only because UTF-8 BOM appears as one single Unicode code characte, even though it is 3 bytes (EF BB BF) on disk.
71
- yiniContent = yiniContent.slice(1);
72
- (0, print_1.devPrint)('runParse(..): BOM was detected and stripped BOM in UTF-8 content');
73
- }
74
- // Handle optional shebang line (if line starts with "#!").
75
- if (yiniContent.startsWith('#!')) {
76
- const newlineIndex = yiniContent.indexOf('\n');
77
- (0, print_1.devPrint)('runParse(..): Shebang detected at first line, stripped line 1.');
78
- if (newlineIndex < 2) {
79
- throw new Error('Syntax-Error: Unexpected YINI input');
80
- }
81
- yiniContent = yiniContent.slice(newlineIndex + 1);
82
- }
83
68
  // Runtime guard to catch illegal/ambiguous calls coming from JS or any-cast code
84
69
  if ((0, optionsFunctions_1.isOptionsObjectForm)(arg2) &&
85
70
  (failLevel !== 'auto' || includeMetadata !== false)) {
@@ -111,6 +96,7 @@ class YiniRuntime {
111
96
  }
112
97
  const originalContent = yiniContent;
113
98
  yiniContent = (0, validateShebangPlacement_1.stripBomAndValidShebang)(yiniContent);
99
+ yiniContent = (0, validateShebangPlacement_1.normalizeShebangCommentLines)(yiniContent);
114
100
  if (originalContent.startsWith('\uFEFF')) {
115
101
  (0, print_1.devPrint)('runParse(..): BOM was detected and stripped from UTF-8 content.');
116
102
  }
package/dist/index.d.ts CHANGED
@@ -28,6 +28,8 @@ export declare const parseFile: typeof YINI.parseFile;
28
28
  export declare const parseForTooling: typeof YINI.parseForTooling;
29
29
  export declare const getTabSize: typeof YINI.getTabSize;
30
30
  export declare const setTabSize: typeof YINI.setTabSize;
31
+ export { YiniParseError } from './core/errorDataHandler';
32
+ export type { YiniParseErrorDetails } from './core/errorDataHandler';
31
33
  /**
32
34
  * Public type exports for the YINI parser.
33
35
  *
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  return (mod && mod.__esModule) ? mod : { "default": mod };
5
5
  };
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.setTabSize = exports.getTabSize = exports.parseForTooling = exports.parseFile = exports.parse = void 0;
7
+ exports.YiniParseError = exports.setTabSize = exports.getTabSize = exports.parseForTooling = exports.parseFile = exports.parse = void 0;
8
8
  /*
9
9
  This file is a pure barrel file.
10
10
 
@@ -50,4 +50,6 @@ exports.parseFile = YINI_1.default.parseFile;
50
50
  exports.parseForTooling = YINI_1.default.parseForTooling;
51
51
  exports.getTabSize = YINI_1.default.getTabSize;
52
52
  exports.setTabSize = YINI_1.default.setTabSize;
53
+ var errorDataHandler_1 = require("./core/errorDataHandler");
54
+ Object.defineProperty(exports, "YiniParseError", { enumerable: true, get: function () { return errorDataHandler_1.YiniParseError; } });
53
55
  //# sourceMappingURL=index.js.map
@@ -147,17 +147,19 @@ const parseClassicEscapes = (input, isAllowRealLineBreaks = false) => {
147
147
  }
148
148
  return result;
149
149
  };
150
+ const normalizeRealLineBreaks = (value) => value.replace(/\r\n?/g, '\n');
150
151
  const parseStringLiteral = ({ strKind, value }) => {
151
152
  switch (strKind) {
152
153
  case 'raw':
153
- case 'triple-raw':
154
154
  // Raw strings preserve content exactly as provided by the lexer.
155
155
  // Single-line raw string constraints are enforced by the lexer.
156
156
  return value;
157
+ case 'triple-raw':
158
+ return normalizeRealLineBreaks(value);
157
159
  case 'classic':
158
160
  return parseClassicEscapes(value, false);
159
161
  case 'triple-classic':
160
- return parseClassicEscapes(value, true);
162
+ return parseClassicEscapes(normalizeRealLineBreaks(value), true);
161
163
  default:
162
164
  throw new CYiniStringParseError(`Unknown string kind: ${strKind}`);
163
165
  }
@@ -1,3 +1,4 @@
1
1
  import { IPreflightIssue } from '../core/internalTypes';
2
2
  export declare const getShebangPlacementIssue: (input: string, strictMode: boolean) => IPreflightIssue | undefined;
3
+ export declare const normalizeShebangCommentLines: (input: string) => string;
3
4
  export declare const stripBomAndValidShebang: (input: string) => string;
@@ -1,10 +1,51 @@
1
1
  "use strict";
2
2
  // src/parsers/validateShebangPlacement.ts
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.stripBomAndValidShebang = exports.getShebangPlacementIssue = void 0;
4
+ exports.stripBomAndValidShebang = exports.normalizeShebangCommentLines = exports.getShebangPlacementIssue = void 0;
5
+ const isLineTriviaBeforeContent = (line) => {
6
+ const trimmed = line.trimStart();
7
+ return (trimmed === '' ||
8
+ trimmed.startsWith('//') ||
9
+ trimmed.startsWith(';') ||
10
+ trimmed.startsWith('--') ||
11
+ trimmed.startsWith('# ') ||
12
+ trimmed.startsWith('#\t'));
13
+ };
14
+ const isYiniDirectiveLine = (line) => /^\s*@yini(?:\s|$)/i.test(line);
15
+ const getShebangCommentLines = (input) => {
16
+ const text = input.startsWith('\uFEFF') ? input.slice(1) : input;
17
+ const lines = text.split(/\r?\n/);
18
+ const commentLines = new Set();
19
+ let seenYiniDirective = false;
20
+ let seenContent = false;
21
+ for (let index = 0; index < lines.length; index++) {
22
+ const line = lines[index];
23
+ if (index === 0 && line.startsWith('#!')) {
24
+ continue;
25
+ }
26
+ if (line.startsWith('#!')) {
27
+ if (seenYiniDirective && !seenContent) {
28
+ commentLines.add(index);
29
+ continue;
30
+ }
31
+ seenContent = true;
32
+ continue;
33
+ }
34
+ if (isLineTriviaBeforeContent(line)) {
35
+ continue;
36
+ }
37
+ if (!seenContent && isYiniDirectiveLine(line)) {
38
+ seenYiniDirective = true;
39
+ continue;
40
+ }
41
+ seenContent = true;
42
+ }
43
+ return commentLines;
44
+ };
5
45
  const getShebangPlacementIssue = (input, strictMode) => {
6
46
  const text = input.startsWith('\uFEFF') ? input.slice(1) : input;
7
47
  const lines = text.split(/\r?\n/);
48
+ const shebangCommentLines = getShebangCommentLines(input);
8
49
  for (let index = 0; index < lines.length; index++) {
9
50
  const line = lines[index];
10
51
  const trimmedStart = line.trimStart();
@@ -16,6 +57,9 @@ const getShebangPlacementIssue = (input, strictMode) => {
16
57
  if (isFirstLine && startsAtFirstColumn) {
17
58
  return undefined;
18
59
  }
60
+ if (shebangCommentLines.has(index)) {
61
+ continue;
62
+ }
19
63
  const message = 'Misplaced shebang-like sequence. A shebang is only recognized when #! is the first two non-BOM characters of the document.';
20
64
  const lineNumber = index + 1;
21
65
  const columnNumber = line.length - trimmedStart.length + 1;
@@ -34,6 +78,23 @@ const getShebangPlacementIssue = (input, strictMode) => {
34
78
  return undefined;
35
79
  };
36
80
  exports.getShebangPlacementIssue = getShebangPlacementIssue;
81
+ const normalizeShebangCommentLines = (input) => {
82
+ const shebangCommentLines = getShebangCommentLines(input);
83
+ if (shebangCommentLines.size === 0) {
84
+ return input;
85
+ }
86
+ const parts = input.split(/(\r\n|\n|\r)/);
87
+ const result = [];
88
+ let lineIndex = 0;
89
+ for (let index = 0; index < parts.length; index += 2) {
90
+ const line = parts[index] ?? '';
91
+ const eol = parts[index + 1] ?? '';
92
+ result.push(shebangCommentLines.has(lineIndex) ? line.replace(/^#!/, '//') : line, eol);
93
+ lineIndex++;
94
+ }
95
+ return result.join('');
96
+ };
97
+ exports.normalizeShebangCommentLines = normalizeShebangCommentLines;
37
98
  const stripBomAndValidShebang = (input) => {
38
99
  let text = input;
39
100
  if (text.startsWith('\uFEFF')) {
@@ -46,7 +107,9 @@ const stripBomAndValidShebang = (input) => {
46
107
  if (newlineIndex < 0) {
47
108
  return '';
48
109
  }
49
- return text.slice(newlineIndex + 1);
110
+ // Keep the newline so downstream parser locations still match the
111
+ // original source line numbers after ignoring the shebang content.
112
+ return text.slice(newlineIndex);
50
113
  };
51
114
  exports.stripBomAndValidShebang = stripBomAndValidShebang;
52
115
  //# sourceMappingURL=validateShebangPlacement.js.map
@@ -4,6 +4,7 @@
4
4
  title = 'My App'
5
5
  items = 10
6
6
  debug = ON
7
+ isDarkTheme = ON
7
8
 
8
9
  ^ Server
9
10
  host = "localhost"
@@ -11,6 +11,7 @@ import YINI from 'yini-parser'
11
11
  const configPath = path.resolve(__dirname, './basic.yini')
12
12
 
13
13
  // Parse the YINI config file.
14
+ // By default the parser runs in lenient mode, where '/END' is optional.
14
15
  const config = YINI.parseFile(configPath)
15
16
 
16
17
  // Output some example values.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yini-parser",
3
- "version": "1.6.0",
3
+ "version": "1.6.1",
4
4
  "description": "Official Node.js (TypeScript) parser for YINI, an INI-inspired, indentation-insensitive configuration format with clear nested sections and explicit structure.",
5
5
  "keywords": [
6
6
  "yini",