yuin / goldmark

:trophy: A markdown parser written in Go. Easy to extend, standard(CommonMark) compliant, well structured.
MIT License
3.73k stars 257 forks source link

feat(extension/tasklist): Preserve TaskList item source positions to … #452

Open movsb opened 6 months ago

movsb commented 6 months ago

…allow Tasks to be Accomplished later (by updating markdown source and re-rendering).

This is for someone who may want to use Goldmark as a better TODO list implementation.

Reference AST Transformer

// ... implements parser.ASTTransformer.
type PreserveTaskListSourcePosition struct{}

func (a *PreserveTaskListSourcePosition) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
    ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
        if entering && n.Kind() == extast.KindTaskCheckBox {
            c := n.(*extast.TaskCheckBox)
            c.SetAttributeString(`data-source-position`, fmt.Sprintf(`[%d,%d]`, c.Segment.Start, c.Segment.Stop))
        }
        return ast.WalkContinue, nil
    })
}

Reference Renderer (patched)

goldmark (preserve-tasklist-source-position) → git diff
diff --git a/extension/tasklist.go b/extension/tasklist.go
index 6a5e98e..ac12f9e 100644
--- a/extension/tasklist.go
+++ b/extension/tasklist.go
@@ -85,6 +85,13 @@ func (r *TaskCheckBoxHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRe
        reg.Register(ast.KindTaskCheckBox, r.renderTaskCheckBox)
 }

+// TaskCheckBoxAttributeFilter defines attribute names which check elements can have.
+var TaskCheckBoxAttributeFilter = html.GlobalAttributeFilter.Extend(
+       []byte(`checked`),
+       []byte(`disabled`),
+       []byte(`data-source-position`),
+)
+
 func (r *TaskCheckBoxHTMLRenderer) renderTaskCheckBox(
        w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
        if !entering {
@@ -97,6 +104,9 @@ func (r *TaskCheckBoxHTMLRenderer) renderTaskCheckBox(
        } else {
                _, _ = w.WriteString(`<input disabled="" type="checkbox"`)
        }
+       if n.Attributes() != nil {
+               html.RenderAttributes(w, n, TaskCheckBoxAttributeFilter)
+       }
        if r.XHTML {
                _, _ = w.WriteString(" /> ")
        } else {

Finally, the Use case

var source = []byte(`
- [ ] Task1
- [ ] Task2
`)

func main() {
    md := goldmark.New(
        goldmark.WithExtensions(extension.TaskList),
        goldmark.WithParserOptions(
            parser.WithASTTransformers(util.Prioritized(&PreserveTaskListSourcePosition{}, 1000)),
        ),
    )

    doc := md.Parser().Parse(text.NewReader(source))
    md.Renderer().Render(os.Stdout, source, doc)
    // <ul>
    // <li><input disabled="" type="checkbox" data-source-position="[4,5]"> Task1</li>
    // <li><input disabled="" type="checkbox" data-source-position="[16,17]"> Task2</li>
    // </ul>

    // This piece of code should be implemented by the Server
    // to respond to a Click event on the Web page.
    // We assume that we got a request to finish
    // the task at `data-source-position="[16,17]"`.
    {
        var taskPos = `[16,17]`
        var start, stop int
        _, _ = fmt.Sscanf(taskPos, `[%d,%d]`, &start, &stop)

        _ = stop

        // mark it accomplished
        source[start] = 'x'

        doc = md.Parser().Parse(text.NewReader(source))
        md.Renderer().Render(os.Stdout, source, doc)
        // <ul>
        // <li><input disabled="" type="checkbox" data-source-position="[4,5]"> Task1</li>
        // <li><input checked="" disabled="" type="checkbox" data-source-position="[16,17]"> Task2</li>
        // </ul>

        // TODO: Save the new source and rendered content.
    }
}

Result in diff:

tests (main) → git diff 1.html
diff --git a/1.html b/1.html
index e54db85..7bc4284 100644
--- a/1.html
+++ b/1.html
@@ -1,4 +1,4 @@
 <ul>
 <li><input disabled="" type="checkbox" data-source-position="[4,5]"> Task1</li>
-<li><input disabled="" type="checkbox" data-source-position="[16,17]"> Task2</li>
+<li><input checked="" disabled="" type="checkbox" data-source-position="[16,17]"> Task2</li>
 </ul>