rules_markdown
Bazel rules to compose markdown fragments into documents, with a deep-linking aspect that resolves cross-target anchors
| Latest | 0.0.3 |
|---|---|
| Versions | 3 |
| Category | Bazel rules |
| Maintainers | Matt Marshall |
| Registry | https://registry.tbzl.dev/modules/rules_markdown/ |
| Source | github.com/tomato-bazel/rules_markdown |
bazel_dep(name = "rules_markdown", version = "0.0.3")
View source & releases on GitHub ↗
Bazel rules to compose markdown fragments into documents, with a deep-linking aspect that resolves cross-target anchors at build time.
Each section of a doc is a markdown_fragment target; a markdown_document
aggregates them (ordered by weight) into one rendered file — with a generated
table of contents, an optional prose template, and mdref:<handle> deep links
that resolve to the right in-document anchor (and fail the build if they
dangle). Docs assemble from the build graph the same way code does.
rules_readme is a thin façade on top for the dominant case (a templated
README materialized into the source tree).
Install
bazel_dep(name = "rules_markdown", version = "0.0.1")
Usage
load("@rules_markdown//markdown:defs.bzl", "markdown_fragment", "markdown_document")
markdown_fragment(name = "intro", src = "intro.md", title = "Introduction", weight = 0)
markdown_fragment(name = "usage", src = "usage.md", title = "Usage", weight = 10)
markdown_document(
name = "guide",
out = "GUIDE.md",
template = "GUIDE.md.tmpl", # has <!-- TOC --> and <!-- FRAGMENTS -->
fragments = [":intro", ":usage"],
write_to = "GUIDE.md", # bazel run //:guide.write ; bazel test //:guide.write_test
)
A fragment deep-links to another by writing mdref:<handle> in its body, where
<handle> is the target fragment’s anchor (default: its target name):
See [the introduction](https://github.com/tomato-bazel/rules_markdown/blob/main/mdref:intro) for the idea.
md_gen rewrites that to the Introduction section’s final anchor; with
link_check = True (the default) a mdref: to an unknown handle fails the
build.
See examples/basic for a complete, building example.
Status
0.0.1 — composition, ordering, TOC, template splice, deep-link resolution +
dangling-link gate, write_source_files materialization. The renderer is a
parse-free Python tool (host python3); a v0.2 will move to a hermetic
CommonMark renderer (comrak) + Bazel-checked deep-link edges. See the
CHANGELOG.
Usage#
Real usage, taken from the module’s examples/.
examples/basic/BUILD.bazel
load("//markdown:defs.bzl", "markdown_document", "markdown_fragment")
# Two composable sections. `usage` deep-links into `intro` via mdref:intro.
markdown_fragment(
name = "intro",
src = "intro.md",
title = "Introduction",
weight = 0,
)
markdown_fragment(
name = "usage",
src = "usage.md",
title = "Usage",
weight = 10,
)
# Aggregate into GUIDE.md with a TOC + a prose template; resolve + check the
# deep link; materialize into the tree (GUIDE.md) with a drift gate.
markdown_document(
name = "guide",
out = "GUIDE.md",
template = "README.md.tmpl",
fragments = [
":intro",
":usage",
],
link_check = True,
toc = True,
write_to = "GUIDE.md",
)
examples/slots/BUILD.bazel
load("//markdown:defs.bzl", "markdown_document", "markdown_fragment")
# Two fragments targeting two named template slots — generated content placed at
# specific spots (the gitlab-profile two-tables-at-two-locations pattern).
markdown_fragment(
name = "a",
content = "Content routed to the **alpha** slot.",
slot = "alpha",
)
markdown_fragment(
name = "b",
content = "Content routed to the **beta** slot.",
slot = "beta",
)
markdown_document(
name = "doc",
out = "SLOTS.md",
template = "tmpl.md",
fragments = [
":a",
":b",
],
link_check = False,
toc = False,
write_to = "SLOTS.md",
)Rules & providers#
Generated with Stardoc from the module's .bzl sources.
from docs/defs.md
Public API for rules_markdown.
load("@rules_markdown//markdown:defs.bzl", "markdown_fragment", "markdown_document")
markdown_fragment
load("@rules_markdown//markdown:defs.bzl", "markdown_fragment")
markdown_fragment(name, deps, src, anchor, classifiers, content, level, slot, title, weight)
Declare one composable markdown fragment (a section of a document).
ATTRIBUTES
| Name | Description | Type | Mandatory | Default |
|---|---|---|---|---|
| name | A unique name for this target. | Name | required | |
| deps | Child fragments folded in transitively. | List of labels | optional | [] |
| src | The fragment body (.md). Mutually exclusive with content. | Label | optional | None |
| anchor | Explicit deep-link handle (defaults to the target name). Reference it elsewhere as mdref:<handle>. | String | optional | "" |
| classifiers | Free-form classifiers (e.g. category/section); exposed in fragment metadata. | List of strings | optional | [] |
| content | Inline fragment body. Mutually exclusive with src. | String | optional | "" |
| level | Heading level for title (default 2 -> ##). | Integer | optional | 2 |
| slot | Target a named template placeholder <!-- FRAGMENTS:<slot> --> (for generated content placed at a specific spot). Default: the unnamed <!-- FRAGMENTS -->. | String | optional | "" |
| title | Section heading text. If set, md_gen injects a level heading; it’s also the deep-link target. | String | optional | "" |
| weight | Sort key within the document (ascending). | Integer | optional | 0 |
MarkdownDocInfo
load("@rules_markdown//markdown:defs.bzl", "MarkdownDocInfo")
MarkdownDocInfo(md, anchor_index, fragments)
A rendered markdown document — exposed so a doc can be embedded, link-checked, or published without re-rendering.
FIELDS
| Name | Description |
|---|---|
| md | File: the rendered document. |
| anchor_index | File: JSON map of deep-link handle -> final in-doc anchor slug. |
| fragments | depset of the fragment structs that composed it. |
MarkdownFragmentInfo
load("@rules_markdown//markdown:defs.bzl", "MarkdownFragmentInfo")
MarkdownFragmentInfo(fragments)
A markdown fragment + its metadata, gathered transitively by markdown_link_aspect. Each contribution is a hashable struct so it can ride a depset.
FIELDS
| Name | Description |
|---|---|
| fragments | depset of struct(frag_id, md_file, title, level, weight, handle, tags). |
markdown_document
load("@rules_markdown//markdown:defs.bzl", "markdown_document")
markdown_document(name, out, template, fragments, roots, toc, link_check, write_to, **kwargs)
Aggregate fragments into one rendered markdown document.
PARAMETERS
markdown_link_aspect
load("@rules_markdown//markdown:defs.bzl", "markdown_link_aspect")
markdown_link_aspect()
Walk deps/fragments/roots edges collecting markdown fragment contributions.
ASPECT ATTRIBUTES
| Name | Type |
|---|---|
| deps | String |
| fragments | String |
| roots | String |
ATTRIBUTES
Conformance#
No gate findings. 15 contested atoms. See how gating works or the full report.
Contested atoms
Third-party modules where this module resolves a different version than others do. Not a violation of anything this module did — it is the actionable form of a registry-level convergence finding, and the sentence a maintainer can act on.
| Atom | Resolved here | Elsewhere |
|---|---|---|
apple_support | 1.24.2 | 2.2.0 ×1 |
aspect_bazel_lib | 2.22.5 | 2.8.1 ×1 |
bazel_lib | 3.0.0 | 3.2.2 ×17 |
bazel_skylib | 1.8.2 | 1.9.0 ×2 |
gawk | 5.3.2.bcr.1 | 5.3.2.bcr.3 ×17 |
jq.bzl | 0.1.0 | 0.4.0 ×17 |
nlohmann_json | 3.6.1 | 3.12.0.bcr.1 ×1 |
package_metadata | 0.0.2 | 0.0.5 ×3 |
protobuf | 33.4 | 34.0.bcr.1 ×2 |
rules_jvm_external | 6.7 | 6.8 ×4 |
rules_python | 1.7.0 | 2.0.1 ×1 |
rules_swift | 3.1.2 | 3.6.1 ×1 |
tar.bzl | 0.5.1 | 0.10.4 ×170.6.0 ×1 |
upb | 0.0.0-20220923-a547704 | 0.0.0-20230516-61a97ef ×1 |
yq.bzl | 0.1.1 | 0.3.4 ×17 |
Dependencies#
Depends on
Used by (1 in the registry)
Versions#
3 published versions, newest first. Each resolves to an immutable, integrity-checked archive.
| Version | Integrity (sha256) | Source archive |
|---|---|---|
0.0.3 latest | 6e4y8u24DX4TMk85… | tag archive ↗ |
0.0.2 | /ubOv4KhuqaVfNY4… | tag archive ↗ |
0.0.1 | PmH2LokNjMSzad8u… | tag archive ↗ |
Changelog#
All notable changes to rules_markdown.
0.0.3
Additive — no migration needed.
Added
- Named template slots. A
markdown_fragment(slot = "x")routes to a<!-- FRAGMENTS:x -->placeholder, so generated content can be placed at specific spots in a template (e.g. two tables at two locations) instead of all concatenated at the single<!-- FRAGMENTS -->. The default (unnamed) slot is unchanged; a slot with fragments but no matching placeholder is a build error.
0.0.2
Additive — no migration needed.
Added
- Stardoc reference docs (
//docs,bazel run //docs:update) + abzl_libraryfor the public API surface.
0.0.1
Initial release.
Added
markdown_fragment— declare one composable markdown section (a body viasrcor inlinecontent, an optionaltitleheading injected atlevel, a deep-linkhandleviaanchor, anddepson child fragments).markdown_document— aggregate fragments (ordered byweight) into one rendered document: heading injection, generated TOC, optional prosetemplate(<!-- FRAGMENTS -->/<!-- TOC -->), andmdref:<handle>cross-fragment deep-link resolution with a dangling-link gate (link_check).write_toadds a<name>.writematerialization target +<name>.write_testdrift gate viawrite_source_files.markdown_link_aspect— collect fragments across the build graph from amarkdown_document’sroots.- Providers
MarkdownFragmentInfo/MarkdownDocInfo(the latter exposes the rendered doc + ananchor_indexof handle -> final slug).
Notes
- The v0.1 renderer (
markdown/private/md_gen.py) is parse-free and runs viarun_shell(hostpython3). A v0.2 will swap in a hermetic CommonMark renderer (comrak) for full GFM parity + body-heading verification, and add Bazel-checked deep-link edges + sub-heading (export_anchors) deep links.