Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/desktop/src/main/artifact-export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ function injectBaseHref(doc: string, baseHref: string | undefined): string {

function injectTitle(doc: string, title: string): string {
const tag = `<title>${escapeText(title)}</title>`;
if (/<title[^>]*>.*?<\/title>/is.test(doc)) return doc.replace(/<title[^>]*>.*?<\/title>/is, tag);
if (/<title[^>]*>.*?<\/title>/is.test(doc)) return doc.replace(/<title[^>]*>.*?<\/title>/is, () => tag);
if (/<head[^>]*>/i.test(doc)) return doc.replace(/<head[^>]*>/i, (m) => `${m}${tag}`);
return doc;
}
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/pdf-export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ function injectBaseHref(doc: string, baseHref: string | undefined): string {

function injectTitle(doc: string, title: string): string {
const tag = `<title>${escapeHtmlText(title)}</title>`;
if (/<title[^>]*>.*?<\/title>/is.test(doc)) return doc.replace(/<title[^>]*>.*?<\/title>/is, tag);
if (/<title[^>]*>.*?<\/title>/is.test(doc)) return doc.replace(/<title[^>]*>.*?<\/title>/is, () => tag);
if (/<head[^>]*>/i.test(doc)) return doc.replace(/<head[^>]*>/i, (match) => `${match}${tag}`);
if (/<html[^>]*>/i.test(doc)) return doc.replace(/<html[^>]*>/i, (match) => `${match}<head>${tag}</head>`);
return `<!doctype html><html><head>${tag}</head><body>${doc}</body></html>`;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

import { describe, expect, it } from "vitest";

const here = dirname(fileURLToPath(import.meta.url));
const desktopRoot = join(here, "../..");

function readSource(relativePath: string): string {
return readFileSync(join(desktopRoot, relativePath), "utf8");
}

// Extracts a top-level `function <name>(...) { ... }` declaration. The export
// pipeline helpers all end with a column-0 closing brace, so the first
// newline-followed-by-`}` after the declaration is that function's own end
// (the same convention the sibling artifact-export-image-height test relies on).
function extractFunction(source: string, name: string): string {
const start = source.indexOf(`function ${name}(`);
if (start < 0) throw new Error(`${name} not found in source`);
const end = source.indexOf("\n}", start);
if (end < 0) throw new Error(`closing brace for ${name} not found`);
return source.slice(start, end + 2);
}

// Strips the `name: string` / `): string` annotations from the extracted
// helper signatures so the pure helpers can compile as plain JavaScript. These
// helpers are dependency-free and carry no other type syntax in their bodies.
function stripTypeAnnotations(source: string): string {
return source
.replace(/\b(\w+)\s*:\s*string\b/g, "$1")
.replace(/\)\s*:\s*string\b/g, ")");
}

function compileInjectTitle(
source: string,
escapeName: string,
): (doc: string, title: string) => string {
const escape = stripTypeAnnotations(extractFunction(source, escapeName));
const injectTitle = stripTypeAnnotations(extractFunction(source, "injectTitle"));
const factory = new Function(`${escape}\n${injectTitle}\nreturn injectTitle;`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[non-blocking] Keep this regression at the real export seam

apps/desktop/tests/main/export-title-replacement-patterns.test.ts already drives exportPdfFromHtml and exportArtifact with the same replacement-pattern cases. This new Function path reads TypeScript text, strips annotations, and executes a reconstructed helper, so it duplicates coverage while bypassing the actual import/call path; later helper formatting or type-syntax changes can break this test without a product regression. Please move the exact-document assertion and the extra combined HTML/$& case into the existing export integration test, then remove this source-eval harness (or expose a shared pure helper if direct unit coverage is needed).

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

return factory() as (doc: string, title: string) => string;
}

const DOC = "<html><head><title>Old</title></head><body><p>BODY MARKER</p></body></html>";

// Titles carrying JavaScript `String.prototype.replace` replacement patterns.
// With a string replacement argument, ECMA-262 `GetSubstitution` expands
// `$$`, `$&`, `$\``, and `$'` inside the inserted title, corrupting the
// rendered document. Each escaped value is what the surrounding escape helper
// (`escapeHtmlText` / `escapeText`) should produce — `& < >` HTML-escaped,
// with every `$` sequence left intact.
const TITLES: ReadonlyArray<{ title: string; escaped: string }> = [
{ title: "Save $$$ This Quarter", escaped: "Save $$$ This Quarter" },
{ title: "Before $& After", escaped: "Before $&amp; After" },
{ title: "Rock $'n Roll Tour", escaped: "Rock $'n Roll Tour" },
{ title: "Price $`drop", escaped: "Price $`drop" },
{ title: "A <B> & $& C", escaped: "A &lt;B&gt; &amp; $&amp; C" },
];

const FILES = [
["pdf-export.ts", "escapeHtmlText"],
["artifact-export.ts", "escapeText"],
] as const;

function expectedDocument(escapedTitle: string): string {
return `<html><head><title>${escapedTitle}</title></head><body><p>BODY MARKER</p></body></html>`;
}

describe.each(FILES)("%s injectTitle", (file, escapeName) => {
const injectTitle = compileInjectTitle(readSource(join("src/main", file)), escapeName);

for (const { title, escaped } of TITLES) {
it(`inserts the title verbatim for ${JSON.stringify(title)}`, () => {
expect(injectTitle(DOC, title)).toBe(expectedDocument(escaped));
});
}
});
Loading