Skip to content

fix(gemini): preserve linked text and images in html and pdf exports (#110) - #140

Merged
deadrat-in merged 4 commits into
mainfrom
fix/gemini-links-and-images
Oct 19, 2026
Merged

deadrat-in merged 4 commits into
mainfrom
fix/gemini-links-and-images

Conversation

@deadrat-in

Copy link
Copy Markdown
Collaborator

Closes #110

Summary of Changes

  1. Gemini API Image & Media Placeholder Resolution:

    • In packages/decant-core/ai/gemini.js, resolved <Image ... src="image_agent_tag_..."> XML tags into standard Markdown image tags ![alt](url) using image metadata from candidate slot 12 (cand[12][1]).
    • Resolved http://googleusercontent.com/youtube_content/... placeholder links in cand[12][4] to clickable Markdown video links.
    • Extracted candidate-level citation URLs (cand[2]) into extractActivitySources so citations are preserved under **Sources consulted:**.
  2. Gemini DOM Link & Source Extraction:

    • Before stripping UI buttons in parseFromDom, inspected citation chips (citation-chip, button[aria-label*="View source"]) and extracted link URLs and title text, converting them into Markdown links or bracketed citations.
    • Unwrapped <a> links nested inside buttons to prevent link deletion.
    • Extracted source links from .sources-list before removal and appended them under **Sources consulted:**.
    • Unwrapped image-container and picture wrappers so images inside the Gemini markdown container are preserved by Turndown.
  3. HTML Formatter Defense:

    • In content/formatters/html.js (markdownToHtml), added fallback normalization converting any unparsed self-closing <Image ... src="..."> XML tags into standard Markdown images if src is a valid URL, ensuring HTML and PDF exports render them as <img>.
  4. Fixture Secret Sanitization (Alert Output formatting #8):

    • Sanitized Google Search AI client API keys in packages/decant-core/tests/fixtures/google-search-ai-chat.html with masked placeholder tokens.

Verification

  • npm test passing 100% across all 139 tests in decant-core and extension test suites.
  • npm run lint passed (0 errors, 0 warnings).
  • npm run format:check passed.
  • npm run build and npm run build:firefox succeeded.

…110)

- Resolve Gemini API <Image> tags to markdown images via candidate image tag map
- Resolve Gemini API youtube_content media placeholders to markdown links
- Preserve DOM citation chips, source buttons, and .sources-list in Gemini exports
- Add defensive fallback in html.js markdownToHtml for <Image> XML tags
- Sanitize Google Search AI client API keys in test fixtures to complete Alert #8
@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Preserve Gemini links and images in HTML and PDF exports

🐞 Bug fix 🧪 Tests 🕐 20-40 Minutes


AI Description

• Resolve Gemini API image and video placeholders into usable Markdown media and links.
• Preserve DOM citations, source links, and images when extracting Gemini responses.
• Render residual image tags in HTML exports and cover both extraction paths with tests.
Diagram

graph TD
  API["Gemini API"] --> Parser["Gemini parser"] --> Markdown["Markdown messages"] --> Formatter["HTML formatter"] --> Export["HTML and PDF"]
  DOM["Gemini DOM"] --> Parser
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Normalize images only in the Gemini parser
  • ➕ Keeps Gemini-specific handling out of the shared HTML formatter.
  • ➖ Previously stored or otherwise supplied messages with raw image tags would still render incorrectly.

Recommendation: Keep API media resolution in the Gemini parser, where candidate metadata is available, and retain the HTML fallback for unresolved tags. This covers both newly parsed conversations and content that reaches the formatter without parser normalization.

Files changed (4) +367 / -13

Bug fix (2) +232 / -13
html.jsRender residual Gemini image tags +14/-0

Render residual Gemini image tags

• Converts XML-style Image tags with supported image sources into Markdown images before HTML rendering. This provides a fallback when image tags reach the formatter unresolved.

content/formatters/html.js

gemini.jsPreserve Gemini media and citations during extraction +218/-13

Preserve Gemini media and citations during extraction

• Resolves API image tags and video placeholders using candidate metadata, and adds candidate citation URLs to consulted sources. DOM extraction now retains citation text, links inside buttons, source-list URLs, and wrapped images.

packages/decant-core/ai/gemini.js

Tests (2) +135 / -0
gemini.test.jsTest Gemini API and DOM media preservation +101/-0

Test Gemini API and DOM media preservation

• Adds fixture-based assertions for API image and YouTube link resolution. Adds a DOM regression test for citation text, button-contained links, images, and consulted sources.

packages/decant-core/tests/gemini.test.js

html-formatter.test.mjsTest rendered Gemini images and links +34/-0

Test rendered Gemini images and links

• Verifies that the HTML formatter renders Markdown images, residual XML-style image tags, and linked text.

tests/html-formatter.test.mjs

@coderabbitai

coderabbitai Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 5eb2da74-18ff-49c1-9b57-f9ad90be55f7
📥 Commits

Reviewing files that changed from the base of the PR and between 502637d and da3e10b.

📒 Files selected for processing: 3
  • content/formatters/html.js
  • packages/decant-core/ai/gemini.js
  • packages/decant-core/tests/gemini.test.js

Walkthrough

Gemini API conversion now resolves image tags and video placeholders from the selected candidate and collects citations from that candidate. DOM extraction preserves citation content, links, images and source-list content. The HTML formatter converts supported XML-style <Image> tags into Markdown images while preserving tags inside code. Tests cover these conversion and extraction paths.

Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: 🔵 Low · up to 50263

Some edge-case exports can show raw image tags, truncate an image URL, select a preview image, or rewrite a YouTube placeholder in code. The issues are bounded, so merge risk is low with follow-up appropriate.

Pre-merge checks: Passed 3 · Failed 2

Failed Failed checks: 2 warnings

Check name Status Explanation Resolution
Out of Scope Changes check Warning The API-key sanitisation in packages/decant-core/tests/fixtures/google-search-ai-chat.html has no demonstrated connection to Gemini link or image preservation in HTML and PDF exports under issue #110. Remove the fixture API-key sanitisation from this pull request, or move it to a separate security or fixture-maintenance pull request.
Docstring Coverage Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 4 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 3 passed
Check name Status Explanation
Title check Passed The title clearly identifies the Gemini fix and the preservation of linked text and images in HTML and PDF exports. It matches the main change.
Description check Passed The description directly explains the Gemini image, media, link, source extraction, formatter, test, and fixture changes. It is relevant to the changeset.
Linked Issues check Passed Issue #110 requires Gemini exports to preserve linked text and display images in HTML and PDF. The reported changes preserve citation-chip links, links inside buttons, source links, Gemini image tags,…
Full details: Linked Issues check

Explanation

Issue #110 requires Gemini exports to preserve linked text and display images in HTML and PDF. The reported changes preserve citation-chip links, links inside buttons, source links, Gemini image tags, image containers and picture elements. The HTML formatter converts eligible Image tags to Markdown images. Added tests cover links, images, media placeholders and code protection.

  • Fix all pre-merge checks with AI
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread content/formatters/html.js Outdated
Comment thread packages/decant-core/ai/gemini.js
Comment thread packages/decant-core/ai/gemini.js Fixed
Comment thread packages/decant-core/ai/gemini.js Outdated
Comment thread packages/decant-core/ai/gemini.js Outdated
Comment on lines +738 to +742
for (const cite of citeGroup) {
if (!Array.isArray(cite)) continue;
const meta = cite[2];
const url =
Array.isArray(meta) &&

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.

🟠 Medium

2. Candidate citations vanish from exports 🐞 Bug ≡ Correctness

extractActivitySources iterates through the elements of each candidate citation tuple and then
looks for a URL at cite[2], rather than reading the tuple's URL-bearing field. For citations with
the shape present in the API fixture, no candidate source is appended unless it also appears in the
separate activity trail.

Dismiss ↗ | View ↗

Comment thread content/formatters/html.js Outdated
@qodo-code-review

Copy link
Copy Markdown
Contributor

Qodo Fixer

No findings are within the configured fix scope. To change which findings are fixed, adjust the setting on your Qodo configuration page.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 4 issues

Fixed security issues:

  • packages/decant-core/tests/fixtures/google-search-ai-chat.html (link)
Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="content/formatters/html.js" line_range="1405-1416" />
<code_context>
   });

