UNPKG

oparser

Version:

A very forgiving key-value option parser

1,188 lines (1,039 loc) 38.4 kB
const { convert, isObjectLike, isArrayLike, parseJSON } = require('./utils/convert') const { ensureWrap } = require('./utils/ensure-wrap') const { replaceInnerCharPattern } = require('./utils/replace-inner') const WHITE_SPACE = /[\s\n\r]/ const SURROUNDING_QUOTES = /^("|'|`)|("|'|`)$/g // const SURROUNDING_QUOTES = /^("|'|`)(?:[^\1])*(\1)$/g const VALID_KEY_CHAR = /^[A-Za-z0-9_@$¡-￿]/ const VALID_VALUE_CHAR = /(.+)/ const TRAILING_COMMAS = /,+$/ const NOT_OBJECT_LIKE = /^{[^:,]+}/ const START_WITH_PAREN = /^\s*\(/ const INFERRED_QUOTE = 'INFERRED' const SPACES = '__SPACE__' const LINE_BREAK = '__LINEBREAK__' const SINGLE_QUOTE = '_S_Q_' const OUTER_SINGLE_QUOTE = '_OSQ_' const OUTER_DOUBLE_QUOTE = '_ODQ_' const DOUBLE_QUOTE = '_D_Q_' const STARS = '_STAR_' const HASH = '_HASHP_' const DOUBLE_SLASH = '_SLASH_SLASH_' const CURLY_CLOSE = '_C_C_' const CURLY_OPEN = '_O_C_' const PAREN_CLOSE = '_P_C_' const BRACKET_TYPES = { '(': ')', '{': '}', '[': ']', } const QUOTE_CHARS = { "'": true, '"': true, '`': true, } function readQuotedToken(str, start) { const quote = str[start] let value = '' let escape = false for (let i = start + 1; i < str.length; i++) { const char = str[i] if (escape) { value += (char === quote || char === '\\') ? char : `\\${char}` escape = false continue } if (char === '\\') { escape = true continue } if (char === quote) { return { value, end: i } } value += char } return null } function startsKeyAssignmentAt(str, start) { let i = start while (i < str.length && WHITE_SPACE.test(str[i])) { i++ } if (i >= str.length) { return false } if (QUOTE_CHARS[str[i]]) { const quoted = readQuotedToken(str, i) if (!quoted) { return false } i = quoted.end + 1 } else { let hasKeyChar = false while (i < str.length && !WHITE_SPACE.test(str[i]) && str[i] !== '=') { hasKeyChar = true i++ } if (!hasKeyChar) { return false } } while (i < str.length && WHITE_SPACE.test(str[i])) { i++ } return str[i] === '=' } function findClosingQuote(value, quote, start = 1) { let escape = false for (let i = start; i < value.length; i++) { const char = value[i] if (escape) { escape = false continue } if (char === '\\') { escape = true continue } if (char === quote) { return i } } return -1 } function stripTrailingComment(value) { return value.replace(/\s+(?:#|\/\/|\/\*)[\s\S]*$/, '').trimEnd() } function parseIniDocument(str) { if (typeof str !== 'string' || str.indexOf('\n') === -1) { return null } const result = {} const lines = str.replace(/\r\n/g, '\n').split('\n') let pending = null function saveIniValue(key, rawValue, quote) { let value = rawValue if (!quote) { value = stripTrailingComment(value.trim()) result[key] = convert(value) return } result[key] = removeTempCharacters(value) } for (let i = 0; i < lines.length; i++) { const line = lines[i] const trimmed = line.trim() if (pending) { const closing = findClosingQuote(line, pending.quote, 0) if (closing !== -1) { pending.value += '\n' + line.slice(0, closing) saveIniValue(pending.key, pending.value, pending.quote) pending = null continue } if (startsKeyAssignmentAt(line, 0)) { saveIniValue(pending.key, pending.value, pending.quote) pending = null i-- continue } pending.value += '\n' + line continue } if (!trimmed || /^(#|\/\/|\/\*)/.test(trimmed)) { continue } const eq = line.indexOf('=') if (eq === -1) { result[trimmed] = true continue } const key = line.slice(0, eq).trim() let value = line.slice(eq + 1).trimStart() if (!key) { return null } const quote = QUOTE_CHARS[value[0]] ? value[0] : '' if (!quote) { saveIniValue(key, value, '') continue } const closing = findClosingQuote(value, quote) if (closing !== -1) { saveIniValue(key, value.slice(1, closing), quote) continue } pending = { key, quote, value: value.slice(1) } } if (pending) { saveIniValue(pending.key, pending.value, pending.quote) } return Object.keys(result).length ? result : null } function hasInternalParseArtifacts(value) { return Object.keys(value).some((key) => { if ( key.indexOf(SINGLE_QUOTE) > -1 || key.indexOf(DOUBLE_QUOTE) > -1 || key.indexOf(OUTER_SINGLE_QUOTE) > -1 || key.indexOf(OUTER_DOUBLE_QUOTE) > -1 ) { return true } const val = value[key] return typeof val === 'string' && ( val.indexOf(SINGLE_QUOTE) > -1 || val.indexOf(DOUBLE_QUOTE) > -1 || val.indexOf(OUTER_SINGLE_QUOTE) > -1 || val.indexOf(OUTER_DOUBLE_QUOTE) > -1 ) }) } function removeTempCharacters(val, rep) { if (typeof val === 'string') { return val .replace(/_S_Q_/g, `'`) .replace(/_D_Q_/g, `"`) // .replace(/_O_C_/g, `{`) .replace(/_C_C_/g, `}`) // .replace(/_P_C_/g, `)`) .replace(/_STAR_/g, `*`) .replace(/_HASHP_/g, `#`) .replace(/_SLASH_SLASH_/g, `//`) } return val } const space = ' ' // bob='co ol' steve='c ool' --> add temp spaces const SPACES_IN_SINGLE_QUOTE_RE = replaceInnerCharPattern(space, "'", "'", 2) // console.log('SPACES_IN_SINGLE_QUOTE_RE', SPACES_IN_SINGLE_QUOTE_RE) // bob="co ol" steve="c ool" --> add temp spaces const SPACES_IN_DOUBLE_QUOTE_RE = replaceInnerCharPattern(space, '"', '"', 2) // // bob='co ol' steve='c ool' --> add temp spaces // const DOUBLE_IN_SINGLE_QUOTE_RE = replaceInnerCharPattern('"', "'", "'", 2) // // bob="co ol" steve="c ool" --> add temp spaces // const SINGLE_IN_DOUBLE_QUOTE_RE = replaceInnerCharPattern("'", '"', '"', 2) // // bob={co ol} steve={co ol} --> add temp spaces // const BRACKETS_PATTERN = replaceInnerCharPattern(space, '{', '}', 2, true) // // bob=`co ol` steve=`c ool` --> add temp spaces // const TICKS = replaceInnerCharPattern(space, '`', '`', 2) // // bob={co ol} steve={co ol} --> add temp spaces // const TAGS = replaceInnerCharPattern(space, '{<', '>}') const LINEBREAKS_IN_SINGLE_QUOTE_RE = replaceInnerCharPattern('\\n', "'", "'", 2) const LINEBREAKS_IN_DOUBLE_QUOTE_RE = replaceInnerCharPattern('\\n', '"', '"', 2) /* Construct regex patterns */ const CONFLICTING_CURLIES_IN_SINGLE = replaceInnerCharPattern("}", `'`, `'`, 2) const CONFLICTING_CURLIES_IN_DOUBLE = replaceInnerCharPattern("}", `"`, `"`, 2) const CONFLICTING_HASH_IN_SINGLE = replaceInnerCharPattern("#", `'`, `'`, 2) const CONFLICTING_HASH_IN_DOUBLE = replaceInnerCharPattern("#", `"`, `"`, 2) const CONFLICTING_SLASHSLASH_IN_SINGLE = replaceInnerCharPattern("\\/\\/", `'`, `'`, 2) const CONFLICTING_SLASHSLASH_IN_DOUBLE = replaceInnerCharPattern("\\/\\/", `"`, `"`, 2) const SINGLE_QUOTES_STAR_PATTERN = replaceInnerCharPattern('\\*', `'`, `'`, 2) const DOUBLE_QUOTES_STAR_PATTERN = replaceInnerCharPattern('\\*', '"', '"', 2) // const ASYNC_ARROW_FULL_FN = /(?:async\s+)?\s?\(([\s\S]*)\)\s?(=>|_≡►)\s*(?:(?:[^}{]+|\{(?:[^}{]+|\{[^}{]*\})*\})*(?:\s?\(.*\)\s?\)\s?)?)?(?:\;)?/ // https://regex101.com/r/XO8rRl/1 // const ASYNC_ARROW = /(?:async\s+)?\s?\(([\s\S]*)\)\s?(=>|_≡►)\s*{?/ // https://regex101.com/r/Es4r3P/1 const ASYNC_ARROW = /(?:async\s+)?\s?\(([\s\S]{0,1000}?)\)\s?(=>|_≡►)\s*{?/ // const ASYNC_ARROW = /(?:async\s+)?\s?\(([^\)]*)\)\s?(=>|_≡►)\s*{?/ /* console.log('Patterns') console.log('SPACES_IN_SINGLE_QUOTE_RE', SPACES_IN_SINGLE_QUOTE_RE) console.log('SPACES_IN_DOUBLE_QUOTE_RE', SPACES_IN_DOUBLE_QUOTE_RE) console.log('LINEBREAKS_IN_SINGLE_QUOTE_RE', LINEBREAKS_IN_SINGLE_QUOTE_RE) console.log('LINEBREAKS_IN_DOUBLE_QUOTE_RE', LINEBREAKS_IN_DOUBLE_QUOTE_RE) console.log('CONFLICTING_CURLIES_IN_SINGLE', CONFLICTING_CURLIES_IN_SINGLE) console.log('CONFLICTING_CURLIES_IN_DOUBLE', CONFLICTING_CURLIES_IN_DOUBLE) console.log('CONFLICTING_HASH_IN_SINGLE', CONFLICTING_HASH_IN_SINGLE) console.log('CONFLICTING_HASH_IN_DOUBLE', CONFLICTING_HASH_IN_DOUBLE) console.log('CONFLICTING_SLASHSLASH_IN_SINGLE', CONFLICTING_SLASHSLASH_IN_SINGLE) console.log('CONFLICTING_SLASHSLASH_IN_DOUBLE', CONFLICTING_SLASHSLASH_IN_DOUBLE) // console.log('DOUBLE_IN_SINGLE_QUOTE_RE', DOUBLE_IN_SINGLE_QUOTE_RE) // console.log('SINGLE_IN_DOUBLE_QUOTE_RE', SINGLE_IN_DOUBLE_QUOTE_RE) /** */ const DEBUG = false /** * Parse config string into key-value object * @template {Record<string, any>} [T=Record<string, any>] * @param {string|null|undefined} s - Config string to parse * @returns {T} */ function parse(s) { if (typeof s !== 'string' || s === '') { return {} } /* Trim string and remove comment blocks */ let str = s.trim() const originalInput = str /* If surrounded by double quotes, remove them */ const wrappedString = QUOTE_CHARS[str[0]] ? readQuotedToken(str, 0) : null if (str[0] === '"' && str[str.length - 1] === '"' && wrappedString && wrappedString.end === str.length - 1) { str = str.replace(/^"|"$/g, '') } else if (str[0] === "'" && str[str.length - 1] === "'" && wrappedString && wrappedString.end === str.length - 1) { str = str.replace(/^'|'$/g, '') } else if (str[0] === "`" && str[str.length - 1] === "`" && wrappedString && wrappedString.end === str.length - 1) { str = str.replace(/^`|`$/g, '') } /* If string is a single character, return it as bool */ if (str.length === 1) { return { [str]: true } } // console.log(str.length) // return if (str.length > 20000) { // For large strings, try JSON.parse first (assuming well-formed data) try { return JSON.parse(str) } catch (e) { const largeKeyValue = parseLargeKeyValueJSON(str) if (largeKeyValue) { return largeKeyValue } throw new Error(`String is too long (${str.length} chars) for forgiving parser. JSON.parse failed: ${e.message}`) } } /* Fast path: if looks like JSON object/array, try JSON.parse first */ const firstChar = str[0] const lastChar = str[str.length - 1] if ((firstChar === '{' && lastChar === '}') || (firstChar === '[' && lastChar === ']')) { try { return JSON.parse(str) } catch (e) { // Fall through to forgiving parser } } /* if (DEBUG) { console.log('>> start str') console.log(str) console.log('───────────────────────────────') } /** */ const isMultiline = str.indexOf('\n') > -1 if (isMultiline) { str = str /* fix unbalanced inner single quote conflicts https://regex101.com/r/kLNXg8/1 */ .replace(/(=)(')([^\n']*)(')(\s*\n\s*)(?=(?:(?:[^']*(?:')){2})*[^']*(?:')[^']*$)/g, `$1${OUTER_SINGLE_QUOTE}$3${OUTER_SINGLE_QUOTE}$5`) .replace(/(=)(")([^\n']*)(")(\s*\n\s*)(?=(?:(?:[^"]*(?:")){2})*[^"]*(?:")[^"]*$)/g, `$1${OUTER_DOUBLE_QUOTE}$3${OUTER_DOUBLE_QUOTE}$5`) /* Replace spaces in single quotes with temporary spaces */ .replace(LINEBREAKS_IN_SINGLE_QUOTE_RE, `${LINE_BREAK}\n`) /* Replace spaces in double quotes with temporary spaces */ .replace(LINEBREAKS_IN_DOUBLE_QUOTE_RE, `${LINE_BREAK}\n`) } /* if (DEBUG) { console.log('pre pass', str) console.log('end pre-pass') } /** */ const hasInnerSpacesInSinglesQuote = str.search(SPACES_IN_SINGLE_QUOTE_RE) !== -1 /* if (DEBUG) { console.log('end hasInnerSpacesInSinglesQuote', hasInnerSpacesInSinglesQuote) } /** */ // const hasInnerDoubleInSinglesQuote = DOUBLE_IN_SINGLE_QUOTE_RE.test(str) // DOUBLE_IN_SINGLE_QUOTE_RE.lastIndex = 0 // Reset regex pattern due to g flag https://bit.ly/2UCNhJz // const hasInnerSingleInDoubleQuote = SINGLE_IN_DOUBLE_QUOTE_RE.test(str) // SINGLE_IN_DOUBLE_QUOTE_RE.lastIndex = 0 // Reset regex pattern due to g flag https://bit.ly/2UCNhJz // if (hasInnerSingleInDoubleQuote) { // str = str.replace(SINGLE_IN_DOUBLE_QUOTE_RE, 'INNER_SINGLE') // } // if (hasInnerDoubleInSinglesQuote) { // str = str.replace(DOUBLE_IN_SINGLE_QUOTE_RE, 'INNER_DOUBLE') // } if (hasInnerSpacesInSinglesQuote) { str = str /* Replace spaces in single quotes with temporary spaces */ .replace(SINGLE_QUOTES_STAR_PATTERN, STARS) .replace(SPACES_IN_SINGLE_QUOTE_RE, SPACES) /* Fix inner conflicting single quotes */ .replace(/__SPACE__'/g, ` ${SINGLE_QUOTE}`) .replace(/'__SPACE__/g, `${SINGLE_QUOTE} `) /* Unbalanced single or double quotes break previous regex, so we need to fix */ /* Fix trailing close quote - match __SPACE__ placeholders or real spaces that precede _S_Q_ */ .replace(/(\s*)((__SPACE__|\s)+)_S_Q_(\s*)$/g, `$1$2'$4`) .replace(/(\s*)((__SPACE__)+)+(\s*)_S_Q_(\s*)/g, `$1$2$4'$5`) /* Fix unbalanced quote bracket replacement */ .replace(/}__SPACE__([\S])/, '} $1') if (isMultiline) { str = str.replace(/([^=\]\}])'((__LINEBREAK__)+)+$/gm, `$1${SINGLE_QUOTE}`) } else { /* Fix Single ' space key=val */ str = str.replace(/_S_Q_ ([A-Za-z0-9_]*=)/, "' $1") /* Fix opening quote for whitespace-only single quoted values */ str = str.replace(/=_S_Q_((?:__SPACE__|\s)+)'/g, "='$1'") } /* if (DEBUG) { console.log('end hasInnerSpacesInSinglesQuote fix') } /** */ } /* console.log('>>>>> 1 pass') console.log(str) console.log('───────────────────────────────') /** */ /* Fix conflicting double quotes bob="inner "quote" conflict" steve='cool' */ const hasInnerSpacesInDoubleQuote = str.search(SPACES_IN_DOUBLE_QUOTE_RE) !== -1 /* if (DEBUG) { console.log('end hasInnerSpacesInDoubleQuote', hasInnerSpacesInDoubleQuote) } /** */ if (hasInnerSpacesInDoubleQuote) { str = str .replace(SPACES_IN_DOUBLE_QUOTE_RE, SPACES) .replace(DOUBLE_QUOTES_STAR_PATTERN, STARS) /* Fix inner conflicting double quotes */ .replace(/__SPACE__"/g, ` ${DOUBLE_QUOTE}`) .replace(/"__SPACE__/g, `${DOUBLE_QUOTE} `) /* Unbalanced single or double quotes break previous regex, so we need to fix */ /* Fix trailing close quote - match both __SPACE__ placeholders and real spaces */ .replace(/(\s*)((__SPACE__|\s)+)+(\s*)_D_Q_(\s*)/g, `$1$2$4"$5`) /* Fix unbalanced quote bracket replacement */ .replace(/}__SPACE__([\S])/, '} $1') if (isMultiline) { str = str.replace(/([^=\]\}])"((__LINEBREAK__)+)+$/gm, `$1${DOUBLE_QUOTE}`) } else { /* Fix Double " space key=val */ str = str.replace(/_D_Q_ ([A-Za-z0-9_]*=)/, '" $1') } /* Fix opening quote that was incorrectly converted (whitespace-only values) */ str = str.replace(/=_D_Q_(\s+)((__SPACE__|\s)*)/g, '="$1$2') /* if (DEBUG) { console.log('end hasInnerSpacesInDoubleQuote fix') } /** */ } /* console.log('>>>>> 2 pass') console.log(str) console.log('───────────────────────────────') /** */ /* Conflicting inner } */ const hasConflictingCurliesInSingle = str.search(CONFLICTING_CURLIES_IN_SINGLE) !== -1 const hasConflictingCurliesInDouble = str.search(CONFLICTING_CURLIES_IN_DOUBLE) !== -1 /* Conflicting inner # */ const hasConflictingHashesInSingle = str.search(CONFLICTING_HASH_IN_SINGLE) !== -1 const hasConflictingHashesInDouble = str.search(CONFLICTING_HASH_IN_DOUBLE) !== -1 /* Conflicting inner '//' */ const hasConflictingSlashesInSingle = str.search(CONFLICTING_SLASHSLASH_IN_SINGLE) !== -1 const hasConflictingSlashesInDouble = str.search(CONFLICTING_SLASHSLASH_IN_DOUBLE) !== -1 /* conflicting inner JSON */ /* console.log('Conflicts') console.log('hasInnerSpacesInSinglesQuote', hasInnerSpacesInSinglesQuote) console.log('hasInnerSpacesInDoubleQuote', hasInnerSpacesInDoubleQuote) console.log('hasConflictingCurliesInSingle', hasConflictingCurliesInSingle) console.log('hasConflictingCurliesInDouble', hasConflictingCurliesInDouble) console.log('hasConflictingHashesInSingle', hasConflictingHashesInSingle) console.log('hasConflictingHashesInDouble', hasConflictingHashesInDouble) console.log('hasConflictingSlashesInSingle', hasConflictingSlashesInSingle) console.log('hasConflictingSlashesInDouble', hasConflictingSlashesInDouble) /** */ // const hasConflictingParen = CONFLICTING_PARENS_IN_SINGLE.test(str) // console.log('hasConflictingParen', hasConflictingParen) // if (hasConflictingParen) { // str = str // .replace(CONFLICTING_PARENS_IN_SINGLE, PAREN_CLOSE) // // Fix trailing )} closes // // .replace(/_P_C_(\s*})/g, ')$1') // } /* Has inner "}" in single quotes */ if (hasConflictingCurliesInSingle) { str = str .replace(CONFLICTING_CURLIES_IN_SINGLE, `${CURLY_CLOSE}`) /* Replace conflicting inner close parens ) to support jsx */ .replace(/(\s+)?\)_C_C_(__LINEBREAK__|__SPACE__|\s+)/g, '$1)}$2') // {{ color: 'red' }} val Object jsx style weird test .replace(/_C_C__C_C_ /g, '}} ') } /* Has inner "}" in double quotes */ if (hasConflictingCurliesInDouble) { str = str .replace(CONFLICTING_CURLIES_IN_DOUBLE, `${CURLY_CLOSE}`) /* Replace conflicting inner close parens ) to support jsx */ .replace(/(\s+)?\)_C_C_(__LINEBREAK__|__SPACE__|\s+)/g, '$1)}$2') // {{ color: 'red' }} val Object jsx style weird test .replace(/_C_C__C_C_ /g, '}} ') } /* Has inner "#" in single quotes */ if (hasConflictingHashesInSingle) { str = str.replace(CONFLICTING_HASH_IN_SINGLE, HASH) } /* Has inner "#" in double quotes */ if (hasConflictingHashesInDouble) { str = str.replace(CONFLICTING_HASH_IN_DOUBLE, HASH) } /* Has inner '//' in single quotes */ if (hasConflictingSlashesInSingle) { str = str.replace(CONFLICTING_SLASHSLASH_IN_SINGLE, DOUBLE_SLASH) } /* Has inner "//" in double quotes */ if (hasConflictingSlashesInDouble) { str = str.replace(CONFLICTING_SLASHSLASH_IN_DOUBLE, DOUBLE_SLASH) } /* console.log('>>>>> 3 pass') console.log(str) console.log('───────────────────────────────') /** */ if (hasInnerSpacesInSinglesQuote || hasInnerSpacesInDoubleQuote) { str = str /* Replace temporary spaces */ .replace(/__SPACE__/g, ' ') /* Replace temporary outer single quotes */ .replace(/_OSQ_/g, "'") /* Replace temporary outer single quotes */ .replace(/_ODQ_/g, '"') } if (isMultiline) { /* Replace temporary line breaks */ str = str.replace(/__LINEBREAK__/g, '') } /* Remove all comments outside of values */ // console.log('str', str) str = removeComments(str) /* console.log('>>> CLEAN str') console.log(str) console.log('───────────────────────────────') /** */ const vals = {} let openQuote let bufferKey = '' let bufferValue = '' let keyIsOpen = false let valueIsOpen = false let openInnerQuote = '' let valueUsesInnerQuoteTracking = false function save(key, value, from) { /* Debug values console.log(`Save ${key} from "${from}" in quote ▶ ${openQuote} ◀`, value) /** */ vals[key] = value // vals[removeTempCharacters(key)] = value openQuote = '' bufferKey = '' bufferValue = '' keyIsOpen = true valueIsOpen = false openInnerQuote = '' valueUsesInnerQuoteTracking = false } for (let i = 0; i < str.length; i++) { const char = str[i] const nextChar = str[i + 1] || '' const prevChar = str[i - 1] /* console.log('───────────────────────────────') console.log(`> key "${bufferKey}"`, `char: "${char}"`) console.log(`> val "${bufferValue}"`, `char: "${char}"`) console.log('───────────────────────────────') /** */ /* if (openQuote) { console.log('Inside Quote:', `"${openQuote}"`) } /** */ if (keyIsOpen && char === ',') { // console.log('EXIT ON', bufferValue) continue; } if (!bufferKey && !valueIsOpen && QUOTE_CHARS[char]) { const quotedKey = readQuotedToken(str, i) if (quotedKey) { bufferKey = quotedKey.value keyIsOpen = true i = quotedKey.end continue } } /* If last char and not white space, add to key */ if (keyIsOpen && !nextChar && VALID_KEY_CHAR.test(char)) { bufferKey+= char save(bufferKey, true, 'last key') continue; } /* If has key and is break, set bool */ if (keyIsOpen && bufferKey && char !== '=' && (WHITE_SPACE.test(char) || !nextChar)) { if (!nextChar) { // Last char add it bufferKey+= char } /* If not white spaces before separator, set as true boolean */ if (nextChar !== '=') { save(bufferKey, true, 'true bool') continue; } } /* If k/v separator, and not inside value, open up value collector */ if (bufferKey && keyIsOpen && char === '=') { // console.log('Seal key and open value') keyIsOpen = false valueIsOpen = true continue; } if (valueIsOpen && !openQuote && !bufferValue && WHITE_SPACE.test(char) && startsKeyAssignmentAt(str, i + 1)) { save(bufferKey, '', 'empty value before key') continue } /* trim trailing spaces from after separator: "bob =( trimmed spaces )cool" */ if (!openQuote && !bufferValue && char === ' ') { // console.log('EXIT ON', bufferValue) continue; } /* If collecting key pieces and is valid character add to key */ if (keyIsOpen && !WHITE_SPACE.test(char)) { bufferKey+= char continue; } if (!bufferKey && VALID_KEY_CHAR.test(char)) { bufferKey+= char keyIsOpen = true continue; } /* If value buffer open, collect characters */ if (valueIsOpen) { // if (openQuote) { // console.log(`valueIsOpen openQuote >>>> ${openQuote}` ) // } if ( (openQuote === '[' && (valueUsesInnerQuoteTracking || shouldTrackSimpleArrayQuote(bufferValue))) || (openQuote === '{' && (valueUsesInnerQuoteTracking || shouldTrackSimpleObjectQuote(bufferValue))) ) { if (openInnerQuote) { bufferValue+= char if (char === openInnerQuote && prevChar !== '\\') { openInnerQuote = '' } continue } if (QUOTE_CHARS[char]) { valueUsesInnerQuoteTracking = true openInnerQuote = char bufferValue+= char continue } } /* If key + value and not inside known quotes */ if ( (openQuote === INFERRED_QUOTE) && ((char === ',' && WHITE_SPACE.test(nextChar)) || WHITE_SPACE.test(char))) { save(bufferKey, convert(bufferValue), 'inferred') continue } /* If opening bracket is brackets {}. Ensure balance */ if ( !openInnerQuote && (openQuote === '{' && char === '}' && (!nextChar || nextChar !== '}') || openQuote === '[' && char === ']' && (!nextChar || nextChar !== ']')) ) { /* Debug object values console.log('{} bufferValue', bufferValue) /** */ // if (!isObjectLike(bufferValue)) { // save(bufferKey, preFormat(bufferValue), 'NOT_OBJECT_LIKE') // continue; // } if (bufferValue.match(NOT_OBJECT_LIKE)) { save(bufferKey, preFormat(trimBrackets(bufferValue, '{', '}')), 'NOT_OBJECT_LIKE') continue; } if (openQuote === '{' && isLooseCurlyScalar(bufferValue)) { save(bufferKey, preFormat(bufferValue), 'LOOSE_CURLY_SCALAR') continue } // console.log('hang') const theOpenQuote = START_WITH_PAREN.test(bufferValue) && !ASYNC_ARROW.test(bufferValue) ? '(' : openQuote const newBalance = isBalanced(bufferValue, theOpenQuote, valueUsesInnerQuoteTracking) if (newBalance) { const openIsBracket = openQuote === '[' // bufferValue = bufferValue + char const value = (openIsBracket) ? `[${bufferValue}]` : ensureWrap(bufferValue, '{', '}') const cleanValue = preFormat(value) /* console.log('char', char) console.log(`>>>> Close bracket value`, value) console.log(`>>>> Close bracket cleanValue`, cleanValue) /** */ save(bufferKey, cleanValue, 'New balance') continue } // const bracketsBalanced = areAllBracketsBalanced(bufferValue) // console.log('bracketsBalanced', bracketsBalanced) // if (bracketsBalanced) { // const openIsBracket = openQuote === '[' // const value = (openIsBracket) ? `[${bufferValue}]` : ensureWrap(bufferValue, '{', '}') // const cleanValue = preFormat(value) // /* // console.log(`>>>> Close bracket value`, value) // console.log(`>>>> Close bracket cleanValue`, cleanValue) // /** */ // save(bufferKey, cleanValue, 'Object') // continue // } } /* Last loop */ if (!nextChar) { bufferValue+= char save(bufferKey, preFormat(bufferValue, openQuote), 'LAST LOOP') continue } /* Reset inner quote */ // if (openInnerQuote && char === "\\" && openInnerQuote === nextChar) { // openInnerQuote = '' // } // if (openInnerQuote) { // bufferValue+= char // continue; // } // /* Set inner quote and escape text */ // if (openQuote && char === "\\" && openQuote === nextChar) { // console.log('Escaped quote', nextChar) // console.log('Escaped quote current buffer', bufferValue) // openInnerQuote = nextChar // continue; // } if ( (openQuote && openQuote !== '{' && openQuote !== '[') // Isn't value in brackets && (char === openQuote && (WHITE_SPACE.test(nextChar) || nextChar === ',')) // Matching closing close with trailing space ) { bufferValue+= char save(bufferKey, preFormat(bufferValue), 'quoteClose') continue } // Is inferred quote value if ( openQuote === INFERRED_QUOTE && ((char === ',' && WHITE_SPACE.test(nextChar))) ) { // console.log('char', char) // console.log('nextChar', WHITE_SPACE.test(nextChar)) bufferValue+= char save(bufferKey, preFormat(bufferValue), 'INFERRED_QUOTE') continue } const isBracketStart = char === '[' if (!openQuote && isBracketStart) { // bufferValue+= char openQuote = char continue; } const isCurlyBracketStart = !openQuote && char === '{' if (!openQuote && isCurlyBracketStart) { openQuote = char continue; } const isQuoteStart = char === '\'' || char === '"' || char === '`' // console.log('isQuoteStart', isQuoteStart) if (!openQuote && isQuoteStart) { // bufferValue+= char openQuote = char bufferValue+= char continue; } if (!bufferValue && (prevChar === '=' || prevChar === ' ') && VALID_VALUE_CHAR.test(char)) { // console.log('Set inferred quote') openQuote = INFERRED_QUOTE bufferValue+= char continue; } if (openQuote === INFERRED_QUOTE && (char === ',' && WHITE_SPACE.test(nextChar))) { continue; } /* Add char to buffer */ bufferValue+= char } } if (valueIsOpen && bufferKey && !bufferValue) { save(bufferKey, '', 'empty value at end') } if (hasInternalParseArtifacts(vals)) { const iniParsed = parseIniDocument(originalInput) if (iniParsed) { return iniParsed } } return vals } /** * Parse freeform value into object * @param {string|null|undefined} value - freeform string value to parse into object, array or value. * @returns {any} */ function parseValue(value) { if (typeof value !== 'string' || !value) { return value } return parse(`internal=${value.trim()}`).internal } function parseLargeKeyValueJSON(str) { const eq = str.indexOf('=') if (eq === -1) { return null } const key = str.slice(0, eq).trim() const value = str.slice(eq + 1).trim() const first = value[0] const last = value[value.length - 1] if (!key || !((first === '{' && last === '}') || (first === '[' && last === ']'))) { return null } try { return { [key]: JSON.parse(value) } } catch (e) { return null } } function preFormat(val, quoteType) { // console.log('preFormat start', val, quoteType) let value = removeTempCharacters(val).replace(TRAILING_COMMAS, '') // console.log('preFormat value 1', value) if (quoteType === '{') { value = trimBrackets((!value.match(/^{{1,}/) ? quoteType + value : value)) } // console.log('preFormat value 2', value) if (value.match(ASYNC_ARROW)) { // console.log('try', value) value = isBalanced(value, '{') ? removeSurroundingBrackets(value) : removeSurroundingBrackets(value + '}') // console.log('value', value) } else if (value.match(/^{\s*\(([\s\S]+?)\)\s*}$/)) { // JSX style tag value={( stuff )} value = value.replace(/^{\s*\(/, '').replace(/\)\s*}$/, '') // console.log('preFormat value tow', value) } // If Doesn't look like JSON object else if (value.match(/^{[^:,]+}/)) { value = removeSurroundingBrackets(value) } // If looks like array in brackets {[ thing, thing, thing ]} else if (value.match(/^{\s*\[\s*[^:]*\s*\]\s*\}/)) { // Match { [ one, two ,3,4 ] } value = removeSurroundingBrackets(value) // console.log('preFormat value 2', value) } // If matches {` stuff `} & {[ stuff ]} else if (value.match(/^{(?:`|\[)([\s\S]*?)(?:`|\])}$/)) { value = removeSurroundingBrackets(value) } // If matches JSX tag {<html>} & {(<html>)} https://regex101.com/r/KSARnK/1 else if (value.match(/^{\s*\(?\s*<([a-zA-Z1-6]+)\b([^>]*)>*(?:>([\s\S]{0,4000}?)<\/\1>|\s?\/?>)\s*\)?\s*}$/)) { // else if (value.match(/^{\s*\(?\s*<([a-zA-Z1-6]+)\b([^>]*)>*(?:>([\s\S]*?)<\/\1>|\s?\/?>)\s*\)?\s*}$/)) { // else if (isJSXElement(value)) { // Safer JSX check value = removeSurroundingBrackets(value) } // console.log('preFormat value 3', value) /* Check if remaining value is surrounded by quotes */ const surroundingQuotes = value.match(SURROUNDING_QUOTES) || [] // console.log('surroundingQuotes', surroundingQuotes) const hasSurroundingQuotes = surroundingQuotes.length === 2 && (surroundingQuotes[0] === surroundingQuotes[1]) // console.log('hasSurroundingQuotes', hasSurroundingQuotes) return hasSurroundingQuotes ? value.replace(SURROUNDING_QUOTES, '') : convert(value) } const JSX_OPENING = /^{\s*\(?\s*</; // Match opening {< or {( const JSX_TAG_NAME = /([a-zA-Z][a-zA-Z0-9_-]*)/; // Match valid tag names const JSX_CLOSING = />|\/>/; // Match > or /> const JSX_END = /\s*\)?\s*}$/; // Match ending )} or } function isJSXElement(value) { // Early exit if doesn't have basic JSX structure if (!JSX_OPENING.test(value)) return false; // Get the tag name from start of element const tagMatch = value.match(JSX_TAG_NAME); if (!tagMatch) return false; const tagName = tagMatch[1]; // Look for matching closing tag // const closeTag = new RegExp(`</${tagName}>`); // Either self-closing or has matching end tag return ( // Self-closing tag case: <tag/>} (value.match(JSX_CLOSING) && value.match(JSX_END)) || // Full tag case: <tag>...</tag>} (value.includes(`</${tagName}>`) && value.match(JSX_END)) ); } function isLooseCurlyScalar(value) { const trimmed = value.trim() if (!trimmed || trimmed.indexOf('\n') > -1) return false if (START_WITH_PAREN.test(value) || /^\s*</.test(value)) return false if (trimmed.indexOf(':') > -1 || trimmed.indexOf(',') > -1) return false const open = (trimmed.match(/{/g) || []).length const close = (trimmed.match(/}/g) || []).length return close > open } function shouldTrackSimpleArrayQuote(value) { return value.indexOf('{') === -1 && value.indexOf('[') === -1 } function shouldTrackSimpleObjectQuote(value) { const trimmed = value.trimStart() return trimmed.indexOf('\n') === -1 && trimmed.indexOf('<') === -1 && !START_WITH_PAREN.test(trimmed) && (trimmed[0] === '{' || trimmed.indexOf(':') > -1 || trimmed.indexOf(',') > -1) } function removeSurroundingBrackets(val) { // console.log('val', val) return val.replace(/^{/, '').replace(/}$/, '') } function removeComments(input) { return input // Remove JS comment blocks and single line comments https://regex101.com/r/XKHU18/2 | alt https://regex101.com/r/ywd8TT/1 .replace(/\s+\/\*[\s\S]*?\*\/|\s+\/\/.*$/gm, '') // Remove single line comments .replace(/^\s*(\/\/+|\/\*+|#+)(.*)\n?$/gm, '') // Trailing single line comments .replace(/\s*(\/\/+|\/\*+|#+)(.*)\n$/gm, '') // trailing yaml comments not in quotes .replace(/\s+(\/\/+|\/\*+|#+)([^"'\n]*)$/gm, '') // .replace(/#.*$/gm, '') } // trimBrackets(`{{cool}}}`) => cool} // trimBrackets(`{{cool}}`) => cool // trimBrackets(`{{{cool}}`) => {cool function trimBrackets(value, open = '', close = '') { // console.log('>>> trimBrackets value', value) const leadingCurlyBrackets = value.match(/^{{1,}/) const trailingCurlyBrackets = value.match(/}{1,}$/) // console.log('leadingCurlyBrackets', leadingCurlyBrackets) // console.log('trailingCurlyBrackets', trailingCurlyBrackets) if (leadingCurlyBrackets && trailingCurlyBrackets) { const len = leadingCurlyBrackets[0].length <= trailingCurlyBrackets[0].length ? leadingCurlyBrackets : trailingCurlyBrackets const trimLength = len[0].length // console.log('trimLength', trimLength) const trimLeading = new RegExp(`^{{${trimLength}}`) const trimTrailing = new RegExp(`}{${trimLength}}$`) // console.log('trimLeading', trimLeading) // console.log('trimTrailing', trimTrailing) if (trimLength) { value = value // Trim extra leading brackets .replace(trimLeading, open) // Trim extra trailing brackets .replace(trimTrailing, close) } } // console.log('>>> trimBrackets out value', value) return value } /** * Verify brackets are balanced * @param {string} str - string with code * @return {Boolean} */ function areAllBracketsBalanced(str) { let count = 0 for (let i = 0; i < str.length; i++) { const c = str[i] if (c === '(' || c === '{' || c === '[') count++ else if (c === ')' || c === '}' || c === ']') count-- } return count === 0 } function isBalanced(str, open = '{', ignoreQuotedBrackets = false) { const close = BRACKET_TYPES[open] let count = 0 let quote = '' for (let i = 0; i < str.length; i++) { const c = str[i] if (ignoreQuotedBrackets) { if (quote) { if (c === quote && str[i - 1] !== '\\') { quote = '' } continue } if (QUOTE_CHARS[c]) { quote = c continue } } if (c === open) count++ else if (c === close) count-- } return count === 0 } /** * Parse string of key value options. Template tag version * @template {Record<string, any>} [T=Record<string, any>] * @param {TemplateStringsArray} input - template strings array * @param {...any} substitutions - template substitutions * @returns {T} */ function options(input = '', ...substitutions) { const rendered = substitutions.map((value) => { if (typeof value === 'string') { return /\s/.test(value) ? encodeOptionsString(value) : value } if (typeof value === 'undefined') { return '' } if (value === null) { return 'null' } if (typeof value === 'object') { return encodeOptionsValue(value) } return String(value) }) let str = String.raw(input, ...rendered) return parse(str) } /* Encode a string as an oparser-readable quoted literal. Picks a quote that doesn't appear inside the value so round-tripping doesn't need backslash escaping (which the forgiving parser doesn't unescape). */ function encodeOptionsString(value) { if (value.indexOf('"') === -1) return `"${value}"` if (value.indexOf("'") === -1) return `'${value}'` if (value.indexOf('`') === -1) return `\`${value}\`` return `"${value.split('"').join('\\"')}"` } function encodeOptionsValue(value) { if (value === null) return 'null' if (Array.isArray(value)) { return '[' + value.map(encodeOptionsValue).join(', ') + ']' } const t = typeof value if (t === 'string') return encodeOptionsString(value) if (t === 'number' || t === 'boolean') return String(value) if (t === 'object') { const parts = Object.keys(value).map((k) => { return encodeOptionsKey(k) + ': ' + encodeOptionsValue(value[k]) }) return '{ ' + parts.join(', ') + ' }' } return '' } function encodeOptionsKey(key) { return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : encodeOptionsString(key) } module.exports = { parse, parseValue, options } // function repeatStringNumTimes(string, times) { // var repeatedString = ""; // while (times > 0) { // repeatedString += string; // times--; // } // return repeatedString; // } // function replacer(match, open, content, close, offset) { // console.log(arguments) // return repeatStringNumTimes(CURLY_OPEN, open.length) + content + repeatStringNumTimes(CURLY_CLOSE, close.length) // // return (offset === 0 ? "FIRST" : "") + match // } // function replaceCloseCurly(match, open, _, extra) { // console.log(arguments) // console.log('close', open) // console.log('extra', extra) // return repeatStringNumTimes(CURLY_CLOSE, open.length) + (extra || '') // // return (offset === 0 ? "FIRST" : "") + match // }