Format rendered Markdown wherever it renders

2092c8f6f637 · AtlantisPleb · · parent c32e86bd05a0

Format rendered Markdown wherever it renders

The repository view asked for a class called `code-markdown` that was never
written, so a README arrived as unstyled text: headings the size of body
copy, no list markers, no code chrome. It also rendered with hard breaks on,
which belong to a typed message and not to an authored file — every wrap in
the source became a line break, so the README read at the width of its own
source rather than the width of the column.

The docs pages already had the ruleset. It is now named `.markdown`, after
what it renders rather than where, and the README and the source browser's
Markdown view share it; `.docs-prose` keeps only its column. Headings below
h3 join it, since a README goes deeper than a docs page does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016o8HwTaqLKEWCHTjsjFtrB
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • modified assets/css/openagents.css
  • modified lib/openagents_web/live/code_blob_live.ex
  • modified lib/openagents_web/live/code_repo_live.ex
  • modified lib/openagents_web/live/docs_live.ex
  • modified test/openagents_web/live/code_live_test.exs

Diff

5 files changed, +82 -29

assets/css/openagents.css modified +45 -21

@@ -2368,8 +2368,21 @@

2368 2368
2369 2369
/* ── Rendered Markdown ────────────────────────────────────────────────────── */
2370 2370
2371
/* One ruleset for every surface that renders Markdown to HTML: the docs pages,
2372
 * a repository's README, and a Markdown file in the source browser. The class
2373
 * used to be `.docs-prose`, which is why the README rendered as unstyled text
2374
 * for as long as it did -- the markup asked for `.code-markdown`, a class that
2375
 * was never written. Naming the ruleset after what it renders rather than
2376
 * where it renders keeps the next surface from repeating that. Chat keeps its
2377
 * own `.message-markdown`, tuned for a bubble rather than a document. */