+  // Fallback: normalize XML-style <Image alt="..." caption="..." src="..."/> tags into markdown images
+  mdText = mdText.replace(/<Image\s+([^>]*?)(?:\/>|>(?:<\/Image>)?)/gi, (match, attrsStr) => {
+    const altMatch = attrsStr.match(/alt="([^"]*)"/i) || attrsStr.match(/alt='([^']*)'/i);
+    const captionMatch =
+      attrsStr.match(/caption="([^"]*)"/i) || attrsStr.match(/caption='([^']*)'/i);
+    const srcMatch = attrsStr.match(/src="([^"]*)"/i) || attrsStr.match(/src='([^']*)'/i);
+    const src = srcMatch ? srcMatch[1] : '';
+    const alt = altMatch ? altMatch[1] : captionMatch ? captionMatch[1] : 'Image';
+    if (src && /^(?:https?:|data:image|blob:|\/|\.\/)/i.test(src)) {
+      return `\n\n![${alt}](${src})\n\n`;
+    }
+    return '';
+  });
+
   // 1. Protect fenced code blocks (``` ... ``` or ~~~ ... ~~~)
</code_context>
<issue_to_address>
**Code examples are rewritten**

When a response contains a literal `<Image ...>` example inside inline or fenced code, `markdownToHtml` rewrites matching tags before protecting code, converting them to image syntax or removing them when the `src` is unsupported; exported code no longer matches the original.

