From 1650a26eb58ac71492d21f2d817d28990c100cb5 Mon Sep 17 00:00:00 2001 From: Jonathan Tran Date: Tue, 13 Jun 2023 14:22:59 -0400 Subject: [PATCH] Fix task list checkbox toggle to work with YAML front matter (#25184) (#25236) Backport https://github.com/go-gitea/gitea/pull/25184 by @jtran Closes #25225. Fixes https://github.com/go-gitea/gitea/issues/25160. `data-source-position` of checkboxes in a task list was incorrect whenever there was YAML front matter. This would result in issue content or PR descriptions getting corrupted with random `x` or space characters when a user checked or unchecked a task. --- modules/markup/markdown/ast.go | 4 ++- modules/markup/markdown/goldmark.go | 12 ++++---- modules/markup/markdown/markdown.go | 9 ++++++ modules/markup/markdown/markdown_test.go | 37 ++++++++++++++++++++++++ modules/markup/markdown/renderconfig.go | 3 ++ web_src/js/markup/tasklist.js | 8 +++++ 6 files changed, 66 insertions(+), 7 deletions(-) diff --git a/modules/markup/markdown/ast.go b/modules/markup/markdown/ast.go index e844f801c4..3e6e291ab2 100644 --- a/modules/markup/markdown/ast.go +++ b/modules/markup/markdown/ast.go @@ -76,7 +76,8 @@ func IsSummary(node ast.Node) bool { // TaskCheckBoxListItem is a block that represents a list item of a markdown block with a checkbox type TaskCheckBoxListItem struct { *ast.ListItem - IsChecked bool + IsChecked bool + SourcePosition int } // KindTaskCheckBoxListItem is the NodeKind for TaskCheckBoxListItem @@ -86,6 +87,7 @@ var KindTaskCheckBoxListItem = ast.NewNodeKind("TaskCheckBoxListItem") func (n *TaskCheckBoxListItem) Dump(source []byte, level int) { m := map[string]string{} m["IsChecked"] = strconv.FormatBool(n.IsChecked) + m["SourcePosition"] = strconv.FormatInt(int64(n.SourcePosition), 10) ast.DumpHelper(n, source, level, m, nil) } diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go index 50b438219b..126ea3de21 100644 --- a/modules/markup/markdown/goldmark.go +++ b/modules/markup/markdown/goldmark.go @@ -167,6 +167,11 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa newChild := NewTaskCheckBoxListItem(listItem) newChild.IsChecked = taskCheckBox.IsChecked newChild.SetAttributeString("class", []byte("task-list-item")) + segments := newChild.FirstChild().Lines() + if segments.Len() > 0 { + segment := segments.At(0) + newChild.SourcePosition = rc.metaLength + segment.Start + } v.AppendChild(v, newChild) } } @@ -441,12 +446,7 @@ func (r *HTMLRenderer) renderTaskCheckBoxListItem(w util.BufWriter, source []byt } else { _, _ = w.WriteString("
  • ") } - _, _ = w.WriteString(` 0 { - segment := segments.At(0) - _, _ = w.WriteString(fmt.Sprintf(` data-source-position="%d"`, segment.Start)) - } + fmt.Fprintf(w, ` + + +foo + + + + +bar + + + + +`, + }, + } + + for _, test := range testcases { + res, err := RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase) + assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase) + assert.Equal(t, test.expected, res, "Unexpected result in testcase %q", test.testcase) + } +} diff --git a/modules/markup/markdown/renderconfig.go b/modules/markup/markdown/renderconfig.go index f9c9cbc5f4..ebcad1e04e 100644 --- a/modules/markup/markdown/renderconfig.go +++ b/modules/markup/markdown/renderconfig.go @@ -18,6 +18,9 @@ type RenderConfig struct { TOC bool Lang string yamlNode *yaml.Node + + // Used internally. Cannot be controlled by frontmatter. + metaLength int } // UnmarshalYAML implement yaml.v3 UnmarshalYAML diff --git a/web_src/js/markup/tasklist.js b/web_src/js/markup/tasklist.js index 0f03837baa..ad1c6964a7 100644 --- a/web_src/js/markup/tasklist.js +++ b/web_src/js/markup/tasklist.js @@ -29,6 +29,14 @@ export function initMarkupTasklist() { const encoder = new TextEncoder(); const buffer = encoder.encode(oldContent); + // Indexes may fall off the ends and return undefined. + if (buffer[position - 1] !== '['.codePointAt(0) || + buffer[position] !== ' '.codePointAt(0) && buffer[position] !== 'x'.codePointAt(0) || + buffer[position + 1] !== ']'.codePointAt(0)) { + // Position is probably wrong. Revert and don't allow change. + checkbox.checked = !checkbox.checked; + throw new Error(`Expected position to be space or x and surrounded by brackets, but it's not: position=${position}`); + } buffer.set(encoder.encode(checkboxCharacter), position); const newContent = new TextDecoder().decode(buffer);