diff options
author | Jonathan Tran <jon@allspice.io> | 2023-06-13 02:44:47 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-06-13 14:44:47 +0800 |
commit | f62cd2f4738c1b3cf7c31e8b98702a709bdd4072 (patch) | |
tree | 17dac9b30739851d0a90b848f79f726c05659979 | |
parent | 419804fd4d5cb655a51f245010b8eb1163b26bc2 (diff) | |
download | gitea-f62cd2f4738c1b3cf7c31e8b98702a709bdd4072.tar.gz gitea-f62cd2f4738c1b3cf7c31e8b98702a709bdd4072.zip |
Fix task list checkbox toggle to work with YAML front matter (#25184)
Fixes #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.
-rw-r--r-- | modules/markup/markdown/ast.go | 4 | ||||
-rw-r--r-- | modules/markup/markdown/goldmark.go | 12 | ||||
-rw-r--r-- | modules/markup/markdown/markdown.go | 9 | ||||
-rw-r--r-- | modules/markup/markdown/markdown_test.go | 37 | ||||
-rw-r--r-- | modules/markup/markdown/renderconfig.go | 3 | ||||
-rw-r--r-- | 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 f03a780900..ff4e6b1bd0 100644 --- a/modules/markup/markdown/goldmark.go +++ b/modules/markup/markdown/goldmark.go @@ -177,6 +177,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) } } @@ -457,12 +462,7 @@ func (r *HTMLRenderer) renderTaskCheckBoxListItem(w util.BufWriter, source []byt } else { _, _ = w.WriteString("<li>") } - _, _ = w.WriteString(`<input type="checkbox" disabled=""`) - segments := node.FirstChild().Lines() - if segments.Len() > 0 { - segment := segments.At(0) - _, _ = w.WriteString(fmt.Sprintf(` data-source-position="%d"`, segment.Start)) - } + fmt.Fprintf(w, `<input type="checkbox" disabled="" data-source-position="%d"`, n.SourcePosition) if n.IsChecked { _, _ = w.WriteString(` checked=""`) } diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go index d4a7195dc5..43885889d1 100644 --- a/modules/markup/markdown/markdown.go +++ b/modules/markup/markdown/markdown.go @@ -178,6 +178,9 @@ func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer) } buf = giteautil.NormalizeEOL(buf) + // Preserve original length. + bufWithMetadataLength := len(buf) + rc := &RenderConfig{ Meta: renderMetaModeFromString(string(ctx.RenderMetaAs)), Icon: "table", @@ -185,6 +188,12 @@ func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer) } buf, _ = ExtractMetadataBytes(buf, rc) + metaLength := bufWithMetadataLength - len(buf) + if metaLength < 0 { + metaLength = 0 + } + rc.metaLength = metaLength + pc.Set(renderConfigKey, rc) if err := converter.Convert(buf, lw, parser.WithContext(pc)); err != nil { diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go index e81869d7a4..4bd2ca8d41 100644 --- a/modules/markup/markdown/markdown_test.go +++ b/modules/markup/markdown/markdown_test.go @@ -520,3 +520,40 @@ func TestMathBlock(t *testing.T) { } } + +func TestTaskList(t *testing.T) { + testcases := []struct { + testcase string + expected string + }{ + { + // data-source-position should take into account YAML frontmatter. + `--- +foo: bar +--- +- [ ] task 1`, + `<details><summary><i class="icon table"></i></summary><table> +<thead> +<tr> +<th>foo</th> +</tr> +</thead> +<tbody> +<tr> +<td>bar</td> +</tr> +</tbody> +</table> +</details><ul> +<li class="task-list-item"><input type="checkbox" disabled="" data-source-position="19"/>task 1</li> +</ul> +`, + }, + } + + 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 691df74312..f4c48d1b3d 100644 --- a/modules/markup/markdown/renderconfig.go +++ b/modules/markup/markdown/renderconfig.go @@ -20,6 +20,9 @@ type RenderConfig struct { TOC string // "false": hide, "side"/empty: in sidebar, "main"/"true": in main view Lang string yamlNode *yaml.Node + + // Used internally. Cannot be controlled by frontmatter. + metaLength int } func renderMetaModeFromString(s string) markup.RenderMetaMode { 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); |