Protect inline and fenced code before rewriting image tags, or skip image-tag matches inside code.

Also at `packages/decant-core/ai/gemini.js:516-517`.
</issue_to_address>

### Comment 2
<location path="packages/decant-core/ai/gemini.js" line_range="1050-1064" />
<code_context>
             const clone = markdownDiv.cloneNode(true);

+            // Extract sources from .sources-list before removal
+            const domSources = [];
+            clone.querySelectorAll(".sources-list").forEach((sl) => {
+              sl.querySelectorAll("a[href]").forEach((a) => {
+                const href = a.getAttribute("href")?.trim();
+                const title = a.textContent?.trim() || href;
+                if (
+                  href &&
+                  href.startsWith("http") &&
+                  !domSources.some((s) => s.href === href)
+                ) {
+                  domSources.push({ href, title });
+                }
+              });
+            });
+
+            // Convert citation chips and source buttons to links or text before generic button removal
+            clone
</code_context>
<issue_to_address>
**Sources appear twice**

When a model response contains linked entries in `.sources-list` inside the selected Markdown container, `parseFromDom` converts the `.sources-list` links to Markdown and then appends the same links under “Sources consulted,” so DOM exports show each source twice.

Remove `.sources-list` nodes from the clone after collecting their links, or exclude them from Markdown conversion.

Also at `packages/decant-core/ai/gemini.js:1128-1133`.
</issue_to_address>

### Comment 3
<location path="packages/decant-core/ai/gemini.js" line_range="559" />
<code_context>
+        const title = videoDetail[0] || "Video";
+        const url = videoDetail[2];
+        if (url && typeof url === "string" && url.startsWith("http")) {
+          resolved = resolved.split(tagUrl).join(`[${title}](${url})`);
+        }
+      }
</code_context>
<issue_to_address>
**Generated links are malformed**

When a video or source title contains `]`, or a URL contains an unescaped `)`, `resolveMediaPlaceholders` interpolates titles and URLs into Markdown link syntax without escaping delimiters, so `inlineParse` fails to recognize the link and the export shows raw Markdown instead of a clickable link.

Escape Markdown delimiters in link titles and URLs before generating the link syntax.

Also at `packages/decant-core/ai/gemini.js:1130`.
</issue_to_address>

### Comment 4
<location path="packages/decant-core/ai/gemini.js" line_range="1051-1063" />
<code_context>

+            // Extract sources from .sources-list before removal
+            const domSources = [];
+            clone.querySelectorAll(".sources-list").forEach((sl) => {
+              sl.querySelectorAll("a[href]").forEach((a) => {
+                const href = a.getAttribute("href")?.trim();
+                const title = a.textContent?.trim() || href;
+                if (
+                  href &&
+                  href.startsWith("http") &&
+                  !domSources.some((s) => s.href === href)
+                ) {
+                  domSources.push({ href, title });
+                }
+              });
+            });
+
</code_context>
<issue_to_address>
**DOM source links are missed**

When gemini places populated `.sources-list` links in the response footer outside `.markdown`, `parseFromDom` searches for `.sources-list` only inside a clone of `markdownDiv`, so source links in the response footer are never collected or appended to the exported message.

Collect `.sources-list` links from the full `modelResponse` or another subtree that includes the response footer before converting the markdown content.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread content/formatters/html.js Outdated
Comment thread packages/decant-core/ai/gemini.js
Comment thread packages/decant-core/ai/gemini.js Outdated
const title = videoDetail[0] || "Video";
const url = videoDetail[2];
if (url && typeof url === "string" && url.startsWith("http")) {
resolved = resolved.split(tagUrl).join(`[${title}](${url})`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium · Generated links are malformed

When a video or source title contains ], or a URL contains an unescaped ), resolveMediaPlaceholders interpolates titles and URLs into Markdown link syntax without escaping delimiters, so inlineParse fails to recognize the link and the export shows raw Markdown instead of a clickable link.

Escape Markdown delimiters in link titles and URLs before generating the link syntax.

Also at packages/decant-core/ai/gemini.js:1130.

Prompt for AI agents
In `packages/decant-core/ai/gemini.js` at line 559:

