{
  "version": 1,
  "workflows": [
    {
      "id": "D39DBF56-0C85-48D9-8E2A-C3BD2C7FB7BF",
      "workflowVersion": 1,
      "name": "CSV / JSON Converter",
      "summary": "CSV · JSON · Markdown Table",
      "symbolName": "tablecells",
      "isEnabled": true,
      "requiresTrustConfirmation": false,
      "sortOrder": 0,
      "mode": "scriptFilter",
      "supportedTypes": [
        "text",
        "json",
        "fileURL"
      ],
      "script": {
        "source": "inline",
        "code": "ObjC.import(\"Foundation\");\n\nfunction readJSON() {\n    var data = $.NSFileHandle.fileHandleWithStandardInput.readDataToEndOfFile;\n    var text = $.NSString.alloc.initWithDataEncoding(data, $.NSUTF8StringEncoding);\n    if (!text) throw new Error(\"Workflow input is not valid UTF-8\");\n    return JSON.parse(text.js);\n}\n\nfunction writeJSON(value) {\n    var text = JSON.stringify(value);\n    var data = $(text).dataUsingEncoding($.NSUTF8StringEncoding);\n    $.NSFileHandle.fileHandleWithStandardOutput.writeData(data);\n}\n\nfunction textResult(value) {\n    writeJSON({\n        version: 1,\n        result: {\n            type: \"text\",\n            value: String(value)\n        }\n    });\n}\n\nfunction filesResult(paths) {\n    writeJSON({\n        version: 1,\n        result: {\n            type: \"files\",\n            paths: paths\n        }\n    });\n}\n\nfunction preview(value) {\n    var compact = String(value).replace(/\\s+/g, \" \").trim();\n    return compact.length > 90 ? compact.slice(0, 87) + \"...\" : compact;\n}\n\nfunction normalizedLineEndings(value) {\n    return String(value).replace(/\\r\\n?/g, \"\\n\");\n}\n\nfunction fileRepresentations(payload) {\n    var items = (payload.paste && payload.paste.representations) || [];\n    return items.filter(function (item) {\n        return item.path && String(item.path).length > 0;\n    });\n}\n\nfunction inputFileRepresentations(payload, allowedTypes) {\n    var pasteType = payload.paste && payload.paste.type;\n    if (allowedTypes.indexOf(pasteType) < 0) return [];\n    return fileRepresentations(payload);\n}\n\nfunction firstInputFile(payload, allowedTypes) {\n    var files = inputFileRepresentations(payload, allowedTypes);\n    return files.length ? files[0] : null;\n}\n\nfunction readFileData(filePath) {\n    var data = $.NSData.dataWithContentsOfFile($(filePath));\n    if (!data) throw new Error(\"Unable to read input file\");\n    return data;\n}\n\nfunction utf8String(data) {\n    var text = $.NSString.alloc.initWithDataEncoding(data, $.NSUTF8StringEncoding);\n    return text ? text.js : null;\n}\n\nfunction readFileText(filePath) {\n    var text = utf8String(readFileData(filePath));\n    if (text === null) throw new Error(\"Input file is not valid UTF-8 text\");\n    return text;\n}\n\nfunction safeFileName(value) {\n    var name = String(value || \"output\").split(\"/\").pop();\n    name = name.replace(/[:\\u0000-\\u001f]/g, \"-\");\n    return name || \"output\";\n}\n\nfunction uniqueFileName(prefix, extensionName) {\n    var timestamp = Math.floor(Date.now() / 1000);\n    var suffix = $.NSUUID.UUID.UUIDString.js.slice(0, 8).toLowerCase();\n    return prefix + \"-\" + timestamp + \"-\" + suffix + \".\" + extensionName;\n}\n\nfunction outputPath(payload, fileName) {\n    return payload.directories.output + \"/\" + safeFileName(fileName);\n}\n\nfunction runTask(executable, arguments) {\n    var task = $.NSTask.alloc.init;\n    var outputPipe = $.NSPipe.pipe;\n    var errorPipe = $.NSPipe.pipe;\n    task.launchPath = executable;\n    task.arguments = $(arguments);\n    task.standardOutput = outputPipe;\n    task.standardError = errorPipe;\n    task.launch;\n    task.waitUntilExit;\n\n    var stdoutData = outputPipe.fileHandleForReading.readDataToEndOfFile;\n    var stderrData = errorPipe.fileHandleForReading.readDataToEndOfFile;\n    var stdoutText = $.NSString.alloc.initWithDataEncoding(\n        stdoutData,\n        $.NSUTF8StringEncoding\n    );\n    var stderrText = $.NSString.alloc.initWithDataEncoding(\n        stderrData,\n        $.NSUTF8StringEncoding\n    );\n    return {\n        status: Number(task.terminationStatus),\n        stdout: stdoutText ? stdoutText.js : \"\",\n        stderr: stderrText ? stderrText.js : \"\"\n    };\n}\n\nfunction inputText(payload) {\n    var representation = firstInputFile(payload, [\"fileURL\"]);\n    return representation ? readFileText(representation.path) :\n        ((payload.paste && payload.paste.text) || \"\");\n}\n\nfunction inputExtension(payload) {\n    var representation = firstInputFile(payload, [\"fileURL\"]);\n    if (!representation) return \"\";\n    var name = String(representation.fileName || representation.path || \"\");\n    var separator = name.lastIndexOf(\".\");\n    return separator >= 0 ? name.slice(separator + 1).toLowerCase() : \"\";\n}\n\nfunction parseCSV(input) {\n    var text = normalizedLineEndings(input);\n    var rows = [];\n    var row = [];\n    var cell = \"\";\n    var quoted = false;\n\n    for (var index = 0; index < text.length; index++) {\n        var character = text.charAt(index);\n        if (quoted) {\n            if (character === \"\\\"\") {\n                if (text.charAt(index + 1) === \"\\\"\") {\n                    cell += \"\\\"\";\n                    index += 1;\n                } else {\n                    quoted = false;\n                }\n            } else {\n                cell += character;\n            }\n        } else if (character === \"\\\"\" && cell.length === 0) {\n            quoted = true;\n        } else if (character === \",\") {\n            row.push(cell);\n            cell = \"\";\n        } else if (character === \"\\n\") {\n            row.push(cell);\n            rows.push(row);\n            row = [];\n            cell = \"\";\n        } else {\n            cell += character;\n        }\n    }\n    if (quoted) throw new Error(\"CSV contains an unterminated quoted field\");\n    row.push(cell);\n    rows.push(row);\n    if (rows.length > 1 && rows[rows.length - 1].length === 1 &&\n        rows[rows.length - 1][0] === \"\" && /\\n$/.test(text)) {\n        rows.pop();\n    }\n    if (!rows.length || !rows[0].length) throw new Error(\"CSV is empty\");\n    return rows;\n}\n\nfunction headersFrom(row) {\n    var used = {};\n    return row.map(function (value, index) {\n        var base = String(value).trim() || \"column\" + String(index + 1);\n        var name = base;\n        var suffix = 2;\n        while (used[name]) {\n            name = base + \"_\" + String(suffix);\n            suffix += 1;\n        }\n        used[name] = true;\n        return name;\n    });\n}\n\nfunction csvToObjects(input) {\n    var rows = parseCSV(input);\n    var headers = headersFrom(rows[0]);\n    return rows.slice(1).filter(function (row) {\n        return row.some(function (value) { return value.length > 0; });\n    }).map(function (row) {\n        var object = {};\n        headers.forEach(function (header, index) {\n            object[header] = index < row.length ? row[index] : \"\";\n        });\n        return object;\n    });\n}\n\nfunction parseObjectArray(input) {\n    var value = JSON.parse(input);\n    if (!Array.isArray(value) || !value.every(function (item) {\n        return item && typeof item === \"object\" && !Array.isArray(item);\n    })) {\n        throw new Error(\"JSON input must be an array of objects\");\n    }\n    return value;\n}\n\nfunction columnsFrom(objects) {\n    var seen = {};\n    var columns = [];\n    objects.forEach(function (object) {\n        Object.keys(object).forEach(function (key) {\n            if (!seen[key]) {\n                seen[key] = true;\n                columns.push(key);\n            }\n        });\n    });\n    return columns;\n}\n\nfunction scalar(value) {\n    if (value === null || value === undefined) return \"\";\n    return typeof value === \"object\" ? JSON.stringify(value) : String(value);\n}\n\nfunction csvCell(value) {\n    var text = scalar(value);\n    return /[\",\\r\\n]/.test(text) ? \"\\\"\" + text.replace(/\"/g, \"\\\"\\\"\") + \"\\\"\" : text;\n}\n\nfunction objectsToCSV(objects) {\n    var columns = columnsFrom(objects);\n    if (!columns.length) throw new Error(\"JSON objects do not contain fields\");\n    var rows = [columns.map(csvCell).join(\",\")];\n    objects.forEach(function (object) {\n        rows.push(columns.map(function (column) {\n            return csvCell(object[column]);\n        }).join(\",\"));\n    });\n    return rows.join(\"\\n\");\n}\n\nfunction markdownCell(value) {\n    return scalar(value).replace(/\\|/g, \"\\\\|\").replace(/\\r?\\n/g, \"<br>\");\n}\n\nfunction objectsToMarkdown(objects) {\n    var columns = columnsFrom(objects);\n    if (!columns.length) throw new Error(\"JSON objects do not contain fields\");\n    var rows = [\n        \"| \" + columns.map(markdownCell).join(\" | \") + \" |\",\n        \"| \" + columns.map(function () { return \"---\"; }).join(\" | \") + \" |\"\n    ];\n    objects.forEach(function (object) {\n        rows.push(\"| \" + columns.map(function (column) {\n            return markdownCell(object[column]);\n        }).join(\" | \") + \" |\");\n    });\n    return rows.join(\"\\n\");\n}\n\nfunction main() {\n    var payload = readJSON();\n    var extensionName = inputExtension(payload);\n    var hasFile = firstInputFile(payload, [\"fileURL\"]) !== null;\n    var inline = (payload.paste && payload.paste.text) || \"\";\n    if (hasFile && extensionName !== \"csv\" && extensionName !== \"json\") {\n        if (payload.phase === \"filter\") {\n            writeJSON({ version: 1, actions: [] });\n            return;\n        }\n        throw new Error(\"CSV / JSON Converter requires a .csv or .json file\");\n    }\n    var appearsJSON = payload.paste.type === \"json\" ||\n        extensionName === \"json\" ||\n        (!extensionName && /^\\s*\\[/.test(inline));\n\n    if (payload.phase === \"filter\") {\n        var actions = appearsJSON\n            ? [\n                {\n                    id: \"jsonToCSV\",\n                    title: \"JSON to CSV\",\n                    subtitle: \"Convert an object array to CSV\",\n                    icon: \"tablecells\",\n                    argument: { operation: \"jsonToCSV\" }\n                },\n                {\n                    id: \"jsonToMarkdown\",\n                    title: \"JSON Array to Markdown Table\",\n                    subtitle: \"Create a Markdown table\",\n                    icon: \"tablecells.badge.ellipsis\",\n                    argument: { operation: \"jsonToMarkdown\" }\n                }\n            ]\n            : [\n                {\n                    id: \"csvToJSON\",\n                    title: \"CSV to JSON\",\n                    subtitle: \"Use the first row as field names\",\n                    icon: \"curlybraces\",\n                    argument: { operation: \"csvToJSON\" }\n                }\n            ];\n        writeJSON({ version: 1, actions: actions });\n        return;\n    }\n\n    if (payload.phase === \"execute\") {\n        var operation = (payload.argument && payload.argument.operation) || payload.actionID;\n        var input = inputText(payload);\n        if (operation === \"csvToJSON\") {\n            textResult(JSON.stringify(csvToObjects(input), null, 2));\n        } else if (operation === \"jsonToCSV\") {\n            textResult(objectsToCSV(parseObjectArray(input)));\n        } else if (operation === \"jsonToMarkdown\") {\n            textResult(objectsToMarkdown(parseObjectArray(input)));\n        } else {\n            throw new Error(\"Unknown CSV / JSON conversion\");\n        }\n        return;\n    }\n    throw new Error(\"Unsupported workflow phase\");\n}\n\nmain();\n",
        "scriptPath": "",
        "interpreterPath": "/usr/bin/osascript",
        "arguments": [
          "-l",
          "JavaScript"
        ],
        "workingDirectory": "",
        "filterTimeout": 5,
        "executeTimeout": 15
      }
    }
  ]
}
