yini-parser
Version:
Node.js parser for YINI — a clean, structured INI alternative with types, simple section nesting, comments, and strict mode.
142 lines (140 loc) • 5.9 kB
JavaScript
;
/**
* This file contains specific YINI helper functions (utils).
* @note More general helper functions should go into the dir "src/utils/".
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.isValidBacktickedIdent = exports.isValidSimpleIdent = exports.stripCommentsAndAfter = exports.isMarkerCharacter = void 0;
const print_1 = require("./utils/print");
const string_1 = require("./utils/string");
const SECTION_MARKER1 = '^';
const SECTION_MARKER2 = '<';
const SECTION_MARKER3 = '\u00A7'; // Section sign §.
const SECTION_MARKER4 = '\u20AC'; // Euro sign €.
/**
* Check if the character is a section marker character.
* @param character A character in a string.
* @note The string must be of length 1.
* @throws Will throw if not exactly of length 1.
*/
const isMarkerCharacter = (character) => {
if (character.length !== 1) {
throw Error('Argument into function isMarkerCharacter(..) is not of length 1');
}
const ch = character;
if (ch === SECTION_MARKER1 ||
ch === SECTION_MARKER2 ||
ch === SECTION_MARKER3 ||
ch === SECTION_MARKER4) {
return true;
}
return false;
};
exports.isMarkerCharacter = isMarkerCharacter;
/**
* @returns Returns the beginning up to (but not including) any comments
* starting with //, #, ; or --.
* @throws Will throw if consisting more than 1 lines.
*/
const stripCommentsAndAfter = (line) => {
if ((0, string_1.splitLines)(line).length > 1) {
throw new Error('Internal error: Detected several row lines in line: >>>' +
line +
'<<<');
}
let idx1 = line.indexOf('//');
let idx2 = line.indexOf('# '); // NOTE: (!) Hash comments requires a WS after the hash!
let idx3 = line.indexOf('#\t'); // NOTE: (!) Hash comments requires a WS after the hash!
let idx4 = line.indexOf(';');
let idx5 = line.indexOf('--');
if (idx1 < 0)
idx1 = Number.MAX_SAFE_INTEGER;
if (idx2 < 0)
idx2 = Number.MAX_SAFE_INTEGER;
if (idx3 < 0)
idx3 = Number.MAX_SAFE_INTEGER;
if (idx4 < 0)
idx4 = Number.MAX_SAFE_INTEGER;
if (idx5 < 0)
idx5 = Number.MAX_SAFE_INTEGER;
// debugPrint('stripCommentsAndAfter(..): idx1 = ' + idx1)
// debugPrint('stripCommentsAndAfter(..): idx2 = ' + idx2)
// debugPrint('stripCommentsAndAfter(..): idx3 = ' + idx3)
// debugPrint('stripCommentsAndAfter(..): idx4 = ' + idx4)
// debugPrint('stripCommentsAndAfter(..): idx5 = ' + idx5)
const idx = Math.min(idx1, idx2, idx3, idx4, idx5);
const resultLine = idx === Number.MAX_SAFE_INTEGER ? line : line.substring(0, idx);
(0, print_1.debugPrint)('stripCommentsAndAfter(..), line: >>>' + line + '<<<');
(0, print_1.debugPrint)('stripCommentsAndAfter(..), resultLine: >>>' + resultLine + '<<<');
return resultLine;
};
exports.stripCommentsAndAfter = stripCommentsAndAfter;
/**
* Checks if a string conforms to the identifier rules for section headers and
* member key names as defined by the YINI specification, following
* the ANTLR4 lexer rule:
* IDENT: ('a'..'z' | 'A'..'Z' | '_') ('a'..'z' | 'A'..'Z' | '0'..'9' | '_')*
* @throws Will only throw if blank string.
*
* @satisfies Should satisfy YINI spec 7, chapter: 3.4. Identifiers, Form 1:
* Identifier of Simple Form.
* @link https://github.com/YINI-lang/YINI-spec/blob/develop/YINI-Specification.md#34-identifiers
*/
const isValidSimpleIdent = (str) => {
if (!str.trim()) {
throw Error('Internal error: isValidSimpleIdent(..) received an empty string.');
}
// Regex: ^[a-zA-Z_][a-zA-Z0-9_]*$
return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(str);
};
exports.isValidSimpleIdent = isValidSimpleIdent;
/**
* Checks if a string is a valid backticked phrase/identifier:
* - Wrapped in backticks.
* - No raw tabs, newlines, or control characters (U+0000–U+001F), except as
* escaped sequences (e.g., \n).
* - May contain ordinary spaces.
* @note Empty is allowed: ``, as in spec (due to conform with the JSON empty key "").
* @throws Will only throw if missing enclosed backtick(s).
*
* @satisfies Should satisfy YINI spec 7, chapter: 3.4. Identifiers, Form 2:
* Backticked Identifier.
* @link https://github.com/YINI-lang/YINI-spec/blob/develop/YINI-Specification.md#34-identifiers
*/
const isValidBacktickedIdent = (str) => {
// if (str.length >= 2 && str.startsWith('`') && str.endsWith('`')) {
// // OK, and will let possible raw newlines and tabs pass so they
// // are checked further down.
// } else {
if (!(0, string_1.isEnclosedInBackticks)(str)) {
// NOTE: Only missing backtick(s) should throw error!
throw Error('Internal error: isValidBacktickedIdent(..) is missing backtick(s) "`".');
}
// Get the contents inside backticks.
const content = str.slice(1, -1);
// Allowed escapes: \n, \r, \t, \\, \`
const allowedEscapes = ['n', 'r', 't', '\\', '`'];
for (let i = 0; i < content.length; i++) {
const ch = content[i];
const code = content.charCodeAt(i);
if (ch === '\\') {
// If this is an escape, check next char.
i++;
if (i >= content.length)
return false; // Trailing backslash is not valid.
const nextCh = content[i];
if (!allowedEscapes.includes(nextCh))
return false;
continue;
}
// Allow space (U+0020), but not other control chars (U+0000–U+001F).
if (code < 0x20 && code !== 0x20)
return false;
// Disallow raw newlines, tabs, carriage returns, and backtick.
if (ch === '\n' || ch === '\r' || ch === '\t' || ch === '`')
// Raw newlines and tabs will yield false.
return false;
}
return true;
};
exports.isValidBacktickedIdent = isValidBacktickedIdent;