tasklist.go (3087B)
1 package extension 2 3 import ( 4 "github.com/yuin/goldmark" 5 gast "github.com/yuin/goldmark/ast" 6 "github.com/yuin/goldmark/extension/ast" 7 "github.com/yuin/goldmark/parser" 8 "github.com/yuin/goldmark/renderer" 9 "github.com/yuin/goldmark/renderer/html" 10 "github.com/yuin/goldmark/text" 11 "github.com/yuin/goldmark/util" 12 "regexp" 13 ) 14 15 var taskListRegexp = regexp.MustCompile(`^\[([\sxX])\]\s*`) 16 17 type taskCheckBoxParser struct { 18 } 19 20 var defaultTaskCheckBoxParser = &taskCheckBoxParser{} 21 22 // NewTaskCheckBoxParser returns a new InlineParser that can parse 23 // checkboxes in list items. 24 // This parser must take precedence over the parser.LinkParser. 25 func NewTaskCheckBoxParser() parser.InlineParser { 26 return defaultTaskCheckBoxParser 27 } 28 29 func (s *taskCheckBoxParser) Trigger() []byte { 30 return []byte{'['} 31 } 32 33 func (s *taskCheckBoxParser) Parse(parent gast.Node, block text.Reader, pc parser.Context) gast.Node { 34 // Given AST structure must be like 35 // - List 36 // - ListItem : parent.Parent 37 // - TextBlock : parent 38 // (current line) 39 if parent.Parent() == nil || parent.Parent().FirstChild() != parent { 40 return nil 41 } 42 43 if _, ok := parent.Parent().(*gast.ListItem); !ok { 44 return nil 45 } 46 line, _ := block.PeekLine() 47 m := taskListRegexp.FindSubmatchIndex(line) 48 if m == nil { 49 return nil 50 } 51 value := line[m[2]:m[3]][0] 52 block.Advance(m[1]) 53 checked := value == 'x' || value == 'X' 54 return ast.NewTaskCheckBox(checked) 55 } 56 57 func (s *taskCheckBoxParser) CloseBlock(parent gast.Node, pc parser.Context) { 58 // nothing to do 59 } 60 61 // TaskCheckBoxHTMLRenderer is a renderer.NodeRenderer implementation that 62 // renders checkboxes in list items. 63 type TaskCheckBoxHTMLRenderer struct { 64 html.Config 65 } 66 67 // NewTaskCheckBoxHTMLRenderer returns a new TaskCheckBoxHTMLRenderer. 68 func NewTaskCheckBoxHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { 69 r := &TaskCheckBoxHTMLRenderer{ 70 Config: html.NewConfig(), 71 } 72 for _, opt := range opts { 73 opt.SetHTMLOption(&r.Config) 74 } 75 return r 76 } 77 78 // RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs. 79 func (r *TaskCheckBoxHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { 80 reg.Register(ast.KindTaskCheckBox, r.renderTaskCheckBox) 81 } 82 83 func (r *TaskCheckBoxHTMLRenderer) renderTaskCheckBox(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) { 84 if !entering { 85 return gast.WalkContinue, nil 86 } 87 n := node.(*ast.TaskCheckBox) 88 89 if n.IsChecked { 90 w.WriteString(`<input checked="" disabled="" type="checkbox"`) 91 } else { 92 w.WriteString(`<input disabled="" type="checkbox"`) 93 } 94 if r.XHTML { 95 w.WriteString(" /> ") 96 } else { 97 w.WriteString("> ") 98 } 99 return gast.WalkContinue, nil 100 } 101 102 type taskList struct { 103 } 104 105 // TaskList is an extension that allow you to use GFM task lists. 106 var TaskList = &taskList{} 107 108 func (e *taskList) Extend(m goldmark.Markdown) { 109 m.Parser().AddOptions(parser.WithInlineParsers( 110 util.Prioritized(NewTaskCheckBoxParser(), 0), 111 )) 112 m.Renderer().AddOptions(renderer.WithNodeRenderers( 113 util.Prioritized(NewTaskCheckBoxHTMLRenderer(), 500), 114 )) 115 }