Skip to content

[publish 1-5] code + sheet static renderers#2047

Merged
2witstudios merged 2 commits into
pu/publish-any-page-typefrom
pu/pub-1-5-code-sheet
Jul 13, 2026
Merged

[publish 1-5] code + sheet static renderers#2047
2witstudios merged 2 commits into
pu/publish-any-page-typefrom
pu/pub-1-5-code-sheet

Conversation

@2witstudios

@2witstudios 2witstudios commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add packages/lib/src/publish/render-code-page.ts (renderCodePage) and render-sheet-page.ts (renderSheetPage): static, script-free HTML renderers for CODE and SHEET pages. No syntax highlighting or client-side grid in v1 — renderCodePage HTML-escapes the raw code string into <pre><code>; renderSheetPage parses via sheets/parseSheetContent + evaluateSheet (reusing the sheet engine's own evaluation, no formula engine reimplemented) and emits a static <table>, with an optional hasHeaders flag to render the first row as <th>. Malformed/unparseable sheet content — and any parse/eval failure — falls back to an empty-state message; both renderers never throw.
  • Both renderers mirror render-document-page.ts exactly (1-4, [publish 1-4] document CSP + renderDocumentPage #2046): build escaped bodyHtmlwrapDocumentBody → prepend DOCUMENT_TYPOGRAPHY_CSS as a <style> block → delegate the whole head assembly (SEO/OG/Twitter/JSON-LD, favicon, CSP <meta>) to renderCanvasDocument with cspOverride: buildDocumentCsp() (from ../canvas/csp, script-src 'none') and injectThemeBridge: false. Both accept and pass through the same head-field inputs as renderDocumentPage (pageUrl, ogImageUrl, ogDescription, description, robots, faviconHref, faviconBaseUrl, lang, allowedAssetHosts).
  • Rebased onto origin/pu/publish-any-page-type after [publish 1-4] document CSP + renderDocumentPage #2046 merged; no parallel shell or duplicate CSP builder — document-shell.ts is untouched from its 1-3 state.

Test plan

  • bun run typecheck clean
  • bun run build (packages/lib) clean
  • turbo run test --filter=@pagespace/lib — 8062 passed, 0 failed (24 tests across render-code-page.test.ts / render-sheet-page.test.ts, including OG/canonical pass-through and CSP-override coverage)
  • No any types
  • Manually verified: escaped script-injection attempts in code/cell content never produce a literal <script in output; CSP meta always carries buildDocumentCsp()'s script-src 'none' verbatim

🤖 Generated with Claude Code

https://claude.ai/code/session_01JDNXDo1shhfCzWjUFLV1Ni

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 871ddf46-dd70-40f9-a2f1-8cd3d02c40e5

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pu/pub-1-5-code-sheet

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

2witstudios and others added 2 commits July 12, 2026 23:46
Add packages/lib/src/publish/render-code-page.ts and render-sheet-page.ts:
static, script-free HTML renderers for CODE and SHEET pages, sharing the
same document shell + CSP as DOCUMENT pages. No syntax highlighting or
client-side grid in v1 (Shiki is client-side; sheet is a plain <table>
snapshot of evaluated values).

- renderCodePage: HTML-escapes the raw code string into <pre><code>.
- renderSheetPage: parses via sheets/parseSheetContent + evaluateSheet
  (reuses the sheet engine's own evaluation, no formula engine
  reimplemented), emits a static <table>; optional hasHeaders renders
  the first row as <th>. Malformed/unparseable content and parse/eval
  failures fall back to an empty-state message, never throw.

1-4 (renderDocumentPage) hasn't landed on pu/publish-any-page-type yet,
so this also fills the missing shared pieces it depends on in
document-shell.ts: buildDocumentCsp() (script-src 'none', the documents
CSP contract) and renderDocumentShell() (doctype/head/CSP/typography
assembly) — both renderers, and eventually renderDocumentPage, delegate
to these instead of duplicating head assembly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDNXDo1shhfCzWjUFLV1Ni
Rebased onto origin/pu/publish-any-page-type now that 1-4 (#2046) landed
the canonical buildDocumentCsp() (packages/lib/src/canvas/csp.ts) and
renderCanvasDocument's cspOverride param.

- Deleted the parallel renderDocumentShell + duplicate buildDocumentCsp
  this PR had added to document-shell.ts (and their tests) — document-shell.ts
  is back to its 1-3 state (DOCUMENT_TYPOGRAPHY_CSS, documentStartsWithH1,
  wrapDocumentBody only).
- render-code-page.ts / render-sheet-page.ts now mirror
  render-document-page.ts exactly: build escaped bodyHtml, wrapDocumentBody,
  prepend DOCUMENT_TYPOGRAPHY_CSS as a <style> block, delegate the whole
  head assembly to renderCanvasDocument with cspOverride: buildDocumentCsp()
  (from ../canvas/csp) and injectThemeBridge: false.
- Both renderers now accept and pass through the same head-field inputs as
  renderDocumentPage (pageUrl, ogImageUrl, ogDescription, description,
  robots, faviconHref, faviconBaseUrl, lang, allowedAssetHosts).

Body-building, HTML-escaping, and sheet parsing/evaluation logic is
unchanged from the prior revision; shell-related test assertions updated
to match, plus new coverage for OG/canonical pass-through.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDNXDo1shhfCzWjUFLV1Ni
@2witstudios
2witstudios force-pushed the pu/pub-1-5-code-sheet branch from 3f535e7 to d977f0b Compare July 13, 2026 04:49
@2witstudios
2witstudios merged commit 45bb324 into pu/publish-any-page-type Jul 13, 2026
1 check passed
@2witstudios
2witstudios deleted the pu/pub-1-5-code-sheet branch July 13, 2026 14:12
2witstudios added a commit that referenced this pull request Jul 13, 2026
…de, sheets) (#2055)

* feat(publish): neutralize leftover dashboard links in published documents (#2033)

At publish time, links to published pages get rewritten to public URLs,
but anchors pointing at UNPUBLISHED pages keep their in-app
/dashboard/{driveId}/{pageId} href — a dead, login-walled link for an
anonymous visitor. Add a pure string transform that converts those
leftover anchors to inert <span>s, preserving inner HTML and the
data-mention-type/data-page-id attributes so mention-chip CSS still
applies. Published https:// URLs, fragments, non-dashboard hrefs, and
malformed/unclosed anchors are left byte-identical; the transform is
idempotent.


Claude-Session: https://claude.ai/code/session_01FYot8EYcBEA77oaNW3mZgo

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* fix(publish): respect quoted attribute values when matching dashboard anchors (#2036)

A valid anchor may carry a quoted attribute containing '>' after the href
(e.g. <a href="/dashboard/d/p" title="2 > 1">x</a>). The opening-tag
matcher treated that '>' as the end of the tag, so neutralizing emitted
corrupted markup (<span> 1">x</span>). Make the attribute region
quote-aware — quoted values may contain '>' — while still rejecting bare
'<'/'>' outside quotes so malformed/unclosed anchors stay untouched.
Alternatives start with disjoint characters, so backtracking stays linear
(verified against 100k-char pathological inputs).

Addresses the P2 review finding on #2033.


Claude-Session: https://claude.ai/code/session_01FYot8EYcBEA77oaNW3mZgo

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* fix(publish): tokenizer-grammar attribute matching in neutralize-dashboard-links (#2037)

The quote-aware fix stopped at the opening-tag matcher; deeper review
found the same bug class one level down plus new hazards it introduced:

- HREF_RE/MENTION_ATTR_RE scanned the raw attribute region quote-blind,
  so `href=` or `data-page-id=` text inside another attribute's quoted
  value was taken as real — destroying live links, letting dead
  dashboard links survive behind a decoy, and fabricating mention
  attributes on the emitted span.
- The attr region treated ANY quote as opening a value, so an unclosed
  quote or a quote in name position let one match span multiple tags
  and delete rendered content.
- Each complete `<a …>` with a missing close made the lazy inner group
  scan to end-of-input: measured 6.2s CPU on 1MB of malformed input.

Rework: the attribute region now follows the HTML tokenizer's grammar —
a run of name(=value)? tokens where quotes open a value only after `=`
and a quoted value may contain `>` but never `<` — so a match can never
cross a tag boundary; attributes are extracted by sequentially
tokenizing the validated region (one shared value grammar). Inner
content refuses to cross another `<a` opening, so an unclosed anchor
can't steal a later anchor's close tag and failed scans stop at the
next anchor (1MB pathological input: 6.2s -> 5ms). Hrefs are trimmed
like browsers trim URL attributes, and a `/dashboard/` substring
pre-filter skips the pass for documents with no dashboard links.

Grammar violations never match — when corrupt output and a missed
anchor are the only options, the module prefers the miss.


Claude-Session: https://claude.ai/code/session_01FYot8EYcBEA77oaNW3mZgo

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* [publish 1-2] sanitize-document-html pure module (#2034)

* feat(publish): sanitize-document-html pure module

Published DOCUMENT pages must never execute author scripts (unlike
CANVAS, whose isolation is by origin). CSP (script-src 'none') is the
enforcement layer; this is the hygiene layer on TipTap-authored HTML:

- <script>/<style>/<iframe>/<object>/<form> removed with content
  (any casing, junk-padded close tags, unclosed at EOF);
  <embed>/<base>/<meta>/<link> tags removed
- on* attributes removed, element kept
- href/src with javascript: or data:text/html schemes removed
  (case/whitespace/entity tricks normalized for the check only)

Quote-aware tag tokenizer with the (?=[\s/>]) delimiter lookahead from
canvas/render-document.ts (so <script-template> survives and quoted ">"
can't end a tag early), looped to a fixpoint so removals can't
reassemble surrounding text into a new executable tag. Pure function,
idempotent, benign TipTap output passes through byte-identical.

Adds the packages/lib exports + typesVersions entries for the new
publish/ subpath.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wrz5eTSEE6z3QxhUgW6iVP

* fix(publish): decode &sol; entities before URL scheme checks

data:text&sol;html hrefs slipped past hasDangerousScheme because the
minimal entity decoder handled &colon;/&tab;/&newline; but not &sol;.
ASCII letters have no named entities, so colon + sol + whitespace named
entities plus numeric references make the decode complete for the two
denied scheme prefixes.

Codex review P2 on PR #2034.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wrz5eTSEE6z3QxhUgW6iVP

* test(publish): review-pass hardening — sticky tag-name regex, coverage gaps

- TAG_NAME now a sticky regex matched in place (no per-tag slice)
- Tests: unquoted javascript: href, SVG xlink:href, /-separated on* attrs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wrz5eTSEE6z3QxhUgW6iVP

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* feat(lib): add required publishable capability to page-type configs (#2040)

Publishing is currently CANVAS-only via a hard gate in publish-page.ts.
This adds a required `publishable: boolean` to PageTypeCapabilities so
future page types must decide publishability at declaration time, plus
isPublishablePageType()/getPublishablePageTypes() helpers.

Publishable: CANVAS, DOCUMENT, CODE, SHEET (content in pages.content).
Not publishable: FOLDER, CHANNEL, AI_CHAT, FILE, TASK_LIST, MACHINE
(content lives in chat/tasks tables, object storage, or machine state).


Claude-Session: https://claude.ai/code/session_01B3WTcZGWpLBszpdkuqNpan

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* feat(publish): generalize PublishControls into ViewHeader for all publishable page types (#2045)

Move CanvasPublishControls to content-header/PublishControls (behavior and
props unchanged) and mount it from ViewHeader, gated by
isPublishablePageType(page.type). contentDirty is wired through the same
useDocumentManagerStore selector pattern as DocumentSaveStatus, so the
Stale/Update flow works for every editor that flows through the document
manager, not just canvas. The CanvasPageView mount is removed — single
mount point, hard cutover.


Claude-Session: https://claude.ai/code/session_019hKLnVwZfHqDxhT6X7vm2L

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* [publish 1-3] document shell + typography CSS (#2035)

* [publish 1-3] document shell + typography CSS

Add packages/lib/src/publish/document-shell.ts: DOCUMENT_TYPOGRAPHY_CSS
(.ps-document-scoped blog-post typography with hardcoded light values and
a prefers-color-scheme dark override — published pages have no app CSS
vars), documentStartsWithH1 (tolerates leading whitespace/comments), and
wrapDocumentBody (article shell with escaped <h1> title header, suppressed
when the body already leads with an h1). Mention anchors
(a[data-mention-type]) render as subtle inline chips.

Creates the publish/ module dir + exports entry (leaf 1-2 not landed yet).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011NfYFfxkMtytdp4zAhPyNZ

* fix(publish): dark page canvas + UA margin reset in document shell

Two review fixes: published pages load no app stylesheet, so (1) dark
mode must style the page canvas, not just the article foreground —
:root color-scheme + explicit body backgrounds (#ffffff / #0d1117) are
the one deliberate exception to .ps-document scoping; and (2) browser
default element margins are zeroed via a low-specificity :where() reset
placed before the sibling rhythm rule, so the rhythm isn't doubled by
UA margins (and can't be beaten by a higher-specificity reset).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011NfYFfxkMtytdp4zAhPyNZ

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* [publish 1-4] document CSP + renderDocumentPage (#2046)

* [publish 1-4] document CSP + renderDocumentPage

- buildDocumentCsp() in csp.ts: baseline directives with script-src
  'none' and form-action 'none' (documents never run author scripts).
- renderCanvasDocument gains optional cspOverride, used verbatim
  instead of buildBaselineCsp(...) when present; unchanged output
  when absent.
- renderDocumentPage() in publish/render-document-page.ts: sanitizes
  author HTML, wraps it in the document shell, prepends the shared
  typography CSS, and delegates to renderCanvasDocument with
  cspOverride: buildDocumentCsp() and injectThemeBridge: false.

* [publish 1-4] add exports entry for publish/render-document-page

apps/web (leaf 1-7) will import renderDocumentPage; without this
entry the subpath import hits TS2307. Mirrors the exports +
typesVersions declaration of the sibling ./publish/sanitize-document-html
entry exactly.

* [publish 1-5] code + sheet static renderers (#2047)

* [publish 1-7] per-type render branch: publish any page type (#2052)

publishCanvasPage/renderNotFoundPageHtml gate on isPublishablePageType
(from getPublishablePageTypes) instead of a hardcoded CANVAS check.
Extracts a shared preparePublishContent() that branches per page type:

- CANVAS: unchanged (rewriteCanvasAssets -> rewriteInterPageLinksForDrive
  -> extractAndStripOgMeta -> renderPublishedPage), byte-identical.
- DOCUMENT: markdown->HTML via marked (contentMode-gated) -> same asset/
  link rewriting -> neutralizeDashboardLinks -> extractAndStripOgMeta ->
  renderPublishedDocument.
- CODE/SHEET: raw content passed straight to renderPublishedCode/
  renderPublishedSheet (not HTML, no rewriting needed).

Adds renderPublishedDocument/renderPublishedCode/renderPublishedSheet
wrappers in render-published.ts with the same allowedAssetHosts
plumbing as renderPublishedPage.


Claude-Session: https://claude.ai/code/session_015j7689HB9YWSyBL2PpfeS2

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>

* fix(publish): OG-image state sync, XSS/CSS/UI review findings

- publishCanvasPage now returns the effective (server-resolved) SEO
  overrides; PublishControls reads settings back from the response
  instead of caching its own request payload, fixing a bug (flagged
  by Codex review) where publishing via an uploaded OG image file
  cached a blank ogImageUrl locally and could re-clear the resolved
  CDN URL on the next save.
- sanitizeDocumentHtml also blocks data:image/svg+xml href/src values
  (SVG documents can carry <script>, unlike inert raster data: URIs),
  closing a gap in the published-DOCUMENT script-src 'none' hygiene
  layer.
- HeaderPublishControls is now keyed on pageId so navigating between
  publishable pages remounts it instead of leaking local UI state
  (e.g. an open Publish Settings dialog) across pages.
- Mention-chip CSS now also matches the neutralized `<span
  data-mention-type>` markup that unpublished-target mentions render
  as, not just `<a data-mention-type>` — neutralizeDashboardLinks
  already preserved the attribute, the selector just never matched it.
- CODE pages no longer derive a fallback SEO description from raw
  source text (deriveDescription's HTML-tag stripping mangled
  generics/comparisons); left empty like SHEET's already-accepted
  non-prose fallback.
- buildDocumentCsp/buildBaselineCsp now share one asset/font CSP
  prefix constant instead of two independently maintained literals.

Ralph-loop convergence pass on PR #2055: CI was already green with no
unresolved review threads; these are proactive findings from an
8-angle review (correctness, removed-behavior, cross-file, reuse,
simplification, efficiency, altitude, conventions) plus the one
existing Codex bot comment. All 692 apps/web + 402 packages/lib tests
pass; typecheck, build, and lint are green.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.

1 participant