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 +8 -0
- package/LICENSE +1 -1
- package/README.md +16 -0
- package/dist/YINI.js +3 -0
- package/dist/core/astBuilder.js +3 -3
- package/dist/core/errorDataHandler.d.ts +23 -0
- package/dist/core/errorDataHandler.js +90 -20
- package/dist/core/pipeline/pipeline.js +0 -2
- package/dist/core/runtime.js +1 -15
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -1
- package/dist/parsers/parseString.js +4 -2
- package/dist/parsers/validateShebangPlacement.d.ts +1 -0
- package/dist/parsers/validateShebangPlacement.js +65 -2
- package/examples/basic.yini +1 -0
- package/examples/parse-example.ts +1 -0
- package/package.json +1 -1
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
|
|
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
|
}
|
package/dist/core/astBuilder.js
CHANGED
|
@@ -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),
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 &&
|
package/dist/core/runtime.js
CHANGED
|
@@ -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
|
-
|
|
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
|
package/examples/basic.yini
CHANGED
|
@@ -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.
|
|
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",
|