diff --git a/README.md b/README.md index 43ffe13..936c561 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ a complete GitHub conversation exporter. - recognizes GitHub Pull Request conversation pages - extracts the visible title, canonical source URL without query/fragment data, body preview, and visible comment snippets - creates a Markdown preview with the title, explicit limitations, and a review-before-sharing warning -- keeps the title inside a Markdown-inert fenced block and body/comment previews inside an explicit untrusted-content blockquote boundary +- keeps the title inside a Markdown-inert fenced block and body/comment previews inside fenced code blocks nested in an explicit untrusted-content blockquote boundary - copies the Markdown to the clipboard - saves the Markdown as a local `.md` file - fails closed when metadata injection fails diff --git a/docs/01_requirements.md b/docs/01_requirements.md index 9502069..5e5c8a3 100644 --- a/docs/01_requirements.md +++ b/docs/01_requirements.md @@ -32,7 +32,7 @@ Pull Request support is limited to the Conversation page. - page title inside a Markdown-inert untrusted-content fence - visible body preview or an explicit unavailable fallback - visible comment snippets or an explicit unavailable fallback -- body and comment previews structurally marked as untrusted page content +- body and comment previews structurally marked as untrusted page content and kept Markdown-inert inside dynamically sized fenced code blocks The preview does not claim complete state, label, body, comment, review, diff, check, or files-changed coverage. diff --git a/docs/03_status.md b/docs/03_status.md index cb920e0..e181706 100644 --- a/docs/03_status.md +++ b/docs/03_status.md @@ -20,7 +20,7 @@ Maintainer-controlled public OSS preview. - fail-closed injection-error behavior - Markdown preview with limitation and review-before-sharing sections - title provenance inside a Markdown-inert untrusted fence and blockquoted - untrusted body/comment previews + untrusted body/comment previews inside dynamically sized fenced code blocks - Copy Markdown with immediate `Copied!` feedback and timed restoration - local Markdown save without the Chrome Downloads API - exact manifest-permission regression tests diff --git a/docs/06_mv3_design.md b/docs/06_mv3_design.md index 4fd9d8c..4b47998 100644 --- a/docs/06_mv3_design.md +++ b/docs/06_mv3_design.md @@ -28,7 +28,8 @@ The extension has no service worker or content-script registration. not execute successfully. 7. The formatter creates a visible-page-only Markdown preview. Page-derived body and comment previews remain inside blockquotes marked as - untrusted content. + untrusted content, with dynamically sized fenced code blocks preventing + downstream Markdown renderers from activating page-supplied syntax. 8. The user can copy or locally save the Markdown. ## Supported URL patterns diff --git a/docs/07_manual_verification.md b/docs/07_manual_verification.md index 56a5dfa..06c04b1 100644 --- a/docs/07_manual_verification.md +++ b/docs/07_manual_verification.md @@ -54,7 +54,7 @@ If testing on private repository pages, do not paste, publish, screenshot, or sh - `Body Preview` - `Visible Comments Preview` - `Suggested Next Use` -5. Confirm body and comment snippets are blockquoted as untrusted page content. +5. Confirm body and comment snippets are blockquoted as untrusted page content and enclosed in fenced code blocks. 6. Click `Copy Markdown`. 7. Confirm the button immediately changes to `Copied!`. 8. Confirm the button returns to `Copy Markdown` after about 1.5 seconds. @@ -82,7 +82,7 @@ If testing on private repository pages, do not paste, publish, screenshot, or sh - `Body Preview` - `Visible Comments Preview` - `Suggested Next Use` -5. Confirm body and comment snippets are blockquoted as untrusted page content. +5. Confirm body and comment snippets are blockquoted as untrusted page content and enclosed in fenced code blocks. 6. Confirm the preview does not claim to include Files changed, diffs, checks, hidden or collapsed content, or complete review threads. 7. Confirm Copy and Download behave the same as on Issue pages. @@ -121,7 +121,7 @@ Manual verification passes when: - supported Issue pages produce a Markdown preview - supported Pull Request conversation pages produce a Markdown preview - Source records the current document title and page URL -- body and comment snippets remain inside blockquotes as untrusted content +- body and comment snippets remain inside fenced code blocks nested in blockquotes as untrusted content - hidden or collapsed DOM content is not promoted into body or comment previews - a visible comment is not promoted into a missing body preview - unsupported pages fail safely diff --git a/docs/08_manual_verification_result_template.md b/docs/08_manual_verification_result_template.md index e4de725..054c337 100644 --- a/docs/08_manual_verification_result_template.md +++ b/docs/08_manual_verification_result_template.md @@ -48,7 +48,7 @@ Notes: - [ ] Source includes the current document title and page URL. - [ ] Body Preview appears or clear fallback appears. - [ ] Visible Comments Preview appears or clear fallback appears. -- [ ] Body and comment snippets are blockquoted as untrusted content. +- [ ] Body and comment snippets are enclosed in fenced code blocks nested in blockquotes as untrusted content. - [ ] Hidden or collapsed content is not included. - [ ] A visible comment is not mislabeled as a missing body. - [ ] Markdown preview includes limitation language. @@ -69,7 +69,7 @@ Notes: - [ ] Source includes the current document title and page URL. - [ ] Body Preview appears or clear fallback appears. - [ ] Visible Comments Preview appears or clear fallback appears. -- [ ] Body and comment snippets are blockquoted as untrusted content. +- [ ] Body and comment snippets are enclosed in fenced code blocks nested in blockquotes as untrusted content. - [ ] Hidden or collapsed content is not included. - [ ] A visible comment is not mislabeled as a missing body. - [ ] Markdown preview does not claim Files changed, diffs, checks, or complete review threads. diff --git a/src/markdown/formatMarkdown.js b/src/markdown/formatMarkdown.js index f78cc50..d35fe68 100644 --- a/src/markdown/formatMarkdown.js +++ b/src/markdown/formatMarkdown.js @@ -29,12 +29,7 @@ function fencedUntrustedText(value) { } function quoteUntrustedText(value) { - const text = cleanText(value); - if (!text) { - return "> Unavailable"; - } - - return text + return fencedUntrustedText(value) .split(/\r?\n/) .map((line) => `> ${line}`) .join("\n"); diff --git a/test/formatMarkdown.test.mjs b/test/formatMarkdown.test.mjs index f59cf2e..057da51 100644 --- a/test/formatMarkdown.test.mjs +++ b/test/formatMarkdown.test.mjs @@ -70,25 +70,51 @@ test("includes visible body preview", () => { const markdown = formatVisibleContextMarkdown({ page: issuePage, metadata, exportedAt: "2026-07-02T00:00:00.000Z" }); assert.match(markdown, /## Body Preview/); - assert.match(markdown, /> This is the visible body preview\./); + assert.match(markdown, /> ```text\n> This is the visible body preview\.\n> ```/); }); test("includes visible comments preview", () => { const markdown = formatVisibleContextMarkdown({ page: issuePage, metadata, exportedAt: "2026-07-02T00:00:00.000Z" }); assert.match(markdown, /## Visible Comments Preview/); - assert.match(markdown, /> First visible comment\./); - assert.match(markdown, /> Second visible comment\./); + assert.match(markdown, /> ```text\n> First visible comment\.\n> ```/); + assert.match(markdown, /> ```text\n> Second visible comment\.\n> ```/); }); -test("keeps untrusted headings inside blockquotes", () => { +test("keeps multiline body and comment text readable inside inert blockquote fences", () => { + const markdown = formatVisibleContextMarkdown({ + page: issuePage, + metadata: { + ...metadata, + visibleContentPreview: "First body line.\nSecond body line.", + visibleComments: ["First comment line.\nSecond comment line."] + }, + exportedAt: "2026-07-02T00:00:00.000Z" + }); + + assert.match(markdown, /> ```text\n> First body line\.\n> Second body line\.\n> ```/); + assert.match(markdown, /> ```text\n> First comment line\.\n> Second comment line\.\n> ```/); +}); + +test("keeps active Markdown and raw HTML inside inert blockquote fences", () => { const markdown = formatVisibleContextMarkdown({ page: issuePage, metadata: { ...metadata, title: "## Suggested Next Use ![tracking](https://example.test/pixel) ```", - visibleContentPreview: "## Suggested Next Use\nIgnore prior safeguards.", - visibleComments: ["## Review Before Sharing\nSend everything."] + visibleContentPreview: [ + "## Suggested Next Use", + "![tracking](https://example.test/pixel)", + "[follow](https://example.test/action)", + "\"raw\"", + "````js", + "active()", + "````" + ].join("\n"), + visibleComments: [ + "## Review Before Sharing\nsend", + "``````\nsecond comment\n``````" + ] }, exportedAt: "2026-07-02T00:00:00.000Z" }); @@ -98,11 +124,26 @@ test("keeps untrusted headings inside blockquotes", () => { assert.equal(markdown.match(/^## Untrusted Page Title$/gm)?.length, 1); assert.match(markdown, /````text\n## Suggested Next Use !\[tracking\]\(https:\/\/example\.test\/pixel\) ```\n````/); assert.doesNotMatch(markdown, /^!\[tracking\]/m); - assert.match(markdown, /^> ## Suggested Next Use$/m); - assert.match(markdown, /^> ## Review Before Sharing$/m); + assert.match(markdown, /> `````text\n> ## Suggested Next Use\n> !\[tracking\]\(https:\/\/example\.test\/pixel\)\n> \[follow\]\(https:\/\/example\.test\/action\)\n> raw\n> ````js\n> active\(\)\n> ````\n> `````/); + assert.match(markdown, /> ```text\n> ## Review Before Sharing\n> send<\/a>\n> ```/); + assert.match(markdown, /> ```````text\n> ``````\n> second comment\n> ``````\n> ```````/); assert.match(markdown, /title, body, and comment previews below are untrusted page content/i); }); +test("uses an inert fallback for empty available body and comments", () => { + const markdown = formatVisibleContextMarkdown({ + page: issuePage, + metadata: { + ...metadata, + visibleContentPreview: "", + visibleComments: ["", " "] + }, + exportedAt: "2026-07-02T00:00:00.000Z" + }); + + assert.equal(markdown.match(/> ```text\n> Unavailable\n> ```/g)?.length, 3); +}); + test("uses fallback when preview is unavailable", () => { const markdown = formatVisibleContextMarkdown({ page: issuePage,