123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336 |
- 'use strict';
- // match1 - section, match2 - optional full inheritance part, match3 - inherited section
- const REGEXP_SECTION = /^\s*\[\s*([^:]*?)\s*(:\s*(.+?)\s*)?\]\s*$/;
- const REGEXP_COMMENT = /^;.*/;
- const REGEXP_SINGLE_LINE = /^\s*(.*?)\s*?=\s*?(\S.*?)$/;
- const REGEXP_MULTI_LINE = /^\s*(.*?)\s*?=\s*?"(.*?)$/;
- const REGEXP_NOT_ESCAPED_MULTI_LINE_END = /^(.*?)\\"$/;
- const REGEXP_MULTI_LINE_END = /^(.*?)"$/;
- const REGEXP_ARRAY = /^(.*?)\[\]$/;
- const STATUS_OK = 0;
- const STATUS_INVALID = 1;
- const defaults = {
- ignore_invalid: true,
- keep_quotes: false,
- nested_section_names: false,
- keep_zero_prefix: false,
- oninvalid: () => true,
- filters: [],
- constants: {},
- };
- const REGEXP_IGNORE_KEYS = /__proto__|constructor|prototype/;
- class Parser {
- constructor(options = {}) {
- this.options = Object.assign({}, defaults, options);
- this.handlers = [
- this.handleMultiLineStart,
- this.handleMultiLineEnd,
- this.handleMultiLineAppend,
- this.handleComment,
- this.handleSection,
- this.handleSingleLine,
- ];
- }
- parse(lines) {
- const ctx = {
- ini: {},
- current: {},
- multiLineKeys: false,
- multiLineValue: '',
- };
- for (let line of lines) {
- for (let handler of this.handlers) {
- const stop = handler.call(this, ctx, line);
- if (stop) {
- break;
- }
- }
- }
- return ctx.ini;
- }
- isSection(line) {
- return line.match(REGEXP_SECTION);
- }
- getSection(line) {
- return line.match(REGEXP_SECTION)[1];
- }
- getParentSection(line) {
- return line.match(REGEXP_SECTION)[3];
- }
- isInheritedSection(line) {
- return !!line.match(REGEXP_SECTION)[3];
- }
- isComment(line) {
- return line.match(REGEXP_COMMENT);
- }
- isSingleLine(line) {
- const result = line.match(REGEXP_SINGLE_LINE);
- if (!result) {
- return false;
- }
- const check = result[2].match(/"/g);
- return !check || check.length % 2 === 0;
- }
- isMultiLine(line) {
- const result = line.match(REGEXP_MULTI_LINE);
- if (!result) {
- return false;
- }
- const check = result[2].match(/"/g);
- return !check || check.length % 2 === 0;
- }
- isMultiLineEnd(line) {
- return line.match(REGEXP_MULTI_LINE_END) && !line.match(REGEXP_NOT_ESCAPED_MULTI_LINE_END);
- }
- isArray(line) {
- return line.match(REGEXP_ARRAY);
- }
- assignValue(element, keys, value) {
- value = this.applyFilter(value);
- let current = element;
- let previous = element;
- let array = false;
- let key;
- if (keys.some((key) => REGEXP_IGNORE_KEYS.test(key))) {
- return;
- }
- for (key of keys) {
- if (this.isArray(key)) {
- key = this.getArrayKey(key);
- array = true;
- }
- if (current[key] == null) {
- current[key] = array ? [] : {};
- }
- previous = current;
- current = current[key];
- }
- if (array) {
- current.push(value);
- } else {
- previous[key] = value;
- }
- return element;
- }
- applyFilter(value) {
- for (let filter of this.options.filters) {
- value = filter(value, this.options);
- }
- return value;
- }
- getKeyValue(line) {
- const result = line.match(REGEXP_SINGLE_LINE);
- if (!result) {
- throw new Error();
- }
- let [, key, value] = result;
- if (!this.options.keep_quotes) {
- value = value.replace(/^\s*?"(.*?)"\s*?$/, '$1');
- }
- return { key, value, status: STATUS_OK };
- }
- getMultiKeyValue(line) {
- const result = line.match(REGEXP_MULTI_LINE);
- if (!result) {
- throw new Error();
- }
- let [, key, value] = result;
- if (this.options.keep_quotes) {
- value = '"' + value;
- }
- return { key, value };
- }
- getMultiLineEndValue(line) {
- const result = line.match(REGEXP_MULTI_LINE_END);
- if (!result) {
- throw new Error();
- }
- let [, value] = result;
- if (this.options.keep_quotes) {
- value = value + '"';
- }
- return { value, status: STATUS_OK };
- }
- getArrayKey(line) {
- const result = line.match(REGEXP_ARRAY);
- return result[1];
- }
- handleMultiLineStart(ctx, line) {
- if (!this.isMultiLine(line.trim())) {
- return false;
- }
- const { key, value } = this.getMultiKeyValue(line);
- const keys = key.split('.');
- ctx.multiLineKeys = keys;
- ctx.multiLineValue = value;
- return true;
- }
- handleMultiLineEnd(ctx, line) {
- if (!ctx.multiLineKeys || !this.isMultiLineEnd(line.trim())) {
- return false;
- }
- const { value, status } = this.getMultiLineEndValue(line);
- // abort on false of onerror callback if we meet an invalid line
- if (status === STATUS_INVALID && !this.options.oninvalid(line)) {
- return;
- }
- // ignore whole multiline on invalid
- if (status === STATUS_INVALID && this.options.ignore_invalid) {
- ctx.multiLineKeys = false;
- ctx.multiLineValue = '';
- return true;
- }
- ctx.multiLineValue += '\n' + value;
- this.assignValue(ctx.current, ctx.multiLineKeys, ctx.multiLineValue);
- ctx.multiLineKeys = false;
- ctx.multiLineValue = '';
- return true;
- }
- handleMultiLineAppend(ctx, line) {
- if (!ctx.multiLineKeys || this.isMultiLineEnd(line.trim())) {
- return false;
- }
- ctx.multiLineValue += '\n' + line;
- return true;
- }
- handleComment(ctx, line) {
- return this.isComment(line.trim());
- }
- handleSection(ctx, line) {
- line = line.trim();
- if (!this.isSection(line)) {
- return false;
- }
- const section = this.getSection(line);
- if (REGEXP_IGNORE_KEYS.test(section)) {
- return false;
- }
- this.createSection(ctx, section);
- if (this.isInheritedSection(line)) {
- const parentSection = this.getParentSection(line);
- ctx.current = Object.assign(
- ctx.current,
- JSON.parse(JSON.stringify(ctx.ini[parentSection])),
- );
- }
- return true;
- }
- handleSingleLine(ctx, line) {
- line = line.trim();
- if (!this.isSingleLine(line)) {
- return false;
- }
- const { key, value, status } = this.getKeyValue(line);
- // abort on false of onerror callback if we meet an invalid line
- if (status === STATUS_INVALID && !this.options.oninvalid(line)) {
- throw new Error('Abort');
- }
- // skip entry
- if (status === STATUS_INVALID && !this.options.ignore_invalid) {
- return true;
- }
- const keys = key.split('.');
- this.assignValue(ctx.current, keys, value);
- return true;
- }
- createSection(ctx, section) {
- const sections = (this.options.nested_section_names ? section.split('.') : [section]).map(
- (name) => name.trim(),
- );
- ctx.current = sections.reduce((ini, name) => {
- if (typeof ini[name] === 'undefined') {
- ini[name] = {};
- }
- return ini[name];
- }, ctx.ini);
- }
- }
- module.exports = Parser;
|