Extensions#
Create a block when the report methods do not support the content you need.
Create a block#
A block has a __report__ method. The method returns Markdown text or Markdown tokens.
from dataclasses import dataclass
from mdreport import BlockContent, MarkdownReport
@dataclass(frozen=True)
class Aside:
content: str
def __report__(self, report: MarkdownReport) -> BlockContent:
return f"> {self.content}"
report = MarkdownReport().title("Findings")
report.append(Aside("Numbers are provisional."))
You do not need a base class or a registration step. Do not change the report inside __report__.
Add a block#
report.append(block)
report += block
new_report = report + block
append and += change the report. The + operator returns a changed copy.
Return tokens#
A report holds its content as a Markdown syntax tree, not as text. The string you return from
__report__ is parsed into tokens before it joins that tree, so both routes end in the same place.
Return tokens to skip the parse and build the tree yourself.
The token builders assemble the pieces. This block pairs a bold caption with a fenced query:
from dataclasses import dataclass
from markdown_it.token import Token
from mdreport import MarkdownReport, bold_paragraph_tokens, fence_token
@dataclass(frozen=True)
class Query:
sql: str
title: str
def __report__(self, report: MarkdownReport) -> list[Token]:
tokens = bold_paragraph_tokens(report.parser, self.title)
tokens.append(fence_token(self.sql, "sql"))
return tokens
report.append(Query("select count(*) from orders", "Order volume"))
**Order volume**
```sql
select count(*) from orders
```
Three reasons to build tokens instead of a string:
The builders get the format’s edge cases right.
fence_tokenwidens the fence when the code contains backticks.table_tokensescapes the pipes and turns the newlines in a DataFrame cell into<br>. Written as a string, both corrupt the document instead.raw_tokenbypasses Markdown. Its content reaches the file untouched. No string can do this, because a string is always parsed.A
texttoken stays literal. The builders that takereport.parserparse their argument as inline Markdown, so a caption ofTop _accounts_comes out as emphasis. Put the value in aToken("text", "", 0, content=value)when it has to survive as data, and the renderer escapes it on the way out.
A Markdown string is the better choice when you write the whole block yourself, as Aside does
above. Reach for tokens when your data decides the content.
The API reference lists all token builders.
Read the finished document#
There are cases where you need to wait till the report is fully constructed before you can determine a block’s content. But __report__ runs the moment you append the block, so it cannot see the rest of the report.
So if you want to wait until the report is fully constructed before determining a block’s content, give your block a __resolve__ method instead. This is useful when your block summarizes, counts, or links to other content.
When you append a deferred block, it stores a placeholder. The report calls __resolve__ during render and puts the returned content where the placeholder sits.
from dataclasses import dataclass
from markdown_it.tree import SyntaxTreeNode
from mdreport import BlockContent, MarkdownReport
@dataclass(frozen=True)
class HeadingCount:
def __resolve__(self, document: SyntaxTreeNode, report: MarkdownReport) -> BlockContent:
headings = sum(1 for node in document.walk() if node.type == "heading")
return f"This report has {headings} headings."
report = MarkdownReport().append(HeadingCount())
report.title("Findings").heading("Revenue")
print(report.render())
The count is 2, even though the block went in before either heading:
This report has 2 headings.
# Findings
## Revenue
document is the report parsed into a markdown-it-py
syntax tree. Walk it to find the nodes you care about. report gives you the parser, the same as
in __report__.
Two rules follow from resolving at render time:
A deferred block never sees another deferred block’s output, so the order you append them in does not change what each one reads.
renderresolves the block again every time you call it, so a report that grows between renders produces an up-to-date block each time.
TableOfContents works this way. It is why you place the contents at the top and still list the
headings you add later.