feat(extension): Markdown export of saved posts (Phase 1)#11
Conversation
Export the reader's saved posts to a clean Markdown file, grouped by source thread — the next Phase 1 roadmap item. - src/markdown.ts: a pure savedPostsToMarkdown() that turns saved-post snapshots into Markdown, grouped by thread (first-seen order), each post with author/role/timestamp, a blockquoted body, and a permalink. Empty input yields an explicit "no saved posts" note. - src/sidepanel.ts: an "Export saved" button gathers every saved post, builds the file, and downloads it via a temporary object URL/anchor — no new permissions, nothing leaves the browser. The button enables only when at least one save exists (on load and after each toggle). - public/sidepanel.html: the Export button and its disabled styling. - Docs synced: ROADMAP.md, Initial Plan.md checklist, extension README. Verification: pnpm -r typecheck passes; pnpm test 120 tests pass (+7 markdown export); extension build bundles dist/. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014AiyKriV2X3fhvG6htg46m
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 641303e1eb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
A saved post's body, author name, and thread title are untrusted forum page text. Writing them verbatim into the exported Markdown let syntax like  or a raw <img> become active content when the file is opened — including remote image loads that leak a signal — undermining the untrusted-content/privacy model. - escapeMarkdown(): backslash-escape every ASCII-punctuation character that can begin a Markdown/HTML construct, applied to post bodies (per blockquote line), the author/role/timestamp heading, and the thread title heading. - mdLink()/escapeUrl(): percent-encode characters that could close or break out of a link destination, so a captured permalink/thread href can't inject trailing Markdown. Verification: pnpm -r typecheck passes; pnpm test 123 tests pass (+3 escaping); extension build bundles dist/. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014AiyKriV2X3fhvG6htg46m
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b9856819b0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
A saved post's permalink is an untrusted page href. The export only percent-encoded delimiter characters, so a scheme like javascript: or data: would survive and export as an active Markdown link. Reuse the HTML sanitizer's scheme allowlist instead of duplicating it: export safeHref() from sanitize.ts and apply it to the permalink (and the thread "Open thread" link). The permalink resolves against the thread URL, so relative hrefs become working absolute links and unsafe or unparseable ones are omitted from the export. Verification: pnpm -r typecheck passes; pnpm test 125 tests pass (+2: unsafe-scheme omission, relative-permalink resolution); build OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014AiyKriV2X3fhvG6htg46m
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 78373e43bd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
When a thread has no extracted title, the export wrote the raw thread URL into the `##` heading. A forum URL/slug can itself contain Markdown (e.g. ), so that fallback bypassed the escaping applied to every other untrusted field. Escape it through escapeMarkdown() too, so no heading can become active content on export. Verification: pnpm -r typecheck passes; pnpm test 126 tests pass (+1: Markdown-bearing URL fallback is escaped); build OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014AiyKriV2X3fhvG6htg46m
Summary
First, handled the one outstanding unmerged PR (#10, "local user notes") — its only review comment (a Codex P2 about keeping hidden note editors collapsed) had already been fixed and the thread resolved in commit
1e045a9, CI was green, andmergeable_statewasclean, so it merged intomainwith no conflicts. Then continued Phase 1 with the next roadmap item: Markdown export.ForumForge now lets the reader export their saved posts to a clean Markdown file. The plan promises that saved comments "can later be exported as Markdown for personal notes, troubleshooting logs, research, or documentation" — this delivers that. Saved posts are grouped by the thread they came from, each rendered with its author/role/timestamp, a blockquoted body, and a permalink back to the source.
Everything stays on-device (local-first, see
docs/PRIVACY.md): export reads only what the reader already saved locally and builds the file in the panel, then downloads it via the page's own anchor — nothing leaves the browser, and no new permissions are needed.What changed
src/markdown.ts(new) — a puresavedPostsToMarkdown(saved)that turns saved-post snapshots into a Markdown note, grouped by thread in first-seen order (callers passSavedPosts.all(), freshest first). Each post becomes anauthor · role · timestampheading, a blockquoted body (so post content can never be mistaken for document structure; empty bodies become an em dash), and a permalink. Empty input yields an explicit "no saved posts yet" note rather than a blank file.nowis injectable for deterministic tests.src/sidepanel.ts— an "Export saved" button gathers every saved post, builds the Markdown, and downloadsforumforge-saved-<date>.mdvia a temporary object URL + anchor (revoked after the click). The DOM/URLglue lives here, not in the pure module. The button enables only when at least one save exists — refreshed on load and after every Save toggle — so the action never produces an empty note. Status text reports the result.public/sidepanel.html— the Export button in the bar and its disabled styling.Docs synced in the same change:
ROADMAP.md, the canonicalInitial Plan.mdchecklist, and the extensionREADME.md.Roadmap (Phase 1)
Verification
pnpm -r typecheck— passes (core + parser + storage + extension)pnpm test— 120 tests pass (+7: grouping by thread, first-seen order, URL-fallback heading, post entry fields, blockquote/empty-body rendering, count subtitle, empty state)pnpm --filter @forumforge/extension build— bundlesdist/Intentionally not done
contentTextas a blockquote, which is deterministic and safe. Faithful Markdown from richcontentHtmlis a possible later refinement.🤖 Generated with Claude Code
Generated by Claude Code