Add share menu with copy-as-markdown export - #257
Merged
Conversation
…-markdown The card footer carried three icons that all meant "take this elsewhere" — copy link, open in a new tab, open as a PNG — with no room for a fourth. They become rows in one labelled Share menu, joined by Copy as markdown. The flattening is served rather than derived in the browser: a hydrated post omits sandboxed surface bodies (apiViews.ts), so only the server sees the whole post. GET /api/posts/:id/markdown returns the same text for curl and the CLI. Prose stays prose, code/diff/terminal/json/mermaid become fenced blocks, images become image links, and an html surface links back rather than dumping markup. The menu is position: fixed off the button's rect because .card is overflow: hidden, so scroll and resize dismiss it rather than let it drift. The markdown is prefetched when the menu opens and handed to the clipboard as a promise, so a slow fetch still copies inside the user gesture (Safari).
…pply Review of the previous commit found the hand-rolled unified diff (the fallback for a diff surface sent as before/after file pairs) never emitted `\ No newline at end of file`. Fuzzing it against `git apply` failed 368 of 600 random pairs: a file with no trailing newline produced a patch git rejects, and the asymmetric case applied but silently reintroduced a newline the "after" text never had. Three fixes, one root cause — the end-of-file newline was not modelled: - The flag is now part of a line's identity, so a last line with no newline after it cannot match one with. That also fixes a trailing-newline-only change reading as an empty diff and degrading to a link. - splitLines() treats an empty file as zero lines rather than one blank one, so an insertion into an empty file is `@@ -0,0 +1,N @@`, not `@@ -1,1 @@`. - codeHeading() reuses splitLines() instead of a raw split, so an excerpt whose code ends in a newline no longer overstates its line range by one. Tests now apply each patch with `git apply` and compare the result byte for byte, instead of regex-matching hunk text — the old assertions passed against patches git refuses. Same fuzz run over the fixed code: 482 applied, 0 failures.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Consolidates the card's three separate export actions (copy link, open in new tab, open as PNG) into a single Share menu, and adds Copy as markdown to export the entire post as portable markdown.
Changes
Server-side markdown flattening (
server/postMarkdown.ts):node:imports) so it runs on both Node and Cloudflare Workersgit applypostToMarkdown()andsurfaceToMarkdown()for converting posts/surfaces, plusstripAnsi()for cleaning terminal output andunifiedDiff()for generating patchesViewer share menu (
viewer/src/ShareMenu.tsx):Clipboard utility (
viewer/src/clipboard.ts):writeClipboard()function that handles both ready strings and pending promisesAPI route (
server/app.ts):GET /api/posts/:id/markdownendpoint that serves the post as portable markdown with absolute links (so it survives pasting elsewhere)Tests:
test/postMarkdown.test.ts) covering all surface kinds, edge cases like backticks in code blocks, line number tracking for excerpts, and unified diff correctness validated againstgit applyviewer/test/clipboard.test.ts) for both ready and pending text, promise-valued ClipboardItem fallback behaviore2e/viewer.spec.ts) verifying the menu appears, markdown copies correctly, and keyboard/click dismissal workstest/api.test.ts) confirming the markdown endpoint returns proper content-type and absolute linksUI updates:
Notable details
https://claude.ai/code/session_01EnBQ4vuJfaVNmQsZLYZQRT