diff --git a/README.md b/README.md
index 09cd42be..356fdb0d 100644
--- a/README.md
+++ b/README.md
@@ -143,7 +143,8 @@ theorem nat_add_zero_right (n : Nat) : n + 0 = n := by
Blueprint supports three main ways to connect informal nodes to Lean:
- inline code with a labeled Lean code block
-- compiled code tagged with `@[blueprint "addition_right_identity"]`
+- compiled code tagged with `@[blueprint]` or
+ `@[blueprint "addition_right_identity"]`
- existing declarations referenced with `(lean := "Nat.add_assoc")`
```lean
@@ -152,6 +153,37 @@ theorem nat_add_zero_right (n : Nat) : n + 0 = n := by
simp
```
+The string selects a short, explicit Blueprint label. Omitting it uses the
+declaration's qualified Lean name, so `@[blueprint] theorem
+MyProject.nat_add_zero_right ...` is referenced as
+`"MyProject.nat_add_zero_right"`. Attribute options remain available in the
+bare form, for example `@[blueprint (uses := ["addition_spec"])]`.
+
+After importing a Lean module containing tagged declarations, include one node
+per distinct directly owned label as a source-ordered Manual part with:
+
+```lean
+{includeBlueprintModule 0 MyProject.Formalization.Addition (title := "Compiled Addition Results")}
+```
+
+The `import MyProject.Formalization.Addition` statement still belongs in the
+Lean file header. When only one declaration should appear at a particular
+location in an existing Manual chapter, place that node with:
+
+```lean
+{blueprint_node "addition_right_identity"}
+```
+
+The declaration docstring supplies the informal statement when present; without
+one, Blueprint still renders a code-only node in either workflow. Docstrings are
+prose, not Blueprint dependency syntax: put `uses` and `proofUses` on the
+attribute. Standard `doc.verso` structure, including math, is preserved in both
+the statement and the attached “Lean code for…” declaration panel. For the
+precise dependency, prose, proof, metadata, and source-rendering boundaries, see
+the Manual's
+[“Attribute-first use-case matrix”](doc/MANUAL.md#attribute-first-use-case-matrix)
+section.
+
Add `(autoDeps := true)` when a tagged declaration, labeled inline Lean block,
or `(lean := "...")` statement should infer statement/proof dependency edges to
directly referenced Lean declarations that are already associated with Blueprint
diff --git a/doc/API.md b/doc/API.md
index 56ee8680..226c7f67 100644
--- a/doc/API.md
+++ b/doc/API.md
@@ -129,7 +129,8 @@ Generated Blueprint sites write reusable data under `-verso-data/`:
- `blueprint-manifest.json` contains semantic entries keyed by preview key,
generated-page hrefs, graph records, labels, dependency data, Lean-code
associations, a shared group catalog, ownership, tags, priority, effort,
- status metadata, and display metadata.
+ status metadata, display metadata, and the folding policy needed by reusable
+ block renderers.
- `blueprint-html-cache.json` contains rendered body fragments keyed by
preview keys for entries that have generated preview bodies. Some semantic
entries, such as source-backed external markup generated with
@@ -662,6 +663,19 @@ The useful data boundary is small:
- Slide generators and other generated consumers should import and use the
`Informal.Graft` node/config names directly.
+Manual documents expose two attribute-owned authoring paths before reaching
+this rendering boundary. `{includeBlueprintModule 0 Some.Module}` reads that
+imported module's persistent, source-ordered label catalog and creates one
+Manual part containing all directly owned nodes. `{blueprint_node "label"}`
+materializes one imported attribute node at the command's source position. In
+both cases a docstring supplies the statement body when present; otherwise a
+code-only preview entry is produced. Ordinary informal nodes must already be
+in the traversal, and Slides always use the manifest/cache supplied by their
+generator. The module command is deliberately Manual-only. Module selection and
+statement-facet materialization are document-elaboration concerns; persisted
+informal proof bodies are not yet materialized from imported modules. The
+config, manifest entry, and rendering APIs below remain shared.
+
`VersoBlueprint.Graft.Render` packages that lookup-and-render path for custom
interfaces. A consumer such as an audit view can provide its own wrapper
classes and diagnostics while reusing the same manifest/cache content:
diff --git a/doc/DESIGN_RATIONALE.md b/doc/DESIGN_RATIONALE.md
index 3411899e..89349484 100644
--- a/doc/DESIGN_RATIONALE.md
+++ b/doc/DESIGN_RATIONALE.md
@@ -1,6 +1,6 @@
# Blueprint Design Rationale
-Last updated: 2026-06-20
+Last updated: 2026-07-25
This document records the current architecture boundaries and the reasons the
Blueprint implementation is shaped the way it is.
@@ -212,13 +212,19 @@ flowchart TD
The same flow can be read as four contracts:
1. **Elaboration to environment.**
- Source directives, inline Lean blocks, `@[blueprint "..."]` attributes,
+ Source directives, inline Lean blocks, `@[blueprint]` attributes with
+ declaration-name or explicit labels,
external `(lean := "...")` references, group declarations, author
declarations, citations, and metadata are elaborated into
`Informal.Environment.State`. This is the canonical semantic store for
Blueprint-owned facts. It is persisted through Lean environment extensions
and imported through compiled oleans, so downstream modules see one merged
- object database.
+ object database. Attribute registration also writes a small per-module
+ catalog of distinct labels that preserves first application order. The node
+ itself remains the single source of truth for associated Lean declarations.
+ This is the ownership and ordering source for `{includeBlueprintModule}`;
+ the include path does not infer chapters by sorting labels or reparsing Lean
+ source.
2. **Environment to traversal.**
During Verso traversal, Blueprint reads the semantic environment and writes
@@ -229,6 +235,23 @@ The same flow can be read as four contracts:
public graph data records, and external declaration row anchors. These
facts are intentionally not pushed back into `Environment.State`, because
their values depend on the current rendered document and output mode.
+ An imported attribute-owned node has no source block of its own, so a Manual
+ `{blueprint_node}` placement expands to an invisible materialization block
+ followed by the ordinary graft. The materializer writes the same node,
+ statement-preview, Lean-code, anchor, numbering, and relation indexes as an
+ informal statement block; its only HTML is the empty destination anchor
+ immediately before the visible graft. This keeps placement phase-safe
+ without introducing a second renderer for attribute nodes. Persisted
+ provider-module proof bodies are not yet projected into proof-facet traversal
+ entries.
+ `{includeBlueprintModule}` builds a real Verso part by applying that same
+ materializer-plus-graft expansion to every entry in one imported module's
+ catalog. Catalog lookup is exact-module, so transitive imports appear only
+ when explicitly included as their own parts.
+ Statement payloads that already contain elaborated Manual blocks are
+ reconstructed through Manual's typed JSON instances. This is a localized
+ value-quotation bridge, not a second persisted body schema and not a
+ synthetic document elaboration pass.
3. **Traversal to generated artifacts.**
Page rendering and preview-data emission both consume the traversal state.
@@ -271,10 +294,11 @@ that owner.
| Fact family | Owner | Stored as | Main consumers |
| --- | --- | --- | --- |
| Blueprint labels, node kind, declared dependencies, parent/group, owner, tags, priority, effort, PR URL | Elaboration | `Environment.State.data` and related environment maps | traversal, graph, summary, manifest construction |
+| Attribute-module ownership and first-application order | Attribute elaboration | `Environment.State.blueprintAttributeLabelsByModule`; node semantics remain in `Environment.State.data` | `{includeBlueprintModule}`, exact-module diagnostics |
| Group and author declarations | Elaboration | `Environment.State.groups` and `Environment.State.authors` | block rendering, summary, graph/group panels |
| Inline Lean and Rust attachments | Elaboration plus traversal | semantic code refs in environment; render-time code-panel indexes in `TraversalIndex.InlineCode` and `TraversalIndex.RustInlineCode` | block renderers, code panels, manifest entries |
| External Lean declaration snapshots | Elaboration / declaration snapshot registration | `ExternalRef` records on semantic nodes, enriched with presence/status/source/render data | block renderers, code-summary badges, summary, graph, manifest |
-| Numbering, hrefs, anchors, preview keys | Traversal | `TraverseState` and `TraversalIndex` domains | page rendering, preview manifest, browser triggers |
+| Numbering, hrefs, anchors, preview keys, and placement folding policy | Traversal | `TraverseState` and `TraversalIndex` domains, projected into semantic preview entries where rendering needs them | page rendering, preview manifest, browser triggers |
| Statement/proof preview source blocks | Traversal | `TraversalIndex.TraversalPreviews` | manifest/cache emission, same-document manual grafts |
| Public graph data | Elaboration plus completed traversal | semantic `Informal.Graph.GraphModel` plus options cached in `TraversalIndex.Graphs`, then topology-finalized once through `Informal.GraphApi.finishData` into private-constructor `GraphData`; manifest emission subsequently resolves preview candidates against the artifact index without reopening topology | graph command rendering, browser runtime, custom graph consumers |
| Lean code preview fragments | Traversal | `TraversalIndex.LeanCodePreviews` | Lean links, manifest/cache emission |
@@ -343,6 +367,9 @@ flowchart TD
previewExtra["Preview-data extra step
emitBlueprintPreviewData"]
previewFiles["Manifest/cache files
blueprint-manifest.json
blueprint-html-cache.json"]
+ attributeEnv["Persistent attribute node/catalog
Environment.State"]
+ moduleInclude["Attribute module part command
includeBlueprintModule"]
+ attributeMaterializer["Attribute traversal materializer
blueprintAttributeNodeSource"]
manualGraft["Manual graft command
Graft.renderManualGraftNode"]
traversalPreview["Traversal preview lookup
PreviewSource / TraversalPreviews"]
manualPreviewHtml["Manual preview-body render
renderManualBlocksHtmlWithStateAndHovers"]
@@ -362,6 +389,10 @@ flowchart TD
manualMain --> previewExtra
previewExtra --> previewFiles
+ attributeEnv --> moduleInclude
+ attributeEnv --> attributeMaterializer
+ moduleInclude --> attributeMaterializer
+ attributeMaterializer --> traversalPreview
manualGraft --> traversalPreview
traversalPreview --> manualPreviewHtml
manualPreviewHtml --> graftContent
@@ -385,7 +416,8 @@ The current paths are:
| --- | --- | --- | --- | --- |
| Normal Manual site pages | `Informal.PreviewManifest.blueprintMainWithPreviewData` | `Environment.State` plus `TraverseState` | `Informal.Block.Render.renderInformalBlockModel` for informal blocks; command-specific renderers for graph, summary, and bibliography | generated Manual HTML pages and assets |
| Preview manifest/cache emission | `Informal.PreviewManifest.emitBlueprintPreviewData` via `blueprintMainWithPreviewData` | completed Manual `TraverseState` and `TraversalIndex` domains | Manual preview render helpers plus manifest entry builders | `blueprint-manifest.json`, `blueprint-html-cache.json`, merged hover docs |
-| Manual same-document graft | `Informal.Graft.renderManualGraftNode` through `{blueprint_node}` in Manual | current page traversal preview entry and current `TraverseState` | `Informal.Graft.renderNodeWithContent` | grafted Manual HTML block |
+| Manual attribute materialization | `{blueprint_node}` for an untraversed attribute node, or `{includeBlueprintModule}` for a module catalog | persistent node/catalog data from `Environment.State` plus persisted statement blocks | `Block.blueprintAttributeNodeSource` registers through the ordinary block traversal path | current-document traversal entries followed by grafted Manual HTML |
+| Manual same-document graft | `Informal.Graft.renderManualGraftNode` through `{blueprint_node}` in Manual | current page traversal preview entry and current `TraverseState`, whether authored directly or attribute-materialized | `Informal.Graft.renderNodeWithContent` | grafted Manual HTML block |
| Manual side-by-side graft wrapper | `Block.blueprintGraftSideBySide.toHtml` | already elaborated/rendered child blocks | wrapper only; child nodes follow the Manual graft path | side-by-side Manual HTML wrapper |
| Slides graft node | `Informal.Slides.slidesMainWithBlueprintPreviews` plus `Informal.Slides.renderBlueprintSlideNode` | serialized manifest/cache files copied from the Blueprint site | `Informal.Graft.renderNodeFromManifestCache` then `renderNodeWithContent` | static slide-node HTML plus slide assets |
| Slides side-by-side wrapper | `VersoSlides.BlockExt.wrap` emitted by `blueprint_side_by_side` in Slides | already rendered child slide blocks | upstream Slides wrapper; child nodes follow the Slides graft-node path | side-by-side slide HTML wrapper |
@@ -879,6 +911,10 @@ rather than page-local template bodies:
then hydrates links, math, and related-entry preview panels; it does not
reconstruct Blueprint block markup or relationship topology from ad hoc
manifest scans.
+ `VersoBlueprint.ModuleInclude` owns the module-to-part authoring boundary:
+ it reads the persistent attribute catalog, creates the Verso part, and
+ delegates every contained node to the same Manual graft materialization
+ path.
Inline Blueprint references, citation references, and the `used by`/group
relationship panels are now preview-data callers: the rendered page carries the
diff --git a/doc/GETTING_STARTED.md b/doc/GETTING_STARTED.md
index d645ebc1..d0557517 100644
--- a/doc/GETTING_STARTED.md
+++ b/doc/GETTING_STARTED.md
@@ -36,9 +36,17 @@ Those labels are the key to the whole system. They are used to:
- attach inline Lean code with a labeled `lean` code block
- attach external TeX or Markdown source for porting with `tex` or `md` code
blocks
-- tag compiled declarations with `@[blueprint "label"]`, optionally using
- `(autoDeps := true)` or `set_option verso.blueprint.autoDeps true` to infer
- edges to directly referenced Lean declarations associated with Blueprint labels
+- tag compiled declarations with `@[blueprint]`, which defaults to the
+ qualified declaration name, or `@[blueprint "label"]` for an explicit short
+ label; either form can use `(autoDeps := true)` or
+ `set_option verso.blueprint.autoDeps true` to infer edges to directly
+ referenced Lean declarations associated with Blueprint labels
+- turn the distinct labels owned directly by one imported Lean module into a
+ source-ordered Manual chapter with `{includeBlueprintModule 0 Some.Module}`;
+ declarations that share a label become one node
+- place an imported attribute-owned declaration at a specific chapter location
+ with `{blueprint_node "label"}`; its docstring becomes the statement body and
+ an undocumented declaration renders as a code-only node
If you pick stable labels early, the rest of the project structure becomes much
easier to maintain.
@@ -93,6 +101,8 @@ Use [project_template/](../project_template/) as the starting point.
Its key files are:
- `ProjectTemplate/Chapters/Addition.lean`: the first chapter
+- `ProjectTemplate/Formalization/Addition.lean`: an ordinary Lean module whose
+ attribute-owned declarations become a generated chapter
- `ProjectTemplate/Chapters/Multiplication.lean`: the second chapter
- `ProjectTemplate/Chapters/Collatz.lean`: a third chapter with a deliberately
unfinished open problem
@@ -104,16 +114,18 @@ Its key files are:
The template is intentionally small. It is meant to teach the shape of a
Blueprint project before you scale it up.
-## The three Verso forms to recognize first
+## The four Verso forms to recognize first
-If you are new to Verso, there are only three forms you need to understand at
+If you are new to Verso, there are only four forms you need to understand at
the start:
- `#doc (Manual) "Title" =>` starts a document module
- `{include 0 Some.Module}` includes a chapter into the top-level file
+- `{includeBlueprintModule 0 Some.Formalization}` turns that imported Lean
+ module's tagged declarations into a generated Manual chapter
- `:::definition "label_1"` starts a Blueprint block
-You can get a long way just by following those three patterns in the template.
+You can get a long way just by following those four patterns in the template.
## Read the first chapters
@@ -141,15 +153,18 @@ small enough to copy and adapt.
The top-level file in
[project_template/ProjectTemplate/Blueprint.lean](../project_template/ProjectTemplate/Blueprint.lean)
-does two jobs:
+does three jobs:
1. it includes the chapter modules into the document
-2. it chooses which rendered overview pages to include
+2. it projects the tagged formalization module into a generated chapter
+3. it chooses which rendered overview pages to include
The starter template includes:
- the chapter pages with `{include 0 ProjectTemplate.Chapters.Addition}` and
the other chapter includes
+- the compiled-results page with
+ `{includeBlueprintModule 0 ProjectTemplate.Formalization.Addition ...}`
- a dependency graph with `{blueprint_graph}`
- a progress summary with `{blueprint_summary}`
diff --git a/doc/MAINTAINER_GUIDE.md b/doc/MAINTAINER_GUIDE.md
index 12c8e21e..fbd852cd 100644
--- a/doc/MAINTAINER_GUIDE.md
+++ b/doc/MAINTAINER_GUIDE.md
@@ -1,6 +1,6 @@
# Blueprint Maintainer Guide
-Last updated: 2026-05-04
+Last updated: 2026-07-25
This document is the repository-level workflow guide for maintaining Blueprint
support in `verso-blueprint`, its in-repo validation projects, and its
@@ -147,6 +147,26 @@ interaction behavior:
uv run --project tests/browser --extra test python -m pytest tests/browser -q --browser chromium
```
+### Cover Feature Intersections
+
+User-facing authoring behavior should be tested through its final rendered
+surface, not only at the parser or environment-extension boundary. For a feature
+that crosses phases, cover the applicable rows of this matrix:
+
+| Boundary | What to assert | Current attribute-first fixtures |
+| --- | --- | --- |
+| Declaration and import | persisted label, declaration association, dependencies, module ownership/order | `BlueprintAttribute.lean` with `BlueprintAttribute/Provider.lean`, `HybridProvider.lean`, and `DefaultLabelProvider.lean` |
+| Consumer traversal | numbering, folding/options, relation data, preview keys | `BlueprintAttributeRendering.lean` |
+| Final Manual HTML | statement body, code-only fallback, structural docstrings/math, code-panel disclosure state | `BlueprintAttributeRendering.lean` |
+| Generated site | embedded assets and the reusable external-declaration renderer | `preview_runtime_showcase` and `check_blueprint_code_panels.py` |
+| Browser runtime | transformations or hydration that cannot be proved from static HTML | `test_preview_runtime_regressions.py` |
+
+When a new option or source form applies to more than one placement path, add
+one compact cross-feature regression that exercises those paths together. A
+unit test for each isolated component is not sufficient when data is projected
+through the environment, traversal store, preview manifest, HTML cache, and
+browser runtime.
+
Browser tests that need the public Blueprint render API should use
`blueprint_render_api_script` or `wait_for_blueprint_render_api` from
`tests/browser/support.py`. Those helpers import
diff --git a/doc/MANUAL.md b/doc/MANUAL.md
index f49585db..f62ee411 100644
--- a/doc/MANUAL.md
+++ b/doc/MANUAL.md
@@ -58,7 +58,7 @@ These identifiers are used by:
- labeled inline Lean code blocks
- labeled inline Rust code blocks
- `tex` and `md` code blocks carrying external markup source
-- `@[blueprint "label"]` on compiled Lean declarations
+- `@[blueprint]` or `@[blueprint "label"]` on compiled Lean declarations
- summary and graph nodes
- preview lookup and exported metadata
@@ -116,6 +116,8 @@ ProjectTemplate/
Addition.lean
Multiplication.lean
Collatz.lean
+ Formalization/
+ Addition.lean
ProjectTemplate.lean
ProjectTemplateMain.lean
lakefile.lean
@@ -124,6 +126,8 @@ lakefile.lean
The role of each file is:
- `ProjectTemplate/Chapters/Addition.lean`: a chapter with Blueprint blocks
+- `ProjectTemplate/Formalization/Addition.lean`: ordinary Lean declarations
+ tagged with `@[blueprint]` and included as a generated module chapter
- `ProjectTemplate/Chapters/Multiplication.lean`: another chapter with the same
pattern
- `ProjectTemplate/Chapters/Collatz.lean`: a separate chapter for an
@@ -147,6 +151,7 @@ import VersoBlueprint.Commands.Summary
import ProjectTemplate.Chapters.Addition
import ProjectTemplate.Chapters.Collatz
import ProjectTemplate.Chapters.Multiplication
+import ProjectTemplate.Formalization.Addition
open Verso.Genre
open Verso.Genre.Manual
@@ -158,6 +163,7 @@ This small Blueprint tracks a few basic arithmetic facts on natural numbers,
then ends with a separate Collatz chapter that is intentionally unfinished.
{include 0 ProjectTemplate.Chapters.Addition}
+{includeBlueprintModule 0 ProjectTemplate.Formalization.Addition (title := "Compiled Addition Results")}
{include 0 ProjectTemplate.Chapters.Multiplication}
{include 0 ProjectTemplate.Chapters.Collatz}
@@ -271,13 +277,29 @@ theorem nat_add_zero_right (n : Nat) : n + 0 = n := by
This is the clearest way to connect a Blueprint entry to local formalization
work in the same project.
-### Compiled code tagged with `@[blueprint "addition_assoc_compiled"]`
+### Compiled code tagged with `@[blueprint]`
+
+Use the `@[blueprint]` attribute when a compiled definition-like
+declaration or theorem should appear as a compiled-declaration-backed Blueprint
+node. With no string argument, its Blueprint label is the declaration's
+qualified Lean name:
+
+```lean
+/-- Associativity of addition under its qualified declaration name. -/
+@[blueprint]
+theorem MyProject.addition_assoc (a b c : Nat) :
+ (a + b) + c = a + (b + c) := by
+ simpa [Nat.add_assoc]
+```
-Use the `@[blueprint "label"]` attribute when a compiled definition-like declaration or theorem
-should appear as a Lean-owned Blueprint node:
+Use `@[blueprint "label"]` when the document should own a shorter or otherwise
+independent label:
```lean
-/-- Associativity of addition, exposed as a Lean-owned blueprint node. -/
+/--
+Associativity of addition, exposed as a compiled-declaration-backed Blueprint
+node.
+-/
@[blueprint "addition_assoc_compiled"]
theorem addition_assoc_compiled (a b c : Nat) : (a + b) + c = a + (b + c) := by
simpa [Nat.add_assoc]
@@ -287,10 +309,154 @@ This mode is useful when the formal declaration already exists as ordinary Lean
code and you want to register it as a Blueprint node.
If the declaration has a docstring, Blueprint tries to reuse it as the informal
-statement body for that Lean-owned node. Plain docstrings are parsed through the
-manual Markdown path when possible, and richer internal docstring structures are
-converted into Manual blocks directly. If no docstring is available, the node is
-still registered, but there is no imported informal statement body.
+statement body for that attribute-owned node. Plain docstrings are parsed
+through the Manual Markdown path when possible. With the `doc.verso` option
+enabled, standard structural content such as paragraphs, emphasis, lists,
+links, code, math, quotations, and section content is converted into Manual
+blocks. The same structural content is rendered inside the attached “Lean code
+for…” declaration panel. If no docstring is available, the node is still
+registered, but there is no imported informal statement body.
+
+Enabling `doc.verso` does not elaborate a declaration docstring as a Blueprint
+Manual fragment. Blueprint currently flattens every Lean docstring extension
+node that successfully elaborates to its child content, discarding the
+extension wrapper rather than looking up a Manual adapter. Custom Lean
+docstring-extension semantics are therefore not preserved.
+
+Blueprint Manual roles are a separate registry. In particular,
+`{uses ...}[]` is not a Lean `doc.verso` role and is rejected during docstring
+elaboration; it is neither flattened nor recorded as Blueprint dependency
+metadata. Record those edges with the attribute's `(uses := [...])` or
+`(proofUses := [...])` options. Blueprint deliberately does not create a
+synthetic `DocElabM` context to reinterpret an imported docstring.
+
+#### Including an attribute module as a chapter
+
+When a regular Lean module is the primary Blueprint source, import it in the
+document module's Lean header and include all of its directly tagged
+declarations as one Verso part:
+
+```lean
+import MyProject.Formalization.Interpolation
+
+open Verso.Genre
+open Verso.Genre.Manual
+open Informal
+
+#doc (Manual) "Project Blueprint" =>
+
+{includeBlueprintModule 0 MyProject.Formalization.Interpolation (title := "Interpolation Spaces")}
+```
+
+The generated Manual part contains one materialized Blueprint node for each
+distinct `@[blueprint]` label owned directly by the named module, in first
+attribute-application order. If several declarations in that module use the
+same label, the part contains one node with all of their Lean panels.
+Dependencies and declarations accumulate in source order. The first docstring
+that supplies a statement body remains authoritative; a later docstring fills
+the statement only while the earlier registrations are bodyless. Re-exported or
+otherwise transitive modules are not folded into the part: include each desired
+module explicitly. Every node follows the same docstring/code-only, numbering,
+relation, preview, manifest, and cache path as an individual placement. Its
+local display number is assigned in the consuming document's traversal order;
+the generated placement does not retain a display number from the provider
+module.
+
+This is Blueprint's current Verso-native counterpart to
+[LeanArchitect's `\inputleanmodule`](https://github.com/hanwenzhu/LeanArchitect#extracting-entire-lean-file-to-latex):
+it turns tagged declarations from a regular imported Lean module into document
+content. The current command includes declaration-backed nodes only. It does
+not yet have LeanArchitect's ordered `blueprint_comment` equivalent for prose
+interleaved among declarations; put compact prose in declaration docstrings, or
+use individual `{blueprint_node "label"}` placements inside an ordinary Verso
+chapter when the prose needs its own position.
+
+Module inclusion currently materializes the statement facet only. A separate
+informal `:::proof` body persisted in the defining module is not automatically
+registered in the consuming document. Proof prose written and traversed in the
+consuming document remains available through the ordinary proof facet. This is
+separate from the compiled Lean proof: Blueprint's external-declaration panel
+does not reproduce the original `:= by ...` source text.
+
+The first positional number has the same structural role as in Verso's regular
+`{include 0 Some.Document}` command. It is optional; without it, the generated
+part is a child of the current part. The optional `(title := "...")` overrides
+the generated title, whose default is the final component of the module name:
+
+```lean
+{includeBlueprintModule MyProject.Formalization.Interpolation}
+```
+
+The command is available only in Manual documents. It reads Blueprint metadata
+from the imported `.olean`; it does not perform a Lean import from inside the
+document body. If the exact named module is not available through the Lean
+module's import graph, or if it directly owns no `@[blueprint]` declarations,
+the command reports an error. A module include is best when declaration
+docstrings are the chapter prose. To interleave longer prose between selected
+declarations, use individual `{blueprint_node "label"}` placements instead.
+
+#### Placing an attribute-owned node in a chapter
+
+Import the module containing the tagged declaration, then write
+`{blueprint_node "label"}` at the exact place where the node should appear:
+
+```lean
+import MyProject.Formalization.Interpolation
+
+open Verso.Genre
+open Verso.Genre.Manual
+open Informal
+
+#doc (Manual) "Interpolation spaces" =>
+
+The next result packages the formal definition used throughout this chapter.
+
+{blueprint_node "k-interpolation-space"}
+
+The surrounding chapter can continue with examples, motivation, or links to
+later Blueprint nodes.
+```
+
+For an imported `@[blueprint]` node, this command does two jobs. It projects the
+persistent Lean-side node into the current document traversal, which gives it a
+number, page destination, relation metadata, and generated preview entries; it
+then renders that entry through the ordinary Blueprint graft path. A docstring
+becomes the statement body. A declaration without a docstring renders as a
+code-only node instead of failing with “Blueprint node not found”.
+
+This placement behavior is specific to attribute-owned nodes in Manual
+documents. For ordinary `:::theorem` and related blocks, `{blueprint_node}`
+continues to mean “render another view of a node already traversed in this
+document”. In Slides, grafts continue to read the Blueprint site manifest/cache
+passed to the Slides generator.
+
+The usual graft options apply at the placement site, including `+compact`,
+`-header`, `(displayLabel := "...")`, and `(facet := "proof")`. Compact mode
+intentionally hides the attached Lean panel. For an imported attribute-owned
+node, the initial placement currently materializes only its statement facet; a
+proof facet is available after a matching `:::proof` has been traversed in the
+consuming document. A persisted provider-module proof is not automatically
+materialized, and a compiled theorem proof does not automatically become
+informal proof prose.
+
+Additional prose can simply surround the placement command. If the tagged
+declaration has no docstring and the prose should live inside the numbered
+statement shell, write a matching statement block instead; it fills the
+attribute-created node and keeps the Lean association and dependency metadata:
+
+```lean
+:::theorem "k-interpolation-space"
+An interpolation space satisfying the conditions used in this chapter.
+:::
+
+:::proof "k-interpolation-space"
+The informal proof outline can be maintained separately from the Lean proof.
+:::
+```
+
+Do not also use `{blueprint_node "k-interpolation-space"}` merely to create the
+first occurrence in that case: the statement block is already the canonical
+placement. Later `{blueprint_node}` commands may reuse it elsewhere.
Automatic dependency inference is opt-in. Enable it locally with
`(autoDeps := true)`, or set the file/section default with:
@@ -339,8 +505,8 @@ by the declaration's type and compiled body. Declarations associated with
Blueprint labels and used directly by the type become statement dependencies;
declarations associated with Blueprint labels and used directly by the body
become proof dependencies. A Lean declaration is associated with a Blueprint
-label by `@[blueprint "..."]`, by a labeled inline Lean code block, or by an
-informal statement block with `(lean := "...")`.
+label by `@[blueprint]` or `@[blueprint "..."]`, by a labeled inline Lean code
+block, or by an informal statement block with `(lean := "...")`.
Lean associations are many-to-many. One Blueprint label may be associated with
several Lean code items, and one Lean declaration may be associated with several
@@ -379,6 +545,27 @@ same Blueprint label. If the attribute has only created dependency metadata for
that statement or proof, the later block fills in the rendered body and keeps
the inferred dependency edges.
+#### Attribute-first use-case matrix
+
+| Use case | Current behavior |
+| --- | --- |
+| Definitions, theorems, structures, and inductives | Supported. They become definition- or theorem-shaped Blueprint nodes. Constructors, recursors, axioms, and declarations introduced with `opaque` are not accepted as direct attribute targets. |
+| Omit an explicit Blueprint label | Supported with bare `@[blueprint]`; the label defaults to the declaration's qualified Lean name. Attribute options such as `uses`, `proofUses`, and `autoDeps` remain available. |
+| Direct and transitive imports | Supported. Attribute nodes, Lean associations, docstring bodies, and dependency metadata persist through imported `.olean` files. Duplicate imported Blueprint labels are diagnosed. |
+| Include a regular Lean module as a Blueprint chapter | Supported in Manual documents with `{includeBlueprintModule 0 Some.Module}` after importing the module. Distinct directly owned labels are emitted in first attribute-application order; transitive modules must be named and included explicitly. |
+| Place a tagged declaration on a specific Manual page | Supported with `{blueprint_node "label"}` after importing its module. The placement participates in numbering, links, relations, previews, the manifest, and the rendered-fragment cache. |
+| Add chapter prose around the declaration | Supported with ordinary prose before and after the placement command. For an attribute node without a docstring, a matching statement directive can instead supply prose inside the node shell. |
+| Reuse the same node in several places | Supported. The node keeps one semantic identity; later `{blueprint_node}` occurrences are presentation views and may use compact/header/display-label options. |
+| Use the declaration docstring as the statement | Supported for plain Markdown and standard structural `doc.verso` content that can be converted to Manual blocks. Structural `doc.verso` markup and math are also preserved in the attached external-declaration panel. Custom docstring extension semantics are flattened to child content rather than re-elaborated. An absent docstring produces a code-only placement. |
+| Infer formal dependencies | Supported with `(autoDeps := true)` or `set_option verso.blueprint.autoDeps true`. Type references become statement dependencies and body references become proof dependencies. Inference is direct, not transitive through untagged helpers. |
+| Curate dependencies manually | Supported with attribute options `uses` and `proofUses`, using either Blueprint label strings or tagged Lean declaration names. Prefixing an entry with `-` excludes it on that axis. Blueprint's `{uses ...}[]` Manual role is not registered for Lean `doc.verso` docstrings and is rejected there rather than interpreted as dependency metadata. |
+| Attach several labels to one Lean declaration, or several Lean declarations to one label | Supported. Associations are many-to-many and are deduplicated by canonical Lean name or Blueprint label as appropriate. |
+| Add a separate informal proof | Supported with `:::proof "label"` once the node has a statement payload. For an undocumented, dependency-free attribute node, first add a matching statement directive. A proof body persisted in an imported provider module is not yet materialized by `{includeBlueprintModule}` or an initial `{blueprint_node}` placement. |
+| Show the formal declaration | Supported as a highlighted external-declaration panel with its signature, kind-specific structure information, docstring, proof/completeness status, and source link when available. |
+| Show the original definition body or `:= by ...` proof text | Not currently supported by the compiled-declaration renderer. The panel renders the declaration interface, not the original source body. Use the source link, or a labeled inline Lean block when the exact authored proof text must be embedded in the page. |
+| Put `parent`, `owner`, `tags`, `effort`, `priority`, or `pr_url` directly on `@[blueprint]` | Not currently supported. These remain Blueprint statement-block metadata. A separate attribute-side metadata surface needs an ownership and validation design before it is added. |
+| Use an unplaced attribute node in global views | The persistent node can contribute semantic graph/summary facts, but it has no page destination or rendered preview until it is placed in a Manual document. |
+
### Existing Lean declarations
Use `(lean := "Nat.add_assoc")` when Lean already owns the declaration and you
@@ -402,7 +589,10 @@ Notes:
- `(lean := "Nat.add_assoc")` points at Lean-owned declaration names
- `(lean := "Nat.add, Nat.succ")` supports comma-separated declaration lists
-- `@[blueprint "addition_assoc_compiled"]` registers a Lean-owned Blueprint node
+- `@[blueprint "addition_assoc_compiled"]` registers a
+ compiled-declaration-backed Blueprint node
+- bare `@[blueprint]` uses the qualified declaration name as its Blueprint
+ label
- `(autoDeps := true)` is accepted by `@[blueprint]`, labeled inline Lean blocks,
and statement blocks with `(lean := "...")`
- Blueprint labels are Blueprint-owned metadata
@@ -964,8 +1154,8 @@ contract shared by those workflows.
Use `{blueprint_node "label"}` when an overview, introduction, roadmap, or slide
needs to feature an existing Blueprint entry without rewriting it.
-In Manual documents, the command resolves the target from the current traversal
-state:
+In Manual documents, an ordinary informal node resolves from the current
+traversal state:
```lean
import VersoBlueprint
@@ -984,6 +1174,13 @@ The statement to feature.
:::::::
```
+An imported node owned by bare `@[blueprint]` or `@[blueprint "label"]` is the
+other Manual case. If the label is not yet in the traversal, the command first
+materializes the persistent attribute node at that source position, then
+renders the same graft shell. See [Placing an attribute-owned node in a
+chapter](#placing-an-attribute-owned-node-in-a-chapter) for the attribute-first
+workflow and its current code/proof limitations.
+
In Slides decks, the same source command is available after importing
`VersoBlueprint.Slides`, but the rendered node comes from the manifest/cache
files passed to the deck generator:
@@ -1222,7 +1419,7 @@ renders a pointer to the HTML output, and `Not in PDF` is absent from
| External Markdown or TeX markup attachments | Stored in the manifest; headers show attachment badges; bodyless Markdown-backed nodes can render source-backed HTML cache fragments | Partial | Explicit external-markup blocks render only when shown with `(display := summary)` or `(display := source)`; source-backed HTML cache bodies are not converted into PDF bodies |
| Source provenance and source-PDF spans | Source chips, manifest entries, and data/preview API access for source document ids and text/PDF spans | Not in PDF | Not shown as source chips or page overlays in the PDF |
| Dependency graph and progress summary pages | Interactive graph and summary views with runtime controls and previews | Notice only | Static notice pointing readers to the HTML output |
-| Grafted Blueprint nodes | Rendered from the preview manifest and HTML cache | Partial | Inserted graft nodes render as a static notice; side-by-side authored content still renders statically |
+| Grafted Blueprint nodes, including `{includeBlueprintModule}` and attribute-owned `{blueprint_node}` placements | Rendered from current traversal preview data and emitted to the preview manifest and HTML cache | Partial | Inserted graft nodes render as a static notice; side-by-side authored content still renders statically |
| Browser preview runtime, relation panels, and interactive controls | Supported in generated HTML | Not in PDF | Not available in PDF |
| Preview manifest, HTML cache, and JavaScript APIs | Emitted for generated-data and browser consumers | Not in PDF | Not embedded in `main.pdf`; still emitted alongside HTML unless those outputs are disabled |
| Slides and other generator-side consumers | Supported through their own HTML/data render paths | Not in PDF | Not part of the `--pdf` output path |
@@ -1271,7 +1468,9 @@ prefixes with document-order block counts.
- renders proof blocks as collapsed disclosure blocks
- `verso.blueprint.foldCodeBlocks`
- default: `false`
- - renders Lean, Rust, and external code panels as collapsed disclosure blocks
+ - renders Lean, Rust, and external code panels as collapsed disclosure blocks,
+ including panels produced by attribute-owned `{blueprint_node}` placements
+ and `{includeBlueprintModule}`
- `verso.blueprint.trimTeXLabelPrefix`
- default: `false`
- trims TeX-style label prefixes when deriving Lean names
diff --git a/doc/ROADMAP.md b/doc/ROADMAP.md
index 8a84c40f..5ab57246 100644
--- a/doc/ROADMAP.md
+++ b/doc/ROADMAP.md
@@ -1,6 +1,6 @@
# Blueprint Roadmap
-Last reviewed: 2026-07-16
+Last reviewed: 2026-07-26
This document tracks repository-local engineering work for `verso-blueprint`.
Scoped planning cards live under [`roadmap/`](./roadmap/). Requests that should
@@ -146,9 +146,9 @@ Work:
requirements onto `PreviewManifest.Entry`; remaining candidates include
repeated traversal-store decoding and validation-message assembly patterns
across preview, source, and status data
-2. revisit `Informal.Environment.InProgress` after the widget path no longer
- needs elaboration-time syntax; today it remains separate from `Data.Node`
- because it owns directive-stack state, preview blocks, and `elabStx`
+2. keep `Informal.Environment.InProgress` separate from `Data.Node` while it
+ owns directive-stack state and typed preview blocks; attribute docstring term
+ syntax belongs only to persisted `InformalData`
3. keep `Informal.Environment.State` as the persisted semantic store and
traversal indexes as rendered-site projections; consolidate only if the
replacement keeps numbering, hrefs, preview ids, and HTML-cache keys
@@ -169,6 +169,20 @@ Work:
9. revisit external declaration footer/status semantics once out-of-workspace
declarations are represented precisely enough to distinguish declaration
completeness from dependency completeness
+10. design facet-aware materialization for persisted informal proof bodies and
+ decide whether `{includeBlueprintModule}` renders them by default
+11. decide whether explicit Blueprint prose should override an imported
+ docstring fallback
+12. design attribute-side `parent`/owner/tag/effort/priority metadata without
+ duplicating statement-block validation
+13. evaluate `only`/`except` filters and an ordered module-prose representation
+ for `{includeBlueprintModule}`, including a `blueprint_comment`-like way to
+ interleave prose among tagged declarations rather than only adding one
+ introductory paragraph
+14. extract or upstream the shared structural `Lean.Doc` conversion before
+ designing typed adapters for custom extensions that need Manual semantics
+15. capture exact definition/proof source only after Lean exposes a reliable
+ post-declaration range or syntax hook to the attribute pipeline
### Asset and Build Reliability
diff --git a/lakefile.lean b/lakefile.lean
index 68908ae1..5723598d 100644
--- a/lakefile.lean
+++ b/lakefile.lean
@@ -24,47 +24,7 @@ lean_exe «vbp» where
@[default_target, test_driver]
lean_lib VersoBlueprintTests where
srcDir := "tests"
- roots := #[
- `VersoBlueprintTests.Blueprint.Support,
- `VersoBlueprintTests.BlueprintAssets,
- `VersoBlueprintTests.BlueprintAutoDeps,
- `VersoBlueprintTests.BlueprintAttribute,
- `VersoBlueprintTests.BlueprintCodeRenderMatrix,
- `VersoBlueprintTests.BlueprintImportedDuplicates.Direct,
- `VersoBlueprintTests.BlueprintImportedDuplicates.ProviderA,
- `VersoBlueprintTests.BlueprintImportedDuplicates.ProviderB,
- `VersoBlueprintTests.BlueprintImportedDuplicates.Reexport,
- `VersoBlueprintTests.BlueprintImportedDuplicates.Transitive,
- `VersoBlueprintTests.BlueprintExternalHeadingStatus,
- `VersoBlueprintTests.BlueprintGraft,
- `VersoBlueprintTests.BlueprintGraph,
- `VersoBlueprintTests.BlueprintHeaderExtras,
- `VersoBlueprintTests.BlueprintInformal,
- `VersoBlueprintTests.BlueprintInlinePrecision,
- `VersoBlueprintTests.BlueprintLinkHover,
- `VersoBlueprintTests.BlueprintMainWrapper,
- `VersoBlueprintTests.BlueprintMathLint,
- `VersoBlueprintTests.BlueprintMetadataPanel,
- `VersoBlueprintTests.BlueprintNumbering,
- `VersoBlueprintTests.BlueprintSlides,
- `VersoBlueprintTests.BlueprintPreviewPanels,
- `VersoBlueprintTests.BlueprintPreviewSchema,
- `VersoBlueprintTests.BlueprintPreviewSource,
- `VersoBlueprintTests.BlueprintPreviewWiring,
- `VersoBlueprintTests.BlueprintSource,
- `VersoBlueprintTests.BlueprintRustCode,
- `VersoBlueprintTests.BlueprintSummaryLinks,
- `VersoBlueprintTests.BlueprintSummaryStatus,
- `VersoBlueprintTests.BlueprintTeXCleanup,
- `VersoBlueprintTests.BlueprintTexMacros,
- `VersoBlueprintTests.BlueprintExternalMarkup,
- `VersoBlueprintTests.ExternalDeclRender,
- `VersoBlueprintTests.RuntimeCache,
- `VersoBlueprintTests.TestBlueprintRegistryMeta,
- `VersoBlueprintTests.TestBlueprintRegistryChecks,
- `VersoBlueprintTests.TestBlueprintRegistryCoverage,
- `VersoBlueprintTests.Vbp
- ]
+ globs := #[.submodules `VersoBlueprintTests]
lean_lib VersoBlueprintTestDocs where
srcDir := "tests"
diff --git a/project_template/ProjectTemplate/Blueprint.lean b/project_template/ProjectTemplate/Blueprint.lean
index 87680111..b92393eb 100644
--- a/project_template/ProjectTemplate/Blueprint.lean
+++ b/project_template/ProjectTemplate/Blueprint.lean
@@ -6,6 +6,7 @@ import VersoBlueprint.Commands.Summary
import ProjectTemplate.Chapters.Addition
import ProjectTemplate.Chapters.Collatz
import ProjectTemplate.Chapters.Multiplication
+import ProjectTemplate.Formalization.Addition
open Verso.Genre
open Verso.Genre.Manual
@@ -18,6 +19,7 @@ then ends with a separate Collatz chapter that is intentionally unfinished. It
is intentionally small, so it can serve as a starting point for a new project.
{include 0 ProjectTemplate.Chapters.Addition}
+{includeBlueprintModule 0 ProjectTemplate.Formalization.Addition (title := "Compiled Addition Results")}
{include 0 ProjectTemplate.Chapters.Multiplication}
{include 0 ProjectTemplate.Chapters.Collatz}
diff --git a/project_template/ProjectTemplate/Formalization/Addition.lean b/project_template/ProjectTemplate/Formalization/Addition.lean
new file mode 100644
index 00000000..69eca305
--- /dev/null
+++ b/project_template/ProjectTemplate/Formalization/Addition.lean
@@ -0,0 +1,15 @@
+import VersoBlueprint
+
+namespace ProjectTemplate.Formalization.Addition
+
+/-- Addition of natural numbers is commutative. -/
+@[blueprint "addition_comm_compiled" (uses := ["addition_spec"])]
+theorem addition_comm_compiled (a b : Nat) : a + b = b + a := by
+ simpa using Nat.add_comm a b
+
+/-- Zero is a left identity for addition of natural numbers. -/
+@[blueprint "addition_zero_compiled" (uses := ["addition_right_identity"])]
+theorem addition_zero_compiled (a : Nat) : 0 + a = a := by
+ simp
+
+end ProjectTemplate.Formalization.Addition
diff --git a/project_template/README.md b/project_template/README.md
index 305b2e95..1d56e7dc 100644
--- a/project_template/README.md
+++ b/project_template/README.md
@@ -33,6 +33,8 @@ project_template/
Addition.lean
Multiplication.lean
Collatz.lean
+ Formalization/
+ Addition.lean
ProjectTemplateMain.lean
source/
addition-source.pdf
@@ -43,6 +45,9 @@ project_template/
The important files are:
- `ProjectTemplate/Chapters/Addition.lean`: the first chapter
+- `ProjectTemplate/Formalization/Addition.lean`: an ordinary Lean module whose
+ tagged declarations become a generated chapter through
+ `{includeBlueprintModule}`
- `ProjectTemplate/Chapters/Multiplication.lean`: the second chapter
- `ProjectTemplate/Chapters/Collatz.lean`: a separate exploratory chapter with
the intentionally unfinished conjecture
@@ -64,6 +69,8 @@ The important files are:
- local Lean code attached to a Blueprint label
- local Rust code attached to a Blueprint label
- a statement linked to an existing Lean declaration
+- an imported Lean module whose `@[blueprint]` declarations are included as a
+ source-ordered Blueprint chapter
- source-document metadata attached to one theorem
- group and author metadata
- rendered progress summary and dependency graph pages
diff --git a/src/VersoBlueprint.lean b/src/VersoBlueprint.lean
index c62d453d..5961283f 100644
--- a/src/VersoBlueprint.lean
+++ b/src/VersoBlueprint.lean
@@ -54,6 +54,7 @@ import VersoBlueprint.LeanNameParsing
import VersoBlueprint.PreviewCache
import VersoBlueprint.PreviewManifest
import VersoBlueprint.Graft
+import VersoBlueprint.ModuleInclude
import VersoBlueprint.Resolve
import VersoBlueprint.TraversalIndex
import VersoBlueprint.StyleSwitcher
diff --git a/src/VersoBlueprint/Attribute.lean b/src/VersoBlueprint/Attribute.lean
index 0750cb83..d44d0349 100644
--- a/src/VersoBlueprint/Attribute.lean
+++ b/src/VersoBlueprint/Attribute.lean
@@ -8,10 +8,10 @@ import Lean
import Lean.DocString.Extension
import VersoManual
import VersoBlueprint.DependencyAnalysis
+import VersoBlueprint.Docstring
import VersoBlueprint.Environment
import VersoBlueprint.ExternalRefSnapshot
import VersoBlueprint.LabelNameParsing
-import VersoBlueprint.Math
namespace Informal
@@ -23,7 +23,7 @@ declare_syntax_cat blueprintAttrOption
syntax (name := blueprintAutoDepsAttrOption) "(" &"autoDeps" " := " ident ")" : blueprintAttrOption
syntax (name := blueprintUsesAttrOption) "(" &"uses" " := " blueprintDepList ")" : blueprintAttrOption
syntax (name := blueprintProofUsesAttrOption) "(" &"proofUses" " := " blueprintDepList ")" : blueprintAttrOption
-syntax (name := blueprint) "blueprint" ppSpace str (ppSpace blueprintAttrOption)* : attr
+syntax (name := blueprint) "blueprint" (ppSpace str)? (ppSpace blueprintAttrOption)* : attr
private inductive AutoDepTarget where
| label (label : Data.Label)
@@ -54,9 +54,6 @@ private def classifyDeclKind (decl : Name) (info : ConstantInfo) : CoreM Data.No
| none =>
throwError "invalid '[blueprint]' target '{decl}': expected a definition-like declaration or theorem, got {Informal.Data.ConstantInfo.blueprintKindText info}"
-private def pushLabelUnique (labels : Array Data.Label) (label : Data.Label) : Array Data.Label :=
- if labels.contains label then labels else labels.push label
-
private def manualUseRef (label : Data.Label) : Data.UseRef :=
{ label }
@@ -76,6 +73,16 @@ private def pushTargetUnique (targets : Array AutoDepTarget) (target : AutoDepTa
private def parseLabel (label : String) : Data.Label :=
LabelNameParsing.parse label
+/--
+Use a declaration's fully qualified spelling as an opaque Blueprint label.
+
+The `Name.mkSimple` representation is deliberate: string-authored Blueprint
+references use the same opaque-label parser rather than Lean namespace
+resolution.
+-/
+private def defaultLabelForDecl (decl : Name) : Data.Label :=
+ parseLabel decl.eraseMacroScopes.toString
+
private def parseDepList : TSyntax ``blueprintDepList → CoreM AutoDepEntries
| `(blueprintDepList| [$[$deps:blueprintDepTerm],*]) => do
deps.foldlM (init := {}) fun cfg dep => do
@@ -93,109 +100,34 @@ private def parseDepList : TSyntax ``blueprintDepList → CoreM AutoDepEntries
| _ => throwError "unsupported dependency syntax in '[blueprint]' attribute"
| _ => throwError "unsupported dependency list syntax in '[blueprint]' attribute"
-private def elabBlueprintConfig : Syntax → CoreM BlueprintAttrConfig
- | `(attr| blueprint $label:str $[$opts:blueprintAttrOption]*) => do
- let mut cfg : BlueprintAttrConfig := { label := parseLabel label.getString }
- for opt in opts do
- match opt with
- | `(blueprintAttrOption| (autoDeps := $value:ident)) =>
- match value.getId.eraseMacroScopes with
- | `true => cfg := { cfg with autoDeps := some true }
- | `false => cfg := { cfg with autoDeps := some false }
- | _ => throwErrorAt value "'autoDeps' expects 'true' or 'false'"
- | `(blueprintAttrOption| (uses := $deps:blueprintDepList)) =>
- let deps ← parseDepList deps
- cfg := { cfg with uses := cfg.uses.append deps }
- | `(blueprintAttrOption| (proofUses := $deps:blueprintDepList)) =>
- let deps ← parseDepList deps
- cfg := { cfg with proofUses := cfg.proofUses.append deps }
- | _ => throwError "unsupported option syntax in '[blueprint]' attribute"
- return cfg
+private def elabBlueprintOptions
+ (cfg : BlueprintAttrConfig)
+ (opts : Array (TSyntax `blueprintAttrOption)) :
+ CoreM BlueprintAttrConfig := do
+ let mut cfg := cfg
+ for opt in opts do
+ match opt with
+ | `(blueprintAttrOption| (autoDeps := $value:ident)) =>
+ match value.getId.eraseMacroScopes with
+ | `true => cfg := { cfg with autoDeps := some true }
+ | `false => cfg := { cfg with autoDeps := some false }
+ | _ => throwErrorAt value "'autoDeps' expects 'true' or 'false'"
+ | `(blueprintAttrOption| (uses := $deps:blueprintDepList)) =>
+ let deps ← parseDepList deps
+ cfg := { cfg with uses := cfg.uses.append deps }
+ | `(blueprintAttrOption| (proofUses := $deps:blueprintDepList)) =>
+ let deps ← parseDepList deps
+ cfg := { cfg with proofUses := cfg.proofUses.append deps }
+ | _ => throwError "unsupported option syntax in '[blueprint]' attribute"
+ return cfg
+
+private def elabBlueprintConfig (decl : Name) : Syntax → CoreM BlueprintAttrConfig
+ | `(attr| blueprint $label:str $[$opts:blueprintAttrOption]*) =>
+ elabBlueprintOptions { label := parseLabel label.getString } opts
+ | `(attr| blueprint $[$opts:blueprintAttrOption]*) =>
+ elabBlueprintOptions { label := defaultLabelForDecl decl } opts
| _ => throwError "invalid syntax for '[blueprint]' attribute"
-mutual
-
-private partial def inlineToManualStx (inl : Lean.Doc.Inline Lean.ElabInline) : CoreM (TSyntax `term) := do
- match inl with
- | .text s => `(Verso.Doc.Inline.text $(quote s))
- | .emph content =>
- let content ← content.mapM inlineToManualStx
- `(Verso.Doc.Inline.emph #[$content,*])
- | .bold content =>
- let content ← content.mapM inlineToManualStx
- `(Verso.Doc.Inline.bold #[$content,*])
- | .code s => `(Verso.Doc.Inline.code $(quote s))
- | .math .inline s => Informal.Math.mkBpMathInlineTerm .inline s
- | .math .display s => Informal.Math.mkBpMathInlineTerm .display s
- | .linebreak s => `(Verso.Doc.Inline.linebreak $(quote s))
- | .link content url =>
- let content ← content.mapM inlineToManualStx
- `(Verso.Doc.Inline.link #[$content,*] $(quote url))
- | .footnote name content =>
- let content ← content.mapM inlineToManualStx
- `(Verso.Doc.Inline.footnote $(quote name) #[$content,*])
- | .image alt url => `(Verso.Doc.Inline.image $(quote alt) $(quote url))
- | .concat content =>
- let content ← content.mapM inlineToManualStx
- `(Verso.Doc.Inline.concat #[$content,*])
- -- Fallback for docstring extensions not available in the Manual genre.
- | .other _ content =>
- let content ← content.mapM inlineToManualStx
- `(Verso.Doc.Inline.concat #[$content,*])
-
-private partial def listItemToManualStx
- (item : Lean.Doc.ListItem (Lean.Doc.Block Lean.ElabInline Lean.ElabBlock)) : CoreM (TSyntax `term) := do
- let contents ← item.contents.mapM blockToManualStx
- `(Verso.Doc.ListItem.mk #[$contents,*])
-
-private partial def descItemToManualStx
- (item : Lean.Doc.DescItem (Lean.Doc.Inline Lean.ElabInline) (Lean.Doc.Block Lean.ElabInline Lean.ElabBlock)) :
- CoreM (TSyntax `term) := do
- let term ← item.term.mapM inlineToManualStx
- let desc ← item.desc.mapM blockToManualStx
- `(Verso.Doc.DescItem.mk #[$term,*] #[$desc,*])
-
-private partial def blockToManualStx (b : Lean.Doc.Block Lean.ElabInline Lean.ElabBlock) : CoreM (TSyntax `term) := do
- match b with
- | .para contents =>
- let contents ← contents.mapM inlineToManualStx
- `(Verso.Doc.Block.para #[$contents,*])
- | .code content => `(Verso.Doc.Block.code $(quote content))
- | .ul items =>
- let items ← items.mapM listItemToManualStx
- `(Verso.Doc.Block.ul #[$items,*])
- | .ol start items =>
- let items ← items.mapM listItemToManualStx
- `(Verso.Doc.Block.ol $(quote start) #[$items,*])
- | .dl items =>
- let items ← items.mapM descItemToManualStx
- `(Verso.Doc.Block.dl #[$items,*])
- | .blockquote items =>
- let items ← items.mapM blockToManualStx
- `(Verso.Doc.Block.blockquote #[$items,*])
- | .concat content =>
- let content ← content.mapM blockToManualStx
- `(Verso.Doc.Block.concat #[$content,*])
- -- Fallback for docstring extensions not available in the Manual genre.
- | .other _ content =>
- let content ← content.mapM blockToManualStx
- `(Verso.Doc.Block.concat #[$content,*])
-
-end
-
-private partial def partToManualBlocksStx
- (p : Lean.Doc.Part Lean.ElabInline Lean.ElabBlock Empty) : CoreM (Array (TSyntax `term)) := do
- let mut out : Array (TSyntax `term) := #[]
- if !p.title.isEmpty then
- let title ← p.title.mapM inlineToManualStx
- let titleBold ← `(Verso.Doc.Inline.bold #[$title,*])
- let titleBlock ← `(Verso.Doc.Block.para #[$titleBold])
- out := out.push titleBlock
- out := out ++ (← p.content.mapM blockToManualStx)
- for child in p.subParts do
- out := out ++ (← partToManualBlocksStx child)
- pure out
-
private def statementFromDocstring? (decl : Name) (ref : Syntax) : CoreM (Option Data.InformalData) := do
let env ← getEnv
let internalDoc? ← liftM <| findInternalDocString? env decl
@@ -214,11 +146,8 @@ private def statementFromDocstring? (decl : Name) (ref : Syntax) : CoreM (Option
(handleHeaders := Verso.Genre.Manual.Markdown.strongEmphHeaders))
| none =>
pure #[← `(Verso.Doc.Block.para #[Verso.Doc.Inline.text $(quote doc)])]
- | some (.inr d) =>
- let mut blocks ← d.text.mapM blockToManualStx
- for part in d.subsections do
- blocks := blocks ++ (← partToManualBlocksStx part)
- pure blocks
+ | some (.inr doc) =>
+ Informal.Docstring.versoDocstringToManualBlocksStx doc
if elabStx.isEmpty then
pure none
else
@@ -251,14 +180,14 @@ private def resolveManualTargets
(currentDecl currentLabel : Name) (targets : Array AutoDepTarget) : CoreM (Array Data.Label) := do
targets.foldlM (init := #[]) fun acc target => do
let labels ← labelsForManualTarget currentDecl currentLabel target
- return labels.foldl pushLabelUnique acc
+ return labels.foldl Data.Label.pushUnique acc
private def mergeAxisDeps
(currentDecl currentLabel : Name) (inferred : Array Data.Label) (manual : AutoDepEntries) :
CoreM (Array Data.UseRef) := do
let explicit ← resolveManualTargets currentDecl currentLabel manual.add
let excluded ← resolveManualTargets currentDecl currentLabel manual.exclude
- let excluded := pushLabelUnique excluded currentLabel
+ let excluded := Data.Label.pushUnique excluded currentLabel
let mut out := #[]
for label in DependencyAnalysis.sortLabels inferred do
if !excluded.contains label then
@@ -285,20 +214,24 @@ private def resolveAutoDeps
private def payloadWithDeps
(ref : Syntax) (deps : Array Data.UseRef) (incoming? existing? : Option Data.InformalData) :
Option Data.InformalData :=
- let mergeDeps (payload : Data.InformalData) : Data.InformalData :=
- { payload with deps := deps.foldl Data.UseRef.pushMergeByLabel payload.deps }
- match existing? with
- | some payload => some (mergeDeps payload)
- | none =>
- match incoming? with
- | some payload => some (mergeDeps payload)
- | none =>
- if deps.isEmpty then
- none
- else
- some { stx := ref, deps }
+ let incoming? := incoming?.map (·.withMergedDeps deps)
+ match existing?, incoming? with
+ | some existing, some incoming =>
+ if existing.hasBody then
+ some (existing.withMergedDeps deps)
+ else
+ some (existing.fillBodyless incoming)
+ | some existing, none =>
+ some (existing.withMergedDeps deps)
+ | none, some incoming =>
+ some incoming
+ | none, none =>
+ if deps.isEmpty then
+ none
+ else
+ some { stx := ref, deps }
-private def registerLeanOnlyDecl (decl : Name) (cfg : BlueprintAttrConfig) (ref : Syntax) : CoreM Unit := do
+private def registerBlueprintDecl (decl : Name) (cfg : BlueprintAttrConfig) (ref : Syntax) : CoreM Unit := do
let decl := decl.eraseMacroScopes
let label := cfg.label.eraseMacroScopes
let some info := (← getEnv).find? decl
@@ -326,6 +259,7 @@ private def registerLeanOnlyDecl (decl : Name) (cfg : BlueprintAttrConfig) (ref
data.insert label node
| none => data
return data
+ Environment.registerBlueprintAttributeLabel label
open Lean in
initialize
@@ -338,9 +272,9 @@ initialize
throwError "invalid attribute '[blueprint]', must be global"
unless ((← getEnv).getModuleIdxFor? decl).isNone do
throwError "invalid attribute '[blueprint]', declaration is in an imported module"
- let cfg ← elabBlueprintConfig stx
- registerLeanOnlyDecl decl cfg stx
- descr := "Registers a definition/theorem as a Lean-only blueprint node; supports opt-in automatic dependency inference"
+ let cfg ← elabBlueprintConfig decl stx
+ registerBlueprintDecl decl cfg stx
+ descr := "Registers a compiled declaration as a Blueprint node, using its qualified declaration name as the default label; supports opt-in automatic dependency inference"
}
end Informal
diff --git a/src/VersoBlueprint/Data.lean b/src/VersoBlueprint/Data.lean
index 580b0f97..a54805de 100644
--- a/src/VersoBlueprint/Data.lean
+++ b/src/VersoBlueprint/Data.lean
@@ -44,6 +44,10 @@ set_option doc.verso true
def Label := Name
deriving Repr, Inhabited, DecidableEq, ToString, ToMessageData, ToJson, FromJson, Quote
+/-- Append a Blueprint label only when it is not already present. -/
+def Label.pushUnique (labels : Array Label) (label : Label) : Array Label :=
+ if labels.contains label then labels else labels.push label
+
def LabelMap A := NameMap A
instance [Repr A] : Repr (LabelMap A) := inferInstanceAs <| Repr (NameMap A)
@@ -494,7 +498,8 @@ inductive CodeRef where
/-
Blueprint code references can currently come from two sources:
1. An inline Lean block processed by Verso/Lean integration (`.literate`).
- 2. A regular Lean declaration tagged with `@[blueprint "..."]` (`.external`, origin `.blueprintAttr`).
+ 2. A regular Lean declaration tagged with `@[blueprint]` or
+ `@[blueprint "..."]` (`.external`, origin `.blueprintAttr`).
A `(lean := "...")` directive reference to Lean code we do not directly control
also lands in `.external` (origin `.directiveLean`).
@@ -540,7 +545,12 @@ structure InformalData where
/-- Structured dependency edges declared from this informal payload. -/
deps : Array UseRef := #[]
previewBlocks : Array (Verso.Doc.Block Verso.Genre.Manual) := #[]
- elabStx : Array Syntax := #[] -- Syntax is going to have type Verso.Block ...
+ /--
+ Manual block term syntax retained when the producing phase cannot evaluate it
+ into typed preview blocks, as with a docstring on an imported Blueprint
+ attribute.
+ -/
+ elabStx : Array Syntax := #[]
deriving Repr, Inhabited
def InformalData.hasBody (data : InformalData) : Bool :=
@@ -549,6 +559,19 @@ def InformalData.hasBody (data : InformalData) : Bool :=
def InformalData.dependencyLabels (data : InformalData) : Array Label :=
data.deps.map (·.label)
+/-- Merge dependency metadata into an informal payload without changing its body source. -/
+def InformalData.withMergedDeps
+ (data : InformalData) (deps : Array UseRef) : InformalData :=
+ { data with deps := UseRef.mergeByLabel data.deps deps }
+
+/--
+Fill a bodyless informal payload from the incoming payload, preserving and merging the
+dependency metadata that was registered before the body became available.
+-/
+def InformalData.fillBodyless
+ (current incoming : InformalData) : InformalData :=
+ { incoming with deps := UseRef.mergeByLabel current.deps incoming.deps }
+
structure Node where
kind : NodeKind := .lemma
count : Nat := 0
@@ -711,9 +734,6 @@ private def mergeTags (current incoming : Array String) : Array String :=
incoming.foldl (init := current) fun acc tag =>
if acc.contains tag then acc else acc.push tag
-private def fillBodylessPayload (current incoming : InformalData) : InformalData :=
- { incoming with deps := UseRef.mergeByLabel current.deps incoming.deps }
-
private def fillPayload? (current? : Option InformalData) (incoming : InformalData) :
Option InformalData :=
match current? with
@@ -722,7 +742,7 @@ private def fillPayload? (current? : Option InformalData) (incoming : InformalDa
if current.hasBody then
none
else
- some (fillBodylessPayload current incoming)
+ some (current.fillBodyless incoming)
private def Data.nextCount (data : Data) : Nat :=
data.foldl (init := 0) (fun count _label node => max count node.count) + 1
diff --git a/src/VersoBlueprint/DependencyAnalysis.lean b/src/VersoBlueprint/DependencyAnalysis.lean
index bf7e911b..977445d0 100644
--- a/src/VersoBlueprint/DependencyAnalysis.lean
+++ b/src/VersoBlueprint/DependencyAnalysis.lean
@@ -48,10 +48,6 @@ deriving Inhabited, Repr
def enabled (opts : Options) (local? : Option Bool) : Bool :=
local?.getD (verso.blueprint.autoDeps.get opts)
-def pushLabelUnique (labels : Array Data.Label) (label : Data.Label) :
- Array Data.Label :=
- if labels.contains label then labels else labels.push label
-
def automaticUseRef (label : Data.Label) : Data.UseRef :=
{ label, origin := .automatic }
@@ -60,8 +56,8 @@ def sortLabels (labels : Array Data.Label) : Array Data.Label :=
def InferredDeps.merge (current incoming : InferredDeps) : InferredDeps :=
{
- statement := incoming.statement.foldl pushLabelUnique current.statement
- proof := incoming.proof.foldl pushLabelUnique current.proof
+ statement := incoming.statement.foldl Data.Label.pushUnique current.statement
+ proof := incoming.proof.foldl Data.Label.pushUnique current.proof
}
private def automaticUseRefs (labels : Array Data.Label) : Array Data.UseRef :=
@@ -95,7 +91,7 @@ private def directLabelsForExpr (root : Name) (expr : Expr) : CoreM (Array Data.
return labels
else
let declLabels ← Environment.labelsForLeanDecl decl
- return declLabels.foldl pushLabelUnique labels
+ return declLabels.foldl Data.Label.pushUnique labels
private def directBodyLabels (root : Name) (info : ConstantInfo) : CoreM (Array Data.Label) := do
match info with
@@ -111,7 +107,7 @@ private def directBodyLabels (root : Name) (info : ConstantInfo) : CoreM (Array
match (← getEnv).find? ctor with
| some (.ctorInfo ctorInfo) =>
let ctorLabels ← directLabelsForExpr root ctorInfo.type
- return ctorLabels.foldl pushLabelUnique labels
+ return ctorLabels.foldl Data.Label.pushUnique labels
| _ => return labels
def infer (decl : Name) (info : ConstantInfo) : CoreM InferredDeps := do
@@ -141,7 +137,7 @@ private def payloadWithUseRefs
else
match current? with
| some payload =>
- some { payload with deps := Data.UseRef.mergeByLabel payload.deps useRefs }
+ some (payload.withMergedDeps useRefs)
| none =>
some { stx := ref, deps := useRefs }
diff --git a/src/VersoBlueprint/Docstring.lean b/src/VersoBlueprint/Docstring.lean
new file mode 100644
index 00000000..5726cfdc
--- /dev/null
+++ b/src/VersoBlueprint/Docstring.lean
@@ -0,0 +1,199 @@
+/-
+Copyright (c) 2026 Lean FRO LLC. All rights reserved.
+Released under Apache 2.0 license as described in the file LICENSE.
+Author: Emilio J. Gallego Arias
+-/
+
+import Lean.DocString.Extension
+import VersoManual
+import VersoBlueprint.Math
+
+namespace Informal.Docstring
+
+open Lean
+
+mutual
+
+private partial def inlineToManualStx
+ (inl : Lean.Doc.Inline Lean.ElabInline) : CoreM (TSyntax `term) := do
+ match inl with
+ | .text s => `(Verso.Doc.Inline.text $(quote s))
+ | .emph content =>
+ let content ← content.mapM inlineToManualStx
+ `(Verso.Doc.Inline.emph #[$content,*])
+ | .bold content =>
+ let content ← content.mapM inlineToManualStx
+ `(Verso.Doc.Inline.bold #[$content,*])
+ | .code s => `(Verso.Doc.Inline.code $(quote s))
+ | .math .inline s => Informal.Math.mkBpMathInlineTerm .inline s
+ | .math .display s => Informal.Math.mkBpMathInlineTerm .display s
+ | .linebreak s => `(Verso.Doc.Inline.linebreak $(quote s))
+ | .link content url =>
+ let content ← content.mapM inlineToManualStx
+ `(Verso.Doc.Inline.link #[$content,*] $(quote url))
+ | .footnote name content =>
+ let content ← content.mapM inlineToManualStx
+ `(Verso.Doc.Inline.footnote $(quote name) #[$content,*])
+ | .image alt url => `(Verso.Doc.Inline.image $(quote alt) $(quote url))
+ | .concat content =>
+ let content ← content.mapM inlineToManualStx
+ `(Verso.Doc.Inline.concat #[$content,*])
+ -- Extensions without a Manual adapter retain their converted child content.
+ | .other _ content =>
+ let content ← content.mapM inlineToManualStx
+ `(Verso.Doc.Inline.concat #[$content,*])
+
+private partial def listItemToManualStx
+ (item : Lean.Doc.ListItem (Lean.Doc.Block Lean.ElabInline Lean.ElabBlock)) :
+ CoreM (TSyntax `term) := do
+ let contents ← item.contents.mapM blockToManualStx
+ `(Verso.Doc.ListItem.mk #[$contents,*])
+
+private partial def descItemToManualStx
+ (item :
+ Lean.Doc.DescItem
+ (Lean.Doc.Inline Lean.ElabInline)
+ (Lean.Doc.Block Lean.ElabInline Lean.ElabBlock)) :
+ CoreM (TSyntax `term) := do
+ let term ← item.term.mapM inlineToManualStx
+ let desc ← item.desc.mapM blockToManualStx
+ `(Verso.Doc.DescItem.mk #[$term,*] #[$desc,*])
+
+private partial def blockToManualStx
+ (block : Lean.Doc.Block Lean.ElabInline Lean.ElabBlock) :
+ CoreM (TSyntax `term) := do
+ match block with
+ | .para contents =>
+ let contents ← contents.mapM inlineToManualStx
+ `(Verso.Doc.Block.para #[$contents,*])
+ | .code content => `(Verso.Doc.Block.code $(quote content))
+ | .ul items =>
+ let items ← items.mapM listItemToManualStx
+ `(Verso.Doc.Block.ul #[$items,*])
+ | .ol start items =>
+ let items ← items.mapM listItemToManualStx
+ `(Verso.Doc.Block.ol $(quote start) #[$items,*])
+ | .dl items =>
+ let items ← items.mapM descItemToManualStx
+ `(Verso.Doc.Block.dl #[$items,*])
+ | .blockquote items =>
+ let items ← items.mapM blockToManualStx
+ `(Verso.Doc.Block.blockquote #[$items,*])
+ | .concat content =>
+ let content ← content.mapM blockToManualStx
+ `(Verso.Doc.Block.concat #[$content,*])
+ -- Extensions without a Manual adapter retain their converted child content.
+ | .other _ content =>
+ let content ← content.mapM blockToManualStx
+ `(Verso.Doc.Block.concat #[$content,*])
+
+end
+
+/--
+Convert an elaborated Verso docstring into the Manual blocks used by an
+attribute-owned Blueprint statement.
+
+The conversion preserves standard structural nodes and deliberately flattens
+custom extensions whose semantics are not available in the Manual genre.
+-/
+partial def versoDocstringToManualBlocksStx
+ (doc : Lean.VersoDocString) : CoreM (Array (TSyntax `term)) := do
+ let mut blocks ← doc.text.mapM blockToManualStx
+ for part in doc.subsections do
+ blocks := blocks ++ (← partToManualBlocksStx part)
+ pure blocks
+where
+ partToManualBlocksStx
+ (part : Lean.Doc.Part Lean.ElabInline Lean.ElabBlock Empty) :
+ CoreM (Array (TSyntax `term)) := do
+ let mut out : Array (TSyntax `term) := #[]
+ if !part.title.isEmpty then
+ let title ← part.title.mapM inlineToManualStx
+ let titleBold ← `(Verso.Doc.Inline.bold #[$title,*])
+ let titleBlock ← `(Verso.Doc.Block.para #[$titleBold])
+ out := out.push titleBlock
+ out := out ++ (← part.content.mapM blockToManualStx)
+ for child in part.subParts do
+ out := out ++ (← partToManualBlocksStx child)
+ pure out
+
+private def mathAttrs (mode : Lean.Doc.MathMode) (texPrelude : String) :
+ Array (String × String) :=
+ let classes :=
+ "bp_math " ++ match mode with
+ | .inline => "inline"
+ | .display => "display"
+ if texPrelude.isEmpty then
+ #[("class", classes)]
+ else
+ #[("class", classes), ("data-bp-tex-prelude", texPrelude)]
+
+private def elaboratedDocstringGenre : Verso.Doc.Genre where
+ PartMetadata := Empty
+ Block := Lean.ElabBlock
+ Inline := Lean.ElabInline
+ TraverseContext := Unit
+ TraverseState := Unit
+
+private instance : Verso.Doc.TraverseBlock elaboratedDocstringGenre := {}
+
+private instance : Verso.Doc.Html.GenreHtml elaboratedDocstringGenre Id where
+ part _ metadata := nomatch metadata
+ -- Docstring extensions do not carry rendering behavior once elaborated outside
+ -- their owning genre, so retain their standard children.
+ block _ blockHtml _ contents := .seq <$> contents.mapM blockHtml
+ inline inlineHtml _ contents := .seq <$> contents.mapM inlineHtml
+
+private def inlineToHtml
+ (inline : Lean.Doc.Inline Lean.ElabInline) : Verso.Output.Html :=
+ let action :=
+ elaboratedDocstringGenre.toHtml (m := Id)
+ {} () () {} {} {} (show Verso.Doc.Inline elaboratedDocstringGenre from inline)
+ (action.run .empty).1
+
+private def blockToHtml
+ (block : Lean.Doc.Block Lean.ElabInline Lean.ElabBlock) : Verso.Output.Html :=
+ let action :=
+ elaboratedDocstringGenre.toHtml (m := Id)
+ {} () () {} {} {} (show Verso.Doc.Block elaboratedDocstringGenre from block)
+ (action.run .empty).1
+
+private def rewriteMathHtml
+ (texPrelude : String) (html : Verso.Output.Html) : Verso.Output.Html :=
+ html.visitM (m := Id) (tag := fun name attrs contents =>
+ if name == "code" && attrs.contains ("class", "math inline") then
+ some (.tag name (mathAttrs .inline texPrelude) contents)
+ else if name == "code" && attrs.contains ("class", "math display") then
+ some (.tag name (mathAttrs .display texPrelude) contents)
+ else
+ none)
+
+/--
+Render the standard structural subset of an elaborated Verso docstring as
+static HTML for external declaration panels.
+
+Custom extension wrappers are flattened to their children, matching statement
+materialization. Blueprint math receives the same classes and TeX prelude
+metadata as normal Blueprint math nodes.
+-/
+partial def versoDocstringToHtml
+ (doc : Lean.VersoDocString) (texPrelude : String := "") :
+ Verso.Output.Html :=
+ let text := .seq <| doc.text.map blockToHtml
+ let subsections := .seq <| doc.subsections.map partToHtml
+ rewriteMathHtml texPrelude <| .seq #[text, subsections]
+where
+ partToHtml
+ (part : Lean.Doc.Part Lean.ElabInline Lean.ElabBlock Empty) :
+ Verso.Output.Html :=
+ let title :=
+ if part.title.isEmpty then
+ .empty
+ else
+ .tag "p" #[] <|
+ .tag "strong" #[] (.seq <| part.title.map inlineToHtml)
+ let content := .seq <| part.content.map blockToHtml
+ let children := .seq <| part.subParts.map partToHtml
+ .seq #[title, content, children]
+
+end Informal.Docstring
diff --git a/src/VersoBlueprint/Environment.lean b/src/VersoBlueprint/Environment.lean
index b14d20e9..7f901d84 100644
--- a/src/VersoBlueprint/Environment.lean
+++ b/src/VersoBlueprint/Environment.lean
@@ -18,9 +18,9 @@ open Informal.Data
Elaboration-time builder for a node that is currently open on the directive
stack.
-This intentionally stays separate from `Data.Node`: it carries phase-specific
-inputs such as preview blocks and elaboration syntax before the final persisted
-semantic node can be assembled.
+This intentionally stays separate from `Data.Node`: it carries directive-stack
+metadata and typed preview blocks before the final persisted semantic node can
+be assembled.
-/
structure InProgress where
label : Label
@@ -34,7 +34,6 @@ structure InProgress where
prUrl : Option String := none
deps : Array UseRef := #[]
previewBlocks : Array (Verso.Doc.Block Verso.Genre.Manual) := #[]
- elabStx : Array Syntax := #[]
deriving Inhabited, Repr
inductive ImportedConflictKind where
@@ -59,6 +58,10 @@ data.
structure State where
data : Data := Data.empty
localData : NameMap Node := {}
+ /-- Attribute-owned labels grouped by their defining module, in application order. -/
+ blueprintAttributeLabelsByModule : NameMap (Array Label) := {}
+ /-- Current-module subset exported through the persistent extension. -/
+ localBlueprintAttributeLabelsByModule : NameMap (Array Label) := {}
groups : NameMap String := {}
localGroups : NameMap String := {}
authors : NameMap AuthorInfo := {}
@@ -93,26 +96,28 @@ private def sortImportedConflicts (conflicts : Array ImportedConflict) : Array I
inductive Entry where
| node (label : Name) (node : Node)
+ | blueprintAttributeLabel (moduleName : Name) (label : Label)
| group (label : Name) (header : String)
| author (label : Name) (info : AuthorInfo)
deriving Inhabited, Repr
-private def pushLabelUnique (labels : Array Label) (label : Label) : Array Label :=
- if labels.contains label then labels else labels.push label
-
-private def nodeLeanDecls (node : Node) : Array Name :=
- node.leanDecls
+private def addBlueprintAttributeLabel
+ (modules : NameMap (Array Label))
+ (moduleName : Name) (label : Label) :
+ NameMap (Array Label) :=
+ modules.insert moduleName <|
+ Label.pushUnique (modules.getD moduleName #[]) label
private def addLeanDeclLabel
(leanNameLabels : NameMap (Array Label)) (decl label : Name) : NameMap (Array Label) :=
let decl := decl.eraseMacroScopes
let labels := leanNameLabels.getD decl #[]
- leanNameLabels.insert decl (pushLabelUnique labels label)
+ leanNameLabels.insert decl (Label.pushUnique labels label)
private def addNodeLeanDeclLabels
(leanNameLabels : NameMap (Array Label)) (label : Name) (node : Node) :
NameMap (Array Label) :=
- (nodeLeanDecls node).foldl (init := leanNameLabels) fun acc decl =>
+ node.leanDecls.foldl (init := leanNameLabels) fun acc decl =>
addLeanDeclLabel acc decl label
private def addRegisteredNodeLeanDeclLabels
@@ -154,6 +159,13 @@ initialize informalExt : PersistentEnvExtension Entry Entry State ←
localData := state.localData.insert label node
leanNameLabels := addNodeLeanDeclLabels state.leanNameLabels label node
}
+ | .blueprintAttributeLabel moduleName label =>
+ { state with
+ blueprintAttributeLabelsByModule :=
+ addBlueprintAttributeLabel state.blueprintAttributeLabelsByModule moduleName label
+ localBlueprintAttributeLabelsByModule :=
+ addBlueprintAttributeLabel state.localBlueprintAttributeLabelsByModule moduleName label
+ }
| .group label header =>
{ state with
groups := state.groups.insert label header
@@ -165,34 +177,54 @@ initialize informalExt : PersistentEnvExtension Entry Entry State ←
localAuthors := state.localAuthors.insert label info
}
addImportedFn entries := do
- let (data, groups, authors, leanNameLabels, importedConflicts) := entries.foldl
+ let (data, attributeLabelsByModule, groups, authors, leanNameLabels, importedConflicts) :=
+ entries.foldl
(init := (
({} : NameMap Node),
+ ({} : NameMap (Array Label)),
({} : NameMap String),
({} : NameMap AuthorInfo),
({} : NameMap (Array Label)),
(#[] : Array ImportedConflict)
)) fun acc entry =>
- entry.foldl (init := acc) fun (dataAcc, groupAcc, authorAcc, leanNameAcc, conflictsAcc) item =>
+ entry.foldl (init := acc) fun
+ (dataAcc, attributeLabelsAcc, groupAcc, authorAcc, leanNameAcc, conflictsAcc) item =>
match item with
| .node label node =>
if dataAcc.contains label then
- (dataAcc, groupAcc, authorAcc, leanNameAcc, pushImportedConflict conflictsAcc .node label)
+ (dataAcc, attributeLabelsAcc, groupAcc, authorAcc, leanNameAcc,
+ pushImportedConflict conflictsAcc .node label)
else
let leanNameAcc := addNodeLeanDeclLabels leanNameAcc label node
- (dataAcc.insert label node, groupAcc, authorAcc, leanNameAcc, conflictsAcc)
+ (dataAcc.insert label node, attributeLabelsAcc, groupAcc, authorAcc,
+ leanNameAcc, conflictsAcc)
+ | .blueprintAttributeLabel moduleName label =>
+ (dataAcc,
+ addBlueprintAttributeLabel attributeLabelsAcc moduleName label,
+ groupAcc, authorAcc, leanNameAcc, conflictsAcc)
| .group label header =>
if groupAcc.contains label then
- (dataAcc, groupAcc, authorAcc, leanNameAcc, pushImportedConflict conflictsAcc .group label)
+ (dataAcc, attributeLabelsAcc, groupAcc, authorAcc, leanNameAcc,
+ pushImportedConflict conflictsAcc .group label)
else
- (dataAcc, groupAcc.insert label header, authorAcc, leanNameAcc, conflictsAcc)
+ (dataAcc, attributeLabelsAcc, groupAcc.insert label header, authorAcc,
+ leanNameAcc, conflictsAcc)
| .author label info =>
if authorAcc.contains label then
- (dataAcc, groupAcc, authorAcc, leanNameAcc, pushImportedConflict conflictsAcc .author label)
+ (dataAcc, attributeLabelsAcc, groupAcc, authorAcc, leanNameAcc,
+ pushImportedConflict conflictsAcc .author label)
else
- (dataAcc, groupAcc, authorAcc.insert label info, leanNameAcc, conflictsAcc)
- pure { data, groups, authors, leanNameLabels, importedConflicts := sortImportedConflicts importedConflicts }
- -- Strip transient elaboration cache before exporting nodes to the environment.
+ (dataAcc, attributeLabelsAcc, groupAcc, authorAcc.insert label info,
+ leanNameAcc, conflictsAcc)
+ pure {
+ data
+ blueprintAttributeLabelsByModule := attributeLabelsByModule
+ groups
+ authors
+ leanNameLabels
+ importedConflicts := sortImportedConflicts importedConflicts
+ }
+ -- Prefer typed preview blocks and strip redundant term syntax before export.
exportEntriesFnEx env := fun state =>
let nodeEntries := state.localData.toArray.map fun (name, node) =>
let statement := node.statement.map fun s =>
@@ -200,11 +232,15 @@ initialize informalExt : PersistentEnvExtension Entry Entry State ←
let proof := node.proof.map fun p =>
if p.previewBlocks.isEmpty then p else { p with elabStx := #[] }
Entry.node name { node with statement, proof }
+ let attributeLabelEntries :=
+ state.localBlueprintAttributeLabelsByModule.toArray.flatMap fun (moduleName, labels) =>
+ labels.map (Entry.blueprintAttributeLabel moduleName)
let groupEntries := state.localGroups.toArray.map fun (label, header) =>
Entry.group label header
let authorEntries := state.localAuthors.toArray.map fun (label, info) =>
Entry.author label info
- OLeanEntries.uniform (nodeEntries ++ groupEntries ++ authorEntries)
+ OLeanEntries.uniform
+ (nodeEntries ++ attributeLabelEntries ++ groupEntries ++ authorEntries)
}
section EnvOps
@@ -224,6 +260,22 @@ def modifyDataForLabel (label : Label) (f : Data -> m Data) : m Unit := do
let data ← f state.data
return state.commitDataForLabel label data
+/--
+Record one successful `@[blueprint]` label under the module currently being
+compiled. Repeated registrations keep the first application position while the
+semantic node continues to collect every Lean declaration.
+-/
+def registerBlueprintAttributeLabel (label : Label) : m Unit := do
+ let moduleName := (← getEnv).mainModule
+ modifyEnv fun env =>
+ informalExt.addEntry env <|
+ .blueprintAttributeLabel moduleName label.eraseMacroScopes
+
+/-- Attribute-owned Blueprint labels declared directly by `moduleName`, in source order. -/
+def blueprintAttributeLabelsForModule (moduleName : Name) : m (Array Label) := do
+ let state := informalExt.getState (← getEnv)
+ return state.blueprintAttributeLabelsByModule.getD moduleName #[]
+
def importedConflicts : m (Array ImportedConflict) := do
return (informalExt.getState (← getEnv)).importedConflicts
@@ -311,7 +363,6 @@ def pop (ref : Syntax) : m Nat := do
stx := ref
deps := cur.deps
previewBlocks := cur.previewBlocks
- elabStx := cur.elabStx
}
let data ← state.data.register
cur.label cur.kind payload cur.codeHint cur.parent cur.priority cur.owner cur.tags cur.effort cur.prUrl
@@ -345,16 +396,6 @@ def addUse (stx : Syntax) (useRef : UseRef) : m Unit := do
def addDep (stx : Syntax) (dep : Name) : m Unit := do
addUse stx { label := dep }
-def setStatementElab (stxs : Array Syntax) : m Unit := do
- match (informalExt.getState (← getEnv)).stack with
- | [] => pure ()
- | cur :: rest =>
- match cur.kind with
- | .proof => pure ()
- | .statement _ =>
- let cur := { cur with elabStx := stxs }
- modify fun state => { state with stack := cur :: rest }
-
def setPreviewBlocks (blocks : Array (Verso.Doc.Block Verso.Genre.Manual)) : m Unit := do
match (informalExt.getState (← getEnv)).stack with
| [] => pure ()
diff --git a/src/VersoBlueprint/ExternalDeclRender.lean b/src/VersoBlueprint/ExternalDeclRender.lean
index 9c126412..27b5ec5e 100644
--- a/src/VersoBlueprint/ExternalDeclRender.lean
+++ b/src/VersoBlueprint/ExternalDeclRender.lean
@@ -7,8 +7,10 @@ Author: Emilio J. Gallego Arias
import Lean
import Verso
import VersoManual
+import VersoBlueprint.Docstring
import VersoBlueprint.Lib.HtmlId
import VersoBlueprint.Lib.HoverInline
+import VersoBlueprint.Macros
open Lean Meta
@@ -283,9 +285,30 @@ private def plainDocstringHtml (docs? : Option String) : ExternalDeclHtml :=
| some docs =>
{{
{{.text true docs}}}}
-private def docsHtml (docs? : Option String) : ExternalDeclHtml :=
+private def structuralDocstringHtml
+ (doc : Lean.VersoDocString) (texPrelude : String) : ExternalDeclHtml :=
+ .tag "div" #[("class", "docstring")]
+ (Informal.Docstring.versoDocstringToHtml doc texPrelude)
+
+private def internalDocstringHtml
+ (doc? : Option (String ⊕ Lean.VersoDocString))
+ (fallback? : Option String)
+ (texPrelude : String) : ExternalDeclHtml :=
+ match doc? with
+ | some (.inl doc) => plainDocstringHtml (some doc)
+ | some (.inr doc) => structuralDocstringHtml doc texPrelude
+ | none => plainDocstringHtml fallback?
+
+private def docstringHtmlForDecl
+ (env : Lean.Environment) (decl : Name)
+ (fallback? : Option String) (texPrelude : String) :
+ MetaM ExternalDeclHtml := do
+ let doc? ← liftM <| findInternalDocString? env decl
+ pure <| internalDocstringHtml doc? fallback? texPrelude
+
+private def docsHtml (doc : ExternalDeclHtml) : ExternalDeclHtml :=
open Verso.Output.Html in
- {{{{signatureHtml}}
- {{docsHtml docName.docstring?}}
+ {{docsHtml docstring}}
Nat.succ" == 2 &&
+ hasSubstr html "First imported docstring list item." &&
+ hasSubstr html "Second imported docstring list item." &&
+ 3 ≤ (html.splitOn "1 + 1 = 2" &&
+ hasSubstr html "2 + 2 = 4" &&
+ hasSubstr html "hybridSharedFirst" &&
+ hasSubstr html "hybridSharedSecond" &&
+ hasSubstr html
+ "A later declaration docstring fills a dependency-only statement payload" &&
+ hasSubstr html "hybridLateDocstringFirst" &&
+ hasSubstr html "hybridLateDocstringSecond" &&
+ firstBodyMarkerCount == laterBodyMarkerCount + 1 &&
+ hasSubstr html "hybridFirstBody" &&
+ hasSubstr html "hybridLaterBody" &&
+ bodyData.statementUses.map (·.label) ==
+ #[Name.mkSimple "attr.hybrid.verso_docstring"] &&
+ bodyData.proofUses.map (·.label) == #[Name.mkSimple "attr.hybrid.shared"] &&
+ hasEntries
+
+/- A manifest-backed graft must project only the selected dependency facet. -/
+/-- info: true -/
+#guard_msgs in
+#eval
+ show IO Bool from do
+ let files ← buildManualPreviewDataFiles manualImpls includedHybridAttributeModuleDoc
+ let ctx :=
+ Informal.Graft.RenderContext.ofPreviewData? (some files.manifest) (some files.htmlCache)
+ let node :=
+ (({ label := "attr.hybrid.body" } :
+ Informal.Graft.BlueprintNodeConfig).toNode)
+ let rendered ← Informal.Graft.renderNodeFromManifestCache {} ctx node
+ let html := rendered.asString
+ pure <|
+ hasSubstr html "Statement uses 1" &&
+ hasSubstr html "attr.hybrid.verso_docstring" &&
+ !hasSubstr html "attr.hybrid.shared"
+
+/- This is the final-HTML regression behind the attribute-first authoring
+ feedback: an imported bare attribute, a structural Verso docstring, both
+ placement paths, and consumer-side code folding. -/
+/-- info: true -/
+#guard_msgs in
+#eval
+ show IO Bool from do
+ let label := Name.mkSimple
+ "Verso.VersoBlueprintTests.BlueprintAttribute.DefaultLabelProvider.qualifiedDefaultLabel"
+ let (placedHtml, placedState) ←
+ renderManualDocHtmlStringAndState manualImpls placedDefaultLabelDoc
+ let (moduleHtml, moduleState) ←
+ renderManualDocHtmlStringAndState manualImpls includedDefaultLabelModuleDoc
+ let key := Informal.PreviewCache.statementKey label
+ let renderedAsExpected (html : String) : Bool :=
+ countSubstr html "qualified default-label theorem" == 2 &&
+ countSubstr html "3 + 4 = 7" == 2 &&
+ countSubstr html "3 + 5 = 8" == 2 &&
+ countSubstr html
+ "href=\"https://example.com/qualified-docstring\"" == 2 &&
+ countSubstr html "" == 2 && + 2 ≤ countSubstr html "First qualified-label list item." && + countSubstr html "Qualified structural subsection" == 2 && + 2 ≤ countSubstr html + "Subsection content remains part of the imported statement." && + hasSubstr html "class=\"bp_code_block bp_code_panel\"" && + hasSubstr html "qualifiedDefaultLabel" && + !hasSubstr html "open=\"open\"" + pure <| + (Informal.PreviewManifest.findTraversalBlockEntry? placedState key).isSome && + (Informal.PreviewManifest.findTraversalBlockEntry? moduleState key).isSome && + (Informal.PreviewManifest.findTraversalBlockEntry? placedState key).map + (·.2.foldCodeBlock) == some true && + (Informal.PreviewManifest.findTraversalBlockEntry? moduleState key).map + (·.2.foldCodeBlock) == some true && + renderedAsExpected placedHtml && + renderedAsExpected moduleHtml && + hasSubstr moduleHtml "qualifiedDefaultDefinition" + +private def hasCodeOnlyAttributePreview + (files : Informal.PreviewManifest.Files) (label declaration : String) : Bool := + let key := Informal.PreviewCache.statementKey (Name.mkSimple label) + match files.manifest.previews.find? (·.key == key), files.htmlCache.findHtml? key with + | some entry, some body => + body.contains "bp_code_only_preview_body" && + !entry.leanCodePreviewKeys.isEmpty && + entry.kind == some .definition && + (files.htmlCache.codeHtmlBodies entry |>.any (·.contains declaration)) + | _, _ => false + +/- Attribute-owned, code-only nodes remain available in the manifest/cache pair + through both individual placement and module inclusion. -/ +/-- info: true -/ +#guard_msgs in +#eval + show IO Bool from do + let placedFiles ← buildManualPreviewDataFiles manualImpls placedAttributeDoc + let moduleFiles ← buildManualPreviewDataFiles manualImpls includedAttributeModuleDoc + pure <| + hasCodeOnlyAttributePreview placedFiles + "attr.exported.undocumented" "exportedUndocumentedDefinition" && + hasCodeOnlyAttributePreview moduleFiles + "attr.exported.undocumented" "exportedUndocumentedDefinition" + +end Verso.VersoBlueprintTests.BlueprintAttributeRendering diff --git a/tests/VersoBlueprintTests/BlueprintBlockFolding.lean b/tests/VersoBlueprintTests/BlueprintBlockFolding.lean index e0eec7c5..65d6ec1b 100644 --- a/tests/VersoBlueprintTests/BlueprintBlockFolding.lean +++ b/tests/VersoBlueprintTests/BlueprintBlockFolding.lean @@ -57,6 +57,8 @@ Folded proof statement. :::proof "folding.folded.proof" Folded proof body. ::: + +{blueprint_node "folding.folded.proof" (facet := "proof")} ::::::: set_option verso.blueprint.foldProofBlocks false @@ -122,7 +124,8 @@ set_option verso.blueprint.foldCodeBlocks false #eval! do let out ← renderManualDocHtmlString manualImpls foldedProofBlockDoc pure <| - hasSubstr out ".asString + let statement := render .statement + let proof := render .proof + hasSubstr statement "Statement uses 1" && + hasSubstr statement "def:graft.statement.dep" && + !hasSubstr statement "def:graft.proof.dep" && + hasSubstr statement "class=\"bp_extra_slot bp_extra_slot_group\"" && + hasSubstr statement "class=\"bp_extra_slot bp_extra_slot_used_by\"" && + hasSubstr statement "bp_code_panel_wrapper" && + hasSubstr proof "Proof uses 1" && + hasSubstr proof "def:graft.proof.dep" && + !hasSubstr proof "def:graft.statement.dep" && + !hasSubstr proof "class=\"bp_extra_slot bp_extra_slot_group\"" && + !hasSubstr proof "class=\"bp_extra_slot bp_extra_slot_used_by\"" && + !hasSubstr proof "bp_code_panel_wrapper" + +/- Manifest construction keeps statement code associations off the proof facet. -/ +/-- info: true -/ +#guard_msgs in +#eval + show IO Bool from do + let files ← buildManualPreviewDataFiles manualImpls facetCodeProjectionDoc + let label := Lean.Name.mkSimple "thm:graft.facet.code" + let statementKey := Informal.PreviewCache.statementKey label + let proofKey := Informal.PreviewCache.proofKey label + let some statementEntry := files.manifest.previews.find? (·.key == statementKey) + | return false + let some proofEntry := files.manifest.previews.find? (·.key == proofKey) + | return false + let ctx := + Informal.Graft.RenderContext.ofPreviewData? (some files.manifest) (some files.htmlCache) + let statementNode := + (({ label := "thm:graft.facet.code" } : + Informal.Graft.BlueprintNodeConfig).toNode) + let proofNode := + (({ label := "thm:graft.facet.code", facet := some "proof" } : + Informal.Graft.BlueprintNodeConfig).toNode) + let statementHtml ← + Informal.Graft.renderNodeFromManifestCache {} ctx statementNode + let proofHtml ← + Informal.Graft.renderNodeFromManifestCache {} ctx proofNode + pure <| + !statementEntry.leanCodePreviewKeys.isEmpty && + statementEntry.codeData.isSome && + proofEntry.leanCodePreviewKeys.isEmpty && + proofEntry.codeData.isNone && + hasSubstr statementHtml.asString "graftFacetCodeWitness" && + hasSubstr statementHtml.asString "bp_code_panel_wrapper" && + !hasSubstr proofHtml.asString "graftFacetCodeWitness" && + !hasSubstr proofHtml.asString "bp_code_panel_wrapper" + private def renderAuditNode (manifest : Informal.PreviewManifest.File) (htmlCache : Informal.PreviewManifest.HtmlCache.File) diff --git a/tests/VersoBlueprintTests/BlueprintImportedDuplicates/Direct.lean b/tests/VersoBlueprintTests/BlueprintImportedDuplicates/Direct.lean index cc6e9bbc..3174f07c 100644 --- a/tests/VersoBlueprintTests/BlueprintImportedDuplicates/Direct.lean +++ b/tests/VersoBlueprintTests/BlueprintImportedDuplicates/Direct.lean @@ -28,7 +28,7 @@ error: Duplicate imported blueprint author id '«dup.imported.author»' #guard_msgs in #docs (Genre.Manual) directImportedDuplicateDoc "Direct Imported Duplicates" := ::::::: -{blueprint_summary} +{blueprint_node "dup.imported.node"} ::::::: /-- info: true -/ diff --git a/tests/VersoBlueprintTests/BlueprintPreviewSchema.lean b/tests/VersoBlueprintTests/BlueprintPreviewSchema.lean index ef2e9efb..bbea6394 100644 --- a/tests/VersoBlueprintTests/BlueprintPreviewSchema.lean +++ b/tests/VersoBlueprintTests/BlueprintPreviewSchema.lean @@ -67,6 +67,12 @@ open Informal.PreviewManifest let leanCodePreviewKeysDesc? := do let leanCodePreviewKeysJson ← entryProps.get? "leanCodePreviewKeys" leanCodePreviewKeysJson.getObjValAs? String "description" |>.toOption + let foldCodeBlockDesc? := do + let foldCodeBlockJson ← entryProps.get? "foldCodeBlock" + foldCodeBlockJson.getObjValAs? String "description" |>.toOption + let foldProofBlockDesc? := do + let foldProofBlockJson ← entryProps.get? "foldProofBlock" + foldProofBlockJson.getObjValAs? String "description" |>.toOption let sourceLocationDesc? := do let sourceLocationJson ← entryProps.get? "sourceLocation" sourceLocationJson.getObjValAs? String "description" |>.toOption @@ -119,6 +125,8 @@ open Informal.PreviewManifest !useRefProps.contains "intents" && entryProps.contains "leanCodePreviewKeys" && entryProps.contains "codeData" && + entryProps.contains "foldProofBlock" && + entryProps.contains "foldCodeBlock" && entryProps.contains "externalMarkup" && entryProps.contains "sources" && !entryProps.contains "source" && @@ -137,6 +145,10 @@ open Informal.PreviewManifest proofUsesDesc? == some "Structured proof use metadata, preserving origin and intent tags." && displayCaptionDesc? == some "Structured heading caption for renderers that need to lay out the title." && leanCodePreviewKeysDesc? == some "Manifest/cache-backed preview keys for Lean code previews associated with this entry." && + foldProofBlockDesc? == + some "Whether the canonical proof shell is collapsed when this is a proof entry." && + foldCodeBlockDesc? == + some "Whether the associated Lean code panel is collapsed for this canonical traversal entry." && (internalSchemaDesc?.getD "").contains "Internal generated-data schema marker" && sourceLocationDesc? == some "Source location lookup result for this manifest entry." && kindDesc? == some "Kind (definition, proposition, lemma, theorem, corollary)." && diff --git a/tests/browser/test_preview_runtime_regressions.py b/tests/browser/test_preview_runtime_regressions.py index 1986cd13..9e262192 100644 --- a/tests/browser/test_preview_runtime_regressions.py +++ b/tests/browser/test_preview_runtime_regressions.py @@ -101,6 +101,36 @@ def test_external_declaration_docstrings_render_markdown(self, server: str, page expect(fun_doc).to_contain_text("Adds a small preview offset") expect(fun_doc.locator("code").first).to_contain_text("n") + verso_def = page.locator( + '[data-decl="PreviewRuntimeShowcase.CodePanelDecls.previewVersoDocstringedDefinition"]' + ).first + expect(verso_def).to_have_count(1) + verso_doc = verso_def.locator(".bp_external_decl_body > div.docstring").first + expect(verso_doc.locator("strong")).to_contain_text("structural external-panel docstring") + expect(verso_doc.locator("li")).to_have_count(2) + verso_math = verso_doc.locator("code.bp_math.inline").first + expect(verso_math).to_contain_text("6 + 1 = 7") + expect(verso_math).to_have_attribute("data-bp-math-rendered", "1") + expect(verso_math.locator(".katex")).to_have_count(1) + verso_display_math = verso_doc.locator("code.bp_math.display").first + expect(verso_display_math).to_contain_text("6 + 2 = 8") + expect(verso_display_math).to_have_attribute("data-bp-math-rendered", "1") + expect(verso_display_math.locator(".katex-display")).to_have_count(1) + expect(verso_def.locator(".bp_external_decl_body > pre.docstring")).to_have_count(0) + + verso_structure = page.locator( + '[data-decl="PreviewRuntimeShowcase.CodePanelDecls.PreviewVersoDocstringedStructure"]' + ).first + expect(verso_structure).to_have_count(1) + expect(verso_structure.locator(".bp_external_decl_body > div.docstring strong")).to_contain_text( + "structural container docstring" + ) + field_doc = verso_structure.locator(".subdocs div.docstring").first + expect(field_doc.locator("strong")).to_contain_text("structural field docstring") + field_math = field_doc.locator("code.bp_math.inline").first + expect(field_math).to_contain_text("8 + 1 = 9") + expect(field_math).to_have_attribute("data-bp-math-rendered", "1") + decl = page.locator( '[data-decl="PreviewRuntimeShowcase.CodePanelDecls.PreviewFreyPackage.ofCounterexample"]' ).first diff --git a/tests/harness/preview_runtime_showcase/check_blueprint_code_panels.py b/tests/harness/preview_runtime_showcase/check_blueprint_code_panels.py index 0c7fc922..2facdb10 100644 --- a/tests/harness/preview_runtime_showcase/check_blueprint_code_panels.py +++ b/tests/harness/preview_runtime_showcase/check_blueprint_code_panels.py @@ -285,6 +285,10 @@ def main() -> int: fail("missing in-module documented external definition showcase declaration") if "PreviewRuntimeShowcase.CodePanelDecls.previewDocstringedFunction" not in code_panels: fail("missing in-module documented external function showcase declaration") + if "PreviewRuntimeShowcase.CodePanelDecls.previewVersoDocstringedDefinition" not in code_panels: + fail("missing in-module Verso-docstring external definition showcase declaration") + if "PreviewRuntimeShowcase.CodePanelDecls.PreviewVersoDocstringedStructure" not in code_panels: + fail("missing in-module Verso-docstring external structure showcase declaration") if "PreviewRuntimeShowcase.CodePanelDecls.previewExternalTheorem" not in code_panels: fail("missing in-module external theorem showcase declaration") if "PreviewRuntimeShowcase.CodePanelDecls.previewDocstringedTheorem" not in code_panels: @@ -368,6 +372,46 @@ def main() -> int: if docstringed_defs_panel.count('data-kind="def"') < 2: fail("documented multi-definition panel missing def kind markers") + verso_docstring_panel = next( + ( + p + for p in external_panels + if 'data-decl="PreviewRuntimeShowcase.CodePanelDecls.previewVersoDocstringedDefinition"' + in p + ), + None, + ) + if verso_docstring_panel is None: + fail("missing Verso-docstring external code panel") + if "structural external-panel docstring" not in verso_docstring_panel: + fail("Verso-docstring panel missing structural emphasis") + if '6 + 1 = 7' not in verso_docstring_panel: + fail("Verso-docstring panel missing structural inline mathematics") + if '6 + 2 = 8' not in verso_docstring_panel: + fail("Verso-docstring panel missing structural display mathematics") + if verso_docstring_panel.count("structural panel item") < 2: + fail("Verso-docstring panel missing structural list items") + if '' in verso_docstring_panel: + fail("Verso-docstring panel was flattened to a plain docstring") + + verso_structure_panel = next( + ( + p + for p in external_panels + if 'data-decl="PreviewRuntimeShowcase.CodePanelDecls.PreviewVersoDocstringedStructure"' + in p + ), + None, + ) + if verso_structure_panel is None: + fail("missing Verso-docstring external structure panel") + if "structural container docstring" not in verso_structure_panel: + fail("Verso structure panel missing structural declaration emphasis") + if "structural field docstring" not in verso_structure_panel: + fail("Verso structure panel missing structural field emphasis") + if '8 + 1 = 9' not in verso_structure_panel: + fail("Verso structure panel missing structural field mathematics") + theorem_docstring_panel = next( ( p diff --git a/tests/test_blueprints/preview_runtime_showcase/PreviewRuntimeShowcase/Chapters/CodePanels.lean b/tests/test_blueprints/preview_runtime_showcase/PreviewRuntimeShowcase/Chapters/CodePanels.lean index 214b0d10..402b768d 100644 --- a/tests/test_blueprints/preview_runtime_showcase/PreviewRuntimeShowcase/Chapters/CodePanels.lean +++ b/tests/test_blueprints/preview_runtime_showcase/PreviewRuntimeShowcase/Chapters/CodePanels.lean @@ -28,6 +28,29 @@ definitions appear in the same code panel. -/ def previewDocstringedFunction (n : Nat) : Nat := n + 1 +set_option doc.verso true in +/-- +A *structural external-panel docstring* with inline mathematics +$`6 + 1 = 7`. + +A display equation follows: +$$`6 + 2 = 8` + +* First structural panel item. +* Second structural panel item. +-/ +def previewVersoDocstringedDefinition : Nat := 7 + +set_option doc.verso true in +/-- +A *structural container docstring* for a field-docstring regression. +-/ +structure PreviewVersoDocstringedStructure where + /-- + A *structural field docstring* with inline mathematics $`8 + 1 = 9`. + -/ + value : Nat + theorem previewExternalTheorem : True := by trivial @@ -303,3 +326,11 @@ class PanelInlineMixedFold (α : Type) where :::definition "panel_no_code" Statement without associated Lean code. ::: + +:::definition "panel_external_verso_docstring" (lean := "PreviewRuntimeShowcase.CodePanelDecls.previewVersoDocstringedDefinition") +External definition panel sample with a structural Verso docstring. +::: + +:::definition "panel_external_verso_structure_docstring" (lean := "PreviewRuntimeShowcase.CodePanelDecls.PreviewVersoDocstringedStructure") +External structure panel sample with structural declaration and field docstrings. +:::