okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
e4c3bd6
main
1package tui 2 3import ( 4"bytes" 5"io" 6"strings" 7"unicode" 8"unicode/utf8" 9 10"charm.land/glamour/v2" 11"charm.land/glamour/v2/ansi" 12"github.com/doug/termtex" 13"github.com/yuin/goldmark" 14"github.com/yuin/goldmark/ast" 15"github.com/yuin/goldmark/parser" 16"github.com/yuin/goldmark/text" 17"github.com/yuin/goldmark/util" 18) 19 20// Math in a reply: $…$ and \(…\) inline, $$…$$ and \[…\] on lines of their 21// own as display math. termtex lays both out in Unicode. 22var ( 23kindInlineMath = ast .NewNodeKind ("InlineMath" ) 24kindDisplayMath = ast .NewNodeKind ("DisplayMath" ) 25) 26 27type inlineMath struct { 28 ast.BaseInline 29tex ,source string 30} 31 32func (n * inlineMath )Kind () ast.NodeKind {return kindInlineMath } 33func (n * inlineMath )Dump (source []byte ,level int ) {ast .DumpHelper (n ,source ,level ,nil ,nil ) } 34 35// displayMath is closed once its closing delimiter arrives; a reply still 36// streaming may not have sent it yet. 37type displayMath struct { 38 ast.BaseBlock 39delimiter 40tex []byte 41closed bool 42} 43 44func (n * displayMath )Kind () ast.NodeKind {return kindDisplayMath } 45func (n * displayMath )Dump (source []byte ,level int ) {ast .DumpHelper (n ,source ,level ,nil ,nil ) } 46 47// source is the math as written, with the closer once it arrived. 48func (n * displayMath )source ()string { 49written := n .open + string (n .tex ) 50if n .closed { 51written += n .close 52 } 53return strings .TrimRight (written ,"\n" ) 54} 55 56type mathExtension struct {} 57 58func (mathExtension )Extend (m goldmark.Markdown ) { 59m .Parser ().AddOptions ( 60parser .WithBlockParsers (util .Prioritized (displayMathParser {},650 )), 61parser .WithInlineParsers (util .Prioritized (inlineMathParser {},150 )), 62 ) 63} 64 65// mathOptions add the math parsers and render their nodes in color. 66func mathOptions (color * string ) glamour.TermRendererOption { 67return glamour .WithOptions ( 68glamour .WithExtensions (mathExtension {}), 69glamour .WithElement (kindInlineMath ,func (node ast.Node ,_ []byte ,_ ansi.RenderContext ) ansi.Element { 70return ansi.Element {Renderer :inlineMathElement {node .(* inlineMath ),color }} 71 }), 72glamour .WithElement (kindDisplayMath ,func (node ast.Node ,_ []byte ,_ ansi.RenderContext ) ansi.Element { 73return ansi.Element {Entering :"\n" ,Exiting :"\n" ,Renderer :displayMathElement {node .(* displayMath ),color }} 74 }), 75 ) 76} 77 78type inlineMathElement struct { 79node * inlineMath 80color * string 81} 82 83func (e inlineMathElement )Render (w io.Writer ,ctx ansi.RenderContext )error { 84return e .StyleOverrideRender (w ,ctx , ansi.StylePrimitive {}) 85} 86 87// StyleOverrideRender keeps the math color inside bold or a link. Math that 88// needs more than one row, such as a matrix, stays as its source. 89func (e inlineMathElement )StyleOverrideRender (w io.Writer ,ctx ansi.RenderContext ,outer ansi.StylePrimitive )error { 90outer .Color = e .color 91laid ,err := termtex .Render (e .node .tex , termtex.Style {Inline :true }) 92if err != nil || strings .Contains (laid ,"\n" ) { 93laid = e .node .source 94 } 95return (& ansi.BaseElement {Token :laid ,Style :outer ,Verbatim :true }).Render (w ,ctx ) 96} 97 98type displayMathElement struct { 99node * displayMath 100color * string 101} 102 103// Render lays the math out to the block's width. Until it closes, or when 104// termtex cannot read it, the source shows instead. 105func (e displayMathElement )Render (w io.Writer ,ctx ansi.RenderContext )error { 106laid := e .node .source () 107if e .node .closed { 108if out ,err := termtex .Render (strings .TrimSpace (string (e .node .tex )), termtex.Style {Width :ctx .Width ()});err == nil { 109laid = out 110 } 111 } 112return (& ansi.BaseElement {Token :laid ,Style : ansi.StylePrimitive {Color :e .color },Verbatim :true }).Render (w ,ctx ) 113} 114 115// delimiter is one way to write math. Only display delimiters may open a 116// block of their own. 117type delimiter struct { 118open ,close string 119display bool 120} 121 122// delimiters are tried in order, so $$ is found before $. 123var delimiters = []delimiter { 124 {open :"$$" ,close :"$$" ,display :true }, 125 {open :`\[` ,close :`\]` ,display :true }, 126 {open :`\(` ,close :`\)` }, 127 {open :"$" ,close :"$" }, 128} 129 130// delimiterAt is the delimiter that opens math at the start of line. 131func delimiterAt (line []byte ) (delimiter ,bool ) { 132for _ ,d := range delimiters { 133if bytes .HasPrefix (line , []byte (d .open )) { 134return d ,true 135 } 136 } 137return delimiter {},false 138} 139 140// openDisplayMath reads a line that opens display math: its delimiter, the 141// tex after the opener, and whether the line closes it too, in which case 142// nothing may follow the closer. 143func openDisplayMath (line []byte ) (d delimiter ,tex []byte ,closed ,ok bool ) { 144d ,ok = delimiterAt (line ) 145if ! ok || ! d .display { 146return d ,nil ,false ,false 147 } 148tex ,after ,closed := bytes .Cut (line [len (d .open ):], []byte (d .close )) 149if closed && ! util .IsBlank (after ) { 150return d ,nil ,false ,false 151 } 152return d ,tex ,closed ,true 153} 154 155type displayMathParser struct {} 156 157func (displayMathParser )Trigger () []byte {return []byte {'$' ,'\\' } } 158 159func (displayMathParser )Open (_ ast.Node ,reader text.Reader ,pc parser.Context ) (ast.Node , parser.State ) { 160line ,_ := reader .PeekLine () 161pos := pc .BlockOffset () 162if pos < 0 { 163return nil ,parser .NoChildren 164 } 165d ,tex ,closed ,ok := openDisplayMath (line [pos :]) 166if ! ok { 167return nil ,parser .NoChildren 168 } 169reader .AdvanceToEOL () 170return & displayMath {delimiter :d ,tex :bytes .Clone (tex ),closed :closed },parser .NoChildren 171} 172 173func (displayMathParser )Continue (node ast.Node ,reader text.Reader ,_ parser.Context ) parser.State { 174n := node .(* displayMath ) 175if n .closed { 176return parser .Close 177 } 178line ,_ := reader .PeekLine () 179tex ,_ ,closed := bytes .Cut (line , []byte (n .close )) 180n .tex ,n .closed = append (n .tex ,tex ... ),closed 181reader .AdvanceToEOL () 182if closed { 183return parser .Close 184 } 185return parser .Continue | parser .NoChildren 186} 187 188func (displayMathParser )Close (ast.Node , text.Reader , parser.Context ) {} 189func (displayMathParser )CanInterruptParagraph ()bool {return true } 190func (displayMathParser )CanAcceptIndentedLine ()bool {return false } 191 192type inlineMathParser struct {} 193 194func (inlineMathParser )Trigger () []byte {return []byte {'$' ,'\\' } } 195 196func (inlineMathParser )Parse (_ ast.Node ,block text.Reader ,_ parser.Context ) ast.Node { 197line ,_ := block .PeekLine () 198tex ,length := scanInlineMath (line ,block .PrecendingCharacter ()) 199if length == 0 { 200return nil 201 } 202block .Advance (length ) 203return & inlineMath {tex :tex ,source :string (line [:length ])} 204} 205 206// scanInlineMath is the math at the start of line and its length in bytes, 207// or 0 when line does not start any. A lone $ follows Pandoc and goes 208// further, so prices and shell variables stay text: it opens before a 209// non-space that follows no word character, and closes after a non-space 210// before anything but a word character. 211func scanInlineMath (line []byte ,before rune ) (string ,int ) { 212d ,ok := delimiterAt (line ) 213lone := d .open == "$" 214if ! ok || lone && (wordRune (before )|| len (line )< 2 || util .IsSpace (line [1 ])) { 215return "" ,0 216 } 217body ,_ ,_ := bytes .Cut (line , []byte {'\n' }) 218for i := len (d .open );i < len (body );i ++ { 219if d .close [0 ]== '$' && body [i ]== '\\' { 220i ++ // an escaped character never closes 221continue 222 } 223if ! bytes .HasPrefix (body [i :], []byte (d .close )) { 224continue 225 } 226end := i + len (d .close ) 227if after ,_ := utf8 .DecodeRune (body [end :]);lone && (util .IsSpace (body [i - 1 ])|| wordRune (after )) { 228continue 229 } 230tex := bytes .TrimSpace (body [len (d .open ):i ]) 231if len (tex )== 0 { 232return "" ,0 233 } 234return string (tex ),end 235 } 236return "" ,0 237} 238 239func wordRune (r rune )bool {return unicode .IsLetter (r )|| unicode .IsDigit (r )|| r == '_' }