UNPKG

regex-wand

Version:

Typed regular expressions without giving up readable regex authoring.

319 lines (316 loc) 10.2 kB
var __defProp = Object.defineProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; // src/transform.ts import { createContext, runInContext } from "vm"; import { parse as parseAcorn } from "acorn"; import { createUnplugin } from "unplugin"; // src/index.ts var index_exports = {}; __export(index_exports, { anyOf: () => anyOf, carriageReturn: () => carriageReturn, caseInsensitive: () => caseInsensitive, char: () => char, charIn: () => charIn, charNotIn: () => charNotIn, createExactRegExp: () => createExactRegExp, createExactRegExpWithFlags: () => createExactRegExpWithFlags, createRegExp: () => createRegExp, createRegExpWithFlags: () => createRegExpWithFlags, defineRegex: () => defineRegex, digit: () => digit, dotAll: () => dotAll, exactly: () => exactly, fromMagic: () => fromMagic, fromMagicAs: () => fromMagicAs, global: () => global, letter: () => letter, linefeed: () => linefeed, maybe: () => maybe, multiline: () => multiline, not: () => not, oneOrMore: () => oneOrMore, sticky: () => sticky, tab: () => tab, unicode: () => unicode, whitespace: () => whitespace, withIndices: () => withIndices, word: () => word, wordBoundary: () => wordBoundary, wordChar: () => wordChar }); import { createRegExp as createMagicRegExp, exactly as magicExactly } from "magic-regexp"; import { anyOf, carriageReturn, caseInsensitive, char, charIn, charNotIn, digit, dotAll, exactly, global, letter, linefeed, maybe, multiline, not, oneOrMore, sticky, tab, unicode, whitespace, withIndices, word, wordBoundary, wordChar } from "magic-regexp"; function fromMagic(magic) { const ark = new RegExp(magic.source, magic.flags); return Object.assign(ark, { magic, ark, toRegExp: () => new RegExp(magic.source, magic.flags) }); } function fromMagicAs(magic) { return fromMagic(magic); } function defineRegex(options) { const { flags, match = "contains" } = options; const inputs = "inputs" in options ? options.inputs : options.pattern; if (flags !== void 0) { return match === "exact" ? createExactRegExpWithFlags(inputs, flags) : createRegExpWithFlags(inputs, flags); } return match === "exact" ? createExactRegExp(...inputs) : createRegExp(...inputs); } function createRegExp(...inputs) { return fromMagic(createMagicRegExp(...inputs)); } function createRegExpWithFlags(inputs, ...flags) { const flagInput = normalizeFlagInput(flags); const magic = createMagicRegExpWithFlagInput(inputs, flagInput); return fromMagic(magic); } function createExactRegExp(...inputs) { return fromMagic( createMagicRegExp( magicExactly(...inputs).at.lineStart().at.lineEnd() ) ); } function createExactRegExpWithFlags(inputs, ...flags) { const anchored = magicExactly(...inputs).at.lineStart().at.lineEnd(); const flagInput = normalizeFlagInput(flags); const magic = createMagicRegExpWithFlagInput([anchored], flagInput); return fromMagic(magic); } function normalizeFlagInput(flags) { if (flags.length !== 1) { return [...flags]; } const candidate = flags[0]; if (typeof candidate === "string") { return [...candidate]; } return candidate; } function createMagicRegExpWithFlagInput(inputs, flagInput) { return createMagicRegExp(...inputs, flagInput); } // src/transform.ts var REGEX_WAND_SPECIFIER = "regex-wand"; var WRAPPER_EXPORTS = /* @__PURE__ */ new Set([ "defineRegex", "createRegExp", "createExactRegExp", "createRegExpWithFlags", "createExactRegExpWithFlags" ]); var REGEX_WAND_CONTEXT = { ...index_exports }; var RegexWandTransformPlugin = createUnplugin(() => { return { name: "RegexWandTransformPlugin", enforce: "post", transformInclude(id) { const queryIndex = id.indexOf("?"); const pathname = queryIndex >= 0 ? id.slice(0, queryIndex) : id; const search = queryIndex >= 0 ? id.slice(queryIndex) : ""; const type = search ? new URLSearchParams(search).get("type") : null; if (pathname.endsWith(".vue") && (!search || type === "script")) return true; return /\.((c|m)?j|t)sx?$/.test(pathname); }, transform(code) { if (!code.includes(REGEX_WAND_SPECIFIER)) return; const ast = parseCode(code, this.parse); const contextMap = { ...REGEX_WAND_CONTEXT }; const wrapperNames = /* @__PURE__ */ new Set(); const namespaceNames = /* @__PURE__ */ new Set(); const regexWandImports = []; let hasRelevantImport = false; walkAST(ast, (node) => { if (node.type !== "ImportDeclaration") return; const declaration = node; if (declaration.source?.value !== REGEX_WAND_SPECIFIER) return; hasRelevantImport = true; const localNames = declaration.specifiers?.map((specifier) => specifier.local?.name).filter((name) => Boolean(name)); if (declaration.start != null && declaration.end != null && localNames?.length) { regexWandImports.push({ start: declaration.start, end: declaration.end, localNames }); } for (const specifier of declaration.specifiers ?? []) { if (specifier.type === "ImportNamespaceSpecifier" && specifier.local?.name) { namespaceNames.add(specifier.local.name); contextMap[specifier.local.name] = index_exports; continue; } if (specifier.type !== "ImportSpecifier" || !specifier.local?.name) { continue; } const importedName = getImportedName(specifier); if (!importedName || !(importedName in index_exports)) continue; contextMap[specifier.local.name] = REGEX_WAND_CONTEXT[importedName]; if (WRAPPER_EXPORTS.has(importedName)) { wrapperNames.add(specifier.local.name); } } }); if (!hasRelevantImport) return; const context = createContext(contextMap); const replacements = []; walkAST(ast, (node) => { if (node.type !== "CallExpression") return; if (node.start == null || node.end == null) return; const call = node; if (!isRegexWandBuilderCall(call, wrapperNames, namespaceNames)) return; try { const value = runInContext(code.slice(node.start, node.end), context); if (!(value instanceof RegExp)) return; replacements.push({ start: node.start, end: node.end, value: createAdapterExpression(value) }); } catch { } }); if (replacements.length === 0) return; return { code: cleanupUnusedRegexWandImports( applyReplacements(code, replacements), regexWandImports ), map: null }; } }; }); var transform_default = RegexWandTransformPlugin; function walkAST(node, enter) { enter(node); for (const value of Object.values(node)) { if (!value || typeof value !== "object") continue; if (Array.isArray(value)) { for (const child of value) { if (isAstNode(child)) walkAST(child, enter); } continue; } if (isAstNode(value)) walkAST(value, enter); } } function isAstNode(value) { return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string"; } function parseCode(code, parse) { try { return parse(code); } catch (error) { if (!(error instanceof Error) || !error.message.includes("Parse implementation is not set")) { throw error; } } return parseAcorn(code, { ecmaVersion: "latest", sourceType: "module" }); } function getImportedName(specifier) { const imported = specifier.imported; if (!imported) return void 0; if (imported.type === "Identifier") return imported.name; return typeof imported.value === "string" ? imported.value : void 0; } function isRegexWandBuilderCall(call, wrapperNames, namespaceNames) { const callee = call.callee; if (!callee) return false; if (callee.type === "Identifier") { return wrapperNames.has(callee.name ?? ""); } if (callee.type !== "MemberExpression") return false; const member = callee; return namespaceNames.has(member.object?.name ?? "") && WRAPPER_EXPORTS.has(member.property?.name ?? ""); } function createAdapterExpression(value) { const literal = value.toString(); return `/*#__PURE__*/ (() => { const regex = ${literal}; return Object.assign(regex, { magic: ${literal}, ark: regex, toRegExp: () => new RegExp(regex.source, regex.flags) }) })()`; } function applyReplacements(code, replacements) { let result = code; for (const replacement of replacements.sort( (left, right) => right.start - left.start )) { result = result.slice(0, replacement.start) + replacement.value + result.slice(replacement.end); } return result; } function cleanupUnusedRegexWandImports(code, imports) { const codeWithoutImports = blankRanges(code, imports); const removals = imports.filter( (declaration) => declaration.localNames.every( (localName) => !hasIdentifier(codeWithoutImports, localName) ) ); if (removals.length === 0) return code; return applyReplacements( code, removals.map((declaration) => ({ start: declaration.start, end: includeTrailingLineBreak(code, declaration.end), value: "" })) ); } function blankRanges(code, ranges) { let result = code; for (const range of ranges.sort((left, right) => right.start - left.start)) { result = result.slice(0, range.start) + " ".repeat(range.end - range.start) + result.slice(range.end); } return result; } function hasIdentifier(code, identifier) { return new RegExp(`\\b${escapeRegExp(identifier)}\\b`).test(code); } function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } function includeTrailingLineBreak(code, end) { if (code[end] === "\r" && code[end + 1] === "\n") return end + 2; if (code[end] === "\n") return end + 1; return end; } export { RegexWandTransformPlugin, transform_default as default }; //# sourceMappingURL=transform.js.map