regex-wand
Version:
Typed regular expressions without giving up readable regex authoring.
319 lines (316 loc) • 10.2 kB
JavaScript
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