diff --git a/public/parsing.wasm b/public/parsing.wasm index 3138f32..98484b9 100755 Binary files a/public/parsing.wasm and b/public/parsing.wasm differ diff --git a/src/parsing/embed.go b/src/parsing/embed.go index d97d7d2..c342eba 100644 --- a/src/parsing/embed.go +++ b/src/parsing/embed.go @@ -104,6 +104,38 @@ func extract(re *regexp.Regexp, src []byte, subexpName string) []byte { return m[re.SubexpIndex(subexpName)] } +func extractMap(re *regexp.Regexp, src []byte, subexpNames ...string) map[string][]byte { + m := re.FindSubmatch(src) + if m == nil { + return nil + } + res := make(map[string][]byte) + for _, name := range subexpNames { + res[name] = m[re.SubexpIndex(name)] + } + return res +} + +func extractAll(re *regexp.Regexp, src []byte, subexpName string) [][]byte { + m := re.FindAllSubmatch(src, -1) + if m == nil { + return nil + } + return m[re.SubexpIndex(subexpName)] +} + +func extractAllMap(re *regexp.Regexp, src []byte, subexpNames ...string) map[string][][]byte { + m := re.FindAllSubmatch(src, -1) + if m == nil { + return nil + } + res := make(map[string][][]byte) + for _, name := range subexpNames { + res[name] = m[re.SubexpIndex(name)] + } + return res +} + func makeYoutubeEmbed(vid string, preview bool) string { if preview { return ` diff --git a/src/parsing/ggcode.go b/src/parsing/ggcode.go new file mode 100644 index 0000000..4c42895 --- /dev/null +++ b/src/parsing/ggcode.go @@ -0,0 +1,188 @@ +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), + )) +} diff --git a/src/parsing/parsing.go b/src/parsing/parsing.go index 4e83187..67ea120 100644 --- a/src/parsing/parsing.go +++ b/src/parsing/parsing.go @@ -136,6 +136,9 @@ func makeGoldmarkExtensions(opts MarkdownOptions) []goldmark.Extender { Preview: opts.Previews, Education: opts.Education, }, + ggcodeExtension{ + Preview: opts.Previews, + }, ) return extenders