Documentation
¶
Overview ¶
Package markdown turns markdown into terminal rows — including markdown that has not finished arriving.
It is a module of its own, and the reason is a dependency. Rendering markdown needs a parser, and a parser is a tree of somebody else's code; the two modules this is built on promise a dependency list that a terminal library can be adopted for. So the parser lives here, behind a boundary, and nothing above or beside this module hears about it.
What it is for ¶
The commonest thing a streaming interface does is show an answer as it arrives. That is not what a markdown renderer normally does: every one of them takes a finished document and gives back a finished rendering, and a program showing a model's answer has neither. Stream is the difference — it is handed whatever has arrived, hands back the blocks that are certainly finished, and re-renders the one still being written on every keystroke of it.
Finished blocks are finished for good, which is what makes this cheap: a paragraph that has been published is never parsed again, however long the answer becomes.
What it produces ¶
[Block]s, not a string and not cells. A block owns the styled source and the layout rule that gives it physical rows at a width. Keeping those together is what lets prose wrap, rules stretch and tables reflow without turning any of them into cells before the final region is known. Doc composes the blocks; a caller with its own layout can measure and draw them directly.
What it does not do ¶
It does not highlight code or typeset mathematics. Those concerns bring their own parsers, dependencies and policies. Look.SetRenderer is the one seam where recognized semantic blocks receive such a renderer; without one their source stays readable. The Markdown parser and its AST never cross that seam.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Block ¶
type Block struct {
// contains filtered or unexported fields
}
Block is one immutable piece of a rendered document.
A block keeps source and layout semantics together until its final width is known. That distinction matters for more than wrapping: a thematic break stretches, and a table allocates columns or becomes records when columns stop being readable. Exposing pre-laid-out lines would make those decisions too early.
Blocks come from Render and Stream. Their zero value is empty. They can be retained, copied, measured and drawn independently; Doc is the convenient way to compose them with their inter-block spacing.
func Render ¶
Render turns a whole markdown document into blocks.
It is the form for text that has finished arriving. Anything still being written wants a Stream, which is this applied to the part that is certainly finished and again to the part that is not.
func (Block) BlankBefore ¶ added in v0.7.0
BlankBefore reports whether the renderer asked for a blank row before this block. It is false for the first block of a document and between the items of a tight list, and true where separate prose blocks need to read as separate things.
func (Block) Draw ¶ added in v0.7.0
Draw writes the block into v. It excludes any blank row before the block, because only the composer knows whether a preceding block exists.
func (Block) Measure ¶ added in v0.7.0
Measure reports how many rows the block needs at width. It excludes the blank row that may separate this block from the one before it; Block.BlankBefore exposes that relationship to custom composers.
type Doc ¶
type Doc struct {
// contains filtered or unexported fields
}
Doc is a rendered document, ready to be measured and drawn.
It draws into a grid view and is a github.com/Tangerg/oolong/core/layout.Measurer, which is what lets it go into a slot, container or viewport belonging to a package this one has never heard of. Copies detach block and row storage before either can be changed.
func (*Doc) Append ¶
Append adds blocks to the end, which is what a stream does as they are finished.
func (*Doc) Blocks ¶
Blocks returns the immutable rendered blocks in document order. The returned slice is independent; the blocks themselves are values safe to share.
func (*Doc) Rows ¶ added in v0.1.0
Rows returns the meaningful text of each drawn row for selection and search.
Markers and rails are decoration and stay out of Text. Offset carries the content indent separately, so a selection addresses the same columns Draw used without copying a bullet or quotation bar. The result uses core text vocabulary and does not make markdown depend on a component package.
type Extension ¶ added in v0.10.0
type Extension uint8
Extension identifies a semantic block recognized by Markdown. It is not a parser plug-in API: syntax and parse correctness remain this module's responsibility; rendering the resulting domain content is the replaceable part.
const ( // FencedCode is a backtick or tilde fence. The renderer's info is the language // written after the fence. FencedCode Extension // DisplayMath is a $$ block or a fenced block whose language is math. DisplayMath )
type Glyphs ¶
type Glyphs struct {
// Bullet marks an item of an unordered list.
Bullet string
// Bar is what a quotation is barred with, on every row of it.
Bar string
// Divider is what a thematic break and a table's heading rule are drawn with.
Divider string
// Checked and Unchecked mark a task list's items.
Checked, Unchecked string
}
Glyphs are the characters a document's furniture is drawn with.
The zero value draws none of it, which is legible and plain: a list is still indented, a quotation is still inset, and a rule is still a row of its own. That is the right answer for a terminal that cannot draw the characters, and the caller is the one who knows whether it can.
type Look ¶
type Look struct {
// Text is body text.
Text grid.Style
// Headings are the levels, from one: the first entry is a level-one heading, and a
// level deeper than the list gets the last entry.
//
// One field rather than a style for headings and a list that overrides it. Two
// ways to say one thing is a thing to be inconsistent about, and "every heading
// alike" is already sayable — it is a list of one.
Headings []grid.Style
// Strong and Emphasis are bold and italic, or whatever a look prefers them to be.
Strong grid.Style
Emphasis grid.Style
// Struck is text somebody crossed out.
Struck grid.Style
// Code is a span of code in a sentence, and Block is a block of it.
Code grid.Style
Block grid.Style
// Link is the text of a link. The words carry the address themselves — see
// [github.com/Tangerg/oolong/core/text.Span.Link] — so a terminal that shows
// hyperlinks opens what was written.
Link grid.Style
// Target is the address written out after the words, for output going somewhere
// that cannot show a hyperlink. A look with no style for it does not write one,
// which is the shorter reading and the one worth having on a terminal.
Target grid.Style
// Quote is quoted text and Rail the bar beside it.
Quote grid.Style
Rail grid.Style
// Marker is a bullet or a number, and Rule a thematic break.
Marker grid.Style
Rule grid.Style
// Glyphs are the characters the furniture is drawn with, kept apart from the
// styles for the reason the two are different questions: which grey a quotation
// is drawn in is taste, and whether the terminal can draw the bar beside it is a
// fact about the terminal. It is the same division the kit package makes between
// a theme and a glyph set, and a caller with one of those builds this from it.
Glyphs Glyphs
// contains filtered or unexported fields
}
Look is how a document is drawn: a style for every part of one, and the characters its furniture is made of.
The zero Look draws everything in the terminal's own appearance, which is legible and says nothing. A caller with a palette builds one from it — which is deliberately not done here, because this module cannot see the palette without depending on the package that has one, and that is the dependency this whole boundary exists to avoid.
func (*Look) SetRenderer ¶ added in v0.10.0
SetRenderer sets the sole renderer for extension. A nil renderer removes it.
The zero Look is ready. The private registry is copied before mutation, so changing a copied Look does not silently change the value it was copied from.
type Renderer ¶ added in v0.10.0
Renderer turns one extension body into logical styled lines. Markdown retains the layout semantics: fenced code wraps like code already did, while display mathematics clips rather than reflowing its two-dimensional arrangement. Returning nil asks Markdown to show the source in Look.Block; returning a non-nil empty slice intentionally draws no rows.
type Stream ¶
type Stream struct {
// contains filtered or unexported fields
}
Stream renders markdown that is still arriving.
It is the whole reason this module exists rather than a call to somebody's renderer. A markdown parser takes a document; a program showing a model's answer has a prefix of one, growing a few words at a time, and re-rendering the whole of it on every chunk is quadratic in the length of the answer — which is exactly the case where answers are long.
So a stream splits what has arrived into the part that is certainly finished and the part that is not. Stream.Feed hands back blocks for the first, once, and never looks at that text again; Stream.Open renders the second, which is short by construction and is re-rendered as often as anybody likes.
for chunk := range answer {
doc.Append(stream.Feed(chunk)...)
live = stream.Open()
}
doc.Append(stream.Flush()...)
Where it cuts ¶
At a blank line, once a line has arrived after it that does not begin with a space — and never inside fenced code or display mathematics. That is what "certainly finished" can be made of without a parser that can be asked what it is in the middle of: a blank line ends every block markdown has, except that a list or an indented block of code carries on across one when what follows it is indented.
The cost of the rule is stated rather than hidden. A list with blank lines between its items is published in pieces, and reads the same. A link written as a reference — the address on a line of its own further down — is published before its address arrives, and comes out as the words without the link. Both are the price of showing an answer as it is written instead of after it is finished.
A Stream must not be copied after first use, like strings.Builder. It is owned by the goroutine feeding and rendering one source and is not safe for concurrent use. Its zero value is ready.
Example ¶
package main
import (
"fmt"
"github.com/Tangerg/oolong/markdown"
)
func main() {
// An answer arriving a few words at a time. What is certainly finished comes back
// from Feed and is never looked at again; what is still being written is
// re-rendered as often as anybody asks.
var stream markdown.Stream
stream.SetLook(markdown.Look{Glyphs: markdown.Glyphs{Bullet: "•"}})
var doc markdown.Doc
for _, chunk := range []string{"Two things:\n\n- th", "e first\n- the second\n\nAnd a l", "ast word."} {
doc.Append(stream.Feed(chunk)...)
fmt.Printf("published %d, open %d\n", doc.Len(), len(stream.Open()))
}
doc.Append(stream.Flush()...)
fmt.Printf("published %d, %d rows at 20 columns\n", doc.Len(), doc.Measure(20))
}
Output: published 0, open 2 published 1, open 3 published 1, open 3 published 4, 6 rows at 20 columns
func (*Stream) Feed ¶
Feed takes another piece of the answer and returns the blocks it finished.
Nothing is lost by a chunk that finishes nothing: what it added is held, and the blocks it eventually becomes are returned by a later call or by Stream.Flush.
It follows the ordinary streaming-decoder shape: hand over the next piece, take back what is now decidable, and let Flush settle what only the end can. It is deliberately not Write — this is not an io.Writer, and something wired to a command's output is written to from a goroutine that may not touch what is on screen.
func (*Stream) Open ¶
Open is what is still being written, rendered.
It is a rendering of a prefix, so it says what the text says so far: a heading halfway through its own words is a heading, and a fenced block of code with no closing fence yet is a block of code. That is what a reader sees while an answer is written, and it is what they would see if it stopped there.
func (*Stream) Reset ¶
func (s *Stream) Reset()
Reset forgets everything, for a stream about to be given a different answer.
func (*Stream) SetLook ¶ added in v0.2.0
SetLook changes how unpublished text is rendered. Blocks already returned by Feed cannot change; the open tail and everything published afterwards use look. Stream owns the heading styles so later caller mutation cannot silently change its rendering without invalidating the open cache.