Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
64 changes: 39 additions & 25 deletions src/content/__tests__/escapeDrawtext.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,63 +3,77 @@ import { describe, it, expect } from "vitest";
import { escapeDrawtext } from "../escapeDrawtext";

describe("escapeDrawtext", () => {
it("replaces straight apostrophes with escaped quote safe for drawtext", () => {
it("removes straight apostrophes", () => {
const result = escapeDrawtext("didn't");

expect(result).not.toContain("'");
// Must NOT use U+02BC (modifier letter apostrophe) — most fonts lack this glyph
expect(result).not.toContain("\u02BC");
expect(result).toContain("didn");
expect(result).toBe("didnt");
});

it("preserves curly right single quotation marks as-is", () => {
it("removes curly right single quotation marks", () => {
const result = escapeDrawtext("didn\u2019t");

// U+2019 is the replacement char — it should remain
expect(result).toContain("\u2019");
expect(result).not.toContain("\u02BC");
expect(result).not.toContain("\u2019");
expect(result).toBe("didnt");
});

it("replaces curly left single quotation marks with right single quotation mark", () => {
it("removes curly left single quotation marks", () => {
const result = escapeDrawtext("\u2018hello\u2019");

expect(result).not.toContain("\u2018");
expect(result).not.toContain("\u02BC");
// Both should become U+2019
expect(result).toBe("\u2019hello\u2019");
expect(result).not.toContain("\u2019");
expect(result).toBe("hello");
});

it("escapes colons for ffmpeg", () => {
const result = escapeDrawtext("caption: hello");

expect(result).toContain("\\\\:");
});

it("escapes percent to %% for a single literal % in ffmpeg drawtext", () => {
it("escapes percent to %% for ffmpeg drawtext", () => {
const result = escapeDrawtext("100%");

// ffmpeg drawtext: %% renders as single %. So "100%" should become "100%%".
expect(result).toBe("100%%");
});

it("escapes backslashes", () => {
const result = escapeDrawtext("back\\slash");

expect(result).toContain("\\\\\\\\");
});

it("strips newlines and carriage returns", () => {
expect(escapeDrawtext("line1\nline2")).toBe("line1 line2");
expect(escapeDrawtext("line1\r\nline2")).toBe("line1 line2");
expect(escapeDrawtext("line1\rline2")).toBe("line1line2");
});

it("produces text safe inside ffmpeg single-quoted drawtext in filter_complex", () => {
const result = escapeDrawtext("you're my addiction");
expect(result).not.toContain("'");
expect(result).not.toContain("\u2019");
expect(result).toBe("youre my addiction");
});

it("removes double quotes", () => {
const result = escapeDrawtext('"hello world"');
expect(result).not.toContain('"');
expect(result).toBe("hello world");
});

it("removes emoji", () => {
const result = escapeDrawtext("fire 🔥🎶 music");
expect(result).not.toContain("🔥");
expect(result).not.toContain("🎶");
});
Comment on lines +58 to +62
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Assert the normalized output here, not just emoji absence.

This test would still pass if the function returned fire music or left leading/trailing spaces, so the new collapse/trim behavior on src/content/escapeDrawtext.ts Lines 22-23 can regress unnoticed.

✅ Tighten the expectation
   it("removes emoji", () => {
     const result = escapeDrawtext("fire 🔥🎶 music");
-    expect(result).not.toContain("🔥");
-    expect(result).not.toContain("🎶");
+    expect(result).toBe("fire music");
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it("removes emoji", () => {
const result = escapeDrawtext("fire 🔥🎶 music");
expect(result).not.toContain("🔥");
expect(result).not.toContain("🎶");
});
it("removes emoji", () => {
const result = escapeDrawtext("fire 🔥🎶 music");
expect(result).toBe("fire music");
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/content/__tests__/escapeDrawtext.test.ts` around lines 58 - 62, Update
the test for escapeDrawtext to assert the exact normalized output (including
collapsed whitespace and trimming) rather than only checking emoji absence: call
escapeDrawtext("fire 🔥🎶 music") and expect the returned string to equal the
normalized "fire music" (no extra spaces), referencing the escapeDrawtext
function and the new collapse/trim behavior added around lines 22-23 so
regressions in whitespace collapsing/trimming are caught.


it("handles the exact failing caption from production", () => {
const result = escapeDrawtext('Desire ignites: "Yo quiero un chin, tu eres mía." 🎶🔥 #Intensity #LaEquis');
expect(result).not.toContain('"');
expect(result).not.toContain("'");
expect(result).not.toContain("🎶");
expect(result).not.toContain("🔥");
expect(result).toContain("Desire ignites");
expect(result).toContain("Yo quiero");
});

it("handles a real caption with apostrophes and special chars", () => {
const result = escapeDrawtext("didn't think anyone would hear this: it's real");

// Should not contain raw single quotes, left curly quotes, or U+02BC
expect(result).not.toMatch(/['\u2018\u2032\u02BC]/);
// Should contain escaped colon
expect(result).not.toMatch(/['\u2018\u2019\u2032]/);
expect(result).toContain("\\\\:");
});
});
133 changes: 0 additions & 133 deletions src/content/__tests__/generateContentImage.test.ts

This file was deleted.

16 changes: 10 additions & 6 deletions src/content/escapeDrawtext.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
/**
* Escapes a text string for use in ffmpeg drawtext filters.
*
* Handles both -vf and filter_complex contexts by replacing all
* quote-like characters with the right single quotation mark (U+2019),
* which renders as an apostrophe in all standard fonts and is not
* parsed as a delimiter by ffmpeg.
* Handles both -vf and filter_complex contexts by removing all
* characters that could break ffmpeg's parser: quotes, emoji,
* and escaping colons/backslashes/percent signs.
*
* @param text - Raw caption text
* @returns Escaped text safe for ffmpeg drawtext
Expand All @@ -14,7 +13,12 @@ export function escapeDrawtext(text: string): string {
.replace(/\r/g, "")
.replace(/\n/g, " ")
.replace(/\\/g, "\\\\\\\\")
.replace(/['\u2018\u2032]/g, "\u2019")
.replace(/['\u2018\u2019\u2032""\u201C\u201D]/g, "")
.replace(/[\u{1F000}-\u{1FFFF}]/gu, "")
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 10, 2026

Choose a reason for hiding this comment

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

P2: Emoji stripping misses Zero Width Joiners (U+200D) and several common emoji ranges (U+2300–23FF, U+2B00–2BFF). Compound emoji like 👨‍👩‍👧 will have their visible codepoints removed but leave invisible ZWJ characters in the output, which can still break ffmpeg's parser.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/content/escapeDrawtext.ts, line 17:

<comment>Emoji stripping misses Zero Width Joiners (U+200D) and several common emoji ranges (U+2300–23FF, U+2B00–2BFF). Compound emoji like 👨‍👩‍👧 will have their visible codepoints removed but leave invisible ZWJ characters in the output, which can still break ffmpeg's parser.</comment>

<file context>
@@ -13,7 +13,12 @@ export function escapeDrawtext(text: string): string {
     .replace(/\\/g, "\\\\\\\\")
-    .replace(/['\u2018\u2019\u2032]/g, "")
+    .replace(/['\u2018\u2019\u2032""\u201C\u201D]/g, "")
+    .replace(/[\u{1F000}-\u{1FFFF}]/gu, "")
+    .replace(/[\u{2600}-\u{27BF}]/gu, "")
+    .replace(/[\u{FE00}-\u{FE0F}]/gu, "")
</file context>
Fix with Cubic

.replace(/[\u{2600}-\u{27BF}]/gu, "")
.replace(/[\u{FE00}-\u{FE0F}]/gu, "")
.replace(/:/g, "\\\\:")
.replace(/%/g, "%%");
.replace(/%/g, "%%")
.replace(/\s{2,}/g, " ")
.trim();
}
Loading
Loading