{
  "version": 1,
  "workflows": [
    {
      "id": "50D21B65-C442-426A-98D5-D6DA219ED2A2",
      "workflowVersion": 1,
      "name": "Extract Information",
      "summary": "URLs · Emails · Phones · IPs · Hashtags",
      "symbolName": "text.magnifyingglass",
      "isEnabled": true,
      "requiresTrustConfirmation": false,
      "sortOrder": 0,
      "mode": "scriptFilter",
      "supportedTypes": [
        "text",
        "markdown",
        "html",
        "json",
        "url"
      ],
      "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 unique(values) {\n    var seen = {};\n    return values.filter(function (value) {\n        var key = String(value).toLowerCase();\n        if (seen[key]) return false;\n        seen[key] = true;\n        return true;\n    });\n}\n\nfunction trimURL(value) {\n    return value.replace(/[),.;!?\\]}]+$/g, \"\");\n}\n\nfunction validIPv4(value) {\n    var parts = value.split(\".\");\n    return parts.length === 4 && parts.every(function (part) {\n        return /^\\d{1,3}$/.test(part) && Number(part) <= 255;\n    });\n}\n\nfunction emailExtractionSource(input) {\n    var source = String(input).replace(/(?:%[0-9A-F]{2})+/gi, function (sequence) {\n        try {\n            return decodeURIComponent(sequence);\n        } catch (_) {\n            return sequence;\n        }\n    });\n\n    if (/^https?:\\/\\//i.test(source)) {\n        var queryStart = source.indexOf(\"?\");\n        if (queryStart >= 0) {\n            var fragmentStart = source.indexOf(\"#\", queryStart);\n            var queryEnd = fragmentStart >= 0 ? fragmentStart : source.length;\n            source = source.slice(0, queryStart + 1) +\n                source.slice(queryStart + 1, queryEnd).replace(/\\+/g, \" \") +\n                source.slice(queryEnd);\n        }\n    }\n    return source;\n}\n\nfunction extractEmails(input) {\n    var source = emailExtractionSource(input);\n    var expression = /(?:^|[\\s(<\\[{'\",&=?\\/#])([A-Z0-9.!#$%&'*+\\/=?^_{|}~-]+@[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?(?:\\.[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?)+)(?=$|[^A-Z0-9.-])/gi;\n    var values = [];\n    var match;\n    while ((match = expression.exec(source)) !== null) {\n        var email = match[1];\n        var localPart = email.split(\"@\")[0];\n        if (localPart.charAt(0) === \".\" ||\n            localPart.charAt(localPart.length - 1) === \".\" ||\n            localPart.indexOf(\"..\") >= 0) {\n            continue;\n        }\n        values.push(email);\n    }\n    return unique(values);\n}\n\nfunction extract(input) {\n    var urls = unique((input.match(/\\bhttps?:\\/\\/[^\\s<>\"']+/gi) || []).map(trimURL));\n    var emails = extractEmails(input);\n    var phones = unique((input.match(/\\+?\\d[\\d\\s().-]{5,}\\d/g) || []).filter(function (value) {\n        if (validIPv4(value.trim())) return false;\n        var digits = value.replace(/\\D/g, \"\");\n        return digits.length >= 7 && digits.length <= 15;\n    }).map(function (value) {\n        return value.trim();\n    }));\n    var ipv4 = (input.match(/\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b/g) || []).filter(validIPv4);\n    var ipv6 = input.match(/\\b(?:[A-F0-9]{1,4}:){2,7}[A-F0-9]{0,4}\\b/gi) || [];\n    var hashtags = unique(input.match(/#[A-Za-z0-9_\\u00C0-\\uFFFF]+/g) || []);\n    return {\n        urls: urls,\n        emails: emails,\n        phones: phones,\n        ips: unique(ipv4.concat(ipv6)),\n        hashtags: hashtags\n    };\n}\n\nfunction main() {\n    var payload = readJSON();\n    var input = (payload.paste && payload.paste.text) || \"\";\n    var result = extract(input);\n    var definitions = [\n        { id: \"urls\", title: \"Extract URLs\", icon: \"link\" },\n        { id: \"emails\", title: \"Extract Emails\", icon: \"envelope\" },\n        { id: \"phones\", title: \"Extract Phone Numbers\", icon: \"phone\" },\n        { id: \"ips\", title: \"Extract IP Addresses\", icon: \"network\" },\n        { id: \"hashtags\", title: \"Extract Hashtags\", icon: \"number\" }\n    ];\n\n    if (payload.phase === \"filter\") {\n        writeJSON({\n            version: 1,\n            actions: definitions.filter(function (item) {\n                return result[item.id].length > 0;\n            }).map(function (item) {\n                var values = result[item.id];\n                return {\n                    id: item.id,\n                    title: item.title,\n                    subtitle: String(values.length) + \" found · \" + preview(values.join(\", \")),\n                    icon: item.icon,\n                    argument: { operation: item.id }\n                };\n            })\n        });\n        return;\n    }\n\n    if (payload.phase === \"execute\") {\n        var operation = (payload.argument && payload.argument.operation) || payload.actionID;\n        if (!result[operation] || !result[operation].length) {\n            throw new Error(\"No matching information found\");\n        }\n        textResult(result[operation].join(\"\\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
      }
    }
  ]
}
