// Name: Convert Key-Value to JSON // Description: Convert space-separated key=value pairs into a JSON object with robust parsing and error handling. // Author: dallascrilley // GitHub: dallascrilley import "@johnlindquist/kit" // Usage: // - Enter space-separated key=value pairs. // - Values with spaces can be quoted: key="value with spaces" // - Empty values allowed: key= // - Special characters in keys/values supported. // - Odd formatting is handled (e.g., trailing tokens appended to previous value). // // Examples: // key1=value1 key2=value2 key3=value3 // name=John Doe email="john@example.com" city="New York" // key1=value1 key2= key3=value3 // url=https://example.com port=8080 path=/api/v1 // -------------------- Input -------------------- const input = await arg({ placeholder: "Enter space-separated key=value pairs", hint: `Examples: - key1=value1 key2=value2 key3=value3 - name=John Doe email="john@example.com" city="New York" - key1=value1 key2= key3=value3 - url=https://example.com port=8080 path=/api/v1`, strict: false, }) const trimmed = (input || "").trim() // -------------------- Parsing -------------------- // Tokenize string by whitespace, but keep quoted segments (supports " and ') intact (with escapes). function tokenizePreserveQuotes(s: string): string[] { const tokens: string[] = [] let token = "" let inQuotes = false let quoteChar = "" let prev = "" for (let i = 0; i < s.length; i++) { const c = s[i] if (inQuotes) { token += c if (c === quoteChar && prev !== "\\") { inQuotes = false quoteChar = "" } } else { if (c === '"' || c === "'") { inQuotes = true quoteChar = c token += c } else if (/\s/.test(c)) { if (token.length) { tokens.push(token) token = "" } } else { token += c } } prev = c } if (token.length) tokens.push(token) return tokens } // Unquote a token if quoted; track errors for unclosed quotes; unescape \" and \' function unquote(token: string, errors: string[]): string { if (!token) return "" const first = token[0] const last = token[token.length - 1] if ((first === '"' || first === "'")) { // If properly closed and not escaped if (last === first && token.length >= 2 && token[token.length - 2] !== "\\") { const inner = token.slice(1, -1) return inner.replace(/\\(["'\\])/g, "$1") } else { // Unclosed or oddly formatted quote errors.push(`Unclosed or malformed quoted value: ${token}`) // Best effort: remove leading quote and unescape const inner = token.slice(1) return inner.replace(/\\(["'\\])/g, "$1") } } return token } type ParseResult = { obj: Record<string, string> errors: string[] warnings: string[] } function parseKeyValueString(s: string): ParseResult { const obj: Record<string, string> = {} const errors: string[] = [] const warnings: string[] = [] if (!s.trim()) return { obj, errors, warnings } const tokens = tokenizePreserveQuotes(s) let i = 0 let lastKey: string | null = null while (i < tokens.length) { const t = tokens[i] if (t.includes("=")) { const eqIndex = t.indexOf("=") const keyRaw = t.slice(0, eqIndex) let valueRaw = t.slice(eqIndex + 1) const key = keyRaw.trim() if (!key) { errors.push(`Missing key before '=' in token: ${t}`) i++ lastKey = null continue } // Start with the immediate value part from this token let value = unquote(valueRaw, errors) // Handle odd formatting and continuation: // - If next tokens don't contain '=', treat them as continuation of the current value. // - This allows: name=John Doe -> "John Doe" // - Also allows: key= value -> "value" while (i + 1 < tokens.length && !tokens[i + 1].includes("=")) { const nextPart = tokens[i + 1] value += (value ? " " : "") + unquote(nextPart, errors) i++ } // Duplicate key handling: keep the last one but warn if (Object.prototype.hasOwnProperty.call(obj, key)) { warnings.push(`Duplicate key encountered; keeping last value for key: ${key}`) } obj[key] = value lastKey = key } else { // Token without '=', attempt to attach to previous key if any (odd formatting) if (lastKey) { obj[lastKey] = (obj[lastKey] ? obj[lastKey] + " " : "") + unquote(t, errors) warnings.push(`Token without '=' appended to previous key '${lastKey}': ${t}`) } else { errors.push(`Ignoring token without '=' and no previous key to attach: ${t}`) } } i++ } return { obj, errors, warnings } } const { obj, errors, warnings } = parseKeyValueString(trimmed) // -------------------- Output -------------------- if (Object.keys(obj).length === 0) { await notify(`No valid key=value pairs found`) const errorList = errors.length ? `\n\nErrors:\n- ${errors.join("\n- ")}` : "" await div( md(`# Conversion Failed No valid pairs parsed from input. Input: \`${trimmed}\` ${errorList} `) ) exit(1) } const json = JSON.stringify(obj, null, 2) await copy(json) let status = `Converted ${Object.keys(obj).length} pairs. JSON copied to clipboard.` if (errors.length) status += ` (${errors.length} error${errors.length > 1 ? "s" : ""})` if (warnings.length) status += ` (${warnings.length} warning${warnings.length > 1 ? "s" : ""})` await notify(status) // Show result and any issues let issues = "" if (warnings.length) { issues += `\n\n### Warnings\n${warnings.map(w => `- ${w}`).join("\n")}` } if (errors.length) { issues += `\n\n### Errors\n${errors.map(e => `- ${e}`).join("\n")}` } await div( md(` # JSON Output Copied to clipboard. \`\`\`json ${json} \`\`\` ${issues} `) )