markdown

package module
v0.16.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 25, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

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

func Render(source string, look Look) []Block

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

func (b Block) BlankBefore() bool

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

func (b Block) Draw(v grid.View)

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

func (b Block) Measure(width int) int

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.

func (Block) Rows added in v0.7.0

func (b Block) Rows(width int) []text.Row

Rows returns the meaningful text and offsets of the block's physical rows at width. Markers and quotation rails are decoration and are not included in Text.

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

func (d *Doc) Append(blocks ...Block)

Append adds blocks to the end, which is what a stream does as they are finished.

func (*Doc) Blocks

func (d *Doc) Blocks() []Block

Blocks returns the immutable rendered blocks in document order. The returned slice is independent; the blocks themselves are values safe to share.

func (*Doc) Draw

func (d *Doc) Draw(v grid.View)

Draw writes the document, one wrapped row per row of v.

func (*Doc) Len added in v0.2.0

func (d *Doc) Len() int

Len reports how many rendered blocks the document owns.

func (*Doc) Measure

func (d *Doc) Measure(width int) int

Measure is how many rows the document needs at this width.

func (*Doc) Rows added in v0.1.0

func (d *Doc) Rows(width int) []text.Row

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.

func (*Doc) SetBlocks

func (d *Doc) SetBlocks(blocks []Block)

SetBlocks replaces the document. Blocks are immutable values; Doc copies the slice so the caller may reuse it after this returns.

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

func (l *Look) SetRenderer(extension Extension, renderer Renderer)

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

type Renderer func(info, source string) []text.Line

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

func (s *Stream) Feed(chunk string) []Block

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) Flush

func (s *Stream) Flush() []Block

Flush publishes whatever is left, which is what the end of an answer is.

func (*Stream) Look

func (s *Stream) Look() Look

Look returns a snapshot of the stream's current appearance.

func (*Stream) Open

func (s *Stream) Open() []Block

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

func (s *Stream) SetLook(look Look)

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL