You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
bonzai/z/mark.go

370 lines
8.8 KiB
Go

package Z
import (
"fmt"
"regexp"
"strconv"
"github.com/rwxrob/scan"
"github.com/rwxrob/term"
"github.com/rwxrob/to"
)
// IndentBy is the number of spaces to indent in Indent. Default is 7.
// Bonzai command tree creator can change this for every composite
// command imported their application in this one place.
var IndentBy = 7
// Columns is the number of bytes (not runes) at which Wrap will wrap.
// By default detects the terminal width (if possible) otherwise keeps
// 80 standard. Bonzai command tree creator can change this for every
// composite command imported their application in this one place.
var Columns = int(term.WinSize.Col)
// Lines returns the string converted into a slice of lines.
func Lines(in string) []string { return to.Lines(in) }
const (
Paragraph = iota + 1
Numbered
Bulleted
Verbatim
)
type Block struct {
T int
V []byte
}
// String fulfills the fmt.Stringer interface.
func (s *Block) String() string { return string(s.V) }
// Blocks strips preceding and trailing white space and then checks the
// first line for indentation (spaces or tabs) and strips that exact
// indentation string from every line. It then breaks up the input into
// blocks separated by one or more empty lines and applies basic
// formatting to each as follows:
//
// If is one of the following leave alone with no wrapping:
//
// * Bulleted List - beginning with *
// * Numbered List - beginning with 1.
// * Verbatim - beginning with four spaces
//
// Everything else is considered a "paragraph" and will be unwrapped
// into a single long line (which is normally wrapped later).
//
// For now, these blocks are added as is, but plans are to eventually
// add support for short and long lists much like CommonMark.
//
// Note that because of the nature of Verbatim's block's initial (4
// space) token Verbatim blocks must never be first since the entire
// input buffer is first dedented and the spaces would grouped with the
// indentation to be stripped. This is never a problem, however,
// because Verbatim blocks never make sense as the first block in
// a BonzaiMark document. This simplicity and clarity of 4-space tokens
// far outweighs the advantages of alternatives (such as fences).
func Blocks(in string) []*Block {
var blocks []*Block
verbpre := regexp.MustCompile(` {4,}`)
s := scan.R{Buf: []byte(to.Dedented(in))}
MAIN:
for s.Scan() {
if s.Rune == '*' { // bulleted list
if !s.Peek(" ") {
goto PARA
}
m := s.Pos - 1
for s.Scan() {
if s.Peek("\n\n") {
blocks = append(blocks, &Block{Bulleted, s.Buf[m:s.Pos]})
s.Pos += 2
continue MAIN
}
}
}
if s.Rune == '1' { // numbered list
if !s.Peek(". ") {
goto PARA
}
m := s.Pos - 1
for s.Scan() {
if s.Peek("\n\n") {
blocks = append(blocks, &Block{Numbered, s.Buf[m:s.Pos]})
s.Pos += 2
continue MAIN
}
}
}
if s.Rune == ' ' { // verbatim
s.Pos -= 1
ln := s.Match(verbpre)
s.Pos++
if ln < 0 {
continue
}
pre := s.Buf[s.Pos-1 : s.Pos+ln-1]
s.Pos += len(pre) - 1
var block []byte
for s.Scan() {
if s.Rune == '\n' {
// add in indented lines
if s.Peek(string(pre)) {
block = append(block, '\n')
s.Pos += len(pre)
continue
}
// end of the block
blocks = append(blocks, &Block{Verbatim, block})
continue MAIN
}
block = append(block, []byte(string(s.Rune))...)
}
}
if s.Rune == '\n' || s.Rune == '\r' || s.Rune == '\t' {
continue
}
PARA:
{
var block []byte
block = append(block, []byte(string(s.Rune))...)
for s.Scan() {
if s.Peek("\n\n") {
block = append(block, []byte(string(s.Rune))...)
blocks = append(blocks, &Block{Paragraph, block})
s.Scan()
s.Scan()
continue MAIN
}
if s.Rune == '\n' || s.Rune == '\r' {
block = append(block, ' ')
continue
}
block = append(block, []byte(string(s.Rune))...)
}
if len(block) > 0 {
blocks = append(blocks, &Block{Paragraph, block})
}
} // PARA
}
return blocks
}
// Emph renders BonzaiMark emphasis spans specifically for
// VT100-compatible terminals (which almost all are today):
//
// *Italic*
// **Bold**
// ***BoldItalic***
// <under> (keeping brackets)
//
// See Mark for block formatting and term for terminal rendering.
func Emph[T string | []byte | []rune](buf T) string {
var nbuf []rune
s := scan.R{Buf: []byte(string(buf))}
for s.Scan() {
// <under>
if s.Rune == '<' {
nbuf = append(nbuf, '<')
nbuf = append(nbuf, []rune(term.Under)...)
for s.Scan() {
if s.Rune == '>' {
nbuf = append(nbuf, []rune(term.Reset)...)
nbuf = append(nbuf, '>')
break
}
nbuf = append(nbuf, s.Rune)
}
continue
}
// ***BoldItalic***
if s.Rune == '*' && s.Peek("**") {
s.Pos += 2
nbuf = append(nbuf, []rune(term.BoldItalic)...)
for s.Scan() {
if s.Rune == '*' && s.Peek("**") {
s.Pos += 2
nbuf = append(nbuf, []rune(term.Reset)...)
break
}
nbuf = append(nbuf, s.Rune)
}
continue
}
// **Bold**
if s.Rune == '*' && s.Peek("*") {
s.Pos++
nbuf = append(nbuf, []rune(term.Bold)...)
for s.Scan() {
if s.Rune == '*' && s.Peek("*") {
s.Pos++
nbuf = append(nbuf, []rune(term.Reset)...)
break
}
nbuf = append(nbuf, s.Rune)
}
continue
}
// *Italic*
if s.Rune == '*' {
nbuf = append(nbuf, []rune(term.Italic)...)
for s.Scan() {
if s.Rune == '*' {
nbuf = append(nbuf, []rune(term.Reset)...)
break
}
nbuf = append(nbuf, s.Rune)
}
continue
}
nbuf = append(nbuf, s.Rune)
} // end main scan loop
return string(nbuf)
}
// Wrap wraps to Columns width.
func Wrap(in string) string { w, _ := to.Wrapped(in, Columns); return w }
// Indent indents the number of spaces set by IndentBy.
func Indent(in string) string { return to.Indented(in, IndentBy) }
// InWrap combines both Wrap and Indent.
func InWrap(in string) string {
w, _ := to.Wrapped(in, Columns-IndentBy)
return to.Indented(w, IndentBy)
}
// Mark parses the input as a string of BonzaiMark, multiple blocks with
// optional emphasis (see Blocks and Emph) and applies IndentBy and
// Columns wrapping to it.
func Mark(in string) string {
if in == "" {
return ""
}
blocks := Blocks(in)
if len(blocks) == 0 {
return ""
}
var out string
for _, block := range blocks {
switch block.T {
case Paragraph:
out += Emph(InWrap(string(block.V))) + "\n"
case Bulleted:
out += Emph(Indent(string(block.V))) + "\n"
case Numbered:
out += Emph(Indent(string(block.V))) + "\n"
case Verbatim:
out += to.Indented(Indent(string(block.V)), 4) + "\n"
default:
panic("unknown block type: " + strconv.Itoa(block.T))
}
}
return out
}
// Emphf calls fmt.Sprintf on the string before passing it to Emph.
func Emphf(a string, f ...any) string {
return Emph(fmt.Sprintf(a, f...))
}
// Indentf calls fmt.Sprintf on the string before passing it to Indent.
func Indentf(a string, f ...any) string {
return Indent(fmt.Sprintf(a, f...))
}
// Wrapf calls fmt.Sprintf on the string before passing it to Wrap.
func Wrapf(a string, f ...any) string {
return Wrap(fmt.Sprintf(a, f...))
}
// InWrapf calls fmt.Sprintf on the string before passing it to InWrap.
func InWrapf(a string, f ...any) string {
return InWrap(fmt.Sprintf(a, f...))
}
// Markf calls fmt.Sprintf on the string before passing it to Mark.
func Markf(a string, f ...any) string {
return Mark(fmt.Sprintf(a, f...))
}
// PrintEmph passes string to Emph and prints it.
func PrintEmph(a string) { fmt.Print(Emph(a)) }
// PrintWrap passes string to Wrap and prints it.
func PrintWrap(a string) { fmt.Print(Wrap(a)) }
// PrintIndent passes string to Indent and prints it.
func PrintIndent(a string) { fmt.Print(Indent(a)) }
// PrintInWrap passes string to InWrap and prints it.
func PrintInWrap(a string) { fmt.Print(InWrap(a)) }
// PrintMark passes string to Mark and prints it.
func PrintMark(a string) { fmt.Print(Mark(a)) }
// PrintEmphf calls fmt.Sprintf on the string before passing it to Emph
// and then printing it.
func PrintEmphf(a string, f ...any) {
fmt.Print(Emph(fmt.Sprintf(a, f...)))
}
// PrintWrapf calls fmt.Sprintf on the string before passing it to Wrap
// and then printing it.
func PrintWrapf(a string, f ...any) {
fmt.Print(Wrap(fmt.Sprintf(a, f...)))
}
// PrintIndentf calls fmt.Sprintf on the string before passing it to
// Indent and then printing it.
func PrintIndentf(a string, f ...any) {
fmt.Print(Indent(fmt.Sprintf(a, f...)))
}
// PrintInWrapf calls fmt.Sprintf on the string before passing it to
// InWrap and then printing it.
func PrintInWrapf(a string, f ...any) {
fmt.Print(InWrap(fmt.Sprintf(a, f...)))
}
// PrintMarkf calls fmt.Sprintf on the string before passing it to Mark
// and then printing it.
func PrintMarkf(a string, f ...any) {
fmt.Print(Mark(fmt.Sprintf(a, f...)))
}