Skip to content

feat(extension): Markdown export of saved posts (Phase 1)#11

Merged
erichuang1425 merged 4 commits into
mainfrom
claude/unmerged-prs-app-dev-h6tru9
Jun 24, 2026
Merged

feat(extension): Markdown export of saved posts (Phase 1)#11
erichuang1425 merged 4 commits into
mainfrom
claude/unmerged-prs-app-dev-h6tru9

Conversation

@erichuang1425

Copy link
Copy Markdown
Owner

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, and mergeable_state was clean, so it merged into main with 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.

saved posts ─▶ group by source thread ─▶ render Markdown ─▶ download .md on-device

What changed

  • src/markdown.ts (new) — a pure savedPostsToMarkdown(saved) that turns saved-post snapshots into a Markdown note, grouped by thread in first-seen order (callers pass SavedPosts.all(), freshest first). Each post becomes an author · role · timestamp heading, 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. now is injectable for deterministic tests.
  • src/sidepanel.ts — an "Export saved" button gathers every saved post, builds the Markdown, and downloads forumforge-saved-<date>.md via a temporary object URL + anchor (revoked after the click). The DOM/URL glue 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 canonical Initial Plan.md checklist, and the extension README.md.

Roadmap (Phase 1)

  • Clean reading mode
  • OP highlighting
  • New posts since last visit
  • Save comments
  • Local user notes
  • Markdown export ← this PR
  • Discourse adapter
  • Hacker News adapter

Verification

  • pnpm -r typecheck — passes (core + parser + storage + extension)
  • pnpm test120 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 — bundles dist/

Intentionally not done

  • Per-thread / selective export — this exports all saved posts in one file (the plan's example is a single-thread note, which is the one-thread-group special case here). A scoped "export this thread only" can come later.
  • HTML-to-Markdown body conversion — the export uses the post's plain contentText as a blockquote, which is deterministic and safe. Faithful Markdown from rich contentHtml is a possible later refinement.
  • Clicking through in a real browser is a manual step (needs a browser); the generator, button wiring, and download path are unit-tested where deterministic.

🤖 Generated with Claude Code


Generated by Claude Code

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

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment thread apps/extension/src/markdown.ts Outdated
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 ![x](remote) 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

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment thread apps/extension/src/markdown.ts
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

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment thread apps/extension/src/markdown.ts Outdated
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. ![x](remote)), 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
@erichuang1425 erichuang1425 merged commit 74eb6ae into main Jun 24, 2026
1 check passed
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.

2 participants