**Generated links are malformed**

When a video or source title contains `]`, or a URL contains an unescaped `)`, `resolveMediaPlaceholders` interpolates titles and URLs into Markdown link syntax without escaping delimiters, so `inlineParse` fails to recognize the link and the export shows raw Markdown instead of a clickable link.

Escape Markdown delimiters in link titles and URLs before generating the link syntax.

Also at `packages/decant-core/ai/gemini.js:1130`.

Comment thread packages/decant-core/ai/gemini.js Outdated

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 6


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: f6f4d507-426b-4188-b889-a53db2a61a6d
📥 Commits

Reviewing files that changed from the base of the PR and between 3294267 and c84bf44.

📒 Files selected for processing: 5
  • content/formatters/html.js
  • packages/decant-core/ai/gemini.js
  • packages/decant-core/tests/fixtures/google-search-ai-chat.html
  • packages/decant-core/tests/gemini.test.js
  • tests/html-formatter.test.mjs

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

📜 Review details
⏰ Context from checks skipped due to timeout: 4
  • GitHub Check: codeql / analyze
  • GitHub Check: Sourcery review
  • GitHub Check: Gitar
  • GitHub Check: test-and-build
🔇 Additional comments: 5
packages/decant-core/ai/gemini.js: 3

463-468: LGTM!


850-876: LGTM!


1049-1103: LGTM!

Also applies to: 1115-1128

packages/decant-core/tests/gemini.test.js: 1

223-273: LGTM!

Also applies to: 275-323

tests/html-formatter.test.mjs: 1

490-522: LGTM!

Comment thread content/formatters/html.js Outdated
Comment thread content/formatters/html.js Outdated
const altMatch = attrsStr.match(/alt="([^"]*)"/i) || attrsStr.match(/alt='([^']*)'/i);
const captionMatch =
attrsStr.match(/caption="([^"]*)"/i) || attrsStr.match(/caption='([^']*)'/i);
const srcMatch = attrsStr.match(/src="([^"]*)"/i) || attrsStr.match(/src='([^']*)'/i);

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Match the complete src attribute name.

The /src=/ search also matches data-src. For <Image data-src="https://example.com/preview.png" src="https://example.com/full.png"/>, the fallback renders the preview URL instead of src. Match an attribute-name boundary so the fallback selects only src. Based on learnings, attribute extraction must distinguish overlapping attribute names.

Source: Learnings

Comment thread content/formatters/html.js Outdated
const src = srcMatch ? srcMatch[1] : '';
const alt = altMatch ? altMatch[1] : captionMatch ? captionMatch[1] : 'Image';
if (src && /^(?:https?:|data:image|blob:|\/|\.\/)/i.test(src)) {
return `\n\n![${alt}](${src})\n\n`;

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve parentheses in image URLs.

If src is https://example.com/diagram(1).png, the generated Markdown reaches inlineParse, whose image pattern at Line 1270 stops at the first ). The rendered image then receives the truncated URL https://example.com/diagram(1, and .png) appears as text. Encode Markdown-significant URL characters before emitting the image, or pass the image to the renderer without a Markdown round trip. Based on learnings, generated Markdown destinations with parentheses need protection.

Source: Learnings

Comment thread packages/decant-core/ai/gemini.js
Comment thread packages/decant-core/ai/gemini.js
Comment thread packages/decant-core/ai/gemini.js Outdated
@greptile-apps

greptile-apps Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Confidence Score: 4/5

Retrigger · View in Greptile

[Medium impact] The changes affect Gemini conversation exports and a shared HTML export path.

The bracketed-label export issue must be fixed before merging.

Findings

  1. P1 · Bracketed labels break exports ▶

Summary

The PR preserves Gemini images, video links, and source links in exports.

  • Gemini API answers keep images, video links, and citations from the visible answer.
  • Gemini DOM exports keep citation labels, links, and wrapped images.
  • HTML exports turn leftover Gemini image tags into rendered pictures.

Since the previous review, it adds backslash escaping to media labels and tightens a source-link assertion.

The previously reported bracketed-label export problem remains unresolved. Image descriptions and video titles containing brackets are still escaped into sequences that math normalization interprets as formulas. This can break image rendering in HTML and PDF exports or corrupt media labels.

Reviews (4) · Last reviewed commit: "fix(gemini): escape backslashes before b..." · Reviewed by Greptile

Comment thread content/formatters/html.js Outdated
Comment thread packages/decant-core/ai/gemini.js Outdated
Comment thread packages/decant-core/ai/gemini.js Outdated
Comment thread packages/decant-core/tests/gemini.test.js Fixed

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

🟡 Minor · Keep video replacement out of code samples. · gemini.js:467

packages/decant-core/ai/gemini.js:467
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep video replacement out of code samples.

When an answer quotes a YouTube placeholder inside fenced or inline code, resolveImageTags restores that code before this call. resolveMediaPlaceholders then changes the quoted URL to [title](url). Protect code spans across both replacements so the export preserves the sample.


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: f3b4adf0-67c7-4887-b69e-39ce77699a07
📥 Commits

Reviewing files that changed from the base of the PR and between c84bf44 and 4df1a9d.

📒 Files selected for processing: 4
  • content/formatters/html.js
  • packages/decant-core/ai/gemini.js
  • packages/decant-core/tests/gemini.test.js
  • tests/html-formatter.test.mjs
🚧 Files skipped from review as they are similar to previous changes: 1
  • tests/html-formatter.test.mjs

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

📜 Review details
🧰 Additional context used
🪛 ast-grep (0.45.3)
packages/decant-core/ai/gemini.js

[warning] 580-580: Detects non-literal values in regular expressions
Context: new RegExp(${escaped}(?![\\w/]), "g")
Note: [CWE-1333] Inefficient Regular Expression Complexity (ReDoS via non-literal RegExp).

(detect-non-literal-regexp)

🪛 GitHub Check: CodeQL
packages/decant-core/tests/gemini.test.js

[failure] 485-485: Missing regular expression anchor
When this is used as a regular expression on a URL, it may match anywhere, and arbitrary hosts may come before or after it.

🔇 Additional comments: 1
packages/decant-core/tests/gemini.test.js: 1

325-385: LGTM!

Also applies to: 387-440, 442-491

Comment thread content/formatters/html.js Outdated
Comment thread content/formatters/html.js Outdated
const srcMatch = attrsStr.match(/src="([^"]*)"/i) || attrsStr.match(/src='([^']*)'/i);
const src = srcMatch ? srcMatch[1] : '';
const rawAlt = altMatch ? altMatch[1] : captionMatch ? captionMatch[1] : 'Image';
const alt = rawAlt.replace(/[[\]]/g, '\\$&');