2378
2371 2379
@layer components {
2372
  .docs-prose h1 {
2380
  .markdown {
2381
    min-width: 0;
2382
    color: var(--text-body);
2383
  }
2384
2385
  .markdown h1 {
2373 2386
    margin: 0 0 8px;
2374 2387
    color: var(--text-primary);
2375 2388
    font-size: 2rem;

@@ -2378,7 +2391,7 @@

2378 2391
    line-height: 1.2;
2379 2392
  }
2380 2393
2381
  .docs-prose h2 {
2394
  .markdown h2 {
2382 2395
    margin: 40px 0 12px;
2383 2396
    color: var(--text-primary);
2384 2397
    font-size: 1.375rem;

@@ -2387,7 +2400,7 @@

2387 2400
    scroll-margin-top: 24px;
2388 2401
  }
2389 2402
2390
  .docs-prose h3 {
2403
  .markdown h3 {
2391 2404
    margin: 28px 0 8px;
2392 2405
    color: var(--text-primary);
2393 2406
    font-size: 1.0625rem;

@@ -2395,43 +2408,54 @@

2395 2408
    scroll-margin-top: 24px;
2396 2409
  }
2397 2410
2398
  .docs-prose p {
2411
  .markdown p {
2399 2412
    margin: 0 0 16px;
2400 2413
    line-height: 1.7;
2401 2414
  }
2402 2415
2403
  .docs-prose ul,
2404
  .docs-prose ol {
2416
  /* A README goes deeper than a docs page does. Below h3 the difference is
2417
     carried by weight rather than size, because a fourth size in a document
2418
     this shallow reads as noise. */
2419
  .markdown :is(h4, h5, h6) {
2420
    margin: 20px 0 8px;
2421
    color: var(--text-primary);
2422
    font-size: 0.9375rem;
2423
    font-weight: 600;
2424
    scroll-margin-top: 24px;
2425
  }
2426
2427
  .markdown ul,
2428
  .markdown ol {
2405 2429
    margin: 0 0 16px;
2406 2430
    padding-left: 22px;
2407 2431
    line-height: 1.7;
2408 2432
  }
2409 2433
2410
  .docs-prose ul {
2434
  .markdown ul {
2411 2435
    list-style: disc;
2412 2436
  }
2413 2437
2414
  .docs-prose ol {
2438
  .markdown ol {
2415 2439
    list-style: decimal;
2416 2440
  }
2417 2441
2418
  .docs-prose li {
2442
  .markdown li {
2419 2443
    margin-bottom: 6px;
2420 2444
  }
2421 2445
2422
  .docs-prose a {
2446
  .markdown a {
2423 2447
    color: var(--accent-bright);
2424 2448
    text-decoration: underline;
2425 2449
    text-underline-offset: 2px;
2426 2450
  }
2427 2451
2428 2452
  @media (hover: hover) {
2429
    .docs-prose a:hover {
2453
    .markdown a:hover {
2430 2454
      text-decoration: none;
2431 2455
    }
2432 2456
  }
2433 2457
2434
  .docs-prose code {
2458
  .markdown code {
2435 2459
    padding: 1px 5px;
2436 2460
    border: 1px solid var(--line);
2437 2461
    border-radius: 4px;

@@ -2442,7 +2466,7 @@

2442 2466
  }
2443 2467
2444 2468
  /* A fenced block is already a panel; the inline chrome would double it. */
2445
  .docs-prose pre {
2469
  .markdown pre {
2446 2470
    margin: 0 0 16px;
2447 2471
    padding: 14px 16px;
2448 2472
    overflow-x: auto;

@@ -2451,7 +2475,7 @@

2451 2475
    background: var(--ink-raised);
2452 2476
  }
2453 2477
2454
  .docs-prose pre code {
2478
  .markdown pre code {
2455 2479
    padding: 0;
2456 2480
    border: 0;
2457 2481
    background: transparent;

@@ -2459,39 +2483,39 @@

2459 2483
    line-height: 1.6;
2460 2484
  }
2461 2485
2462
  .docs-prose blockquote {
2486
  .markdown blockquote {
2463 2487
    margin: 0 0 16px;
2464 2488
    padding-left: 16px;
2465 2489
    border-left: 2px solid var(--line-strong);
2466 2490
    color: var(--text-muted);
2467 2491
  }
2468 2492
2469
  .docs-prose hr {
2493
  .markdown hr {
2470 2494
    margin: 32px 0;
2471 2495
    border: 0;
2472 2496
    border-top: 1px solid var(--line);
2473 2497
  }
2474 2498
2475
  .docs-prose strong {
2499
  .markdown strong {
2476 2500
    color: var(--text-primary);
2477 2501
    font-weight: 600;
2478 2502
  }
2479 2503
2480
  .docs-prose table {
2504
  .markdown table {
2481 2505
    width: 100%;
2482 2506
    margin: 0 0 16px;
2483 2507
    border-collapse: collapse;
2484 2508
    font-size: 0.875rem;
2485 2509
  }
2486 2510
2487
  .docs-prose th,
2488
  .docs-prose td {
2511
  .markdown th,
2512
  .markdown td {
2489 2513
    padding: 8px 10px;
2490 2514
    border-bottom: 1px solid var(--line);
2491 2515
    text-align: left;
2492 2516
  }
2493 2517
2494
  .docs-prose th {
2518
  .markdown th {
2495 2519
    color: var(--text-primary);
2496 2520
    font-weight: 600;
2497 2521
  }
lib/openagents_web/live/code_blob_live.ex modified +5 -2

@@ -133,8 +133,11 @@ defmodule OpenAgentsWeb.CodeBlobLive do

133 133
          </.alert>
134 134
135 135
          <.card id="code-blob">
136
            <div :if={@markdown?} class="code-markdown">
137
              {OpenAgents.Markdown.to_html(@blob.content)}
136
            <%!-- Authored document, not a message: hard breaks belong to
137
            typed text, and here they would only reproduce the source file's
138
            own wrapping. --%>
139
            <div :if={@markdown?} class="markdown">
140
              {OpenAgents.Markdown.to_html(@blob.content, hardbreaks: false)}
138 141
            </div>
139 142
            <pre :if={not @markdown? and not @blob.binary} class="code-source"><code>{@blob.content}</code></pre>
140 143
            <.empty :if={@blob.binary} id="code-binary" title="Binary file">
lib/openagents_web/live/code_repo_live.ex modified +6 -2

@@ -257,10 +257,14 @@ defmodule OpenAgentsWeb.CodeRepoLive do

257 257
            </:commit>
258 258
          </.file_table>
259 259
260
          <%!-- A README is an authored document, not a message: its source is
261
          wrapped at an editing width, so hard breaks would end every one of
262
          those wraps in a line break and render the file at the width of its
263
          source rather than the width of this column. --%>
260 264
          <.card :if={@readme} id="repo-readme">
261 265
            <h2>{@readme.name}</h2>
262
            <div class="code-markdown">
263
              {OpenAgents.Markdown.to_html(@readme.blob.content)}
266
            <div class="markdown">
267
              {OpenAgents.Markdown.to_html(@readme.blob.content, hardbreaks: false)}
264 268
            </div>
265 269
          </.card>
266 270
lib/openagents_web/live/docs_live.ex modified +2 -2

@@ -51,7 +51,7 @@ defmodule OpenAgentsWeb.DocsLive do

51 51
  @impl true
52 52
  def render(%{live_action: :index} = assigns) do
53 53
    ~H"""
54
    <div id="docs-index" class="docs-prose">
54
    <div id="docs-index" class="docs-prose markdown">
55 55
      <h1>Documentation</h1>
56 56
      <p>
57 57
        Everything documented here is something you can reach today. Where a page

@@ -73,7 +73,7 @@ defmodule OpenAgentsWeb.DocsLive do

73 73
  def render(assigns) do
74 74
    ~H"""
75 75
    <div class="docs-page">
76
      <article class="docs-prose" id={"docs-#{@page.item.slug}"}>
76
      <article class="docs-prose markdown" id={"docs-#{@page.item.slug}"}>
77 77
        {Phoenix.HTML.raw(@page.html)}
78 78
      </article>
79 79
test/openagents_web/live/code_live_test.exs modified +24 -2

@@ -73,7 +73,15 @@ defmodule OpenAgentsWeb.CodeLiveTest do

73 73
  defp seed_repo(repo) do
74 74
    path = Repos.ensure_repo!(repo)
75 75
76
    readme = write_blob(path, "# OpenAgents test repo\n\nFixture readme.\n")
76
    # The trailing paragraph is wrapped the way a file is wrapped, so a test can
77
    # tell a rendered document from one line break per source line.
78
    readme =
79
      write_blob(
80
        path,
81
        "# OpenAgents test repo\n\nFixture readme.\n\n" <>
82
          "This paragraph is wrapped like a file\nrather than like a message.\n"
83
      )
84
77 85
    file = write_blob(path, "hello\n")
78 86
79 87
    tree_one =

@@ -179,6 +187,20 @@ defmodule OpenAgentsWeb.CodeLiveTest do

179 187
      assert html =~ "Fixture readme."
180 188
    end
181 189
190
    test "renders the README as a formatted document", %{conn: conn} do
191
      browsable()
192
      {:ok, _view, html} = live(conn, "/OpenAgentsInc/openagents.com")
193
194
      # Structure, not source: the heading is a heading, and it carries the
195
      # ruleset every rendered-Markdown surface shares.
196
      assert html =~ ~s(class="markdown")
197
      assert html =~ "<h1>OpenAgents test repo</h1>"
198
199
      # A file's own wrapping is not a line break. Hard breaks belong to typed
200
      # messages; here they would render the README at the width of its source.
201
      refute html =~ "wrapped like a file<br"
202
    end
203
182 204
    test "publishes no node internals and no account controls anonymously", %{conn: conn} do
183 205
      browsable()
184 206
      {:ok, _view, html} = live(conn, "/OpenAgentsInc/openagents.com")

@@ -387,7 +409,7 @@ defmodule OpenAgentsWeb.CodeLiveTest do

387 409
388 410
      assert html =~ @audit_heading
389 411
      refute html =~ "# #{@audit_heading}"
390
      assert html =~ ~s(class="code-markdown")
412
      assert html =~ ~s(class="markdown")
391 413
    end
392 414
393 415
    test "?plain=1 renders the raw markdown source", %{conn: conn} do

This page updates live while a promote is in flight · changelog