Files
pad/internal/server/render
xarmian 5af54ddc05 feat(attachments): markdown reference resolver for pad-attachment:UUID (TASK-874) (#291)
* feat(attachments): markdown reference resolver for pad-attachment:UUID (TASK-874)

Add the shared step that translates `pad-attachment:UUID` markdown
references into rendered HTML for image embeds, file chips, and missing
placeholders. Wired into the editor preview path; Go-side helpers seed
the future server-side rendering pipeline (export / shared item view).

TS side (`web/src/lib/markdown/attachments.ts`):
  - Pure helpers: parseAttachmentHref, attachmentDownloadUrl, isImageMime,
    formatAttachmentSize, renderAttachmentImage/Chip/Missing
  - resolveAttachmentImage / resolveAttachmentLink for the marked hooks
  - Image MIME → <img src=...?variant=thumb-md data-attachment-id=...>
  - Non-image MIME (or link syntax) → file chip with download attribute
  - Missing/deleted → "Missing attachment" placeholder span

`web/src/lib/utils/markdown.ts`:
  - renderer.image override (defaulting to marked's standard image when
    href is not pad-attachment:)
  - renderer.link checks for pad-attachment: prefix before the existing
    external/internal-link logic
  - renderMarkdown gains an optional attachmentResolver parameter; the
    resolver is threaded via a per-call module slot (synchronous render)
  - DOMPurify allowlist extended with data-attachment-id, download,
    width, height — ALLOW_DATA_ATTR stays false so only this single
    data-* attribute slips through

Go side (`internal/server/render/attachments.go`):
  - Mirror of the TS API so server-rendered output matches client output
    byte-for-byte for the same input
  - ResolveAttachmentReferences scans markdown source via regex,
    skipping fenced code blocks (backtick + tilde), substitutes both
    image and link forms
  - Comprehensive table-driven tests (24 cases) covering: href parsing,
    URL building, MIME detection, size formatting, image/chip/missing
    rendering, escape safety against script-tag injection in alt /
    filename / display text, fenced-code skip, tilde fences, title
    suffix on link destinations, nil resolver pass-through, no false
    positives on non-attachment URLs, deterministic round-trip

References are stored as opaque `pad-attachment:UUID` so a backend
migration (FS → S3) can rewrite storage_keys without touching item
content. See DOC-865 for the architecture.

Parent: PLAN-866 (Attachments Phase 1).

* fix(attachments): chip label double-escape + escaped-bracket lockstep per Codex review (round 1)

Two findings from the round-1 Codex review:

1. TS chip labels were double-escaped. renderer.link was passing the
   parseInline(tokens) HTML output to resolveAttachmentLink, which feeds
   it into renderAttachmentChip → escapeHtml. A label like
   `[**Report**](pad-attachment:id)` rendered literal
   `&lt;strong&gt;Report&lt;/strong&gt;` instead of plain text. Switched
   to the link token's raw `text` field; markdown emphasis inside chip
   labels now degrades to literal markers (acceptable for filename-style
   labels) and matches what the Go regex extracts.

2. Go regex didn't accept CommonMark `\]` / `\\` escapes inside link/image
   labels, so `[Q1 \] report](pad-attachment:id)` resolved on the TS side
   (marked handles escapes) but stayed literal on the Go side — breaking
   the documented lock-step contract. Updated the regex to accept escaped
   characters inside the alt/text capture, and added unescapeMarkdownText
   to mirror marked's behavior of dropping the backslash before the label
   reaches the render helpers.

Tests added: TestResolveAttachmentReferences_EscapedBrackets covers
image alt, link text, and combined backslash/bracket escapes;
TestUnescapeMarkdownText is the unit-level table for the unescape
helper (including dangling-backslash and non-punctuation pass-through).
2026-04-29 13:09:42 -04:00
..