2022-09-04 00:12:19 +00:00
package parsing
import (
"bytes"
2022-09-04 01:35:21 +00:00
"fmt"
2022-09-04 00:12:19 +00:00
"regexp"
2022-09-04 01:35:21 +00:00
"git.handmade.network/hmn/hmn/src/hmnurl"
2022-09-04 00:12:19 +00:00
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
gast "github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
)
// NOTE(ben): ggcode is my cute name for our custom extension syntax because I got fed up with
// bbcode. It's designed to be a more natural fit for Goldmark's method of parsing, while still
// being a general-purpose tag-like syntax that's easy for us to add instances of without writing
// new Goldmark parsers.
//
// Inline ggcode is delimited by two exclamation marks. Block ggcode is delimited by three. Inline
// ggcode uses parentheses to delimit the start and end of the affected content. Block ggcode is
// like a fenced code block and ends with !!!. ggcode sections can optionally have named string
// arguments inside braces. Quotes around the value are mandatory.
//
// Inline example:
//
// See our article on !!glossary{slug="tcp"}(TCP) for more details.
//
// Block example:
//
// !!!resource{name="Beej's Guide to Network Programming" url="https://beej.us/guide/bgnet/html/"}
// This is a _fantastic_ resource on network programming, suitable for beginners.
// !!!
//
type ggcodeRenderer = func ( w util . BufWriter , n * ggcodeNode , entering bool ) error
var ggcodeTags = map [ string ] ggcodeRenderer {
2022-09-04 01:35:21 +00:00
"glossary" : func ( w util . BufWriter , n * ggcodeNode , entering bool ) error {
if entering {
term , _ := n . Args [ "term" ]
w . WriteString ( fmt . Sprintf (
` <a href="%s" class="glossary-term" data-term="%s"> ` ,
hmnurl . BuildEducationGlossary ( term ) ,
term ,
) )
} else {
w . WriteString ( "</a>" )
}
return nil
} ,
2022-09-04 00:12:19 +00:00
"resource" : func ( w util . BufWriter , n * ggcodeNode , entering bool ) error {
if entering {
w . WriteString ( "a node :o" )
} else {
w . WriteString ( "bai" )
}
return nil
} ,
}
// ----------------------
// Parsers and delimiters
// ----------------------
var reGGCodeBlockOpen = regexp . MustCompile ( ` ^!!!(?P<name>[a-zA-Z0-9-_]+)(\ { (?P<args>.*?)\})?$ ` )
2022-09-04 01:35:21 +00:00
var reGGCodeInline = regexp . MustCompile ( ` ^!!(?P<name>[a-zA-Z0-9-_]+)(\ { (?P<args>.*?)\})?(\((?P<content>.*?)\))? ` )
var reGGCodeArgs = regexp . MustCompile ( ` (?P<arg>[a-zA-Z0-9-_]+)="(?P<val>.*?)" ` )
// Block parser stuff
2022-09-04 00:12:19 +00:00
2022-09-04 01:35:21 +00:00
type ggcodeBlockParser struct {
2022-09-04 00:12:19 +00:00
Preview bool
}
2022-09-04 01:35:21 +00:00
var _ parser . BlockParser = ggcodeBlockParser { }
2022-09-04 00:12:19 +00:00
2022-09-04 01:35:21 +00:00
func ( s ggcodeBlockParser ) Trigger ( ) [ ] byte {
return [ ] byte ( "!" )
2022-09-04 00:12:19 +00:00
}
2022-09-04 01:35:21 +00:00
func ( s ggcodeBlockParser ) Open ( parent ast . Node , reader text . Reader , pc parser . Context ) ( ast . Node , parser . State ) {
2022-09-04 00:12:19 +00:00
restOfLine , _ := reader . PeekLine ( )
2022-09-04 01:35:21 +00:00
if match := extractMap ( reGGCodeBlockOpen , bytes . TrimSpace ( restOfLine ) ) ; match != nil {
2022-09-04 00:12:19 +00:00
name := string ( match [ "name" ] )
var args map [ string ] string
2022-09-04 01:35:21 +00:00
if argsMatch := extractAllMap ( reGGCodeArgs , match [ "args" ] ) ; argsMatch != nil {
2022-09-04 00:12:19 +00:00
args = make ( map [ string ] string )
for i := range argsMatch [ "arg" ] {
arg := string ( argsMatch [ "arg" ] [ i ] )
val := string ( argsMatch [ "val" ] [ i ] )
args [ arg ] = val
}
}
reader . Advance ( len ( restOfLine ) )
return & ggcodeNode {
Name : name ,
Args : args ,
} , parser . Continue | parser . HasChildren
} else {
return nil , parser . NoChildren
}
}
2022-09-04 01:35:21 +00:00
func ( s ggcodeBlockParser ) Continue ( node ast . Node , reader text . Reader , pc parser . Context ) parser . State {
2022-09-04 00:12:19 +00:00
line , _ := reader . PeekLine ( )
if string ( bytes . TrimSpace ( line ) ) == "!!!" {
reader . Advance ( 3 )
return parser . Close
}
return parser . Continue | parser . HasChildren
}
2022-09-04 01:35:21 +00:00
func ( s ggcodeBlockParser ) Close ( node ast . Node , reader text . Reader , pc parser . Context ) { }
2022-09-04 00:12:19 +00:00
2022-09-04 01:35:21 +00:00
func ( s ggcodeBlockParser ) CanInterruptParagraph ( ) bool {
2022-09-04 00:12:19 +00:00
return false
}
2022-09-04 01:35:21 +00:00
func ( s ggcodeBlockParser ) CanAcceptIndentedLine ( ) bool {
2022-09-04 00:12:19 +00:00
return false
}
2022-09-04 01:35:21 +00:00
// Inline parser stuff
type ggcodeInlineParser struct {
Preview bool
}
var _ parser . InlineParser = ggcodeInlineParser { }
func ( s ggcodeInlineParser ) Trigger ( ) [ ] byte {
return [ ] byte ( "!()" )
}
func ( s ggcodeInlineParser ) Parse ( parent gast . Node , block text . Reader , pc parser . Context ) gast . Node {
restOfLine , segment := block . PeekLine ( ) // Gets the rest of the line (starting at the current parser cursor index), and the segment representing the indices in the source text.
if match := extractMap ( reGGCodeInline , restOfLine ) ; match != nil {
name := string ( match [ "name" ] )
var args map [ string ] string
if argsMatch := extractAllMap ( reGGCodeArgs , match [ "args" ] ) ; argsMatch != nil {
args = make ( map [ string ] string )
for i := range argsMatch [ "arg" ] {
arg := string ( argsMatch [ "arg" ] [ i ] )
val := string ( argsMatch [ "val" ] [ i ] )
args [ arg ] = val
}
}
node := & ggcodeNode {
Name : name ,
Args : args ,
}
contentLength := len ( match [ "content" ] )
if contentLength > 0 {
contentSegmentStart := segment . Start + len ( match [ "all" ] ) - ( contentLength + 1 ) // the 1 is for the end parenthesis
contentSegmentEnd := contentSegmentStart + contentLength
contentSegment := text . NewSegment ( contentSegmentStart , contentSegmentEnd )
node . AppendChild ( node , ast . NewTextSegment ( contentSegment ) )
}
block . Advance ( len ( match [ "all" ] ) )
return node
} else {
return nil
}
}
type ggcodeDelimiterParser struct {
Node * ggcodeNode // We need to pass this through 🙄
}
func ( p ggcodeDelimiterParser ) IsDelimiter ( b byte ) bool {
fmt . Println ( "delmit" , string ( b ) )
return b == '(' || b == ')'
}
func ( p ggcodeDelimiterParser ) CanOpenCloser ( opener , closer * parser . Delimiter ) bool {
fmt . Println ( "oopen" )
return opener . Char == '(' && closer . Char == ')'
}
func ( p ggcodeDelimiterParser ) OnMatch ( consumes int ) gast . Node {
fmt . Println ( "out!" )
return p . Node
}
2022-09-04 00:12:19 +00:00
// ----------------------
// AST node
// ----------------------
type ggcodeNode struct {
gast . BaseBlock
Name string
Args map [ string ] string
}
var _ ast . Node = & ggcodeNode { }
func ( n * ggcodeNode ) Dump ( source [ ] byte , level int ) {
gast . DumpHelper ( n , source , level , n . Args , nil )
}
var kindGGCode = gast . NewNodeKind ( "ggcode" )
func ( n * ggcodeNode ) Kind ( ) gast . NodeKind {
return kindGGCode
}
// ----------------------
// Renderer
// ----------------------
type ggcodeHTMLRenderer struct {
html . Config
}
func newGGCodeHTMLRenderer ( opts ... html . Option ) renderer . NodeRenderer {
r := & ggcodeHTMLRenderer {
Config : html . NewConfig ( ) ,
}
for _ , opt := range opts {
opt . SetHTMLOption ( & r . Config )
}
return r
}
func ( r * ggcodeHTMLRenderer ) RegisterFuncs ( reg renderer . NodeRendererFuncRegisterer ) {
reg . Register ( kindGGCode , r . render )
}
func ( r * ggcodeHTMLRenderer ) render ( w util . BufWriter , source [ ] byte , n gast . Node , entering bool ) ( gast . WalkStatus , error ) {
node := n . ( * ggcodeNode )
var renderer ggcodeRenderer = defaultGGCodeRenderer
if tagRenderer , ok := ggcodeTags [ node . Name ] ; ok {
renderer = tagRenderer
}
err := renderer ( w , node , entering )
return gast . WalkContinue , err
}
func defaultGGCodeRenderer ( w util . BufWriter , n * ggcodeNode , entering bool ) error {
if entering {
w . WriteString ( "wat is this tag >:(" )
}
return nil
}
// ----------------------
// Extension
// ----------------------
type ggcodeExtension struct {
Preview bool
}
func ( e ggcodeExtension ) Extend ( m goldmark . Markdown ) {
m . Parser ( ) . AddOptions ( parser . WithBlockParsers (
2022-09-04 01:35:21 +00:00
util . Prioritized ( ggcodeBlockParser { Preview : e . Preview } , 500 ) ,
) )
m . Parser ( ) . AddOptions ( parser . WithInlineParsers (
util . Prioritized ( ggcodeInlineParser { Preview : e . Preview } , 500 ) ,
2022-09-04 00:12:19 +00:00
) )
m . Renderer ( ) . AddOptions ( renderer . WithNodeRenderers (
util . Prioritized ( newGGCodeHTMLRenderer ( ) , 500 ) ,
) )
}