@gitar-bot gitar-bot Bot Oct 19, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Bug: Escaped \[ \] in image alt text gets turned into display math

This commit escapes brackets in alt text, so a label like Paired [V1] becomes ![Paired \[V1\]](url). In markdownToHtml that escaped string reaches step 3 (line 1439), and step 3 converts \[ … \] to display math. Its (?!\() lookahead only looks at the character right after \]. Here that character is the alt's closing ], not (, so the guard doesn't stop the match. The output becomes ![Paired $$V1$$ ](url), which breaks the image into a stray math block and literal text in HTML/PDF exports. This hits both the &lt;Image> fallback in html.js and the Gemini output from resolveImageTags/resolveMediaPlaceholders (gemini.js:545,576), because that markdown later goes through markdownToHtml too. (inlineParse step 1, line 1187, has the same \[…\] pattern.) Possible fixes: drop or replace the brackets instead of backslash-escaping them (e.g. map [/] to (/) or remove them), or move the markdown images out into placeholders before step 3 runs. Add a test that sends an image with a bracketed alt through markdownToHtml and checks the result is an &lt;img>.

Swap brackets for parentheses so the alt text can't be read as math delimiters (apply the same change at gemini.js:545 and 576):

const alt = rawAlt.replace(/\[/g, '(').replace(/\]/g, ')');

Was this helpful? React with 👍 / 👎

Comment thread content/formatters/html.js Fixed
Comment thread packages/decant-core/ai/gemini.js Fixed
Comment thread packages/decant-core/ai/gemini.js Fixed
Comment thread packages/decant-core/tests/gemini.test.js Fixed
);

let replaced = protectedText.replace(
/<Image\s+([^>]*?)(?:\/>|>(?:\s*<\/Image>)?)/gi,

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.

🟠 Medium

4. Image labels break at greater-than signs 🐞 Bug ≡ Correctness

resolveImageTags stops its attribute capture at the first > instead of respecting quoted
attribute values. When Gemini returns an alt or caption such as alt="a > b", the parser falls back
to the default label and leaves the remainder of the tag in the response, while the HTML fallback
has the same failure mode.

Dismiss ↗ | View ↗

Comment thread packages/decant-core/ai/gemini.js Outdated
: capMatch
? capMatch[1]
: "Image";
const alt = rawAlt.replace(/[[\]]/g, "\\$&");

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.

🟠 Medium

5. Image labels gain unintended formatting 🐞 Bug ≡ Correctness

The new image conversion escapes only square brackets before inserting alt or caption text into
Markdown, so delimiters such as * remain active. During HTML conversion those delimiters become
<em> markup inside the generated alt attribute, changing the label text instead of preserving
the Gemini-provided description.

Dismiss ↗ | View ↗

Comment on lines +529 to +550
/<Image\s+([^>]*?)(?:\/>|>(?:\s*<\/Image>)?)/gi,
(match, attrs) => {
const altMatch =
attrs.match(/alt="([^"]*)"/i) || attrs.match(/alt='([^']*)'/i);
const capMatch =
attrs.match(/caption="([^"]*)"/i) ||
attrs.match(/caption='([^']*)'/i);
const srcMatch =
attrs.match(/src="([^"]*)"/i) || attrs.match(/src='([^']*)'/i);

