package parsing import ( "bytes" "regexp" "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{ "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[a-zA-Z0-9-_]+)(\{(?P.*?)\})?$`) var reGGCodeBlockArgs = regexp.MustCompile(`(?P[a-zA-Z0-9-_]+)="(?P.*?)"`) type ggcodeParserBlock struct { Preview bool } var _ parser.BlockParser = ggcodeParserBlock{} func (s ggcodeParserBlock) Trigger() []byte { return []byte("!!!") } func (s ggcodeParserBlock) Open(parent ast.Node, reader text.Reader, pc parser.Context) (ast.Node, parser.State) { restOfLine, _ := reader.PeekLine() if match := extractMap(reGGCodeBlockOpen, bytes.TrimSpace(restOfLine), "name", "args"); match != nil { name := string(match["name"]) var args map[string]string if argsMatch := extractAllMap(reGGCodeBlockArgs, 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 } } reader.Advance(len(restOfLine)) return &ggcodeNode{ Name: name, Args: args, }, parser.Continue | parser.HasChildren } else { return nil, parser.NoChildren } } func (s ggcodeParserBlock) Continue(node ast.Node, reader text.Reader, pc parser.Context) parser.State { line, _ := reader.PeekLine() if string(bytes.TrimSpace(line)) == "!!!" { reader.Advance(3) return parser.Close } return parser.Continue | parser.HasChildren } func (s ggcodeParserBlock) Close(node ast.Node, reader text.Reader, pc parser.Context) {} func (s ggcodeParserBlock) CanInterruptParagraph() bool { return false } func (s ggcodeParserBlock) CanAcceptIndentedLine() bool { return false } // ---------------------- // 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( util.Prioritized(ggcodeParserBlock{Preview: e.Preview}, 500), )) m.Renderer().AddOptions(renderer.WithNodeRenderers( util.Prioritized(newGGCodeHTMLRenderer(), 500), )) }