Conversation
Epic bd-61cd brings Q1-feature-parity listings to Q2 on top of the DocumentProfile + 2-pass architecture. Settled decisions captured in the design-discussion document: - C5 named-listing-item profile field (listing_item: ListingItemInfo with curated fields plus an extra: BTreeMap escape hatch scoped to listings consumers only). DOCUMENT_PROFILE_VERSION 2 → 3. - quarto-doctemplate (Pandoc-style $var$) for both built-ins and custom templates. No JS runtime; hub-client safe. - Generate/render decomposition: a pre-checkpoint ListingItemInfoStage auto-fills holes; author values always win. - L7 (engine-rendered preview upgrade) is bracketed as CLI-only with mandatory L1 fallbacks so hub-client and quarto preview show valid listings without sibling engine execution. Sub-issues bd-n8a4 (L0) through bd-qb4o (L11) filed with parent-child:bd-61cd plus inter-phase blocks deps mirroring the dependency graph in the epic plan. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a Sub-plan column and a Status column to the bd-issue mapping table. Make explicit that no per-phase sub-plans exist yet; the L0/L1/L4 open questions live in the epic plan itself under §Open questions until the per-phase sub-plans are authored. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User-confirmed decisions 2026-05-05: 1. Custom-template extension is .template (Pandoc kinship without compatibility implication; .ejs.md accepted with deprecation). 2. Image-from-first-body-image fallback ships in L1 v1. 3. list.min.js client-side sort/filter is in scope for L3 (with artifact-store fallback path documented). 4. HTML parsing in L7 uses scraper (CSS selector API alignment; verify WASM transitive deps don't break hub-client). 5. listing: stays a top-level frontmatter key (reconcile with bd-n9dr if it lands a namespaced placement). 6. ListingItemInfoStage at crates/quarto-core/src/stage/stages/ listing_item_info.rs. Per-phase scope sections updated to reference these decisions inline. "Open questions" section renamed and rewritten as "Resolved decisions" with audit-trail entries. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sub-plan for L0 of the listings epic. Adds DocumentProfile.listing_item: ListingItemInfo with curated typed sub-fields plus an extra: BTreeMap<String, ConfigValue> bag. Bumps DOCUMENT_PROFILE_VERSION from 3 → 4 (the epic plan said 2 → 3, but bd-o8pr already took v3 for the resources field; sub-plan corrects). Contract doc gets a new §"Scoped feature surfaces" explicitly forbidding non-listing consumers from reaching into profile.listing_item (and especially listing_item.extra), with the discipline written down as code-review-enforced rather than type- system-enforced. L0 is a pure type extension. No stage logic; no rendering. The ListingItemInfoStage that auto-fills holes (description, image, word-count, reading-time, date-modified) is L1. L0 only reads author-supplied frontmatter. Includes 14 tests (13 unit + 1 integration), TDD ordering, full xtask verify before close-out, end-to-end CLI MD5 verification on existing fixtures plus one new listings-l0 fixture. Six in-line decisions captured in §"Decisions log". Updates epic plan's bd table to mark L0 as Filed (draft). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Six clarifications resolved with the user before bd-n8a4 implementation starts: - C1: reading-time semantics → integer minutes only; display formatting is a render-time concern (L3+). - C2: kebab in YAML, snake in Rust; manual extraction lookup, no serde(rename_all). - C3: schema work scoped to test-fixture; quarto-yaml-validation integration deferred via follow-up. - C4: cross-path categories merge uses MergedConfig with the existing !prefer/!concat tag system — tags ride with values, so feeding profile.categories and listing_item.categories into the merge algorithm gives tag-aware behavior for free. - C5: silent-drop type-mismatch handling in L0; strict validation belongs at the L2 schema layer. - C6: extra namespace is distinct from curated fields. Sub-decision D7 added: L0 must preserve originating ConfigValue (with tags) alongside the flattened Vec<String> for both profile.categories and listing_item.categories so the consumer-side merge in C4 has tagged values to dispatch on. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Foundational data substrate for the listings epic (bd-61cd, the listings-feature epic). Adds two new DocumentProfile fields and bumps DOCUMENT_PROFILE_VERSION 3 → 4. No user-visible behavior change yet — later phases (L1+) read these fields. DocumentProfile additions: - listing_item: ListingItemInfo — scoped per-feature surface for listings consumers. Curated typed sub-fields (title, subtitle, description, image, image_alt, date, date_modified, categories, reading_time_minutes, word_count) plus extra: BTreeMap<String, ConfigValue> for custom listing templates. Default empty; serializer omits empty. - categories_raw: Option<ConfigValue> (D7 in the L0 sub-plan) — preserves the !prefer/!concat merge tags on the top-level categories: value so listings consumers can apply tag-aware merging via quarto_config::MergedConfig. Mirrored on ListingItemInfo as listing_item.categories_raw. Extraction reads listing-item: at frontmatter top level. Authoring keys are kebab-case (Quarto YAML convention); Rust fields are snake_case. Unknown keys at the top level of listing-item: drop silently (strict validation is L2's job, per C5 in the sub-plan). Type mismatches at known keys leave the field at its default rather than panicking. The extra: bag is preserved verbatim. Contract doc updates: added a new §"Scoped feature surfaces" documenting the listings-only access discipline; added Guarantees rows for resources, categories_raw, and listing_item; added v3 (bd-o8pr) and v4 (bd-n8a4) changelog entries (the v3 row was missing from the prior contract revision). YAML schema fixture entry under crates/pampa/test-fixtures/schemas/definitions.yml. Q2 has no production frontmatter-schema gate today, so this is a fixture-only artifact; runtime schema integration is deferred per the L0 plan's §"Schema status reality check". Tests: - 16 new unit tests in document_profile.rs covering is_empty semantics, serde mechanics, frontmatter extraction (curated fields, extra-passthrough, namespace-distinct C6 case, unknown-key drop, type-mismatch graceful drop), version-bump, v3 → v4 rejection, and three D7 categories_raw tests (top-level, listing_item, absent-when-no-frontmatter). - 1 new integration test in document_profile_pipeline.rs (pipeline_extracts_listing_item_from_frontmatter) exercising the full head pipeline through MetadataMergeStage to confirm the extraction works under realistic merged-metadata input. Net workspace test delta: +18 (8407 → 8425). cargo xtask verify (full, including hub-client + WASM) passes. End-to-end CLI verification recorded in the L0 sub-plan: rendering two otherwise-identical fixtures (one with listing-item:, one without) produces byte-identical HTML after normalizing the filename-derived asset paths. L0's new field is rendering-neutral. Plan: claude-notes/plans/2026-05-05-listings-L0-profile-extension.md Parent epic: bd-61cd Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ension) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sub-plan for L1 of the listings epic. New pipeline stage between IncludeExpansionStage and DocumentProfileStage; enriches meta.listing-item with auto-derived values (description, image, word_count, reading_time_minutes, date_modified) when the author hasn't supplied them. Uses pre-checkpoint metadata enrichment, not a side-channel — single extraction path, local author-vs-auto check, no new DocumentAst field, cache invalidation correct for free. L0 handoff items 1–10 incorporated. Notable: the stage writes to ast.meta (not the profile), so the contract-doc "Mutability: profiles are read-only" rule is preserved. Author values always win; L1 only fills holes; idempotence is required and tested. Includes 21 tests (16 unit + 2 stage-trait + 3 integration), TDD ordering, full xtask verify, end-to-end CLI MD5 verification on existing fixtures + new listings-l1 fixture. Three audit checkpoints flagged (inlines_to_plain_text duplicates, ConfigValue mutation API, WASM mtime path) that may surface follow-ups but should not block L1. Updates epic plan: marks L0 Closed (with commit refs), L1 Filed (draft), and adds settled-decisions item 7 recording DOCUMENT_PROFILE_VERSION = 4 plus the categories_raw D7 shape so future sub-plans don't re-stale. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
File bd-zzke (chore, P3) tracking the consolidation of six divergent inlines_to_(plain_)text helpers. Audited 2026-05-06: they are not duplicates — they disagree on Code/Math contribution, Quoted-character wrapping, Note recursion, LineBreak handling, and trimming. A consolidating refactor needs an options-driven helper plus per-site audit and snapshot- diff investigation, which is out of scope for L1. L1 sub-plan revised: replace the "audit four duplicates" checkpoint with a direct instruction to reuse metadata_normalize::inlines_to_plain_text (same crate, most complete coverage). The helper needs a visibility bump (pub(crate)) and a one-line doc-comment pointing at bd-zzke for any future third consumer. L1's word-count walker is now spec'd to strip Inline::Note content before passing inlines to the helper, so footnote prose doesn't count toward reading time (Q1 parity). Description-preview output recurses into footnotes the way authors see them in the rendered document. Decisions log D10, open sub-questions, risks, and orchestrator- handoff sections all updated to reflect the new scope. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
L1 was sketched with std::fs::metadata + a #[cfg(not(target_arch = "wasm32"))] gate. Better shape: route through the existing SystemRuntime::path_metadata trait method, which already abstracts native (std::fs) vs. WASM (Automerge VFS). Native populates modified; WASM currently returns None. Backend semantics live where they belong. bd-a3we (P2 feature) tracks teaching the WASM VFS to populate PathMetadata.modified from Automerge change-op timestamps, which is what makes "last modified" meaningful in hub-client-backed projects. Until it lands, hub-client renders get None for listing_item.date_modified and listings either omit the column or fall back to listing_item.date. L1 sub-plan revisions: - Helper renamed: filesystem_mtime_iso → mtime_iso(runtime, path). - autofill function takes the runtime via StageContext (verify the access pattern in worktree; flag if plumbing is needed). - Test 12 uses a test SystemRuntime impl rather than filetime::set_file_mtime; backend-agnostic. - Test 13 covers the runtime-returns-None contract — the exact behavior bd-a3we will eventually flip. - New decision D7b records the runtime-abstraction call. - Risks, audit checkpoint, implementation step, and orchestrator handoff updated to match. When bd-a3we resolves, L1 needs zero code change — the field just starts populating in WASM contexts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New pre-checkpoint stage that enriches `meta.listing-item.*` from
the post-include AST and runtime mtime when the author hasn't
supplied them. Wired into both native and WASM HTML pipelines
between IncludeExpansionStage and DocumentProfileStage. Author
values always win — the stage strictly fills holes.
Auto-fills:
- description: full first non-empty paragraph (D11; L3 owns
truncation).
- image: first inline-image URL in document order, walking into
Link / Emph / etc.
- word-count: tokenized scan, footnote text excluded for Q1
parity.
- reading-time-minutes: ceil(word_count / 200).
- date-modified: SystemRuntime::path_metadata().modified
formatted as YYYY-MM-DD UTC; native populates, WASM stays None
pending bd-a3we.
Mechanism: pre-checkpoint metadata enrichment via
ConfigValue::insert_path / contains_path. The L0 extractor then
produces a populated ListingItemInfo through its existing
single-extraction path; no side-channels.
Implementation-session decisions:
- D11: store full first-paragraph (no L1 truncation).
- D12: time = "0.3" added to quarto-core (chrono is not a
workspace dep; time is already present via quarto-hub).
- D13: shortcode-bearing image src (e.g. {{< meta thumb >}}.png)
is NOT filtered here; tracked under bd-8h9o for dedicated study.
- D14: ?Send async-trait convention for native+WASM; codified
in .claude/rules/wasm.md.
Tests: 25 new unit tests + 3 new integration tests. Full workspace
tests pass (8448/8448); cargo xtask verify (incl. WASM hub-client
build) green; cargo xtask lint clean.
No user-visible behavior change yet — listing_item is consumed
by L3+. Render output is byte-identical to the pre-L1 baseline.
Plan: claude-notes/plans/2026-05-05-listings-L1-autofill-stage.md
Parent epic: bd-61cd (claude-notes/plans/2026-05-05-listings-epic.md)
Follow-up: bd-8h9o (shortcode-bearing image src filter)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brings in the pre-checkpoint stage that auto-fills `meta.listing-item.*` from post-include AST + runtime mtime, populating the profile field added in L0 (bd-n8a4). No user-visible behavior change yet — wait for L3+ to consume. Plan: claude-notes/plans/2026-05-05-listings-L1-autofill-stage.md Parent epic: bd-61cd Follow-up: bd-8h9o (shortcode-bearing image src filter) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plus bd-8h9o filed as a discovered-from follow-up: investigate filtering shortcode-bearing image src in L1's first_image_src walker. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…5jm)
Foundation work for the L3 listing-resolve-transform sub-plan
(claude-notes/plans/2026-05-06-listings-L3-resolve-transform.md).
This commit lands:
- **L4.1 — pipe evaluator** (`crates/quarto-doctemplate/src/pipes.rs`).
Implements all 16 pipe names the tree-sitter grammar already
enumerates (`pairs`, `first`, `last`, `rest`, `allbutlast`,
`length`, `uppercase`, `lowercase`, `reverse`, `chomp`, `nowrap`,
`alpha`, `roman`, `left`/`center`/`right` with width + borders).
Wired into both `evaluator.rs` TODO sites (variable path and
partial-output path). The plan originally called for `escape` /
`escape_xml` / `date_format` as pipes — discovery during impl
showed these would require grammar changes; instead, dates get
pre-formatted server-side in the listing binding and markdown
re-parse handles HTML escaping.
- **Two latent parser bug fixes** (in `crates/quarto-doctemplate/src/parser.rs`):
the outer `pipe` rule arm was building a fresh `Pipe::new` and
dropping args from `pipe_left/center/right` children; the
bare-partial extraction was early-returning before collecting
sibling pipes inside an interpolation node. Both surfaced once
the evaluator started actually consuming pipes. Fixes both.
- **L4.3 — `project_listing_resolver` helper** in
`crates/quarto-doctemplate/src/resolver.rs`. Chains
`FileSystemResolver` (host-page-relative custom partials, for L8)
primary, `MemoryResolver` (built-ins, for L3) fallback. One-call
construction site for the listing render transform.
- **L4.2 — discovery, no new code.** Pampa already provides
`pampa::template::config_merge::config_to_template_value` over
all `ConfigValueKind` variants. L3's render transform will call
that directly. The plan is updated to reflect the discovery.
- **Q-12-N catalog entries** (`crates/quarto-error-reporting/error_catalog.json`).
Ten codes registered with subsystem `"listing"` covering every
diagnostic the L3 sub-plan promised (custom-template-not-yet,
inline-contents-not-yet, unknown-sort-field, id-collision,
field-display-names-not-string, listing-false-rejected,
template-needs-custom, template-file-missing, ejs-md-deprecated,
reparse-diagnostics).
- **L3 phase 3 — listing data model** under
`crates/quarto-core/src/project/listing/`:
- `config.rs` — `Listing` + supporting enums + the
`ConfigValue → Vec<Listing>` parser handling every shape from
the L2 reference doc (`listing: true` / `default` / `{...}` /
`[...]`). Per-type defaults (default field set + image-align
+ grid-columns + table sort/filter UI). Diagnostic emission
on shape errors.
- `item.rs` — `ListingItem` and the `hydrate_item(profile)`
function; falls through `listing_item.<field>` →
`profile.<field>` → filename stem for title.
- `filter.rs` — `apply_filters(items, include, exclude)` with
the curated → `extra` fallback rule (D12, user-confirmed
2026-05-06): preserves Q1 parity for blogs filtering on
`status: published`-style custom fields.
- `sort.rs` — `apply_sort(items, sort, diagnostics)` with
multi-key stable sort, missing-value-last semantics,
integer-numeric comparison for sort keys that parse as ints,
and a Q-12-3 warning on unknown sort fields.
- `placeholders.rs` — Q1-verbatim `5A0113B34292` /
`9CEB782EFEE6` hex tokens shared with the future L7 upgrade.
The plan file in `claude-notes/plans/2026-05-06-listings-L3-resolve-transform.md`
captures every decision (D1–D15), the three follow-up bd issues
filed (bd-0fd0, bd-0jyl, bd-0wyo), and the impl-time discoveries
that revised L4.1 / L4.2 from the original sub-plan.
Test count: workspace 8448 → 8534 (+86 new, all passing).
`cargo xtask verify` (full, including hub-client + WASM build):
clean. `cargo xtask lint`: clean.
The next L3 sessions implement the generate transform (phase 4),
render transform + built-in templates (phase 5), pipeline wiring +
e2e (phase 6), and vendored client-side assets (phase 7).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three discovered-from issues filed during the L3 (bd-ml8z) sub-plan authoring, before impl began. Each is linked to bd-ml8z via discovered-from. Detailed strawmans and source-code TODO marker locations live in the L3 sub-plan's "Filing reminder" section. - bd-0fd0 (task, p2): Lua filter injection slot between generate and render transforms. Today there is no slot inside AstTransformsStage between *_generate and *_render transforms; UserFiltersStage::pre/post bracket the whole AstTransformsStage. The L3 plan stores listing data on meta.listings.<id> as forward-compat for when this slot exists. - bd-0jyl (task, p2): Source-info threading through listing markdown re-parse. L3 collapses re-parse diagnostics into a single Q-12-10 host-page warning; proper threading (host YAML key → template substitution → markdown span) is a separate design session cutting across quarto-source-map, quarto-doctemplate, and the diagnostic builder. - bd-0wyo (task, p3): Server-precomputed other_metadata_html for the default listing. Q1's item-default.ejs.md iterates over fields *not* in a curated set and emits a div per field; this is dynamic in EJS and not expressible in doctemplate without server-side precomputation. v1 default listing renders curated-fields-only; this issue picks up the gap. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements the Pass-2 generate transform that resolves a host
page's `listing:` frontmatter into one or more `ResolvedListing`
records (config plus a hydrated, filtered, sorted, truncated
item set). Result lands on `RenderContext::resolved_listings`,
where `ListingRenderTransform` (next session) reads it.
Decisions baked in:
- **D2 revised in-flight.** The original D2 stored resolved
listings under `meta.listings.<id>` for Lua-mutation
forward-compat. Per D13 there is no Lua filter slot
between generate and render today, and the
`Listing`/`ListingItem` ConfigValue round-trip would be
~200 lines of boilerplate that drifts. We follow the
`crossref_index` precedent: a typed
`pub resolved_listings: Vec<ResolvedListing>` field on
`RenderContext`. When `bd-0fd0` (Lua injection slot)
lands, the natural integration point is a meta
serialize/deserialize bridge at the injection boundary;
this typed in-memory shape stays unchanged.
- **D10 (item discovery via `ProjectIndex.profiles()`)**.
No filesystem walk. Each listing's `contents:` glob is
matched against the enumerated project profiles, trying
both host-dir-relative (Q1 default `*.qmd` finds
siblings) and project-relative (explicit
`posts/**/*.qmd`) views. Identical behavior on native
and WASM, naturally excludes non-`.qmd` files Q2 doesn't
parse, naturally excludes the host page itself.
Code changes:
- `crates/quarto-core/src/project/discovery.rs` — expose
`glob_match_path(pattern, path) -> bool` and
`path_to_forward_slashes(&Path) -> String` so the
listing module reuses the existing matcher (no new
walker, no Phase-8 churn).
- `crates/quarto-core/src/project/listing/mod.rs` — add
`ResolvedListing { listing, items }` and re-export.
- `crates/quarto-core/src/render.rs` — add
`pub resolved_listings: Vec<ResolvedListing>` to
`RenderContext`. Initialized empty.
- `crates/quarto-core/src/transforms/listing_generate.rs` —
`ListingGenerateTransform` (`?Send` async). Reads
`meta.listing` (skips when absent or `false`), parses
via `parse_listings`, walks `ProjectIndex.profiles()`
filtering by glob, hydrates items, applies
include/exclude filters, applies explicit-or-default
sort (default = date desc except for `table` type
which preserves insertion order), truncates to
`max-items`. The pre-populated-resolved-listings case
is treated as an override (mirrors navbar's
skip-when-already-set rule).
- Make `transforms::navigation_active` `pub(crate)` so
the listing transform can reuse `page_relative_source`.
- Plan file updated to reflect D2 revision and phase-4
completion.
Tests: 10 new unit tests in `listing_generate::tests` covering
absent-key, listing-false, write-resolved, include-filter,
explicit sort, host-dir-relative glob, project-relative glob in
subdir, max-items, default-sort-is-date-desc, and the
override-via-pre-populated path.
Test count: workspace 8534 → 8544 (+10 new, all passing).
`cargo xtask verify` (full, including hub-client + WASM build):
clean.
Next session: L3 phase 5 (render transform + built-in templates +
binding builder + helpers).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements the Pass-2 render transform that consumes
`RenderContext::resolved_listings` (populated by phase 4) and
splices rendered listing markup into the host page's AST.
End-to-end shape:
1. `ListingRenderTransform` runs after `ListingGenerateTransform`
inside `AstTransformsStage`. It is a no-op when
`resolved_listings` is empty or when the host page disabled
listings via `listing: false`.
2. For each `ResolvedListing`:
a. **Binding build** (`project/listing/binding.rs`):
construct a `TemplateContext` matching L2 §"Per-item
template binding" — a top-level `listing` map (id, type,
fields, image-align, …), an `items` list (curated typed
fields + pre-rendered helpers + free-form `extra`), and a
`project` map (site-url, title from `meta.website.*`).
`extra` values pass through pampa's authoritative
`config_to_template_value` bridge so PandocInlines/Blocks
render correctly.
b. **Helper pre-rendering** (`project/listing/helpers.rs`):
each item carries an `image-html` string (an already-built
`<img>` tag or empty), a `metadata-attrs` string for
`list.min.js` (data-index + data-categories), and a
`description-placeholder` HTML comment in Q1's verbatim
hex-token format that the future L7 post-render upgrade
will substitute.
c. **Template apply** (`project/listing/templates.rs` +
`templates/*.template`): six embedded doctemplate sources
(`listing-default`/`-grid`/`-table` plus their
`item-*` partials), served by a `MemoryResolver` chained
with `FileSystemResolver` via L4.3's
`project_listing_resolver`. The render transform compiles
with the chain so L8 custom partials can shadow built-ins
by placing same-named files next to the host page.
d. **Markdown re-parse** via `pampa::readers::qmd::read`. The
fresh `SourceContext` is discarded; any re-parse
diagnostics collapse into a single `Q-12-10` warning on
the host page (per D3 + bd-0jyl).
e. **Splice** into the host AST. If a top-level
`Div #<listing.id>` exists, replace its content and mark
it with `data-listing-rendered="1"`. Otherwise append a
fresh `Div` with that id and class `quarto-listing`. The
data-* marker keeps the transform idempotent across
replays.
3. `type: custom` with no L8 implementation falls back to the
`default` built-in template and emits `Q-12-1`.
v1 simplifications recorded in the plan:
- The Q1 `otherFields` loop (per-item `metadata-value` divs for
any field outside the curated set) is deferred to bd-0wyo.
- The Q1 multi-paragraph link wrappers around metadata sections
(e.g. wrapping date + author + reading-time inside a single
`[…](path)`) are dropped — markdown can't represent them.
Each metadata field gets its own block with its own
`.listing-<field>` class. The Q1 click-the-whole-card
affordance comes back when phase 7 wires `quarto-listing.js`.
- `data-index`/`data-categories` on the wrapper Div land in
phase 7 alongside `list.min.js`.
Tests: 19 new unit tests across the listing module
(`binding` × 4, `helpers` × 5, `templates` × 1) plus 7 in
`listing_render::tests` (slot fill, append, idempotent,
custom→default fallback, description placeholder visibility,
skip-when-disabled, grid class). All pass.
Test count: workspace 8544 → 8563 (+19 new). `cargo xtask verify`
(full, including hub-client + WASM build): clean.
Next session: L3 phase 6 (pipeline wiring + e2e CLI fixture +
hub-client smoke).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires `ListingGenerateTransform` + `ListingRenderTransform` into
`build_transform_pipeline` between the navigation Generate and
Render sub-phases. Both the native CLI and WASM pipelines route
through this builder, so one insertion covers both targets.
Also lands two impl-time fixes that the integration tests
exposed:
- **`parse_listings` now handles `PandocInlines`.** Previously
it only matched `Scalar(Yaml::String)`, but YAML strings in
document frontmatter are commonly parsed as `PandocInlines`
(the inline interpretation path). The string / scalar / inline
cases now route through `ConfigValue::as_plain_text()` which
covers Scalar / Path / Glob / Expr / PandocInlines uniformly.
Without this, the literal `listing: default` from a host page's
frontmatter emitted Q-12-1 ("must be a boolean, type name,
object, or array") instead of building a default listing.
- **`try_replace_explicit_slot` now recurses into Div content.**
`SectionizeTransform` runs in the Normalization phase
(well before Navigation), so by the time
`ListingRenderTransform` looks for a user's `::: {#my-blog}`
slot, that Div is wrapped inside a `Div .section` from the
sectionize pass. The earlier top-level-only walker missed the
slot and appended a fresh wrapper Div instead, leaving the
user's empty slot stranded. The recursive walker handles
nested sections + nested user Divs alike. Q1 also recurses.
The slot's `data-listing-rendered="1"` marker keeps the
recursion idempotent.
Adds `crates/quarto-core/tests/listing_pipeline.rs` with five
integration tests that drive a fixture project through the full
`ProjectPipeline` (Pass-1 + Pass-2 across multiple files):
- `default_listing_renders_three_posts_in_date_desc_order` —
the canonical four-file blog. Asserts the wrapper Div + id +
data marker, all three post titles + hrefs + dates + authors,
the L7 description placeholder, and the date-desc ordering.
- `grid_type_emits_grid_classes` — `type: grid` ships
`quarto-listing-grid` + `quarto-grid-item` +
`quarto-listing-cols-3` (the latter sourced from the binding's
`listing.grid-columns`).
- `table_type_emits_listing_table_wrapper` — `type: table` ships
the `quarto-listing-table-wrapper` Div with the title/date/
author rows.
- `explicit_slot_div_id_is_filled` — exercises the recursion
fix above by placing `::: {#my-blog}` *inside* a section
(between two headings).
- `include_filter_drops_non_matching_items` — Alice survives,
Bob doesn't.
End-to-end CLI verification recorded in the L3 plan
(§"End-to-end CLI verification record"). Render of the four-file
blog fixture produced the expected `_site/posts/index.html`
with three items in date-desc order, each linking to the
rendered sibling's output href and carrying the
`<!-- desc(...) -->` placeholder comment.
Test count: workspace 8563 → 8568 (+5 integration). `cargo xtask
verify` (full incl. hub-client + WASM): clean.
`cargo xtask lint`: clean.
Next session: phase 7 (`list.min.js` / `quarto-listing.js` /
`quarto-listing.scss` vendored client-side assets) and close-out.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ships the two listing client-side JS assets to the artifact store as Project-scoped `js:`-prefixed artifacts. Phase-5's auto-emission in `ApplyTemplateStage` picks them up by key prefix and emits `<script>` tags into the rendered HTML; `flush_site_libs` writes the bytes to `_site/site_libs/listing/<name>.js`. The artifacts register from inside `ListingRenderTransform` (rather than as a dedicated transform) so they only ship when at least one listing is rendered — avoids bloating non-listing pages with unused JS. Vendored files: - `resources/listing/list.min.js` (~19KB, third-party MIT) — backs the sort/filter UI markup the templates emit. - `resources/listing/quarto-listing.js` (~7KB, Q1-owned glue) — provides `window.quartoListingCategory(...)` for category click handlers. Both copied verbatim from `external-sources/quarto-cli/src/resources/projects/website/listing/` per CLAUDE.md §"External Sources Policy". **SCSS deferred to bd-57y4.** Per L3 D5's spike-fallback contract, `quarto-listing.scss` (~13KB) requires Bootstrap- variable wiring + media-breakpoint mixins from the existing theme-CSS pipeline (`CompileThemeCssStage` / `quarto_sass::SassLayer`). That's a larger design exercise than the JS bundling. The follow-up issue captures the work; listings ship today with default browser styling — markup + JS are intact, layout less polished than Q1. Q1 parity for the SCSS lands when `bd-57y4` does. End-to-end verification (re-running the four-file fixture): ``` $ /tmp/listings-fixture/_site/posts/index.html grep: <script src="../site_libs/quarto/bootstrap.bundle.min.js"></script> <script src="../site_libs/listing/list.min.js"></script> <script src="../site_libs/listing/quarto-listing.js"></script> $ find _site -name "*.js": _site/site_libs/listing/list.min.js _site/site_libs/listing/quarto-listing.js _site/site_libs/quarto/bootstrap.bundle.min.js ``` Tests: 1 new integration test (`vendored_js_artifacts_emit_script_tags_and_land_under_site_libs`) asserts both `<script>` tags appear in the host HTML and both files land on disk under `_site/site_libs/listing/`. Verifies the depth-relative path resolution (host at `posts/index.html`, `<script src="../site_libs/...">`) so the integration covers the resolver edge case. Test count: workspace 8568 → 8569 (+1 integration). `cargo xtask verify` (full incl. hub-client + WASM): clean. `cargo xtask lint`: clean. Phase 7 closes out the L3 + L4 implementation. Next step: L3 verification + close-out (cargo xtask verify final pass; bd issue close; user approval before push). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Filed during L3 phase 7 (bd-ml8z) when the SCSS deferral path under D5 triggered. The L3 phase 7 commit shipped the JS pair (list.min.js + quarto-listing.js); the SCSS needs Bootstrap- variable wiring + media-breakpoint mixins integrated with the theme-CSS pipeline (CompileThemeCssStage / quarto_sass::SassLayer) — its own design task. Discovered-from bd-ml8z; tracked in claude-notes/plans/ 2026-05-06-listings-L3-resolve-transform.md §"Conditional follow-up issues". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Updates the L3 sub-plan §"Verification and close-out" to reflect all checkboxes that have completed: - cargo build --workspace clean - cargo nextest run --workspace: 8569 / 8569 passing (+121 from baseline 8448, broken down by phase) - cargo xtask lint: 692 files checked, all pass - cargo xtask verify (full incl. hub-client + WASM): green - end-to-end CLI verification recorded earlier in the plan Two items remain deferred to user-driven steps: - Hub-client browser smoke against a fixture (pre-push smoke, not blocking verify). - User approval before push, then bd-ml8z + bd-b5jm close-out and beads-sync commit from main repo. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a Node CLI script that takes a directory containing a Quarto 2
project and uploads it as a brand-new Automerge project on
wss://sync.automerge.org (or any sync server you point at via
--server). Reports the resulting IndexDocument id so you can open
the project in hub-client.
Goes through `@quarto/quarto-sync-client::createNewProject`, which
is the same code path the browser uses when hub-client creates a
project from the UI. The schema (IndexDocument with files map +
per-file TextDocumentContent / BinaryDocumentContent) stays
authoritative; this script doesn't reimplement it.
Plumbing notes:
- `fake-indexeddb/auto` is added as a devDependency and shimmed in
before the sync-client loads — `IndexedDBStorageAdapter` is
hardcoded inside createNewProject, and the Node runtime has no
native IndexedDB. The shim's process-scoped store is fine for a
one-shot upload.
- The script auto-builds the workspace ts-packages
(pandoc-types, quarto-automerge-schema, quarto-sync-client) on
demand by running `npx tsc` in each crate when its
`dist/index.js` is missing. quarto-sync-client has unrelated
test-file type errors that don't block JS emission, so we
swallow tsc's exit code there. (Once those tests are fixed
the swallow becomes redundant.)
- The createNewProject flow uses a 1-second peer-wait timeout
internally; against sync.automerge.org that often expires and
drops into "offline mode", then reconnects in the background.
The script waits on the `onConnectionChange(true)` callback
before disconnecting, then sleeps 5s for sync flush. The Node
TimeoutNegativeWarning that the offline-mode path produces is
filtered from stderr.
Walk rules:
- Recurses into subdirs.
- Skips: _site/, node_modules/, dist/, target/, and any
component starting with `.` (covers .git, .quarto, .beads,
.DS_Store, etc.).
- File classification by extension: known text extensions
upload as TextDocumentContent; everything else uploads as
BinaryDocumentContent (base64 + mimeType).
Optional `--verify` flag reconnects with a fresh sync client and
confirms every uploaded file path is reachable from the IndexDocument
on the server. Verified end-to-end against sync.automerge.org with
the listings fixture: 5 files round-tripped through a separate
process with no shared in-memory state, all reachable.
Usage:
node scripts/upload-project.mjs /path/to/quarto-project
node scripts/upload-project.mjs /path/to/project --server wss://your.server
node scripts/upload-project.mjs /path/to/project --verify
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…les them (bd-ml8z)
User-reported bug: listing items in hub-client preview don't
navigate to the .qmd source on click, while in-body markdown
links (`[label](other.qmd)`) do. Cause: my listing render
transform was emitting `output_href` (the project-relative
.html path) directly, bypassing `LinkRewriteTransform`. Native
CLI renders ended up with a project-relative `posts/a.html`
href instead of the page-relative `a.html` form, and hub-client's
`iframePostProcessor.ts` had nothing to intercept (no `.qmd`
suffix; no `/.quarto/project-artifacts/` prefix).
Fix: the binding's `path` field now carries a host-dir-relative
`.qmd` source-path string — the body-link convention. Downstream
`LinkRewriteTransform` (which already runs after listing render
in the AstTransformsStage Finalization phase, and which I
recently confirmed recurses into Divs) walks each link and
rewrites:
- native CLI: page-relative `.html` form (e.g. `a.html` for a
sibling), so the browser navigates correctly.
- hub-client / VFS-root resolver: artifact-rooted URL like
`/.quarto/project-artifacts/a.html`, which
`iframePostProcessor.ts`'s case-3 anchor handler reverse-maps
to `.qmd` for in-app navigation.
`outputHref` retains the project-relative `.html` path —
templates that genuinely need the post-render output URL (e.g.
the L7 description-placeholder, RSS feed item URLs in L9) read
that field instead of `path`.
Implementation:
- `binding.rs::build_listing_context` takes a new `host_dir`
argument: the host page's project-relative directory string
(forward-slash, no trailing slash; "" when the host is at the
project root). Used to compute host-relative item paths.
- `binding.rs::host_relative_qmd` strips the host-dir prefix
when the item is inside the host's directory; items outside
fall through to the project-relative form.
- `listing_render.rs::transform` derives `host_dir` from the
RenderContext via `page_relative_source(ctx)` and threads it
to `render_one` → `build_listing_context`.
Tests:
- New integration test
`listing_item_links_are_page_relative_after_link_rewrite`
asserts the rewritten `href="a.html"` form for siblings AND
the absence of the `posts/a.html` (unrewritten) form. Failed
before this commit; passes after.
- Updated existing binding-builder unit tests to pass `host_dir`.
- All other listing tests continue to pass; the existing
`default_listing_renders_three_posts_in_date_desc_order`
permissive assertion (`href="a.html"` OR `href="posts/a.html"`)
was already accepting the rewritten form, and the new test
pins it strictly.
End-to-end CLI verification: re-rendered the listings-demo
fixture and confirmed `<a href="a-second-listing.html" ...>`
(page-relative, post-rewrite) instead of
`<a href="posts/a-second-listing.html" ...>` (pre-rewrite).
Test count: workspace 8569 → 8570 (+1). `cargo xtask verify`
(full incl. hub-client + WASM): clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures the state of the branch, commit timeline, the five discovered-from bd issues filed during this work (bd-0fd0, bd-0jyl, bd-0wyo, bd-57y4, bd-xs2u), and five things the next session should know: - D2 was revised in-flight: resolved listings travel via a typed `RenderContext` field, not via `meta.listings.<id>`. The bd-0fd0 follow-up will add the meta serialize/deserialize bridge when a Lua-injection slot lands between Generate and Render transforms. - Listing item hrefs are `.qmd` source paths, not `.html` outputs — `LinkRewriteTransform` rewrites them downstream. Documents the resolver-direction split (native CLI → page-relative `.html`; hub-client / VFS → artifact-rooted URL caught by iframePostProcessor's case-3 handler). - `scripts/upload-project.mjs` is sticky tooling for sync-server testing in subsequent sessions, with quirks documented (auto-build, fake-indexeddb shim, peer-wait timeout). - Hub-client uses a UUID-based project route distinct from the Automerge IndexDocument id. - Worktree convention: implementation commits on the worktree branch; bd state on `feature/listings` in the main repo. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User-reported during L3 listings testing on 2026-05-06: uploading a project containing em-dash characters (U+2014) in document titles caused some hub-client misbehavior the user worked around by replacing them with single dashes. I (Claude) did not reproduce the bug myself — I only observed that the workaround made hub-client behave correctly afterwards. Could be in pampa parser, hub-client display, or sync round-trip. Filed for the next session to investigate; orthogonal to L3. Discovered-from bd-ml8z; tracked in claude-notes/plans/2026-05-06-listings-L3-resolve-transform.md §"Hand-off summary". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reference document only — no runtime code in L2 itself. The L3 session (bd-ml8z) implements the listing types described here.
…ms + doctemplate enhancements Implements the listing data model from L2's reference doc: typed Listing config + ListingItem hydration in crates/quarto-core/src/project/listing/. L3 (ListingResolveTransform): two-transform decomposition ListingGenerateTransform + ListingRenderTransform inside AstTransformsStage. Resolves listing.contents globs against ProjectIndex.profiles(), hydrates items, builds the L2 binding, applies the chosen built-in doctemplate (default | grid | table), re-parses the markdown output, splices into the host AST. Description-preview and preview-image placeholder comments emitted Q1-verbatim for L7's post-render upgrade. Listings are correct without L7 — every item carries the L1 fallback inline. L4 (doctemplate enhancements): pipe evaluator wired to the existing tree-sitter grammar pipe set (pairs, first, last, rest, allbutlast, length, uppercase, lowercase, reverse, chomp, nowrap, alpha, roman, left/center/right). Latent parser bug fixed: sibling pipes were being dropped. Built-ins use pampa's existing config_to_template_value bridge (no new ConfigValue → TemplateValue code in quarto-core). Built-in templates ship as include_str!-embedded MemoryResolver partials. Vendored client-side assets list.min.js + quarto-listing.js routed through the Phase-5 artifact store as Project-scoped artifacts. Q-12-N catalog entries (Q-12-1 through Q-12-10) added for listing-related diagnostics. End-to-end CLI verification recorded in the L3 sub-plan; cargo xtask verify (full incl. hub-client + WASM) was green at branch tip. Test-count delta: +122 (8448 → 8570 passing). Follow-up bd issues filed during impl: bd-0fd0 (Lua filter slot between generate and render), bd-0jyl (source-info threading), bd-0wyo (other_metadata_html helper), bd-57y4 (vendor-and- integrate quarto-listing.scss; deferred under D5), bd-xs2u (em-dash in titles breaks hub-client). See claude-notes/plans/2026-05-06-listings-L3-resolve-transform.md §"Hand-off summary" for the full impl-time decisions log and the source-of-truth state of the work.
Listings phases L2 (data model reference doc), L3 (resolve transforms + built-in templates), and L4 (doctemplate pipes + resolver) merged into feature/listings via b4f2238. Updates the epic table to mark all three closed.
Sub-plan for the listings categories sidebar phase. Adds per-item category chips to item-default/item-grid templates and a new CategoriesSidebarTransform that emits the right-margin sidebar via rendered.navigation.margin_categories. Markup is byte-for-byte Q1-compatible; SCSS bundling is deferred to bd-57y4. Encoding aligns with Q1's b64EncodeUnicode (btoa(encodeURIComponent (s))) so the vendored quarto-listing.js decoder works for non-ASCII categories. A follow-up bd is filed at impl start to revisit the encode/decode pair as a Q2-native review. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Land Q1-feature-parity categories markup for listing host pages: per-item category chips on default + grid item templates and a new right-margin sidebar. What landed: - `helpers::category_html` builds the per-item chips. Encoding matches Q1's `b64EncodeUnicode` (`btoa(encodeURIComponent(s))`) so the vendored `quarto-listing.js` decoder (`decodeURIComponent(atob(...))`) round-trips for non-ASCII category names. The `b64_encode_unicode` + JS-compatible `encode_uri_component` helpers are inlined locally; bd-754f files a future review of the encoding scheme. - New `category-html` key on the per-item template binding; `item-default.template` and `item-grid.template` splice it in inside `$if(show.categories)$ $if(category-html)$ … $endif$` so the field-disable path (drop `categories` from `listing.fields`) suppresses chips. Table template unchanged. - New `CategoriesSidebarTransform` aggregates categories across every resolved listing on the host page (skipping listings with `categories: Disabled`), renders the heading + container + pills HTML, and writes the result to `meta.rendered.navigation.margin_categories`. Three modes with Q1 parity: `category-default` (with "All" pill + counts), `category-unnumbered`, `category-cloud` (`ceil(count/total*10)` clamped to 1..=10). Pills sort case-insensitive. - `Listing.categories_source: SourceInfo` captures the YAML span on `categories:` for the Q-12-12 diagnostic. Parser captures `entry.key_source`; existing call sites get `SourceInfo::default()`. - New diagnostic codes: `Q-12-11` (mixed category modes on one page; first non-disabled mode in declaration order wins) and `Q-12-12` (categories enabled but no item has any; sidebar suppressed). Both emit at warning severity from the transform. - Pipeline wiring: `CategoriesSidebarTransform` runs between `ListingRenderTransform` (which restores `resolved_listings`) and `TocRenderTransform` so both `rendered.navigation.*` keys land before `ApplyTemplate` reads them. The same builder feeds the WASM pipeline through `AstTransformsStage::run`, so hub-client picks up L5 too. - `FULL_HTML_TEMPLATE` extended: the `#quarto-margin-sidebar` region opens whenever either `rendered.navigation.toc` or `rendered.navigation.margin_categories` is set, with both rendered inside (TOC first) when both are present. `MINIMAL_HTML_TEMPLATE` untouched. - `crates/quarto-core/Cargo.toml`: moved `base64.workspace = true` out of the native-only `[target.'cfg(not(target_arch="wasm32"))']` block so the new categories code compiles under the WASM build path. (`wasm-quarto-hub-client` already depended on `base64 = "0.22"` directly.) Tests: 8570 → 8615 (+45). Phase coverage: - helpers: +8 (category_html, encode_uri_component, b64 round-trip). - binding: +2 (`category-html` present/empty on item map). - listing_render templates: +4 (chips on default + grid; off when field disabled; table unchanged). - categories_sidebar unit: +19 (aggregation + emission + 3 modes + b64 + escape + sort). - categories_sidebar transform: +7 (no-ops + writes-html + preserves-resolved-listings + diagnostic emission). - template: +4 (#quarto-margin-sidebar with toc / categories / both / neither). - listing_pipeline integration: +1 (`listing_with_categories_*_e2e`). Followups filed at hand-off: bd-99ru (localize Categories/All), bd-754f (review encoding scheme), bd-ra5j (hub-client browser smoke; deferred per L3 precedent). Existing bd-57y4 description updated to reference L5. End-to-end CLI verification recorded in the plan (claude-notes/plans/2026-05-06-listings-L5-categories-sidebar.md §"End-to-end CLI verification record"): rendered a 4-file website fixture via `cargo run --bin q2 -- render`, inspected `_site/posts/index.html`, confirmed 4 chips + 4 sidebar pills + `#quarto-margin-sidebar` wrapper + `quarto-listing.js` reference all present. `cargo xtask verify` clean (Rust + hub-client build incl. WASM + all test suites). `cargo xtask lint` clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the four insta snapshot tests the L5 plan called for under §"Tests" #30–33. quarto-core didn't have insta in its dev-deps — adding it (insta.workspace = true) matches the convention used in sibling crates (pampa, quarto-highlight). Snapshots cover the three categories modes (default / unnumbered / cloud) on a 3-post fixture and the two-listing-aggregation case on a single host. Each test snapshots only the L5-owned slice of the rendered HTML — chip blocks + sidebar block extracted via depth-counted substring helpers — to keep snapshots small, readable, and robust against unrelated theme/markup changes. Bug surfaced and fixed during snapshot test #33: when the host declares `contents: "posts/*.qmd"` (or any glob with markdown- special characters), Quarto's YAML parser tags the value as `PandocInlines` (a `Span` with class `yaml-markdown-syntax-error`, because `*` triggers the markdown sublexer). `parse_contents` was matching only `Scalar(Yaml::String)` / `Glob` / `Array`, silently dropping the explicit value and letting `apply_type_defaults` overwrite with the sibling-only `*.qmd` default. The result: cross-directory listing globs were invisible at runtime (host page rendered but listing was empty) even though unit tests passed (the unit tests construct `Yaml::String` directly, never `PandocInlines`). Fix: route `parse_contents` through `as_plain_text` first (mirrors `parse_listings`'s top-level shorthand handling). Two new unit tests lock the behavior: - contents_pandoc_inlines_string_parses_as_glob - contents_array_with_pandoc_inlines_items_parses bd-nwyp is filed for the broader audit: other parser branches in listing/config.rs (parse_filter_list, parse_string_list, parse_sort, parse_field_types, etc.) likely share the same vulnerability and need the same as_plain_text-first treatment. Test count: 8615 → 8621 (+6: 4 snapshot tests + 2 parser tests). `cargo xtask verify` clean, `cargo xtask lint` clean. Snapshot files added (4 new .snap files under crates/quarto-core/tests/snapshots/): - listing_pipeline__snapshot_builtin_default_with_categories_default_mode.snap - listing_pipeline__snapshot_builtin_default_with_categories_cloud_mode.snap - listing_pipeline__snapshot_builtin_default_with_categories_unnumbered_mode.snap - listing_pipeline__snapshot_page_with_two_listings_aggregates_sidebar.snap Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Categories sidebar markup for listing host pages: - Per-item category chips on default + grid item templates. - New CategoriesSidebarTransform aggregates categories across resolved listings on the host page; emits the right-margin sidebar HTML with three modes (default/unnumbered/cloud) and matching Q1 markup so the vendored quarto-listing.js click handlers work unchanged. - FULL_HTML_TEMPLATE extended so #quarto-margin-sidebar opens for either rendered.navigation.toc or rendered.navigation.margin_categories. - New diagnostics Q-12-11 (mixed category modes on one page) and Q-12-12 (categories enabled but no item has any). - Encoding matches Q1's b64EncodeUnicode (btoa(encodeURIComponent (s))) so the JS decoder round-trips for non-ASCII categories. Bug fix shipped alongside (surfaced by snapshot test #33): parse_contents (crates/quarto-core/src/project/listing/config.rs) now routes through as_plain_text first so explicit globs like `contents: "posts/*.qmd"` survive Quarto YAML's PandocInlines wrapping (yaml-markdown-syntax-error span). bd-nwyp tracks the broader audit of sibling parser branches. Tests: 8570 → 8621 (+51). cargo xtask verify clean (incl. hub-client + WASM build); cargo xtask lint clean. Followups filed at hand-off: - bd-99ru (localize Categories/All labels) - bd-754f (review b64+percent-encoding scheme) - bd-ra5j (hub-client browser smoke deferred) - bd-nwyp (PandocInlines parser audit) Plan: claude-notes/plans/2026-05-06-listings-L5-categories-sidebar.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
L5 (categories sidebar) merged into feature/listings via 9e8afa0. Beads operations captured in this commit: - bd-5vsr closed (L5 — categories sidebar; close reason references the merge hash and the test-count delta). - bd-99ru filed: localize listing category sidebar labels (Categories, All). - bd-754f filed: review category click-handler encoding scheme (b64 + percent-encoding); the L5 markup matches Q1's b64EncodeUnicode for the JS round-trip but a Q2-native scheme may be simpler. - bd-ra5j filed: hub-client browser smoke for L5; deferred from the L5 session per the L3 precedent. Visual smoke is partial until bd-57y4 (SCSS) lands. - bd-nwyp filed: audit listing config parsing for the PandocInlines / yaml-markdown-syntax-error fallthrough that parse_contents was missing pre-L5 (fixed in commit 67a985f alongside the snapshot tests). - bd-57y4 description extended with an L5 cross-reference. Listings epic table updated to mark L5 closed with the impl + merge commit hashes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Listing hosts now advertise their `listing.*.contents:` glob strings on `DocumentProfile.listing_content_globs`, and the dep-graph builder expands them at graph-build time to add forward edges `host → content` and put the host in `force_render`. Mode B (`quarto render posts/foo.qmd`) now automatically pulls in listing hosts when any of their content files is targeted, so listing pages no longer go stale on partial renders. Changes: - `DocumentProfile`: new `listing_content_globs: Vec<String>` field; `DOCUMENT_PROFILE_VERSION` bumped 4 → 5. Default empty + `skip_serializing_if` keeps non-listing-host profiles compact. - `project::listing::config::extract_content_globs`: new pub fn, routes through `parse_listings` and discards diagnostics for a single source of truth on listing-shape handling. Followup `bd-bqf2` tracks a future shared-shape-walker refactor. - `project::dependency_graph::ProjectDependencyGraph::build`: new per-profile block adds listing-content forward edges and inserts each listing host into `force_render`. Reuses existing `glob_match_path` / `path_to_forward_slashes`. The orchestrator's `compute_augmented_render_set` is unchanged — the existing `augment_targets_with_always_render` primitive does the right thing once force_render carries listing hosts. - `project::discovery::relative_to_dir`: promoted from a private helper in `transforms::listing_generate` to a `pub(crate)` helper so the L3 generate transform and the L6 graph builder share one host-relative-to-project-relative coercion. - `claude-notes/designs/document-profile-contract.md`: new field row + change-log entry for the v5 bump. Tests (TDD, +26 over 8621 baseline → 8647 total): - Unit (`extract_content_globs`, 8): cover listing-shape walks including `listing: true`, map without contents, array of listings, inline records dropped, `listing: false`, missing key, and string-shorthand `contents:`. - Profile (5): default-empty field, extract from frontmatter, v4 rejection on read, round-trip preserves field, default profile omits the field from JSON. - Graph build (9): host-relative defaults, project-relative globs, no-match preserves force_render, force_render gating, empty-globs not in force_render, subdir self-edge avoidance, body-link/listing edge dedup, always_render+listing dedup, and multi-glob hosts. - Augmentation (3): listing host pulled in when content targeted, not pulled in when target unrelated, force_render gating still discriminates body-link-only pages. - Integration (1, `tests/incremental_rebuild.rs`): full ProjectPipeline Mode A → edit content → Mode B; asserts via mtime that `_site/index.html` refreshes and `_site/posts/bar.html` does not. End-to-end CLI verification recorded in the L6 plan (`claude-notes/plans/2026-05-07-listings-L6-dep-graph.md` §"End-to-end CLI verification record"): a real `quarto render` of a fixture showed `_site/index.html` re-rendered with updated sibling title after Mode B targeted only the edited post. Verification: `cargo xtask verify` — all 9 steps green (workspace build + workspace tests + lint + hub-client build + hub-client tests + WASM build + trace-viewer build + trace-viewer tests). The v5 profile field flows cleanly through wasm-quarto-hub-client. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bd-xbnf (L6) closed; merge commit 8b5efb9. The dep-graph integration shipped with `listing_content_globs: Vec<String>` on DocumentProfile (renamed from the original `listing_content_targets: Vec<PathBuf>` in the epic plan, since the field stores unresolved glob strings rather than resolved paths — a per-doc profile cache cannot safely cache resolution against the full project source set). Profile version bumped 4 → 5. Listings epic plan updated to match. Also files bd-bqf2 (open, P3) — followup task to refactor parse_listings + extract_content_globs to share a "walk_listing_shape" helper. Today extract_content_globs delegates to parse_listings and discards diagnostics; one source of truth on shape, but parses more than it needs. Worth tackling if the redundant work ever shows up in profiling, or if a third narrow extractor lands. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Match Q1's listing behavior on `quarto render` for the two
features that require sibling rendered output: per-item
description previews drawn from the engine-rendered first
paragraph, and per-item preview images discovered in
engine-rendered HTML (e.g. ggplot output from a code cell).
L3 emits envelope-marker placeholders around the L1 fallback
content; L7's post_render step regex-finds them, reads each
referenced sibling output, and substitutes the engine-rendered
preview when available — or strips the markers and keeps the
L1 fallback intact (with `Q-12-13`) when not. Bracketed feature
per the listings epic plan §L7 §"Bracketing rules":
- single home: `crates/quarto-core/src/project/listing/post_render_upgrade/`
- CLI-only by construction: module gated `#![cfg(not(target_arch = "wasm32"))]`,
`scraper` dep gated to non-wasm targets in Cargo.toml; verified
no leakage into the wasm32 dep tree.
- header comment carries the load-bearing rule-2 wording verbatim.
- L1 fallback contract preserved: removing L7 from `post_render`
produces correct (if less rich) listings; hub-client preview
shows the L1 fallbacks unchanged.
Changes:
- `project::listing::placeholders`: new `description_placeholder_begin/end`
+ `image_placeholder_begin/end` builders + regex sources.
Token (`5A0113B34292` / `9CEB782EFEE6`) preserved verbatim from
Q1; `-begin` / `-end` suffix is Q2-specific (Q1 has no L1
fallback contract, so it doesn't need to delimit a region).
Old single-comment helpers removed (D11 / D20).
- `project::listing::helpers`: four new helpers for the
begin/end pairs; image marker carries the listing's
`image-placeholder:` URL base64-encoded (URL_SAFE_NO_PAD) so
L7 doesn't have to walk source profiles at substitution time.
- `project::listing::binding`: replaces single `description-placeholder`
key with four new keys; image envelope keys always populated
(the template's `$if(image-html)$ ... $else$` branch decides
visibility, not the binding).
- `project::listing::templates::item-default.template` /
`item-grid.template`: description envelope wrapped in raw
`{=html}` blocks (D15); image envelope's `$else$` branch wraps
the empty placeholder div in `[``...``{=html}]($path$){.no-external}`
so the inline `<div>` is explicitly tagged raw HTML and the
link wrapper makes the substituted thumbnail clickable (D14).
The `$if(image-html)$` branch's `$image-html$` is wrapped the
same way to silence a pre-existing Q-2-9 / Q-12-10 warning
surfaced by the hub-client demo (the bare `<img>` inside link
brackets auto-converted to RawInline and warned).
`item-table.template` is unchanged.
- `project::listing::post_render_upgrade::reader`: listings-only
subset of Q1's `readRenderedContents`. `extract_first_para`
walks `<p>` descendants of `main.content`, skipping any whose
ancestry includes structural scaffolding (`<header>`, `<nav>`,
`<aside>`, `<footer>`) so Quarto's title block is correctly
ignored. v1 returns plain text (matching Q1's observable
behavior); HTML-aware extraction is filed as bd-bpdz (L9
reader-extension surface). `extract_preview_image` walks Q1's
selector chain (explicit `.preview-image`, `cell-output-display`
wrapper, named-pattern src match, `#quarto-document-content img`).
- `project::listing::post_render_upgrade::substitute`: regex-finds
the description / image envelopes in each output file, reads
referenced siblings (per-call HashMap cache keyed on absolute
path), substitutes appropriately. `Q-12-13` (single canonical
message per D17) emits when description sibling is missing or
produces no preview; image path is silent (Q1 parity, D4).
- `project::orchestrator::WebsiteProjectType::post_render`: one
new call inside the existing `#[cfg(not(target_arch = "wasm32"))]`
block, after `write_robots_txt`.
- `quarto-core/Cargo.toml`: adds `scraper` as a target-gated
native-only dependency.
- `quarto-error-reporting/error_catalog.json`: new `Q-12-13`
diagnostic.
- `tests/website_post_render.rs`: three new integration tests
(description success, image substitution via raw-HTML img to
bypass L1's auto-fill, default-project-no-substitution).
- `tests/listing_pipeline.rs`: existing assertion updated — L7
now strips envelope markers in post_render, so the test asserts
their absence + presence of engine first paragraphs instead.
Tests (TDD, +50 over 8647 baseline → 8697 total):
- Phase 1 placeholders (8): builder shapes, regex round-trip
(incl. two-envelope same-page case), drift guard between token
constants and regex sources.
- Phase 2 binding + render-transform (9): new binding keys
present, image envelope conditional on `image-html` branch,
static-image branch produces no Q-12-10 (regression for the
hub-client-discovered bug), b64 default URL flows through.
- Phase 3 reader (15): direct-child `<p>`, section-wrapped `<p>`,
truncation at word boundary, image selector chain
(.preview-image / cell-output / named-pattern / data: URI /
#quarto-document-content), regression for title-block-header
skip and heading-only-post-returns-None.
- Phase 4 substitute + cache (15): description success / L1
retention / Q-12-13 / max truncation / multi-envelope, image
success / listing-default cascade / empty-placeholder retention
/ host-relative URL resolution / no-Q-12-13-on-image-path,
per-call cache hits + no static cross-call cache.
- Phase 5 orchestrator (3): website pipeline substitutes, image
pipeline substitutes via raw-HTML img, default project type
is unaffected.
End-to-end CLI verification recorded in the L7 plan
(`claude-notes/plans/2026-05-07-listings-L7-postrender-upgrade.md`
§"End-to-end CLI verification record"): three fixtures rendered
via real `cargo run --bin q2 -- render` and inspected by hand —
description-substitution success path, L1-fallback path with
Q-12-13, and image-substitution path via raw-HTML img.
`cargo xtask verify` (full, including hub-client + WASM) green.
`cargo tree --target wasm32-unknown-unknown -p wasm-quarto-hub-client | grep -ci scraper` → 0.
Hub-client demo uploaded to wss://sync.automerge.org for visual
verification; surfaced the `Q-2-9` / `Q-12-10` warning on the
static-image branch (pre-existing template issue, fixed in this
commit + locked with the regression test above).
Follow-ups filed: bd-rvpd (source-span threading for Q-12-13 /
L9), bd-bpdz (reader extension surface for L9 RSS),
bd-399t (docs callout when the user-facing docs site comes
online; D13), bd-fx23 (defensive percent-encoding of listing.id
in the L7 image marker; D19, conditional).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Engine-rendered description previews + preview images flow into listing entries on `quarto render`. Hub-client preview shows the L1 fallbacks (per the L7 bracketing rules); native render substitutes engine content via post_render. See the merged feature commit and the L7 plan for the full architecture and end-to-end verification record.
bd-qf7r (L7) closed; merge commit dc3a0f7. Engine-rendered description previews + preview images now substitute into listing entries on `quarto render` via a bracketed post_render step, with the L1 fallback contract preserved for hub-client / non-CLI environments. Also files four follow-ups discovered-from bd-qf7r: - bd-rvpd (P3, task): source-span threading for L7's Q-12-13 diagnostic + future L9 RSS diagnostics. post_render works on rendered HTML, so spans need to thread through the L3 placeholder comment. - bd-bpdz (P3, task): reader extension surface for L9 RSS feeds. v1 ships listings-only `extract_first_para` / `extract_preview_image`; L9 will add math handling, syntax- highlight class maps, urls-to-absolute, anchor stripping. - bd-399t (P4, task): user-facing docs callout for L7's CLI-only behavior. Deferred until the `docs/` site becomes a real published site (D13). Wording locked in the L7 plan. - bd-fx23 (P4, task): defensive percent-encoding of `listing.id` in the L7 image-marker payload. Conditional — only fires if the schema gains permissive id syntax (D19). Listings epic plan table updated to mark L7 closed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wire `template:` config through to the listing render path so users
can supply their own doctemplate file at `<host-dir>/<template>` and
have the same data binding the built-ins receive (`listing.*`,
`items[*].*`, `project.*`, including `item.extra.<key>` and
`listing.template-params.<key>`).
Behavior:
- Custom listings load via `std::fs::read_to_string` rooted at the
host page's directory (Q1-parity); absolute paths are accepted
as-is.
- The chained resolver `project_listing_resolver(builtins_resolver())`
lets a custom template call any built-in partial (`item-default`,
`item-grid`, `item-table`, `listing-default`, …) and shadow it
with a same-named neighboring file.
- Failure modes are graceful (Q2 convention: warn + fall back, not
throw):
* Q-12-14 (new) — `type: custom` set but no `template:` path.
* Q-12-8 (newly emitted) — `template:` path could not be read.
* Q-12-10 (existing) — compile/render failure on the custom
source. Skips the listing rather than falling back, mirroring
the built-in path's behavior on a corrupt template.
- The Q-12-1 "L8 deferral" diagnostic is removed; the corresponding
test (`render_emits_q_12_1_for_custom_type`) is replaced by
`custom_listing_q_12_1_no_longer_emitted`.
WASM behavior: `std::fs::read_to_string` returns NotFound under
wasm32, so hub-client previews of `type: custom` listings fall back
to the default built-in with Q-12-8. Acceptable v1 behavior;
follow-up bd-tmka tracks plumbing the runtime through so VFS reads
work in WASM.
Tests: +14 in `transforms::listing_render::tests` and +1 in
`quarto-error-reporting`. `cargo nextest run --workspace` 8697 →
8712. `cargo xtask verify` (all 9 steps including hub-client + WASM)
green.
End-to-end CLI verification recorded in the sub-plan.
Plan: claude-notes/plans/2026-05-07-listings-L8-custom-templates.md
Follow-ups filed:
bd-tmka — WASM/VFS-aware custom listing template loading
bd-ubjo — broader path resolution for YAML-declared paths
bd-u4ow — docs/ page for custom listing templates
bd-fvuy — Q-12-10 catalog title/message inconsistency
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes: bd-rqgx — L8 custom listing templates (impl 92ca4c5 on beads/bd-rqgx-listings-custom-templates). Filed as L8-discovered follow-ups: bd-tmka — WASM/VFS-aware custom listing template loading (D1 deferred runtime/VFS plumbing; today custom templates fall back to default with Q-12-8 in hub-client previews). bd-ubjo — broader path resolution for YAML-declared paths (D2 — `!path` YAML tag + Q2 metadata-merging design will eventually unify path resolution). bd-u4ow — docs/ page for custom listing templates (D11 — picks up when the user-facing website tree under docs/ becomes a real public site). bd-fvuy — Q-12-10 catalog title/message inconsistency (pre-existing; surfaced again during L8 review). Epic plan updated to mark L8 row as closed with impl hash 92ca4c5; the merge hash will be filled when the L8 branch merges to feature/listings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Custom listing templates (`type: custom`, `template: <path>`) load from the host page's directory and render through the same binding the built-ins use, with `item.extra.<key>` and `listing.template-params.<key>` available verbatim. The chained FileSystemResolver → MemoryResolver lets custom templates call any built-in partial and shadow them by name. Failure modes are graceful (Q-12-14, Q-12-8) and the legacy Q-12-1 deferral diagnostic is removed. WASM/hub-client falls back to the default built-in with Q-12-8 under wasm32 (D1); follow-up bd-tmka tracks the runtime/VFS plumbing. See the L8 plan and feature commit for the full architecture, decisions log, and end-to-end CLI verification record. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
L8 (bd-rqgx) merged at cd2410f. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sub-plan for the L9 phase of the listings epic (bd-61cd):
RSS feed generation per Q1 parity, gated on website.site-url.
The plan documents the staged-file architecture (Pass-2 emits
<host-stem>.feed-{full,partial,metadata}-staged; post_render
substitutes placeholders against sibling rendered HTML), the
new feed/ submodule layout, the Q-12-15 / Q-12-16 diagnostic
codes, the imagesize dep gating (target-gated to native
alongside scraper), and the strict L7 reader bracketing
(L9 ships its own reader_ext.rs sibling rather than
extending L7's reader).
Plan-time clarifications resolved before impl:
- Transform names use ListingGenerateTransform /
ListingRenderTransform throughout (not the older
ListingResolveTransform name from L3's plan).
- date_format doctemplate pipe deferred to a follow-up bd.
L9's binding pre-computes pub_date_rfc822 server-side via
the time crate; adding the pipe would require a
tree-sitter grammar change (the pipe set is grammar-fixed)
with no L9-mandatory caller.
- ListingFeedLinkTransform appends to rendered.includes.header
(the favicon-precedent slot).
- feed/link_inject.rs sits outside the cfg(not(wasm)) gate;
the rest of feed/ is native-only.
- L7 reader is strictly duplicated in feed/reader_ext.rs per
the bracketing rule.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- error_catalog.json: add Q-12-15 (Listing Feed Skipped: Missing site-url) and Q-12-16 (Listing Feed: Sibling Output Unreadable). Both subsystem=listing. - catalog.rs: add error_catalog_has_q_12_15_and_q_12_16 test mirroring the L8 Q-12-14 pattern. - quarto-core/Cargo.toml: add imagesize = "0.13" under [target.'cfg(not(target_arch = "wasm32"))'.dependencies] for the L9 RSS feed <media:content> emission. Sibling to scraper (L7); same target gating. Verified WASM-cleanliness: `cargo tree --target wasm32-unknown-unknown -p wasm-quarto-hub-client | grep imagesize` is empty, and the WASM build succeeds. Phase 1 of L9. The feed/ submodule scaffold is intentionally deferred — empty-file stubs would be half-finished work; each subsequent phase creates its own feed/<file>.rs when the test that drives it is written. Plan-doc updates inline: - Phase 2 (date_format pipe) is now marked deferred. - D7 acknowledges Q1 divergence: Q1 has one feed per host page (kFeed lives on ListingSharedOptions). Q2 has feed per-Listing (already shipped in L2). Plan documents the qualified-filename rule for multi-listing pages. - ListingResolveTransform → ListingGenerateTransform /ListingRenderTransform throughout (matches actual code rather than the older L3-plan name). - Link-injection slot specified as rendered.includes.header (favicon precedent at website_favicon.rs:74). - Module-gate granularity clarified: feed/link_inject.rs outside the cfg(not(wasm)) gate; rest of feed/ native-only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the typed feed-binding layer that the upcoming ListingFeedStageTransform will consume to render RSS 2.0 preamble + per-item + postamble templates. Module layout: - crates/quarto-core/src/project/listing/feed/mod.rs Native-only `pub mod binding;` + a forward-looking header comment describing the module-gate granularity (link-inject will sit outside the cfg gate; everything else native-only). - crates/quarto-core/src/project/listing/feed/binding.rs Typed FeedChannel / FeedChannelImage / FeedItem / FeedItemImage shapes. Server-side XML escaping (text + attr forms) at construction time so the templates emit values verbatim. RFC 2822 / RFC 3339 / ISO date-only parsing for `<pubDate>`. Image dimension lookup via `imagesize::size`, with Q1's feedImageSize scaler (max 400h × 144w, smaller-axis-ratio bottleneck). Q1-verbatim placeholder token "B4F502887207". - crates/quarto-core/src/project/listing/feed/templates/ Three doctemplate files (preamble.template, item.template, postamble.template) — drafted now, wired in via include_str! during phase 3. Project wiring: - listing/mod.rs: declare `pub mod feed;` (unconditional; feed itself applies its own cfg gates). - quarto-core/Cargo.toml: add `parsing` feature to the `time` crate (already had formatting + macros). Needed for OffsetDateTime::parse and Date::parse. Tests (32 new): - xml_escape_text / xml_escape_attr semantics. - absolute_url joins with base+path normalization (handles trailing/leading slashes, external URLs, data URIs). - scale_to_feed_dimensions branches: under-limits passthrough, height-bottlenecked, width-bottlenecked. - mime_for_path for png/jpg/gif/webp/svg/apng/avif. - format_pub_date_rfc822 for RFC3339 / RFC2822 / date-only / unparseable. - build_feed_channel: full-metadata, feed.title-overrides-website, website-fallback when feed unset, last_build_date from supplied ISO, xml-stylesheet plumbing. - build_feed_item: pub_date_rfc822 for date-only inputs, XML escaping for title + categories, metadata-feed CDATA inlining, partial / full placeholder envelope shape. - build_item_image: 100x100 PNG → no scaling; 4000x3000 PNG → scaled to (144, 108); absolute URL → empty attrs; data URI → empty attrs; unreadable file → empty attrs. cargo nextest run -p quarto-core 'project::listing::feed::binding::tests' → 32 passed, 0 failed. cargo xtask verify --skip-hub-build --skip-hub-tests → clean. cargo xtask lint → clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the Pass-2 transform that emits one staged feed file per
feed-configured listing on the host page during native render.
What's in the diff:
- crates/quarto-core/src/project/listing/feed/stage.rs (new)
- ListingFeedStageTransform impls AstTransform.
- Embeds preamble.template / item.template / postamble.template
via include_str! and compiles them once per call.
- Lifts FeedChannel / FeedItem (typed structs from phase 2) into
TemplateContext maps with the keys the templates reference
(channel.title, channel.feed-link, item.description-element,
item.image.attrs, etc.).
- Skips when no listing has feed:; emits Q-12-15 once when
website.site-url is missing.
- Per-category sub-feeds (one extra staged file per declared
feed.categories entry, items pre-filtered to that category,
filename `<stem>-<lowercased-category>.feed-<type>-staged`).
- Multi-feed-per-host qualifier: bare `<stem>.feed-...-staged`
when one listing has feed:; `<stem>-<listing-id>.feed-...-staged`
when multiple do (D7).
- feed.items default 20; feed.items: 0 treated as missing
(matches Q1).
- prepareItems-equivalent: items missing both title and
output_href are skipped.
- lastBuildDate computed from the most-recent item.date
(parsed via the binding's RFC 2822/3339/ISO date helper);
falls back to "now" via OffsetDateTime::now_utc.
- Native-only via the parent module's cfg gate.
- crates/quarto-core/src/project/listing/feed/mod.rs
- `pub mod stage;` + re-export of the transform.
- crates/quarto-core/src/pipeline.rs
- Registers ListingFeedStageTransform after
CategoriesSidebarTransform in
build_html_pipeline_stages_with_apply_config. Push site
gated with `#[cfg(not(target_arch = "wasm32"))]` because the
type is native-only; the WASM pipeline doesn't compile this
arm.
- claude-notes/plans/2026-05-08-listings-L9-rss-feeds.md
- Phase-3 checklist marked complete.
Tests (10 new):
- stage_writes_metadata_feed_with_inline_descriptions: CDATA
inlining for type: metadata.
- stage_writes_partial_feed_with_placeholders: B4F502887207
envelope for type: partial.
- stage_writes_full_feed_with_placeholders: same envelope for
type: full.
- stage_emits_q_12_15_when_no_site_url: exactly one Q-12-15
diagnostic; no staged file.
- stage_writes_per_category_subfeeds: filtered items per
category file; main file still emits all items.
- stage_truncates_to_feed_items_count: feed.items: 3 → 3 items.
- stage_uses_default_20_items: 30 items → 20 emitted.
- stage_skips_when_no_listing_feed: no staged file written.
- stage_xml_stylesheet_pi_emitted_when_set: <?xml-stylesheet ?>
PI present in preamble.
- stage_qualifies_filename_for_multi_feed_hosts: per-listing
qualifier kicks in when >1 listing has feed:.
Verified:
- cargo nextest run -p quarto-core 'project::listing::feed'
→ 42 passed (32 binding + 10 stage), 0 failed.
- cargo nextest run --workspace → 8755 passed, 0 failed.
- cargo xtask lint → 699 files checked, all clean.
- npm run build:wasm (hub-client) → success. The cfg-gated
registration keeps the native-only types out of the WASM
build, which was caught by the first run and fixed before
this commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the Pass-2 transform that injects <link rel="alternate" type="application/rss+xml" title="..." href="..."> into the host page's head metadata for every feed-configured listing. **Runs on both native and WASM** so the rendered HTML matches between the CLI render and the hub-client preview. What's in the diff: - crates/quarto-core/src/project/listing/feed/link_inject.rs (new) — Pass-2 transform. Appends to the canonical rendered.includes.header slot (favicon precedent at website_favicon.rs:74). Includes a small inline copy of `append_to_rendered_header` and `escape_html_attr` — follow-up bd will hoist them to a shared util once a third caller exists. Sits OUTSIDE the cfg(not(wasm32)) gate; the file uses no native-only APIs. - crates/quarto-core/src/project/listing/feed/mod.rs — unconditional `pub mod link_inject;` + re-export. Doc comment now describes module-gate granularity in concrete terms. - crates/quarto-core/src/pipeline.rs — register the transform via `pipeline.push(...)` in `build_transform_pipeline` (the function the AST-transforms stage builds JIT). Both native and WASM pipelines pick it up; the ListingFeedStageTransform registration just above remains cfg(not(wasm32))-gated. Tests (6 new): - link_inject_adds_alternate_for_main_feed: feed:true with website.title produces the expected link tag with href="<host-stem>.xml" and title fallback. - link_inject_skips_when_no_feed: no feed: → no alternate link. - link_inject_skips_when_no_site_url: feed:true with no website.site-url → no alternate link (Q-12-15 emission still belongs to the stage transform; this skips silently). - link_inject_uses_feed_title_when_set: feed.title=My Feed wins over website.title. - link_inject_multi_listing_emits_qualified_hrefs: two feed-configured listings → two alternate links with qualified hrefs (D7). - escape_html_attr_handles_special_characters: helper unit test for & < > ". Verified: - cargo nextest run -p quarto-core 'project::listing::feed::link_inject' → 6 passed, 0 failed. - cargo nextest run -p quarto-core → 1869 passed, 33 skipped. - npm run build:wasm (hub-client) → success. The transform is reachable via `AstTransformsStage::new()` on both native and WASM pipelines. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the listings-RSS subset of Q1's `readRenderedContents`
that the upcoming `complete_staged_feeds` post-render step
will consume to substitute placeholder envelopes with
engine-rendered preview content from sibling outputs.
What's in the diff:
- crates/quarto-core/src/project/listing/feed/reader_ext.rs
(new) — two public extractors:
- `extract_first_para_html(html, max_length)`: inner HTML
of the first non-empty <p> in `main.content`, with <a>
tags unwrapped (Q1 partial-feed behavior). Truncation
falls back to plain-text + word-boundary cut when needed.
- `extract_full_contents(html, site_url, sibling_href)`:
inner HTML of `main.content` with the L9 full-feed
transforms — title-block-header removed, relative
href/src rewritten to absolute (resolved against
site_url + sibling's parent dir), `<a href="#anchor">`
unwrapped. External URLs (http/https//mailto/data/etc.)
and scheme-relative URLs pass through.
Native-only via the parent module's cfg gate (depends on
`scraper` and `regex`, both already in `quarto-core`'s
native dep set).
- crates/quarto-core/src/project/listing/feed/mod.rs —
declare `pub mod reader_ext;` (cfg-gated to native).
Sibling, not extension: per the L7 bracketing rule and L9
plan D11, this file is independent of
`post_render_upgrade/reader.rs`. L7's reader stays scoped
to listings-display extraction; L9's RSS reader lives here.
Shared helpers may emerge later but aren't introduced
speculatively in v1.
v1 limitations (filed as L9 close-out follow-ups):
- Truncation under `max_length` produces plain text rather
than tag-balanced HTML. `scraper` is read-only so DOM
mutation isn't available; Q1's tree-walking truncator
has no direct analogue. Subscribers see truncated plain
text; the linked-to HTML page has the full version.
- Math (`<span class="math">…`) and syntax-highlight class
maps pass through verbatim.
Tests (18 new):
- extract_first_para_html: returns inner HTML, returns None
when no main.content, skips empty paragraphs, unwraps
anchors (with and without inline children), truncates at
word boundary, no truncation when max=0.
- extract_full_contents: rewrites relative href and src to
absolute, passes external/mailto/data URLs through,
resolves site-rooted (`/about.html`) paths, strips local
anchor links (`href="#…"`) while keeping external anchors,
removes `<header id="title-block-header">`, returns None
when no main.content.
- Helper units: `collapse_relative_strips_dotdot`,
`parent_href_string_handles_root_and_nested`,
`visible_text_drops_tags_and_decodes_entities`.
Verified:
- cargo nextest run -p quarto-core
'project::listing::feed::reader_ext' → 18 passed.
- cargo nextest run -p quarto-core → 1887 passed
(was 1869 before phase 5).
- Native build clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the post-render step that finalizes each staged feed
file the stage transform emitted: walks output_dir for
*.feed-{full|partial|metadata}-staged files, substitutes the
description-element placeholder envelopes against engine-
rendered sibling HTML using the L9 reader extractors, and
writes the final .xml (then removes the staged original).
What's in the diff:
- crates/quarto-core/src/project/listing/feed/complete.rs
(new) — `complete_staged_feeds(project, runtime, diagnostics)`.
- Recursive walk under project.output_dir; strict suffix
filter on the three staged extensions.
- StagedType discriminator (Full/Partial/Metadata) lifted
from the staged filename.
- Per-call HashMap<PathBuf, Option<String>> cache so a
sibling shared across multiple feeds (e.g. main +
per-category sub-feeds) is read at most once per
post_render invocation.
- Q-12-16 emitted at most once per missing sibling
(tracked in a separate HashSet so cache misses don't
duplicate the warning).
- Body wrapped in <![CDATA[…]]> after substitution. v1
doesn't escape the rare `]]>` case in body content
(filed as a follow-up bd at L9 close-out).
- File-write goes through `runtime.file_write` for the
final .xml; staged removal uses std::fs (best-effort).
- Errors during one feed are reported as diagnostics
and don't abort the whole step.
- crates/quarto-core/src/project/listing/feed/mod.rs —
declare `pub mod complete;` + re-export
`complete_staged_feeds`.
- crates/quarto-core/src/project/orchestrator.rs —
call `feed::complete_staged_feeds(project, runtime,
diagnostics)` from `WebsiteProjectType::post_render`,
after the L7 substitute step (so any host-page HTML L7
rewrote is on disk before L9's reader extractors read
it). Inside the existing `cfg(not(target_arch = "wasm32"))`
block; native-only end-to-end.
Tests (9 new):
- complete_renames_metadata_staged_to_xml: metadata feed
copied verbatim; staged removed.
- complete_substitutes_partial_descriptions: first-para
HTML wrapped in CDATA replaces the placeholder.
- complete_substitutes_full_descriptions_with_absolute_urls:
full-content HTML with `<a href>` rewritten to absolute
appears inside CDATA.
- complete_emits_q_12_16_when_sibling_missing: exactly
one Q-12-16; description left empty.
- complete_caches_sibling_reads_per_call: two staged files
reference the same sibling; both substitute correctly.
- complete_skips_when_no_site_url: staged files remain;
no .xml produced.
- complete_handles_concurrent_per_category_files: main +
two per-category staged files all finalize correctly;
shared sibling read once (verified implicitly by the
cache structure).
- complete_walks_nested_directories: nested
`_site/posts/index.feed-…` is found and finalized.
- staged_type_from_filename: unit test for the
StagedType discriminator.
Verified:
- cargo nextest run -p quarto-core 'project::listing::feed::complete'
→ 9 passed, 0 failed.
- cargo nextest run -p quarto-core → 1896 passed
(was 1887 before phase 6; +9 new).
- cargo xtask lint → 702 files checked, all clean.
- npm run build:wasm (hub-client) → success. The
feed::complete_staged_feeds call lives inside the
existing `cfg(not(target_arch = "wasm32"))` block in
WebsiteProjectType::post_render, so the WASM target
doesn't need to compile it.
This completes the L9 implementation surface (phases 1–6).
Phase 7 is end-to-end CLI verification — building real-binary
fixtures and inspecting the rendered .xml output.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes out the L9 implementation by exercising the feature
through the real `q2 render` binary against four fixtures —
metadata / partial / full+categories / no-site-url — with
the inspected output snippets captured in the plan doc per
CLAUDE.md §"End-to-end verification before declaring success".
What's in the diff:
- claude-notes/plans/2026-05-08-listings-L9-rss-feeds.md
- §"End-to-end CLI verification record" filled in with
fixture layouts, exact invocations, and verbatim
snippets of the produced .xml (or its absence, for the
no-url case).
- Phase-8 TDD checklist marked complete.
- crates/quarto-core/src/project/listing/feed/mod.rs
- Replace the "(forthcoming)" markers in the module-doc
bullet list now that all five subordinate modules are
in place.
Verification fixtures (all under /tmp; not part of the
repo):
- /tmp/l9-fixture-metadata: metadata feed inlines
descriptions verbatim from the post's frontmatter
description; CDATA-wrapped; pubDate in RFC 2822;
dc:creator per author; channel cascade
(feed.title → website.title) verified.
- /tmp/l9-fixture-partial: partial feed extracts the first
<p> from the rendered HTML and preserves inline formatting
(<strong> survived; subsequent paragraphs absent).
- /tmp/l9-fixture-full-categories: full feed includes the
whole <main class="content"> with relative URLs rewritten
to absolute (../about.html → https://example.com/about.html),
local-anchor href="#summary" unwrapped, and section markup
preserved. Per-category sub-feeds correctly filtered.
- /tmp/l9-fixture-no-url: Q-12-15 emitted exactly once;
no .xml files produced; host page renders normally.
After all four renders, `find _site -name "*.feed-*-staged"`
is empty — the post-render `complete_staged_feeds` step
deletes every staged file once the .xml is written.
Verified:
- Full `cargo xtask verify` (Rust build + nextest +
hub-client build:all + hub-client test:ci) → All
verification steps passed.
- Worked through each fixture's output by hand; the
highlights captured in the plan doc are the actual
observed behavior, not a transcribed expectation.
L9 implementation surface is now complete. Remaining
work is the close-out commit (mark bd-o90m closed, file
follow-up bds for the deferred items in §"Out of scope
for L9", update the listings epic table).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
L9 (RSS feeds) implementation complete on beads/bd-o90m-listings-rss-feeds; awaiting user merge approval. See the L9 plan (committed in `b8c9b8b5`) and the seven phase commits (`8b7a9286` … `0bdd219e`) for the implementation; closing bd-o90m here. Filed 13 follow-up bds with discovered-from links to bd-o90m, covering deferred behaviors (math handling, highlight class maps, Atom 1.0, custom feed templates, date_format pipe, etc.) and v1 limitations (HTML-aware truncation, CDATA ]]> escape, Q-12-15 dedup, generator version, hoisting shared header-include helpers). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
RSS 2.0 feed generation per host page that opts in via
`feed: true` or `feed: { … }`. One staged file per
feed-configured listing during Pass-2; the post-render step
substitutes engine-rendered preview content from sibling
HTML and finalizes each into a real `<stem>.xml` next to the
host page. `<link rel="alternate" type="application/rss+xml">`
is injected into the host's head metadata on both native and
WASM (hub-client preview matches the CLI render byte-for-byte
even though the linked feed file is only written by
`quarto render`).
Three feed types ship: `metadata` (descriptions inlined from
post frontmatter), `partial` (first-paragraph HTML extracted
from the rendered sibling), and `full` (entire `main.content`
with relative URLs rewritten to absolute and local-anchor
`<a href="#…">` tags unwrapped). Per-category sub-feeds via
`feed.categories: [...]` produce extra `<stem>-<category>.xml`
files filtered to items carrying that category.
Architecture:
- Most of `crates/quarto-core/src/project/listing/feed/` is
native-only via cfg(not(target_arch = "wasm32"))
(depends on `imagesize` for `<media:content>` dimensions
and `scraper` + file I/O for the reader extractors).
- `feed/link_inject.rs` sits outside that gate so the
alternate-link tag flows through both pipelines.
- L9's reader is a strict sibling of L7's, not an extension —
preserves the L7 bracketing rule. Shared helpers may
emerge later but aren't introduced speculatively in v1.
Q1 divergence (verified at impl-start): Q1 stores `feed:` on
`ListingSharedOptions` (one feed per host, items merged
across listings on the page). Q2's data model from L2 stores
`feed:` per-Listing, so the common single-listing case
produces the same `<stem>.xml` Q1 emits, but multi-listing
pages get one feed per listing with filenames qualified by
the listing id (`<stem>-<listing-id>.xml`). See the L9 plan
§"Decisions log" D7 for the rationale.
Two new diagnostic codes:
- Q-12-15: `feed:` configured but `website.site-url` is
missing. Feeds skipped; host page renders normally.
- Q-12-16: a sibling output couldn't be read during
substitution. Description left empty for that item;
emitted at most once per missing sibling per
post_render invocation.
Tests: +76 in quarto-core (1820 → 1896). Full
`cargo xtask verify` (Rust + nextest + WASM hub-client +
hub-client tests) clean. End-to-end CLI verification done
against four fixtures (metadata / partial / full+categories
/ no-url); output snippets recorded in the L9 plan doc.
13 follow-up bds filed for deferred behaviors and v1
limitations (math handling, highlight class maps, Atom 1.0,
custom feed templates, `date_format` pipe, HTML-aware
truncation, Q-12-15 dedup, etc.). See the L9 plan and the
seven phase commits (`8b7a9286`…`0bdd219e`) for the full
implementation, decisions log, and end-to-end record.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drafts the close-out plan: rolls up 33 open listings follow-ups into a single table (per-issue ID, priority, type, source phase, quick-win flag), flags 4 candidate quick wins (bd-xhvs, bd-fvuy, bd-varx, bd-2vl0), and surfaces 4 decisions for the user to confirm before any close-out action lands. Paperwork-only L11 recommended; verification + bd close steps are listed as work items pending those decisions. L10 (bd-hzsi, migration docs + LLM skill) intentionally stays open — to be picked up alongside the future Q2 docs site. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Note: this is very much a MVP. Evidenced by, eg, the 33 outstanding issues that remain on the plan document.
But from here we're closer to having our own q2 website, and we truly need that as soon as it's practical.