fix: migrate placeholders after encoded path separators

This commit is contained in:
muena
2026-09-24 19:25:06 +02:00
parent ae62c6f95c
commit 8e928bc69f
2 changed files with 37 additions and 1 deletions

View File

@@ -61,3 +61,36 @@ it("migriert einen künstlichen Altbestand verlustarm ohne den Quellordner zu ä
expect(restorePath(file.output, run.mapping, run.matchModes)).toBe("Müller GmbH.txt"); expect(restorePath(file.output, run.mapping, run.matchModes)).toBe("Müller GmbH.txt");
expect(await fs.readFile(join(project, legacyName), "utf8")).toBe(legacyContent); expect(await fs.readFile(join(project, legacyName), "utf8")).toBe(legacyContent);
}); });
it("ersetzt Platzhalter hinter URL-kodierten Pfadtrennern im Dateinamen", async () => {
const root = await fs.mkdtemp(join(tmpdir(), "velum-encoded-name-test-"));
roots.push(root);
const project = join(root, "Projekt");
const targetSupport = join(root, "VelumSupport");
const manifestFile = join(root, "OldSupport", "projects", "manifest.json");
const previewFile = join(root, "Vorschau.md");
await fs.mkdir(join(project, "KUNDE_A"), { recursive: true });
await fs.mkdir(join(root, "OldSupport", "projects"), { recursive: true });
const source = "KUNDE_A/%2fkunde_a%2fzc.asddls";
await fs.writeFile(join(project, source), "Technischer Inhalt");
await fs.writeFile(manifestFile, JSON.stringify({
version: 1, projectPath: project,
projectTerms: [{ id: "t1", term: "Alpha GmbH", category: "Kunde", enabled: true, aliases: [] }],
variantTable: [
{ termId: "t1", termVariant: "Alpha GmbH", placeholderVariant: "KUNDE_A", style: "typed", boundary: "strict" },
{ termId: "t1", termVariant: "alpha_gmbh", placeholderVariant: "kunde_a", style: "snake", boundary: "strict" },
],
}));
const preview = await migrate({ project, manifestFile, targetSupport, previewFile });
expect(preview.summary.replacements).toBe(2);
const report = await fs.readFile(previewFile, "utf8");
expect(report).toContain('"KUNDE_A/%2fkunde_a%2fzc.asddls"');
expect(report).toContain('"KUNDE_100001/%2fKUNDE_100002%2fzc.asddls"');
expect(report).not.toContain("Alpha GmbH");
const result = await migrate({ project, manifestFile, targetSupport, apply: true });
const run = JSON.parse(await fs.readFile(join(result.runDir!, "run.json"), "utf8"));
expect(restorePath(run.files[0].output, run.mapping, run.matchModes))
.toBe("Alpha GmbH/%2falpha_gmbh%2fzc.asddls");
expect(await fs.readFile(join(project, source), "utf8")).toBe("Technischer Inhalt");
});

View File

@@ -11,6 +11,9 @@ const excluded = new Set([".git", "node_modules", "dist", "build", "out", ".next
".venv", "venv", "__pycache__", "target", "vendor", "coverage", ".cache", ".turbo", ".venv", "venv", "__pycache__", "target", "vendor", "coverage", ".cache", ".turbo",
".idea", ".vscode", "Pods", "DerivedData"]); ".idea", ".vscode", "Pods", "DerivedData"]);
const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
// In URL-kodierten Dateinamen steht nach %2f unmittelbar ein "f" vor dem Platzhalter.
// Das ist ein Pfadtrenner, kein angrenzender Buchstabe des Bezeichners.
const strictPrefix = "(?:(?<![\\p{L}\\p{N}])|(?<=%2[fF]))";
const hasSeparator = (value) => /[^\p{L}\p{N}]/u.test(value); const hasSeparator = (value) => /[^\p{L}\p{N}]/u.test(value);
const shape = (value) => !/\p{L}/u.test(value) ? "mixed" const shape = (value) => !/\p{L}/u.test(value) ? "mixed"
: value === value.toUpperCase() ? "upper" : value === value.toLowerCase() ? "lower" : "mixed"; : value === value.toUpperCase() ? "upper" : value === value.toLowerCase() ? "lower" : "mixed";
@@ -52,7 +55,7 @@ export function createConverter(manifest, globalTerms = []) {
})).sort((a, b) => b.needle.length - a.needle.length || a.needle.localeCompare(b.needle)); })).sort((a, b) => b.needle.length - a.needle.length || a.needle.localeCompare(b.needle));
const lookup = new Map(needles.map((item) => [item.needle, item])); const lookup = new Map(needles.map((item) => [item.needle, item]));
const pattern = needles.length ? new RegExp(needles.map((item) => item.strict const pattern = needles.length ? new RegExp(needles.map((item) => item.strict
? `(?<![\\p{L}\\p{N}])${escapeRegex(item.needle)}(?![\\p{L}\\p{N}])` ? `${strictPrefix}${escapeRegex(item.needle)}(?![\\p{L}\\p{N}])`
: escapeRegex(item.needle)).join("|"), "gu") : null; : escapeRegex(item.needle)).join("|"), "gu") : null;
const mapping = {}; const mapping = {};
const matchModes = {}; const matchModes = {};