Skip to content
quarry Artifact [3b782540ba]
Login

Artifact [3b782540ba]

Artifact 3b782540bacb476d1d77d1386d3c0bd7621698763e450b33a4046bec2c0e7490:


package markup

import (
	"regexp"
	"strings"
	"unicode/utf8"

	gast "github.com/yuin/goldmark/ast"
	east "github.com/yuin/goldmark/extension/ast"
	"github.com/yuin/goldmark/parser"
	"github.com/yuin/goldmark/text"
)

// The rendering hooks the stylesheet keys on: task-list state, table
// column hints and GitHub-style alerts. All of them are computed here,
// at render time, and reach the page as classes the sanitizer admits by
// exact name.

// taskLists marks each task item with its state and a list made only of
// task items with md-tasks, so the stylesheet can drop the bullet that
// would otherwise sit beside the checkbox. The TaskList extension emits
// the checkbox but no class on either.
type taskLists struct{}

func (taskLists) Transform(doc *gast.Document, _ text.Reader, _ parser.Context) {
	gast.Walk(doc, func(n gast.Node, entering bool) (gast.WalkStatus, error) {
		list, ok := n.(*gast.List)
		if !ok || !entering {
			return gast.WalkContinue, nil
		}
		all := list.ChildCount() > 0
		for li := list.FirstChild(); li != nil; li = li.NextSibling() {
			box := taskBox(li)
			if box == nil {
				all = false
				continue
			}
			state := "unchecked"
			if box.IsChecked {
				state = "checked"
			}
			li.SetAttributeString("class", []byte(state))
		}
		if all {
			list.SetAttributeString("class", []byte("md-tasks"))
		}
		return gast.WalkContinue, nil
	})
}

// taskBox is the checkbox opening a list item, or nil.
func taskBox(li gast.Node) *east.TaskCheckBox {
	if fc := li.FirstChild(); fc != nil {
		if b, ok := fc.FirstChild().(*east.TaskCheckBox); ok {
			return b
		}
	}
	return nil
}

const shortCellMax = 24

var numCell = regexp.MustCompile(`^[+-]?[0-9.,]*[0-9][0-9.,]*%?$`)

// tableHints classes every cell of a column md-short when all its cells
// (header included) are at most shortCellMax characters, and md-num when
// every body cell is a number. Done here rather than in script so the
// first paint is already right.
type tableHints struct{}

func (tableHints) Transform(doc *gast.Document, reader text.Reader, _ parser.Context) {
	src := reader.Source()
	gast.Walk(doc, func(n gast.Node, entering bool) (gast.WalkStatus, error) {
		if t, ok := n.(*east.Table); ok && entering {
			hintTable(t, src)
			return gast.WalkSkipChildren, nil
		}
		return gast.WalkContinue, nil
	})
}

func hintTable(t *east.Table, src []byte) {
	var rows [][]gast.Node
	header := 0
	for r := t.FirstChild(); r != nil; r = r.NextSibling() {
		var cells []gast.Node
		for c := r.FirstChild(); c != nil; c = c.NextSibling() {
			cells = append(cells, c)
		}
		if _, ok := r.(*east.TableHeader); ok {
			header++
		}
		rows = append(rows, cells)
	}
	cols := 0
	for _, r := range rows {
		cols = max(cols, len(r))
	}
	for col := 0; col < cols; col++ {
		short, num, body := true, true, 0
		for i, r := range rows {
			if col >= len(r) {
				continue
			}
			s := strings.TrimSpace(cellText(r[col], src))
			if utf8.RuneCountInString(s) > shortCellMax {
				short = false
			}
			if i >= header {
				body++
				if !numCell.MatchString(s) {
					num = false
				}
			}
		}
		var cls []string
		if short {
			cls = append(cls, "md-short")
		}
		if num && body > 0 {
			cls = append(cls, "md-num")
		}
		if len(cls) == 0 {
			continue
		}
		v := []byte(strings.Join(cls, " "))
		for _, r := range rows {
			if col < len(r) {
				r[col].SetAttributeString("class", v)
			}
		}
	}
}

// cellText is the visible text of a cell: text runs and code spans.
func cellText(n gast.Node, src []byte) string {
	var b strings.Builder
	gast.Walk(n, func(c gast.Node, entering bool) (gast.WalkStatus, error) {
		if !entering {
			return gast.WalkContinue, nil
		}
		switch v := c.(type) {
		case *gast.Text:
			b.Write(v.Segment.Value(src))
		case *gast.String:
			b.Write(v.Value)
		}
		return gast.WalkContinue, nil
	})
	return b.String()
}