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 - {{
{{plainDocstringHtml docs?}}
}} + {{
{{doc}}
}} private def externalDeclSectionLabelId (decl : Name) (title : String) : String := Informal.HtmlId.prefixed "bp-external-decl-section" s!"{decl.toString}:{title}" @@ -420,18 +443,22 @@ private def visibilityHtml (v : Verso.Genre.Manual.Block.Docstring.Visibility) : | .private => {{"private"" "}} | .protected => .empty -private def renderDocNameCtor (docName : Verso.Genre.Manual.Block.Docstring.DocName) : +private def renderDocNameCtor + (docName : Verso.Genre.Manual.Block.Docstring.DocName) + (docstring : ExternalDeclHtml) : ExternalDeclHighlightRender ExternalDeclHtml := open Verso.Output.Html in do let signatureHtml ← highlightedToHtml docName.signature pure {{
{{signatureHtml}}
- {{docsHtml docName.docstring?}} + {{docsHtml docstring}}
}} -private def renderFieldSignature (field : Verso.Genre.Manual.Block.Docstring.FieldInfo) : +private def renderFieldSignature + (field : Verso.Genre.Manual.Block.Docstring.FieldInfo) + (docstring : ExternalDeclHtml) : ExternalDeclHighlightRender ExternalDeclHtml := open Verso.Output.Html in do let inheritedInfo : ExternalDeclHtml := @@ -455,10 +482,28 @@ private def renderFieldSignature (field : Verso.Genre.Manual.Block.Docstring.Fie {{visibilityHtml field.visibility}}{{fieldNameHtml}} " : " {{fieldTypeHtml}} {{inheritedInfo}} - {{docsHtml field.docString?}} + {{docsHtml docstring}} }} +private def nestedDocstrings + (env : Lean.Environment) + (declType : Verso.Genre.Manual.Block.Docstring.DeclType) + (texPrelude : String) : + MetaM (NameMap ExternalDeclHtml) := do + let nested : Array (Name × Option String) := + match declType with + | .structure _ ctor? _ fields _ _ => + let ctorDocs := + (ctor?.map fun ctor => #[(ctor.name, ctor.docstring?)]).getD #[] + ctorDocs ++ fields.map fun field => (field.projFn, field.docString?) + | .inductive ctors _ _ => + ctors.map fun ctor => (ctor.name, ctor.docstring?) + | _ => #[] + nested.foldlM (init := {}) fun docs (decl, fallback?) => do + let html ← docstringHtmlForDecl env decl fallback? texPrelude + return docs.insert decl html + private def renderParentsSection (decl : Name) (parents : Array Verso.Genre.Manual.Block.Docstring.ParentInfo) : @@ -521,7 +566,9 @@ private def renderDeclHtmlDocstringFromInfoE withOptions (verso.docstring.allowMissing.set · true) <| Verso.Genre.Manual.Block.Docstring.DeclType.ofName decl (hideStructureConstructor := true) let signature ← Verso.Genre.Manual.Signature.forName decl - let docs? ← liftM <| findDocString? env decl + let texPrelude ← Informal.Macros.getTexPrelude + let docstring ← docstringHtmlForDecl env decl none texPrelude + let nestedDocstrings ← nestedDocstrings env declType texPrelude let rendered := renderWithHoverPayloads <| do let ctorSection? : Option ExternalDeclHtml ← @@ -530,7 +577,8 @@ private def renderDeclHtmlDocstringFromInfoE match ctor? with | some ctor => let title := if isClass then "Instance Constructor" else "Constructor" - let ctorHtml ← renderDocNameCtor ctor + let ctorHtml ← renderDocNameCtor ctor <| + nestedDocstrings.getD ctor.name (plainDocstringHtml ctor.docstring?) pure <| renderTitledSection? decl title #[ctorHtml] | none => pure none | _ => pure none @@ -538,7 +586,10 @@ private def renderDeclHtmlDocstringFromInfoE let methodsOrFieldsSection? : Option ExternalDeclHtml ← match declType with | .structure isClass _ _ fieldInfo _ _ => - let rows ← fieldInfo.filter (fun f => f.subobject?.isNone) |>.mapM renderFieldSignature + let rows ← + fieldInfo.filter (fun f => f.subobject?.isNone) |>.mapM fun field => + renderFieldSignature field <| + nestedDocstrings.getD field.projFn (plainDocstringHtml field.docString?) pure <| renderTitledSection? decl (if isClass then "Methods" else "Fields") rows | _ => pure none @@ -550,7 +601,9 @@ private def renderDeclHtmlDocstringFromInfoE let inductiveCtorsSection? : Option ExternalDeclHtml ← match declType with | .inductive ctors _ _ => - let rows ← ctors.mapM renderDocNameCtor + let rows ← ctors.mapM fun ctor => + renderDocNameCtor ctor <| + nestedDocstrings.getD ctor.name (plainDocstringHtml ctor.docstring?) pure <| renderTitledSection? decl "Constructors" rows | _ => pure none @@ -570,9 +623,9 @@ private def renderDeclHtmlDocstringFromInfoE let body : ExternalDeclHtml := if sections.isEmpty then - plainDocstringHtml docs? + docstring else - {{ {{plainDocstringHtml docs?}} {{sections}} }} + {{ {{docstring}} {{sections}} }} pure <| renderExternalDeclWrapper decl presentation.kindClass presentation.kindMarker signatureHtml body (headerBadge? := headerBadge?) (headerMeta := headerMeta) (headerSource? := headerSource?) diff --git a/src/VersoBlueprint/Graft.lean b/src/VersoBlueprint/Graft.lean index 1291df9d..7828daf5 100644 --- a/src/VersoBlueprint/Graft.lean +++ b/src/VersoBlueprint/Graft.lean @@ -8,7 +8,9 @@ import VersoManual import VersoSlides import Verso.Doc.Elab import VersoBlueprint.Informal.Block.Assets +import VersoBlueprint.Informal.Block.Traversal import VersoBlueprint.Informal.LeanCodePreview +import VersoBlueprint.Environment import VersoBlueprint.Graft.Assets import VersoBlueprint.Graft.Node import VersoBlueprint.Graft.Render @@ -125,12 +127,16 @@ private def renderManualGraftNode renderNotice "bp_graft_node_notice" "error" "Blueprint node not found" node.selectionDescription | some (preview, entry) => - if !preview.hasRenderedBody then + if !preview.hasRenderedBody && entry.leanCodePreviewKeys.isEmpty then pure <| Html.tag "div" (manualNodeAttrs node) <| renderNotice "bp_graft_node_notice" "error" "Blueprint node has no cached content" node.key else - let body ← renderManualBlocks goB preview.renderedBody.blocks + let body ← + if preview.hasRenderedBody then + renderManualBlocks goB preview.renderedBody.blocks + else + pure .empty let codeBodies ← if node.compact then pure #[] @@ -147,6 +153,38 @@ private def renderManualGraftNode content (Informal.PreviewManifest.groupRelationForEntry? state entry) +/- +Invisible source block that moves an imported `@[blueprint]` node from the +persistent environment into the current document's traversal indexes. + +The visible output still goes through the ordinary graft renderer. Keeping the +materializer limited to an empty destination anchor gives attribute-owned nodes +the same numbering, relations, previews, and generated-data path as authored +Blueprint blocks. +-/ +open Verso Doc Elab Genre Manual in +block_extension Block.blueprintAttributeNodeSource (data : Informal.BlockData) where + data := toJson data + usePackages := Informal.TeX.standardMathUsePackages + traverse id data contents := do + let some blockData ← + Informal.ExtensionDecode.decode? + (α := Informal.BlockData) + data + (fun err => s!"Malformed attribute-owned Blueprint node data ({err}): {data}") + | pure none + Informal.registerTraversedBlock id blockData contents + pure none + toTeX := some <| fun _goI _goB _id _data _blocks => pure .empty + toHtml := some <| fun _goI _goB id _data _blocks => do + let state ← Doc.Html.HtmlT.state + pure <| Html.tag "span" + (state.htmlId id ++ #[ + ("class", "bp_attribute_node_anchor"), + ("aria-hidden", "true") + ]) + .empty + open Verso Doc Elab Genre Manual in block_extension Block.blueprintGraftNode (cfg : Informal.Graft.BlueprintNodeConfig) where data := toJson cfg @@ -198,18 +236,100 @@ private meta def currentGenreIs (genreTerm : Term) : DocElabM Bool := do let expected ← Lean.Elab.Term.elabTerm genreTerm (some (.const ``Verso.Doc.Genre [])) Lean.Meta.isDefEq current expected -private meta def inManualGenre : DocElabM Bool := do +public meta def inManualGenre : DocElabM Bool := do currentGenreIs (← `(Verso.Genre.Manual)) private meta def inSlidesGenre : DocElabM Bool := do currentGenreIs (← `(VersoSlides.Slides)) -public meta def blueprintNodeBlock (cfg : Informal.Graft.BlueprintNodeConfig) : - DocElabM Term := do - if ← inManualGenre then +private def nodeIsBlueprintAttributeOwned (node : Informal.Data.Node) : Bool := + node.externalRefs.any fun ref => ref.origin == .blueprintAttr + +/-- +Reconstruct one persisted Manual block inside the consuming document. + +Attribute-owned nodes may have bodies that were already elaborated in their +defining module. Encoding those values through Manual's typed JSON instances +lets the generated materializer feed them back through ordinary traversal +without running a synthetic document elaborator or introducing another body +schema. +-/ +public def persistedManualBlockFromJson (jsonText : String) : Verso.Doc.Block Verso.Genre.Manual := + match Lean.Json.parse jsonText >>= Lean.fromJson? with + | .ok block => block + | .error err => + .para #[.text s!"Blueprint persisted statement block could not be decoded: {err}"] + +private meta def persistedManualBlockTerm + (block : Verso.Doc.Block Verso.Genre.Manual) : DocElabM (TSyntax `term) := do + let jsonText := Lean.toJson block |>.compress + `(Informal.Graft.persistedManualBlockFromJson $(quote jsonText)) + +private meta def attributeNodeBlockData? + (cfg : Informal.Graft.BlueprintNodeConfig) : + DocElabM (Option (Informal.BlockData × Array Syntax)) := do + let graftNode := cfg.toNode + let label := Informal.LabelNameParsing.parse graftNode.label + let some node ← Informal.Environment.getNode? label + | return none + if !nodeIsBlueprintAttributeOwned node then + return none + let statementStxs ← + match node.statement with + | none => pure #[] + | some statement => + if statement.previewBlocks.isEmpty then + pure statement.elabStx + else + statement.previewBlocks.mapM fun block => + return (← persistedManualBlockTerm block).raw + let ownerInfo? ← + match node.owner with + | some owner => Informal.Environment.getAuthor? owner + | none => pure none + let opts ← getOptions + let sourceLocation := + match ← Informal.Data.SourceLocation.ofSyntax? (← getRef) with + | some location => Informal.Data.SourceLocationResult.found location + | none => + Informal.Data.SourceLocationResult.unavailable + s!"placement source location unavailable for {label}" + let blockData : Informal.BlockData := { + kind := .statement node.kind + codeData := Informal.BlockCodeData.ofExternalRefs node.externalRefs + label + sourceLocation + foldProofBlock := verso.blueprint.foldProofBlocks.get opts + foldCodeBlock := verso.blueprint.foldCodeBlocks.get opts + count := 0 + numberingMode := Informal.numberingMode opts + subNumberingPrefix := Informal.subNumberingPrefix opts + subNumberingCounter := Informal.subNumberingCounter opts + } + let blockData := blockData.withSemanticNodeMetadata (some node) ownerInfo? + pure <| some (blockData, statementStxs) + +private meta def manualBlueprintNodeBlock + (cfg : Informal.Graft.BlueprintNodeConfig) : DocElabM Term := do + let graft ← ``(Verso.Doc.Block.other (Informal.Graft.Block.blueprintGraftNode $(quote cfg)) #[]) + let some (blockData, statementStxs) ← attributeNodeBlockData? cfg + | return graft + let statementTerms : Array (TSyntax `term) := statementStxs.map fun stx => ⟨stx⟩ + ``(Verso.Doc.Block.concat #[ + Verso.Doc.Block.other + (Informal.Graft.Block.blueprintAttributeNodeSource $(quote blockData)) + #[$statementTerms,*], + $graft + ]) + +public meta def blueprintNodeBlock (cfg : Informal.Graft.BlueprintNodeConfig) : + DocElabM Term := do + Informal.Environment.reportImportedConflicts + if ← inManualGenre then + manualBlueprintNodeBlock cfg else if ← inSlidesGenre then Informal.Slides.blueprintNodeBlock cfg else diff --git a/src/VersoBlueprint/Informal/Block.lean b/src/VersoBlueprint/Informal/Block.lean index 0de50459..f668abb2 100644 --- a/src/VersoBlueprint/Informal/Block.lean +++ b/src/VersoBlueprint/Informal/Block.lean @@ -70,16 +70,7 @@ block_extension Block.informal (data : BlockData) where | none => pure none | some blockData => - let blockData := blockData.withTraversalNumberingContext (← read) - registerTraversedBlockAssets id blockData _contents - saveTraversedBlockData id blockData - if let some sourceRef := blockData.sourceRef then - match Informal.TraversalIndex.SourceRefs.data? (← get) blockData.label with - | some existing => - unless existing == sourceRef do - Verso.reportError s!"Label {blockData.label} already has conflicting source provenance" - | none => - modify fun st => Informal.TraversalIndex.SourceRefs.saveData st blockData.label sourceRef + registerTraversedBlock id blockData _contents return none toTeX := some <| fun _goI goB _id data blocks => do let .ok data := fromJson? (α := BlockData) data @@ -172,10 +163,6 @@ block_extension Block.informal (data : BlockData) where let usedByEntry ← RelatedPanel.renderUsedByExtra relatedPanelContext data let markupEntry? := renderExternalMarkupHeaderExtra? markup - let foldInformalBlock := - match data.kind with - | .proof => data.foldProofBlock - | .statement _ => false let headerExtras : HeaderExtras := match data.kind with | .proof => @@ -197,7 +184,7 @@ block_extension Block.informal (data : BlockData) where (proofCaption? := some (data.displayTitle s)) (attrs := attrs) (headerExtras := headerExtras) - (folded := foldInformalBlock) + (folded := data.foldInformalShell) content companionPanels := #[externalPanel] } @@ -260,10 +247,6 @@ private def expanderImpl (kind : Data.NodeKind) (isProof : Bool := false) : Dire | .statement _ => let externalRefs := node?.map (·.externalRefs) |>.getD #[] BlockCodeData.ofExternalRefs externalRefs - let statementPayload? := node?.bind (·.statement) - let proofPayload? := node?.bind (·.proof) - let statementUses := statementPayload?.map (·.deps) |>.getD #[] - let proofUses := proofPayload?.map (·.deps) |>.getD #[] let owner := node?.bind (·.owner) let ownerInfo? ← match owner with @@ -283,22 +266,12 @@ private def expanderImpl (kind : Data.NodeKind) (isProof : Bool := false) : Dire sourceLocation foldProofBlock := verso.blueprint.foldProofBlocks.get opts foldCodeBlock := verso.blueprint.foldCodeBlocks.get opts - parent := node?.bind (·.parent) count numberingMode := numberingMode opts subNumberingPrefix := subNumberingPrefix opts subNumberingCounter := subNumberingCounter opts - statementUses - proofUses - owner - ownerDisplayName := ownerInfo?.map (·.displayName) - ownerUrl := ownerInfo?.bind (·.url) - ownerImageUrl := ownerInfo?.bind (·.imageUrl) - tags := node?.map (·.tags) |>.getD #[] - effort := node?.bind (·.effort) - priority := node?.bind (·.priority) - prUrl := node?.bind (·.prUrl) } + let data := data.withSemanticNodeMetadata node? ownerInfo? ``(Block.other (Block.informal $(quote data)) #[$contents,*]) private def directiveName (kind : Data.NodeKind) (isProof : Bool): String := diff --git a/src/VersoBlueprint/Informal/Block/Assets.lean b/src/VersoBlueprint/Informal/Block/Assets.lean index 1eeb2249..c27f196b 100644 --- a/src/VersoBlueprint/Informal/Block/Assets.lean +++ b/src/VersoBlueprint/Informal/Block/Assets.lean @@ -6,6 +6,7 @@ Author: Emilio J. Gallego Arias import VersoManual import VersoBlueprint.Commands.Common +import VersoBlueprint.Macros import VersoBlueprint.StyleSwitcher namespace Informal.Block.Assets @@ -1838,7 +1839,7 @@ def codeAssetBundle : Informal.Commands.BlueprintAssetBundle := def blockAssetBundle : Informal.Commands.BlueprintAssetBundle := Informal.Commands.previewPanelInlinePreviewAssetBundle (cssExtras := [css, Informal.StyleSwitcher.css, Verso.Genre.Manual.docstringStyle]) - (jsAfter := [Informal.StyleSwitcher.jsInteractive]) + (jsAfter := [Informal.Macros.blueprintMathJs, Informal.StyleSwitcher.jsInteractive]) def codeCssAssets : List String := codeAssetBundle.css diff --git a/src/VersoBlueprint/Informal/Block/Model.lean b/src/VersoBlueprint/Informal/Block/Model.lean index a029befa..81d38a9f 100644 --- a/src/VersoBlueprint/Informal/Block/Model.lean +++ b/src/VersoBlueprint/Informal/Block/Model.lean @@ -169,12 +169,17 @@ structure BlockData where /-- Optional original-source provenance attached with directive-local metadata. -/ sourceRef : Option Source.Ref := none label : Data.Label - /-- Source location result for the user-written label token. -/ + /-- Source location for this rendered occurrence, ordinarily the user-written label token. -/ sourceLocation : Data.SourceLocationResult := Data.SourceLocationResult.unavailable "label source location unavailable" foldProofBlock : Bool := false foldCodeBlock : Bool := false parent : Option Data.Parent := none + /-- + Elaboration-assigned source-local count. Traversal replaces zero or a count + behind its source-local cursor with the next available count before applying + the configured numbering policy. + -/ count : Nat numberingMode : NumberingMode := .sub /-- Prefix policy for `numberingMode = .sub`. -/ @@ -206,6 +211,37 @@ structure BlockData where prUrl : Option String := none deriving FromJson, ToJson, Quote +/-- Whether this block's informal statement/proof shell should be collapsed. -/ +def BlockData.foldInformalShell (data : BlockData) : Bool := + match data.kind with + | .proof => data.foldProofBlock + | .statement _ => false + +/-- +Copy the semantic metadata shared by every rendered occurrence of a Blueprint +node into placement-specific block data. + +Callers remain responsible for occurrence-local fields such as the block kind, +code payload, source provenance, numbering options, and source location. +-/ +def BlockData.withSemanticNodeMetadata + (data : BlockData) (node? : Option Data.Node) + (ownerInfo? : Option Data.AuthorInfo := none) : BlockData := + { + data with + parent := node?.bind (·.parent) + statementUses := node?.bind (·.statement) |>.map (·.deps) |>.getD #[] + proofUses := node?.bind (·.proof) |>.map (·.deps) |>.getD #[] + owner := node?.bind (·.owner) + ownerDisplayName := ownerInfo?.map (·.displayName) + ownerUrl := ownerInfo?.bind (·.url) + ownerImageUrl := ownerInfo?.bind (·.imageUrl) + tags := node?.map (·.tags) |>.getD #[] + effort := node?.bind (·.effort) + priority := node?.bind (·.priority) + prUrl := node?.bind (·.prUrl) + } + /-- Slim traversal-store payload for Blueprint node metadata. @@ -216,10 +252,13 @@ main semantic node index. structure StoredBlockData where kind : Data.InProgressKind := .proof label : Data.Label - /-- Source location result for the user-written label token. -/ + /-- Source location for this rendered occurrence, ordinarily the user-written label token. -/ sourceLocation : Data.SourceLocationResult := Data.SourceLocationResult.unavailable "label source location unavailable" + foldProofBlock : Bool := false + foldCodeBlock : Bool := false parent : Option Data.Parent := none + /-- Source-local count copied from `BlockData`; zero remains the unassigned sentinel. -/ count : Nat numberingMode : NumberingMode := .sub /-- Prefix policy for `numberingMode = .sub`. -/ @@ -244,6 +283,8 @@ def BlockData.toStoredData (data : BlockData) : StoredBlockData := { kind := data.kind label := data.label sourceLocation := data.sourceLocation + foldProofBlock := data.foldProofBlock + foldCodeBlock := data.foldCodeBlock parent := data.parent count := data.count numberingMode := data.numberingMode @@ -269,6 +310,8 @@ def StoredBlockData.toBlockData (data : StoredBlockData) codeData label := data.label sourceLocation := data.sourceLocation + foldProofBlock := data.foldProofBlock + foldCodeBlock := data.foldCodeBlock parent := data.parent count := data.count numberingMode := data.numberingMode diff --git a/src/VersoBlueprint/Informal/Block/Store.lean b/src/VersoBlueprint/Informal/Block/Store.lean index 27df7234..3a442092 100644 --- a/src/VersoBlueprint/Informal/Block/Store.lean +++ b/src/VersoBlueprint/Informal/Block/Store.lean @@ -40,6 +40,31 @@ def reserveGlobalBlockNumber (st : TraverseState) : Nat × TraverseState := let next := nextGlobalBlockNumber st (next, st.set numberingCounterState (next + 1)) +/-- +Traversal-state key for the next source-local number available during traversal. +-/ +private def sourceNumberingCounterState : Name := + Lean.Name.mkSimple "Informal.Block.sourceNumberingCounter" + +private def nextSourceBlockNumber (st : TraverseState) : Nat := + match st.get? sourceNumberingCounterState with + | some (.ok (n : Nat)) => n + | _ => 1 + +/-- +Resolve a source-local block number monotonically in traversal order. + +Zero is the unassigned sentinel. A nonzero elaboration-time count is also +reassigned when an earlier generated placement has already advanced beyond it. +-/ +private def resolveSourceBlockNumber (st : TraverseState) (count : Nat) : + Nat × TraverseState := + let next := nextSourceBlockNumber st + if count == 0 || count < next then + (next, st.set sourceNumberingCounterState (next + 1)) + else + (count, st.set sourceNumberingCounterState (count + 1)) + /-- Prefix-local counters, stored as a small association list in traversal state. -/ private def prefixBlockCounters (st : TraverseState) : Array (String × Nat) := match st.get? prefixNumberingCounterState with @@ -91,7 +116,8 @@ def numberedPartPrefix? (mode : SubNumberingPrefix) (ctxt : TraverseContext) : O Resolve the appended number for a sub-numbered block. Only `SubNumberingCounter.prefix` reserves a new prefix-local number. In -document-order mode, the block keeps the elaboration-time `count`. +document-order mode, the block keeps the source-local `count` already resolved +during traversal. -/ def reserveSubBlockNumber (st : TraverseState) (data : StoredBlockData) : Nat × TraverseState := match data.subNumberingCounter, data.partPrefix with @@ -115,10 +141,11 @@ private def StoredBlockData.withReservedNumbering match data.globalCount with | some globalCount => (globalCount, st) | none => reserveGlobalBlockNumber st + let (sourceCount, st) := resolveSourceBlockNumber st data.count let (count, st) := match data.numberingMode with - | .sub => reserveSubBlockNumber st data - | _ => (data.count, st) + | .sub => reserveSubBlockNumber st { data with count := sourceCount } + | _ => (sourceCount, st) ({ data with count, globalCount := some globalCount }, st) /-- Look up the stored semantic payload for an informal block label. -/ diff --git a/src/VersoBlueprint/Informal/Block/Traversal.lean b/src/VersoBlueprint/Informal/Block/Traversal.lean index 8b2def4e..67f89b02 100644 --- a/src/VersoBlueprint/Informal/Block/Traversal.lean +++ b/src/VersoBlueprint/Informal/Block/Traversal.lean @@ -6,6 +6,7 @@ Author: Emilio J. Gallego Arias import VersoManual import VersoBlueprint.Informal.Block.Model +import VersoBlueprint.Informal.Block.Store import VersoBlueprint.Informal.LeanCodePreview import VersoBlueprint.PreviewCache import VersoBlueprint.Resolve @@ -130,7 +131,7 @@ This keeps the block extension focused on orchestration while the traversal store owns hover-preview payloads, external Lean previews, and rendered declaration anchors. -/ -def registerTraversedBlockAssets +private def registerTraversedBlockAssets {m} [Monad m] [MonadReaderOf TraverseContext m] @@ -145,4 +146,32 @@ def registerTraversedBlockAssets registerExternalCodePreviews id externalDecls registerExternalDeclAnchors blockData.label externalDecls +/-- +Register one decoded informal block through the shared traversal path. + +This applies the current numbering context, stores preview and declaration +assets, saves the semantic block entry, and records optional source provenance. +-/ +def registerTraversedBlock + {m} + [Monad m] + [MonadReaderOf TraverseContext m] + [MonadStateOf TraverseState m] + [MonadLiftT IO m] + [MonadBuildLog m] + (id : Verso.Multi.InternalId) + (blockData : BlockData) + (contents : Array (Verso.Doc.Block Verso.Genre.Manual)) : + m Unit := do + let blockData := blockData.withTraversalNumberingContext (← read) + registerTraversedBlockAssets id blockData contents + saveTraversedBlockData id blockData + if let some sourceRef := blockData.sourceRef then + match Informal.TraversalIndex.SourceRefs.data? (← get) blockData.label with + | some existing => + unless existing == sourceRef do + Verso.reportError s!"Label {blockData.label} already has conflicting source provenance" + | none => + modify fun st => Informal.TraversalIndex.SourceRefs.saveData st blockData.label sourceRef + end Informal diff --git a/src/VersoBlueprint/ModuleInclude.lean b/src/VersoBlueprint/ModuleInclude.lean new file mode 100644 index 00000000..af60b554 --- /dev/null +++ b/src/VersoBlueprint/ModuleInclude.lean @@ -0,0 +1,94 @@ +/- +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 VersoManual +import Verso.Doc.Elab +import VersoBlueprint.Environment +import VersoBlueprint.Graft + +set_option doc.verso true + +namespace Informal.ModuleInclude + +open Lean +open Verso Doc Elab Syntax ArgParse + +/-- Selection and presentation options for `{includeBlueprintModule ...}`. -/ +private structure BlueprintModuleConfig where + level? : Option Nat := none + moduleName : Name + title? : Option String := none +deriving Inhabited, Repr + +private meta instance : FromArgs BlueprintModuleConfig PartElabM where + fromArgs := + ((fun level moduleName title? => + { level? := some level, moduleName, title? }) + <$> .positional' `level + <*> .positional' `module + <*> .named' `title true) <|> + ((fun moduleName title? => { moduleName, title? }) + <$> .positional' `module + <*> .named' `title true) + +private def moduleIsImported (env : Lean.Environment) (moduleName : Name) : Bool := + env.header.moduleNames.contains moduleName + +private def defaultTitle (moduleName : Name) : String := + moduleName.getString! + +private meta def mkBlueprintModulePart + (stx : Syntax) (endPos : String.Pos.Raw) (cfg : BlueprintModuleConfig) : + PartElabM FinishedPart := do + unless ← PartElabM.liftDocElabM Informal.Graft.inManualGenre do + throwErrorAt stx + "Blueprint module include is only available in Manual documents" + let env ← getEnv + unless moduleIsImported env cfg.moduleName do + throwErrorAt stx + "Blueprint module include: module '{cfg.moduleName}' is not available through this Lean module's imports; add `import {cfg.moduleName}`" + Informal.Environment.reportImportedConflicts + let labels ← Informal.Environment.blueprintAttributeLabelsForModule cfg.moduleName + if labels.isEmpty then + throwErrorAt stx + "Blueprint module include: imported module '{cfg.moduleName}' has no declarations registered with `@[blueprint]`" + let title := cfg.title?.getD (defaultTitle cfg.moduleName) + let titleInline ← ``(Verso.Doc.Inline.text $(quote title)) + let blocks ← labels.mapM fun label => + PartElabM.liftDocElabM <| Informal.Graft.blueprintNodeBlock { + label := label.getString! + } + pure <| FinishedPart.mk stx stx #[titleInline] title none blocks #[] endPos + +open PartElabM in +/-- +Include the declarations marked with {lit}`@[blueprint]` and owned directly by +an imported Lean module as a source-ordered Manual part. The optional leading +level controls where the part is inserted; omitting it creates a child part. +Use {lit}`(title := "...")` to replace the default final module-name component. +-/ +@[part_command Lean.Doc.Syntax.command] +public meta def includeBlueprintModuleCmd : PartCommand + | stx@`(block|command{includeBlueprintModule $args*}) => do + let ref ← getRef + Hover.addCustomHover ref + r#"Includes the `@[blueprint]` declarations owned directly by an imported Lean module as one Manual part. + + * `{includeBlueprintModule MODULE}`: Includes the declarations as a child part. + * `{includeBlueprintModule N MODULE}`: Includes the declarations at header level `N`. + * `(title := "...")`: Replaces the default title derived from the module name. + +The named module must already be available through the document module's Lean imports. + "# + let cfg ← Verso.ArgParse.parseThe BlueprintModuleConfig (← parseArgs args) + let endPos := stx.getTailPos?.get! + let part ← mkBlueprintModulePart stx endPos cfg + if let some level := cfg.level? then + closePartsUntil level endPos + addPart part + | _ => (Lean.Elab.throwUnsupportedSyntax : PartElabM Unit) + +end Informal.ModuleInclude diff --git a/src/VersoBlueprint/PreviewManifest.lean b/src/VersoBlueprint/PreviewManifest.lean index 13b9dcbd..b2e4cbba 100644 --- a/src/VersoBlueprint/PreviewManifest.lean +++ b/src/VersoBlueprint/PreviewManifest.lean @@ -308,6 +308,14 @@ private def logLeanCodePreviewTimings private def htmlStringIsBlank (html : String) : Bool := html.all Char.isWhitespace +/-- +Non-visual cache body for a semantic block whose only visible payload is an +associated Lean-code panel. Browser cache readers reject empty HTML strings, so +code-only nodes use an explicit inert fragment as their block body. +-/ +private def codeOnlyBlockPreviewHtml : String := + "" + private def callbackLogger (logError : String → IO Unit) : Verso.Logger IO where log severity text loc := do let msg := Verso.LogMessage.format { severity, text, loc } @@ -970,6 +978,10 @@ structure Entry where leanCodePreviewKeys : Array String := #[] /-- Canonical Lean code data associated with this informal node, if any. -/ codeData : Option Informal.BlockCodeData := none + /-- Whether the canonical proof shell is collapsed when this is a proof entry. -/ + foldProofBlock : Bool := false + /-- Whether the associated Lean code panel is collapsed for this canonical traversal entry. -/ + foldCodeBlock : Bool := false /-- Raw external markup attachments keyed by language and slot. -/ externalMarkup : Array Informal.Data.ExternalMarkup := #[] /-- Original-source provenance attached to this entry. Lean entries may aggregate several nodes. -/ @@ -988,6 +1000,17 @@ structure Entry where effort : Option String := none deriving Inhabited, Repr, ToJson, FromJson +/-- Whether a related dependency participates in the selected statement or proof facet. -/ +def RelatedEntry.matchesFacet + (related : RelatedEntry) (facet : PreviewCache.Facet) : Bool := + match facet with + | .statement => related.axes.contains .statement + | .proof => related.axes.contains .proof + +/-- Related dependencies that belong to this manifest entry's selected facet. -/ +def Entry.usesForFacet (entry : Entry) : Array RelatedEntry := + entry.uses.filter (·.matchesFacet entry.facet) + /-- Structured heading text for renderers that rebuild an informal block shell. -/ structure EntryHeading where /-- Heading caption, such as "Definition" or "Theorem". -/ @@ -1013,6 +1036,8 @@ def Entry.blockData (entry : Entry) : Informal.BlockData := { sourceRef := entry.primarySource? label := entry.label sourceLocation := entry.sourceLocation + foldProofBlock := entry.foldProofBlock + foldCodeBlock := entry.foldCodeBlock parent := entry.parent count := 0 statementUses := entry.statementUses @@ -1913,9 +1938,12 @@ private def blockLeanCodePreviewKeys (state : TraverseState) (label : Name) (entry : PreviewCache.Entry) : Array String := - (inlineCodePreviewKeys state label).foldl - (init := entry.leanCodePreviewKeys) - (fun keys key => pushUnique keys key) + match entry.facet with + | .proof => entry.leanCodePreviewKeys + | .statement => + (inlineCodePreviewKeys state label).foldl + (init := entry.leanCodePreviewKeys) + (fun keys key => pushUnique keys key) private def externalDeclsFromLeanPreviewKeys (state : TraverseState) @@ -1930,14 +1958,17 @@ private def blockCodeData? (label : Name) (entry : PreviewCache.Entry) (blockData? : Option Informal.BlockData) : Option Informal.BlockCodeData := - let inline? := Informal.TraversalIndex.InlineCode.data? state label - let externalDecls := externalDeclsFromLeanPreviewKeys state entry.leanCodePreviewKeys - let external? := - if externalDecls.isEmpty then - blockData?.bind (·.codeData) - else - some (Informal.BlockCodeData.external externalDecls) - Informal.BlockCodeData.ofHintAndInline external? inline? + match entry.facet with + | .proof => none + | .statement => + let inline? := Informal.TraversalIndex.InlineCode.data? state label + let externalDecls := externalDeclsFromLeanPreviewKeys state entry.leanCodePreviewKeys + let external? := + if externalDecls.isEmpty then + blockData?.bind (·.codeData) + else + some (Informal.BlockCodeData.external externalDecls) + Informal.BlockCodeData.ofHintAndInline external? inline? private def leanCodePreviewSourceRefs (state : TraverseState) : Std.HashMap String (Array Informal.Source.Ref) := Id.run do @@ -2092,6 +2123,8 @@ private def blockSemanticManifestEntry proofUses := blockData?.map (·.proofUses) |>.getD #[] leanCodePreviewKeys := blockLeanCodePreviewKeys state preview.label preview codeData + foldProofBlock := blockData?.map (·.foldProofBlock) |>.getD false + foldCodeBlock := blockData?.map (·.foldCodeBlock) |>.getD false externalMarkup := externalMarkup?.getD (externalMarkupArray state preview.label) sources := sourceRefsForBlockLabel state preview.label uses := blockData?.map (buildUsesRelations state ·) |>.getD #[] @@ -2130,14 +2163,22 @@ private def buildTraversalEntries logError s!"Blueprint manifest: malformed preview entry {err.canonicalName}: {err.message}" | .ok stored => let entry := stored.entry - if !entry.hasRenderedBody then - continue - let rendered ← Informal.renderManualBlocksHtmlWithStateAndHovers entry.renderedBody.blocks impls state - (logError := logError) (hoverState := hoverState) - hoverState := rendered.hoverState - let html := rendered.html.asString - if htmlStringIsBlank html then + let hasLeanCode := !entry.leanCodePreviewKeys.isEmpty + let hasExternalMarkup := !(externalMarkupArray state entry.label).isEmpty + if !entry.hasRenderedBody && (!hasLeanCode || hasExternalMarkup) then continue + let html ← + if entry.hasRenderedBody then + let rendered ← Informal.renderManualBlocksHtmlWithStateAndHovers + entry.renderedBody.blocks impls state + (logError := logError) (hoverState := hoverState) + hoverState := rendered.hoverState + let html := rendered.html.asString + if htmlStringIsBlank html then + continue + pure html + else + pure codeOnlyBlockPreviewHtml let manifestEntry := blockEntryOfTraversalPreview state entry entries := entries.push manifestEntry htmlEntries := htmlEntries.push { key := stored.key, html } diff --git a/src/VersoBlueprint/PreviewManifest/BlockRender.lean b/src/VersoBlueprint/PreviewManifest/BlockRender.lean index 18a62d23..bcb71a56 100644 --- a/src/VersoBlueprint/PreviewManifest/BlockRender.lean +++ b/src/VersoBlueprint/PreviewManifest/BlockRender.lean @@ -156,10 +156,11 @@ private def renderUsesExtra? cfg .uses (usesPanelConfigForEntry entry) - entry.uses + entry.usesForFacet entry Name.anonymous Informal.HeaderExtra.uses + (showWhenEmpty := true) private def renderCodeExtra? (entry : Entry) (blockData : Informal.BlockData) : Option Informal.HeaderExtra := @@ -189,16 +190,19 @@ private def renderHeaderExtras (blockData : Informal.BlockData) (group? : Option GroupRelation) : Informal.HeaderExtras := - { - group? := renderGroupExtra? cfg entry group? - uses? := renderUsesExtra? cfg entry - code? := renderCodeExtra? entry blockData - usedBy? := renderUsedByExtra? cfg entry - markup? := - match entry.facet with - | .statement => Informal.renderExternalMarkupHeaderExtra? entry.externalMarkup - | .proof => none - } + match entry.facet with + | .proof => + { + uses? := renderUsesExtra? cfg entry + } + | .statement => + { + group? := renderGroupExtra? cfg entry group? + uses? := renderUsesExtra? cfg entry + code? := renderCodeExtra? entry blockData + usedBy? := renderUsedByExtra? cfg entry + markup? := Informal.renderExternalMarkupHeaderExtra? entry.externalMarkup + } private def renderCodePanel (cfg : RenderConfig) @@ -220,6 +224,7 @@ private def renderCodePanel panelSummary.summaryTitle panelSummary.indicator body + (folded := entry.blockData.foldCodeBlock) /-- Render a Blueprint block shell from semantic entry data and rendered content. -/ def renderWithRenderedContent @@ -232,7 +237,7 @@ def renderWithRenderedContent let blockData := entry.blockData let title := entry.heading opts.displayLabelOverride? let codePanel := - if opts.compact then + if opts.compact || entry.facet == .proof then .empty else renderCodePanel cfg title entry content.codeBodies @@ -245,6 +250,7 @@ def renderWithRenderedContent (titleRowAttrs? := cfg.titleRowAttrs? entry) (headerExtras := renderHeaderExtras cfg.relationPanels entry blockData group?) (sourceRefs := entry.sources) + (folded := blockData.foldInformalShell) content := #[content.body] companionPanels := #[codePanel] wrapperClass? := some cfg.wrapperClass diff --git a/src/VersoBlueprint/blueprint-api-types.mjs b/src/VersoBlueprint/blueprint-api-types.mjs index fc2494b6..f1b35304 100644 --- a/src/VersoBlueprint/blueprint-api-types.mjs +++ b/src/VersoBlueprint/blueprint-api-types.mjs @@ -281,6 +281,8 @@ * @property {BlueprintSourceRef[]} [sources] Original source refs for this entry. * @property {BlueprintUseRef[]} statementUses Structured statement dependency refs. * @property {BlueprintUseRef[]} proofUses Structured proof dependency refs. + * @property {boolean} foldProofBlock Whether the canonical proof shell is collapsed for proof entries. + * @property {boolean} foldCodeBlock Whether the associated code panel is collapsed for this canonical traversal entry. * @property {BlueprintRelatedEntry[]} uses Related nodes used by this entry. * @property {BlueprintRelatedEntry[]} usedBy Related nodes that use this entry. */ diff --git a/tests/VersoBlueprintTests.lean b/tests/VersoBlueprintTests.lean deleted file mode 100644 index 6c43662d..00000000 --- a/tests/VersoBlueprintTests.lean +++ /dev/null @@ -1,8 +0,0 @@ -import VersoBlueprintTests.Blueprint -import VersoBlueprintTests.BlueprintMainWrapper -import VersoBlueprintTests.BlueprintTeXCleanup -import VersoBlueprintTests.ExternalDeclRender -import VersoBlueprintTests.RuntimeCache -import VersoBlueprintTests.TestBlueprintRegistry -import VersoBlueprintTests.TestBlueprintRegistryChecks -import VersoBlueprintTests.TestBlueprintRegistryCoverage diff --git a/tests/VersoBlueprintTests/Blueprint.lean b/tests/VersoBlueprintTests/Blueprint.lean deleted file mode 100644 index 594eacde..00000000 --- a/tests/VersoBlueprintTests/Blueprint.lean +++ /dev/null @@ -1,34 +0,0 @@ -/- -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 VersoBlueprintTests.BlueprintAttribute -import VersoBlueprintTests.BlueprintAssets -import VersoBlueprintTests.BlueprintAutoDeps -import VersoBlueprintTests.BlueprintBlockFolding -import VersoBlueprintTests.BlueprintCodeRenderMatrix -import VersoBlueprintTests.BlueprintImportedDuplicates.Direct -import VersoBlueprintTests.BlueprintImportedDuplicates.Transitive -import VersoBlueprintTests.BlueprintExternalHeadingStatus -import VersoBlueprintTests.BlueprintGraft -import VersoBlueprintTests.BlueprintGraph -import VersoBlueprintTests.BlueprintHeaderExtras -import VersoBlueprintTests.BlueprintInformal -import VersoBlueprintTests.BlueprintInlinePrecision -import VersoBlueprintTests.BlueprintLinkHover -import VersoBlueprintTests.BlueprintMainWrapper -import VersoBlueprintTests.BlueprintMathLint -import VersoBlueprintTests.BlueprintMetadataPanel -import VersoBlueprintTests.BlueprintNumbering -import VersoBlueprintTests.BlueprintPreviewSchema -import VersoBlueprintTests.BlueprintPreviewPanels -import VersoBlueprintTests.BlueprintPreviewSource -import VersoBlueprintTests.BlueprintPreviewWiring -import VersoBlueprintTests.BlueprintSource -import VersoBlueprintTests.BlueprintRustCode -import VersoBlueprintTests.BlueprintSummaryLinks -import VersoBlueprintTests.BlueprintSummaryStatus -import VersoBlueprintTests.BlueprintExternalMarkup -import VersoBlueprintTests.BlueprintTexMacros diff --git a/tests/VersoBlueprintTests/BlueprintAssets.lean b/tests/VersoBlueprintTests/BlueprintAssets.lean index 623ceff6..32e56f15 100644 --- a/tests/VersoBlueprintTests/BlueprintAssets.lean +++ b/tests/VersoBlueprintTests/BlueprintAssets.lean @@ -78,7 +78,8 @@ namespace Verso.VersoBlueprintTests.BlueprintAssets [Informal.Commands.blueprintTokensCss, Informal.Commands.previewPanelCss, Informal.Block.Assets.css, Informal.StyleSwitcher.css, Verso.Genre.Manual.docstringStyle, Informal.Commands.previewHeaderCss, Informal.Commands.inlinePreviewCss] && - Informal.Block.Assets.blockAssetBundle.js == [Informal.StyleSwitcher.jsInteractive] + Informal.Block.Assets.blockAssetBundle.js == + [Informal.Macros.blueprintMathJs, Informal.StyleSwitcher.jsInteractive] /-- info: true -/ #guard_msgs in diff --git a/tests/VersoBlueprintTests/BlueprintAttribute.lean b/tests/VersoBlueprintTests/BlueprintAttribute.lean index fc447f2e..436f3dec 100644 --- a/tests/VersoBlueprintTests/BlueprintAttribute.lean +++ b/tests/VersoBlueprintTests/BlueprintAttribute.lean @@ -1,22 +1,43 @@ -/- +/- 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 VersoBlueprintTests.BlueprintAttribute.Reexport +import VersoBlueprintTests.BlueprintAttribute.HybridProvider +import VersoBlueprintTests.BlueprintAttribute.DefaultLabelProvider open Lean open Informal namespace Verso.VersoBlueprintTests.BlueprintAttribute +/- +`uses` is a Manual role, not a Lean `doc.verso` role. Keep the diagnostic +explicit so documentation cannot drift back toward describing it as a flattened +docstring extension. +-/ +/-- +error: `uses : Doc.Elab.RoleExpanderOf UsesConfig` is not registered as a role +-/ +#guard_msgs in +set_option doc.verso true in +/-- +A declaration docstring containing {uses "attr.exported.theorem"}[]. +-/ +@[blueprint "attr.docstring.rejected_uses"] +def rejectedDocstringUsesRole : Nat := 0 + private def importedState : CoreM Informal.Environment.State := do pure <| Informal.Environment.informalExt.getState (← getEnv) private def importedNode? (label : String) : CoreM (Option Informal.Data.Node) := do pure <| (← importedState).data.get? (Name.mkSimple label) +private def importedNodeByName? (label : Name) : CoreM (Option Informal.Data.Node) := do + pure <| (← importedState).data.get? label + private def importedNodeInLocalData (label : String) : CoreM Bool := do pure <| (← importedState).localData.contains (Name.mkSimple label) @@ -67,6 +88,84 @@ private def isBlueprintAttrRef (expectedDecl : Name) (expectedKind : Informal.Da !(← importedNodeInLocalData "attr.exported.inductive") && !(← importedNodeInLocalData "attr.exported.undocumented") +/- Imported module catalogs retain direct attribute ownership and source order. -/ +/-- info: true -/ +#guard_msgs in +#eval + show CoreM Bool from do + let providerLabels ← Informal.Environment.blueprintAttributeLabelsForModule + `VersoBlueprintTests.BlueprintAttribute.Provider + let reexportLabels ← Informal.Environment.blueprintAttributeLabelsForModule + `VersoBlueprintTests.BlueprintAttribute.Reexport + let hybridLabels ← Informal.Environment.blueprintAttributeLabelsForModule + `VersoBlueprintTests.BlueprintAttribute.HybridProvider + let defaultLabelProviderLabels ← Informal.Environment.blueprintAttributeLabelsForModule + `VersoBlueprintTests.BlueprintAttribute.DefaultLabelProvider + pure <| + providerLabels == #[ + Name.mkSimple "attr.exported.theorem", + Name.mkSimple "attr.exported.definition", + Name.mkSimple "attr.exported.inductive", + Name.mkSimple "attr.exported.undocumented" + ] && + hybridLabels == #[ + Name.mkSimple "attr.hybrid.body", + Name.mkSimple "attr.hybrid.verso_docstring", + Name.mkSimple "attr.hybrid.shared", + Name.mkSimple "attr.hybrid.late_docstring", + Name.mkSimple "attr.hybrid.first_body" + ] && + defaultLabelProviderLabels == #[ + Name.mkSimple + "Verso.VersoBlueprintTests.BlueprintAttribute.DefaultLabelProvider.qualifiedDefaultLabel", + Name.mkSimple + "Verso.VersoBlueprintTests.BlueprintAttribute.DefaultLabelProvider.qualifiedDefaultDefinition" + ] && + reexportLabels.isEmpty + +/- Hybrid fixtures exercise persisted bodies, structural Verso docstrings, and many-to-one labels. -/ +/-- info: true -/ +#guard_msgs in +#eval + show CoreM Bool from do + let some bodyNode ← importedNode? "attr.hybrid.body" + | return false + let some sharedNode ← importedNode? "attr.hybrid.shared" + | return false + let some lateDocstringNode ← importedNode? "attr.hybrid.late_docstring" + | return false + let some firstBodyNode ← importedNode? "attr.hybrid.first_body" + | return false + let versoDoc? ← liftM <| findInternalDocString? (← getEnv) + `Verso.VersoBlueprintTests.BlueprintAttribute.HybridProvider.hybridVersoDocstring + let bodyWasPersisted := + match bodyNode.statement with + | some statement => !statement.previewBlocks.isEmpty && statement.elabStx.isEmpty + | none => false + let lateDocstringFilled := + match lateDocstringNode.statement with + | some statement => + statement.hasBody && + statement.dependencyLabels == + #[Name.mkSimple "attr.hybrid.verso_docstring"] + | none => false + pure <| + bodyWasPersisted && + (match versoDoc? with | some (.inr _) => true | _ => false) && + sharedNode.leanDecls == #[ + `Verso.VersoBlueprintTests.BlueprintAttribute.HybridProvider.hybridSharedFirst, + `Verso.VersoBlueprintTests.BlueprintAttribute.HybridProvider.hybridSharedSecond + ] && + lateDocstringFilled && + lateDocstringNode.leanDecls == #[ + `Verso.VersoBlueprintTests.BlueprintAttribute.HybridProvider.hybridLateDocstringFirst, + `Verso.VersoBlueprintTests.BlueprintAttribute.HybridProvider.hybridLateDocstringSecond + ] && + firstBodyNode.leanDecls == #[ + `Verso.VersoBlueprintTests.BlueprintAttribute.HybridProvider.hybridFirstBody, + `Verso.VersoBlueprintTests.BlueprintAttribute.HybridProvider.hybridLaterBody + ] + /-- info: true -/ #guard_msgs in #eval @@ -78,9 +177,15 @@ private def isBlueprintAttrRef (expectedDecl : Name) (expectedKind : Informal.Da let some inductiveNode ← importedNode? "attr.exported.inductive" | return false pure <| - isBlueprintAttrRef `Verso.VersoBlueprintTests.BlueprintAttribute.Provider.exportedTheorem .theorem theoremNode && - isBlueprintAttrRef `Verso.VersoBlueprintTests.BlueprintAttribute.Provider.exportedDefinition .definition definitionNode && - isBlueprintAttrRef `Verso.VersoBlueprintTests.BlueprintAttribute.Provider.exportedInductive .definition inductiveNode + isBlueprintAttrRef + `Verso.VersoBlueprintTests.BlueprintAttribute.Provider.exportedTheorem + .theorem theoremNode && + isBlueprintAttrRef + `Verso.VersoBlueprintTests.BlueprintAttribute.Provider.exportedDefinition + .definition definitionNode && + isBlueprintAttrRef + `Verso.VersoBlueprintTests.BlueprintAttribute.Provider.exportedInductive + .definition inductiveNode /-- Imported statement payloads should keep empty deps and at least one preview source. -/ private def importedStatementExportOk (node : Informal.Data.Node) : Bool := @@ -106,15 +211,28 @@ private def importedStatementExportOk (node : Informal.Data.Node) : Bool := importedStatementExportOk inductiveNode && undocumentedNode.statement.isNone +/- Bare attributes use qualified declaration names and retain explicit dependencies. -/ /-- info: true -/ #guard_msgs in #eval show CoreM Bool from do - let state ← importedState + let decl := + `Verso.VersoBlueprintTests.BlueprintAttribute.DefaultLabelProvider.qualifiedDefaultLabel + let label := Name.mkSimple decl.toString + let some node ← importedNodeByName? label + | return false + let defaultDefinition := + Name.mkSimple + "Verso.VersoBlueprintTests.BlueprintAttribute.DefaultLabelProvider.qualifiedDefaultDefinition" + let some definitionNode ← importedNodeByName? defaultDefinition + | return false pure <| - state.data.contains (Name.mkSimple "attr.exported.theorem") && - state.data.contains (Name.mkSimple "attr.exported.definition") && - state.data.contains (Name.mkSimple "attr.exported.inductive") && - state.data.contains (Name.mkSimple "attr.exported.undocumented") + node.statement.map (·.deps.map (·.label)) == + some #[Name.mkSimple "attr.exported.theorem"] && + isBlueprintAttrRef decl .theorem node && + isBlueprintAttrRef + `Verso.VersoBlueprintTests.BlueprintAttribute.DefaultLabelProvider.qualifiedDefaultDefinition + .definition + definitionNode end Verso.VersoBlueprintTests.BlueprintAttribute diff --git a/tests/VersoBlueprintTests/BlueprintAttribute/DefaultLabelProvider.lean b/tests/VersoBlueprintTests/BlueprintAttribute/DefaultLabelProvider.lean new file mode 100644 index 00000000..feea0c6b --- /dev/null +++ b/tests/VersoBlueprintTests/BlueprintAttribute/DefaultLabelProvider.lean @@ -0,0 +1,38 @@ +/- +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 VersoBlueprintTests.BlueprintAttribute.Provider + +namespace Verso.VersoBlueprintTests.BlueprintAttribute.DefaultLabelProvider + +set_option doc.verso true in +/-- +A *qualified default-label theorem* whose structural external panel preserves +inline mathematics $`3 + 4 = 7`. + +It also preserves display mathematics: +$$`3 + 5 = 8` + +A [structural docstring link](https://example.com/qualified-docstring) remains +clickable. + +> A quoted structural docstring paragraph. + +* First qualified-label list item. +* Second qualified-label list item. + +# Qualified structural subsection + +Subsection content remains part of the imported statement. +-/ +@[blueprint (uses := ["attr.exported.theorem"])] +theorem qualifiedDefaultLabel : True := by + trivial + +@[blueprint] +def qualifiedDefaultDefinition : Nat := 23 + +end Verso.VersoBlueprintTests.BlueprintAttribute.DefaultLabelProvider diff --git a/tests/VersoBlueprintTests/BlueprintAttribute/HybridProvider.lean b/tests/VersoBlueprintTests/BlueprintAttribute/HybridProvider.lean new file mode 100644 index 00000000..85609733 --- /dev/null +++ b/tests/VersoBlueprintTests/BlueprintAttribute/HybridProvider.lean @@ -0,0 +1,69 @@ +/- +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 VersoBlueprint + +open Verso +open Verso.Genre.Manual +open Informal + +namespace Verso.VersoBlueprintTests.BlueprintAttribute.HybridProvider + +@[blueprint "attr.hybrid.body" + (uses := ["attr.hybrid.verso_docstring"]) + (proofUses := ["attr.hybrid.shared"])] +theorem hybridBodyTheorem : True := by + trivial + +#docs (Genre.Manual) hybridBodySourceDoc "Hybrid attribute body source" := +::::::: +:::theorem "attr.hybrid.body" +Hybrid statement body persisted with *structural emphasis*, inline mathematics +$`1 + 1 = 2`, and already elaborated Manual blocks. + +* First persisted Manual list item. +* Second persisted Manual list item. +::: +::::::: + +set_option doc.verso true in +/-- +A *structurally emphasized Verso docstring body* that survives Blueprint +conversion. It keeps the child content of the {name}`Nat.succ` +extension and inline mathematics $`2 + 2 = 4`. + +* First imported docstring list item. +* Second imported docstring list item. +-/ +@[blueprint "attr.hybrid.verso_docstring"] +def hybridVersoDocstring : Nat := 13 + +@[blueprint "attr.hybrid.shared"] +def hybridSharedFirst : Nat := 17 + +@[blueprint "attr.hybrid.shared"] +def hybridSharedSecond : Nat := 19 + +@[blueprint "attr.hybrid.late_docstring" + (uses := ["attr.hybrid.verso_docstring"])] +def hybridLateDocstringFirst : Nat := 23 + +/-- +A later declaration docstring fills a dependency-only statement payload without +discarding the dependencies registered by the first declaration. +-/ +@[blueprint "attr.hybrid.late_docstring"] +def hybridLateDocstringSecond : Nat := 29 + +/-- First repeated-label statement body marker. -/ +@[blueprint "attr.hybrid.first_body"] +def hybridFirstBody : Nat := 31 + +/-- Later repeated-label declaration docstring marker. -/ +@[blueprint "attr.hybrid.first_body"] +def hybridLaterBody : Nat := 37 + +end Verso.VersoBlueprintTests.BlueprintAttribute.HybridProvider diff --git a/tests/VersoBlueprintTests/BlueprintAttributeRendering.lean b/tests/VersoBlueprintTests/BlueprintAttributeRendering.lean new file mode 100644 index 00000000..558569e5 --- /dev/null +++ b/tests/VersoBlueprintTests/BlueprintAttributeRendering.lean @@ -0,0 +1,435 @@ +/- +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 VersoBlueprintTests.BlueprintAttribute.Provider +import VersoBlueprintTests.BlueprintAttribute.Reexport +import VersoBlueprintTests.BlueprintAttribute.HybridProvider +import VersoBlueprintTests.BlueprintAttribute.DefaultLabelProvider +import VersoBlueprintTests.Blueprint.Support + +open Lean +open Informal + +namespace Verso.VersoBlueprintTests.BlueprintAttributeRendering + +open Verso +open Verso.Genre.Manual +open Verso.VersoBlueprintTests.Blueprint.Support + +private def manualImpls : ExtensionImpls := extension_impls% + +#docs (Genre.Manual) placedAttributeDoc "Placed attribute-owned nodes" := +::::::: +Introductory prose before the imported declaration. + +{blueprint_node "attr.exported.theorem"} + +Connecting prose between declarations. + +{blueprint_node "attr.exported.undocumented"} + +Concluding prose after the imported declarations. +::::::: + +#docs (Genre.Manual) includedAttributeModuleDoc "Attribute module inclusion" := +::::::: +{includeBlueprintModule 0 VersoBlueprintTests.BlueprintAttribute.Provider (title := "Imported attribute declarations")} +::::::: + +#docs (Genre.Manual) includedAttributeModuleDefaultDoc "Default attribute module inclusion" := +::::::: +{includeBlueprintModule VersoBlueprintTests.BlueprintAttribute.Provider} +::::::: + +#docs (Genre.Manual) includedHybridAttributeModuleDoc "Hybrid attribute module inclusion" := +::::::: +{includeBlueprintModule VersoBlueprintTests.BlueprintAttribute.HybridProvider} +::::::: + +set_option verso.blueprint.foldCodeBlocks true + +#docs (Genre.Manual) placedDefaultLabelDoc "Placed default-label declaration" := +::::::: +{blueprint_node "Verso.VersoBlueprintTests.BlueprintAttribute.DefaultLabelProvider.qualifiedDefaultLabel"} +::::::: + +#docs (Genre.Manual) includedDefaultLabelModuleDoc "Default-label module inclusion" := +::::::: +{includeBlueprintModule VersoBlueprintTests.BlueprintAttribute.DefaultLabelProvider} +::::::: + +set_option verso.blueprint.foldCodeBlocks false + +set_option verso.blueprint.numbering "global" in +#docs (Genre.Manual) globallyNumberedAttributeModuleDoc "Globally numbered attribute module" := +::::::: +{includeBlueprintModule VersoBlueprintTests.BlueprintAttribute.Provider} +::::::: + +set_option verso.blueprint.numbering "global" in +#docs (Genre.Manual) repeatedAttributePlacementDoc "Repeated attribute placement" := +::::::: +{blueprint_node "attr.exported.theorem"} + +Intervening prose between two placements of the same declaration. + +{blueprint_node "attr.exported.theorem"} +::::::: + +set_option verso.blueprint.numbering "local" in +#docs (Genre.Manual) interleavedLocalAttributePlacementDoc "Interleaved local attribute placement" := +::::::: +:::definition "attr.consumer.before.placement" +A consumer-authored node before an imported attribute placement. +::: + +{blueprint_node "attr.exported.theorem"} + +:::definition "attr.consumer.after.placement" +A consumer-authored node after an imported attribute placement. +::: +::::::: + +set_option verso.blueprint.numbering "local" in +#docs (Genre.Manual) locallyNumberedHybridAttributeModuleDoc "Locally numbered hybrid module" := +::::::: +:::definition "attr.consumer.before.module" +A consumer-authored node that precedes the imported attribute module. +::: + +{includeBlueprintModule VersoBlueprintTests.BlueprintAttribute.HybridProvider} +::::::: + +/-- +error: Blueprint module include: imported module 'VersoBlueprintTests.BlueprintAttribute.Reexport' has no declarations registered with `@[blueprint]` +-/ +#guard_msgs in +#docs (Genre.Manual) rejectedEmptyAttributeModuleDoc "Rejected empty attribute module" := +::::::: +{includeBlueprintModule VersoBlueprintTests.BlueprintAttribute.Reexport} +::::::: + +/-- +error: Blueprint module include: module 'NotImported.BlueprintModule' is not available through this Lean module's imports; add `import NotImported.BlueprintModule` +-/ +#guard_msgs in +#docs (Genre.Manual) rejectedUnimportedAttributeModuleDoc "Rejected unimported attribute module" := +::::::: +{includeBlueprintModule NotImported.BlueprintModule} +::::::: + +/-- +error: Blueprint module include is only available in Manual documents +-/ +#guard_msgs in +#docs (VersoSlides.Slides) rejectedSlidesAttributeModuleDoc "Rejected Slides module include" := +::::::: +{includeBlueprintModule VersoBlueprintTests.BlueprintAttribute.Provider} +::::::: + +private def substringsInOrder (text : String) : List String → Bool + | [] => true + | needle :: rest => + match text.splitOn needle with + | _before :: after => + if after.isEmpty then + false + else + substringsInOrder (String.intercalate needle after) rest + | _ => false + +/- The no-level form creates a child part and derives its title from the module name. -/ +/-- info: true -/ +#guard_msgs in +#eval + show Bool from + includedAttributeModuleDefaultDoc.toPart.subParts.any fun part => + part.titleString == "Provider" && part.content.size == 4 + +/-- info: true -/ +#guard_msgs in +#eval + show IO Bool from do + let (html, state) ← renderManualDocHtmlStringAndState manualImpls placedAttributeDoc + let theoremKey := Informal.PreviewCache.statementKey (Name.mkSimple "attr.exported.theorem") + let undocumentedKey := + Informal.PreviewCache.statementKey (Name.mkSimple "attr.exported.undocumented") + pure <| + hasSubstr html "Introductory prose before the imported declaration." && + hasSubstr html "Connecting prose between declarations." && + hasSubstr html "Concluding prose after the imported declarations." && + hasSubstr html "Exported theorem used to verify" && + hasSubstr html "bp_attribute_node_anchor" && + hasSubstr html "exportedTheorem" && + hasSubstr html "exportedUndocumentedDefinition" && + !hasSubstr html "Blueprint node not found" && + !hasSubstr html "Blueprint node has no cached content" && + (Informal.PreviewManifest.findTraversalBlockEntry? state theoremKey).isSome && + (Informal.PreviewManifest.findTraversalBlockEntry? state undocumentedKey).isSome + +/- A regular imported Lean module can become a source-ordered Verso part. -/ +/-- info: true -/ +#guard_msgs in +#eval + show IO Bool from do + let (html, state) ← renderManualDocHtmlStringAndState manualImpls includedAttributeModuleDoc + let hasIncludedTitle := + includedAttributeModuleDoc.toPart.subParts.any + (·.titleString == "Imported attribute declarations") + let labels := #[ + "attr.exported.theorem", + "attr.exported.definition", + "attr.exported.inductive", + "attr.exported.undocumented" + ] + let hasAnchor := hasSubstr html "bp_attribute_node_anchor" + let ordered := substringsInOrder html [ + "exportedTheorem", + "exportedDefinition", + "exportedInductive", + "exportedUndocumentedDefinition" + ] + let hasEntries := labels.all fun label => + let key := Informal.PreviewCache.statementKey (Name.mkSimple label) + (Informal.PreviewManifest.findTraversalBlockEntry? state key).isSome + pure <| hasIncludedTitle && hasAnchor && ordered && hasEntries + +/- Generated module nodes honor non-default Blueprint numbering options. -/ +/-- info: true -/ +#guard_msgs in +#eval + show IO Bool from do + let (_html, state) ← + renderManualDocHtmlStringAndState manualImpls globallyNumberedAttributeModuleDoc + let theoremLabel := Name.mkSimple "attr.exported.theorem" + let definitionLabel := Name.mkSimple "attr.exported.definition" + let some theoremData := Informal.TraversalIndex.Nodes.data? state theoremLabel + | return false + let some definitionData := Informal.TraversalIndex.Nodes.data? state definitionLabel + | return false + pure <| + theoremData.numberingMode == .global && + theoremData.globalCount == some 1 && + theoremData.count == 1 && + theoremData.displayNumber state == "1" && + definitionData.numberingMode == .global && + definitionData.globalCount == some 2 && + definitionData.count == 2 && + definitionData.displayNumber state == "2" + +/- Repeated placements keep the first number and canonical traversal anchors. -/ +/-- info: true -/ +#guard_msgs in +#eval + show IO Bool from do + let (html, state) ← + renderManualDocHtmlStringAndState manualImpls repeatedAttributePlacementDoc + let label := Name.mkSimple "attr.exported.theorem" + let previewKey := Informal.PreviewCache.statementKey label + let some data := Informal.TraversalIndex.Nodes.data? state label + | return false + let some nodeObject := Informal.TraversalIndex.Nodes.object? state label + | return false + let some previewObject := + Informal.TraversalIndex.TraversalPreviews.object? state previewKey + | return false + pure <| + data.numberingMode == .global && + data.globalCount == some 1 && + data.count == 1 && + Informal.nextGlobalBlockNumber state == 2 && + nodeObject.ids.toArray.size == 1 && + previewObject.ids.toArray.size == 1 && + (html.splitOn "class=\"bp_graft_node bp_graft_manifest_node\"").length == 3 + +/- Generated placements cannot reuse a later authored block's source-local count. -/ +/-- info: true -/ +#guard_msgs in +#eval + show IO Bool from do + let (_html, state) ← + renderManualDocHtmlStringAndState manualImpls interleavedLocalAttributePlacementDoc + let some beforeData := + Informal.TraversalIndex.Nodes.data? state (Name.mkSimple "attr.consumer.before.placement") + | return false + let some attributeData := + Informal.TraversalIndex.Nodes.data? state (Name.mkSimple "attr.exported.theorem") + | return false + let some afterData := + Informal.TraversalIndex.Nodes.data? state (Name.mkSimple "attr.consumer.after.placement") + | return false + pure <| + beforeData.numberingMode == .local && + attributeData.numberingMode == .local && + afterData.numberingMode == .local && + beforeData.count + 1 == attributeData.count && + attributeData.count + 1 == afterData.count && + beforeData.globalCount == some 1 && + attributeData.globalCount == some 2 && + afterData.globalCount == some 3 + +/- Imported placements receive source-local numbers in consumer traversal order. -/ +/-- info: true -/ +#guard_msgs in +#eval + show IO Bool from do + let (_html, state) ← + renderManualDocHtmlStringAndState manualImpls locallyNumberedHybridAttributeModuleDoc + let some consumerData := + Informal.TraversalIndex.Nodes.data? state (Name.mkSimple "attr.consumer.before.module") + | return false + let some bodyData := + Informal.TraversalIndex.Nodes.data? state (Name.mkSimple "attr.hybrid.body") + | return false + let some versoDocstringData := + Informal.TraversalIndex.Nodes.data? state (Name.mkSimple "attr.hybrid.verso_docstring") + | return false + let some sharedData := + Informal.TraversalIndex.Nodes.data? state (Name.mkSimple "attr.hybrid.shared") + | return false + pure <| + consumerData.numberingMode == .local && + bodyData.numberingMode == .local && + consumerData.count + 1 == bodyData.count && + bodyData.count + 1 == versoDocstringData.count && + versoDocstringData.count + 1 == sharedData.count && + consumerData.globalCount == some 1 && + bodyData.globalCount == some 2 && + versoDocstringData.globalCount == some 3 && + sharedData.globalCount == some 4 + +/- Persisted Manual bodies, Verso docstrings, flattened extension children, and + repeated labels share the module path. -/ +/-- info: true -/ +#guard_msgs in +#eval + show IO Bool from do + let (html, state) ← renderManualDocHtmlStringAndState manualImpls includedHybridAttributeModuleDoc + let labels := #[ + "attr.hybrid.body", + "attr.hybrid.verso_docstring", + "attr.hybrid.shared", + "attr.hybrid.late_docstring", + "attr.hybrid.first_body" + ] + let hasEntries := labels.all fun label => + let key := Informal.PreviewCache.statementKey (Name.mkSimple label) + (Informal.PreviewManifest.findTraversalBlockEntry? state key).isSome + let some bodyData := + Informal.TraversalIndex.Nodes.data? state (Name.mkSimple "attr.hybrid.body") + | return false + let firstBodyMarkerCount := + countSubstr html "First repeated-label statement body marker." + let laterBodyMarkerCount := + countSubstr html "Later repeated-label declaration docstring marker." + pure <| + hasSubstr html "structural emphasis" && + hasSubstr html "First persisted Manual list item." && + hasSubstr html "Second persisted Manual list item." && + hasSubstr html "structurally emphasized Verso docstring body" && + countSubstr html "Nat.succ" == 2 && + hasSubstr html "First imported docstring list item." && + hasSubstr html "Second imported docstring list item." && + 3 ≤ (html.splitOn "