const src = srcMatch ? srcMatch[1] : "";
const rawAlt = altMatch
? altMatch[1]
: capMatch
? capMatch[1]
: "Image";
const alt = rawAlt.replace(/[[\]]/g, "\\$&");

const resolvedUrl =
imageMap.get(src) || (src.startsWith("http") ? src : null);
if (resolvedUrl) {
return `![${alt}](${resolvedUrl})`;

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.

🟠 Medium

6. Released extension misses image fixes 🔗 Cross-repo conflict ☼ Reliability

The new resolveImageTags behavior exists only in the workspace parser, while the standalone
decant package still has the old candidate-processing path. When a release build runs before
decant-core is synchronized and published, the extension installs the old package and ships
without the Gemini image-resolution changes.

Dismiss ↗ | View ↗

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 85755358-440f-493c-a35e-e6521a91a7d0
📥 Commits

Reviewing files that changed from the base of the PR and between 4df1a9d and 502637d.

📒 Files selected for processing: 3
  • content/formatters/html.js
  • packages/decant-core/ai/gemini.js
  • packages/decant-core/tests/gemini.test.js

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

📜 Review details
⏰ Context from checks skipped due to timeout: 4
  • GitHub Check: Sourcery review
  • GitHub Check: codeql / analyze
  • GitHub Check: test-and-build
  • GitHub Check: Gitar
🔇 Additional comments: 3
packages/decant-core/ai/gemini.js: 1

529-529: LGTM!

Also applies to: 540-540, 545-545, 575-576

packages/decant-core/tests/gemini.test.js: 1

385-392: LGTM!

Also applies to: 493-496

content/formatters/html.js: 1

1430-1431: LGTM!


// Fallback: normalize XML-style <Image alt="..." caption="..." src="..."/> tags into markdown images
// (Runs after code blocks are protected so code samples are not modified)
mdText = mdText.replace(/<Image\s+([^>]*?)(?:\/>|>(?:\s*<\/Image>)?)/gi, (match, attrsStr) => {

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Match Image tags with no attributes.

For <Image/> or <Image></Image>, \s+ prevents the regex from matching. The raw tag then remains in the export instead of being removed when it has no src. Allow zero attributes while keeping a tag-name boundary.

The supplied change details state that tags without an allowed src are removed.

Proposed fix
--- "a/content/formatters/html.js"
+++ "b/content/formatters/html.js"
@@ -1421,7 +1421,7 @@
 
   // Fallback: normalize XML-style <Image alt="..." caption="..." src="..."/> tags into markdown images
   // (Runs after code blocks are protected so code samples are not modified)
-  mdText = mdText.replace(/<Image\s+([^>]*?)(?:\/>|>(?:\s*<\/Image>)?)/gi, (match, attrsStr) => {
+  mdText = mdText.replace(/<Image(?=\s|\/?>)\s*([^>]*?)(?:\/>|>(?:\s*<\/Image>)?)/gi, (match, attrsStr) => {
     const altMatch = attrsStr.match(/alt="([^"]*)"/i) || attrsStr.match(/alt='([^']*)'/i);
     const captionMatch =
       attrsStr.match(/caption="([^"]*)"/i) || attrsStr.match(/caption='([^']*)'/i);
📝 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
mdText = mdText.replace(/<Image\s+([^>]*?)(?:\/>|>(?:\s*<\/Image>)?)/gi, (match, attrsStr) => {
mdText = mdText.replace(/<Image(?=\s|\/?>)\s*([^>]*?)(?:\/>|>(?:\s*<\/Image>)?)/gi, (match, attrsStr) => {

Comment thread content/formatters/html.js Outdated
const srcMatch = attrsStr.match(/src="([^"]*)"/i) || attrsStr.match(/src='([^']*)'/i);
const src = srcMatch ? srcMatch[1] : '';
const rawAlt = altMatch ? altMatch[1] : captionMatch ? captionMatch[1] : 'Image';
const alt = rawAlt.replace(/[[\]]/g, '\\$&');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟥 P1 · Bracketed labels break exports

Escaping brackets here turns image labels into display-math delimiters. For example, <Image alt="Paired [V1]" src="https://example.com/image.png"/> becomes ![Paired \[V1\]](...). The subsequent math replacement inserts blank lines inside the image Markdown, so the HTML export contains raw Markdown and a math block instead of an image. PDF exports use the same formatter.

The new escaping in Gemini’s image and video resolvers also conflicts with math normalization. Through the complete Gemini API conversion path, the media survives but its image description or video title is corrupted instead.

Protect image and link labels from math normalization in both paths before merging, and add parser-to-export regression tests with bracket-containing labels.

Knowledge Base Used: Export formatting pipeline

Artifacts

Executed bracketed-label pipeline test source

  • The authored harness executes real revision-specific parser and formatter modules and checks exported media labels against controls, making the regression reproducible.

Pipeline results before bracket escaping

  • Ran the 15-check harness at 4df1a9d and captured formatter output plus exported DOM observations; all checks pass.

Pipeline results with bracket escaping at PR head

  • Ran the identical harness at 502637d and captured missing media and corrupted labels alongside passing controls; six checks demonstrate the regression.

Existing formatter and Gemini test results at PR head

  • Ran both existing scoped test files at head and captured 31 passing tests; the current suite does not catch the demonstrated pipeline regression.

🦖 Ran code and verified through TREX

@gitar-bot

gitar-bot Bot commented Oct 19, 2026

Copy link
Copy Markdown
Code Review ⚠️ Changes requested 3 closed / 4 findings

🟡 Medium risk · Changing Gemini parsing and export normalization could misassociate media or citations, producing incorrect links or source attribution in saved conversations.

Fixes Gemini image and media handling so linked text, images, and video placeholders survive HTML and PDF exports, with code blocks and draft citations isolated. Changes requested: escaped brackets in image alt text (e.g. Paired \[V1\]) are converted to display math by the \[...\] step in markdownToHtml (content/formatters/html.js:1439), which breaks the image into a stray math block in HTML/PDF output. The same pattern exists in inlineParse (html.js:1187) and affects Gemini output from resolveImageTags and resolveMediaPlaceholders in packages/decant-core/ai/gemini.js. Fix by replacing brackets in alt text or protecting markdown images before the math step, and add a test that an image with a bracketed alt renders as an <img>.

⚠️ Bug: Escaped \[ \] in image alt text gets turned into display math

📄 content/formatters/html.js:1431 📄 content/formatters/html.js:1433 📄 content/formatters/html.js:1439 📄 packages/decant-core/ai/gemini.js:545 📄 packages/decant-core/ai/gemini.js:576

This commit escapes brackets in alt text, so a label like Paired [V1] becomes ![Paired \[V1\]](url). In markdownToHtml that escaped string reaches step 3 (line 1439), and step 3 converts \[ … \] to display math. Its (?!\() lookahead only looks at the character right after \]. Here that character is the alt's closing ], not (, so the guard doesn't stop the match. The output becomes ![Paired $$V1$$ ](url), which breaks the image into a stray math block and literal text in HTML/PDF exports. This hits both the &lt;Image> fallback in html.js and the Gemini output from resolveImageTags/resolveMediaPlaceholders (gemini.js:545,576), because that markdown later goes through markdownToHtml too. (inlineParse step 1, line 1187, has the same \[…\] pattern.) Possible fixes: drop or replace the brackets instead of backslash-escaping them (e.g. map [/] to (/) or remove them), or move the markdown images out into placeholders before step 3 runs. Add a test that sends an image with a bracketed alt through markdownToHtml and checks the result is an &lt;img>.

Swap brackets for parentheses so the alt text can't be read as math delimiters (apply the same change at gemini.js:545 and 576)
const alt = rawAlt.replace(/\[/g, '(').replace(/\]/g, ')');
✅ 3 closed
✅ Bug: <Image> fallback rewrites/deletes JSX inside fenced and inline code

📄 content/formatters/html.js:1404-1416 📄 packages/decant-core/ai/gemini.js:512-526
In markdownToHtml, the new <Image ...> normalization runs before step 1, where fenced code blocks and inline code get swapped for placeholders. That means it also rewrites code. A Next.js snippet like ```jsx &lt;Image src="/logo.png" alt="Logo" /> ``` comes out as ![Logo](/logo.png) inside the code block. If src is an expression (src={logo}) or missing, the callback returns '' and deletes the tag. The i flag makes it hit lowercase <image> too. Conversations about React or Next.js use exactly this syntax, so HTML and PDF exports will quietly corrupt code samples. The same problem exists upstream in GeminiParser.resolveImageTags, which runs on the whole model text, code fences included. The fix is to move this replacement after the code-block and inline-code protection steps, and to return match rather than '' when no valid src is found. In gemini.js, skip fenced regions too, or only replace tags whose src is found in the image map.

✅ Edge Case: Media placeholder split/join breaks on prefix-colliding indices

📄 packages/decant-core/ai/gemini.js:551-562
resolveMediaPlaceholders swaps each placeholder with plain split(tagUrl).join(...). When a response has 11 or more videos, http://googleusercontent.com/youtube_content/1 is a prefix of .../youtube_content/10. If entry 1 is handled first, .../10 turns into [title1](url1)0, which gives the wrong link plus a stray digit. A regex replace with a boundary check fixes this, e.g. new RegExp(escape(tagUrl) + '(?!\d)', 'g'). Processing entries longest-tagUrl-first also works.

✅ Bug: New <Image> regex leaves a dangling </Image> on paired tags

📄 packages/decant-core/ai/gemini.js:528-529 📄 content/formatters/html.js:1424
The regex was changed from (?:\/>|>(?:<\/Image>)?) to (?:\/>|<\/Image>|>). For a paired tag like <Image src="image_agent_tag_1" alt="x"></Image>, the lazy [^>]*? stops at the first >. At that point \/> and <\/Image> both fail, so the bare > alternative matches and the </Image> closing tag is left in the text. The old pattern consumed it. As a result, the Markdown export from resolveImageTags ends up containing ![x](url)</Image>, and the HTML fallback leaves the same stray tag for the renderer or sanitizer to deal with. Fix: go back to the optional-closing form in both places.

🤖 Prompt for agents
Code Review: Fixes Gemini image and media handling so linked text, images, and video placeholders survive HTML and PDF exports, with code blocks and draft citations isolated. Changes requested: escaped brackets in image alt text (e.g. `Paired \[V1\]`) are converted to display math by the `\[...\]` step in `markdownToHtml` (content/formatters/html.js:1439), which breaks the image into a stray math block in HTML/PDF output. The same pattern exists in `inlineParse` (html.js:1187) and affects Gemini output from `resolveImageTags` and `resolveMediaPlaceholders` in packages/decant-core/ai/gemini.js. Fix by replacing brackets in alt text or protecting markdown images before the math step, and add a test that an image with a bracketed alt renders as an `<img>`.

1. ⚠️ Bug: Escaped \[ \] in image alt text gets turned into display math
   Files: content/formatters/html.js:1431, content/formatters/html.js:1433, content/formatters/html.js:1439, packages/decant-core/ai/gemini.js:545, packages/decant-core/ai/gemini.js:576

   This commit escapes brackets in alt text, so a label like `Paired [V1]` becomes `![Paired \[V1\]](url)`. In `markdownToHtml` that escaped string reaches step 3 (line 1439), and step 3 converts `\[ … \]` to display math. Its `(?!\()` lookahead only looks at the character right after `\]`. Here that character is the alt's closing `]`, not `(`, so the guard doesn't stop the match. The output becomes `![Paired 
   
   $$V1$$
   
   ](url)`, which breaks the image into a stray math block and literal text in HTML/PDF exports. This hits both the `<Image>` fallback in html.js and the Gemini output from `resolveImageTags`/`resolveMediaPlaceholders` (gemini.js:545,576), because that markdown later goes through `markdownToHtml` too. (`inlineParse` step 1, line 1187, has the same `\[…\]` pattern.) Possible fixes: drop or replace the brackets instead of backslash-escaping them (e.g. map `[`/`]` to `(`/`)` or remove them), or move the markdown images out into placeholders before step 3 runs. Add a test that sends an image with a bracketed alt through `markdownToHtml` and checks the result is an `<img>`.

   Fix (Swap brackets for parentheses so the alt text can't be read as math delimiters (apply the same change at gemini.js:545 and 576)):
   const alt = rawAlt.replace(/\[/g, '(').replace(/\]/g, ')');

Verification

Rules No rules evaluated

Functional validation Not enabled

Set up

For more verification details, comment gitar display:verbose.

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing compact verification results.

Comment with these commands to change the behavior for this request:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Important

Your trial ends in 5 days — upgrade now to keep code review, CI analysis, auto-apply, custom automations, and more.

Was this helpful? React with 👍 / 👎 | Gitar

@deadrat-in
deadrat-in merged commit e47b37d into main Oct 19, 2026
6 of 7 checks passed
@deadrat-in
deadrat-in deleted the fix/gemini-links-and-images branch October 19, 2026 21:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: Gemini linked text and images deleted in HTML and PDF exports

2 participants