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.
This commit is contained in:
Jonathan Tran 2023-06-13 14:22:59 -04:00 committed by GitHub
parent a9ebf911fa
commit 1650a26eb5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 66 additions and 7 deletions

View file

@ -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 // TaskCheckBoxListItem is a block that represents a list item of a markdown block with a checkbox
type TaskCheckBoxListItem struct { type TaskCheckBoxListItem struct {
*ast.ListItem *ast.ListItem
IsChecked bool IsChecked bool
SourcePosition int
} }
// KindTaskCheckBoxListItem is the NodeKind for TaskCheckBoxListItem // KindTaskCheckBoxListItem is the NodeKind for TaskCheckBoxListItem
@ -86,6 +87,7 @@ var KindTaskCheckBoxListItem = ast.NewNodeKind("TaskCheckBoxListItem")
func (n *TaskCheckBoxListItem) Dump(source []byte, level int) { func (n *TaskCheckBoxListItem) Dump(source []byte, level int) {
m := map[string]string{} m := map[string]string{}
m["IsChecked"] = strconv.FormatBool(n.IsChecked) m["IsChecked"] = strconv.FormatBool(n.IsChecked)
m["SourcePosition"] = strconv.FormatInt(int64(n.SourcePosition), 10)
ast.DumpHelper(n, source, level, m, nil) ast.DumpHelper(n, source, level, m, nil)
} }

View file

@ -167,6 +167,11 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
newChild := NewTaskCheckBoxListItem(listItem) newChild := NewTaskCheckBoxListItem(listItem)
newChild.IsChecked = taskCheckBox.IsChecked newChild.IsChecked = taskCheckBox.IsChecked
newChild.SetAttributeString("class", []byte("task-list-item")) 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) v.AppendChild(v, newChild)
} }
} }
@ -441,12 +446,7 @@ func (r *HTMLRenderer) renderTaskCheckBoxListItem(w util.BufWriter, source []byt
} else { } else {
_, _ = w.WriteString("<li>") _, _ = w.WriteString("<li>")
} }
_, _ = w.WriteString(`<input type="checkbox" disabled=""`) fmt.Fprintf(w, `<input type="checkbox" disabled="" data-source-position="%d"`, n.SourcePosition)
segments := node.FirstChild().Lines()
if segments.Len() > 0 {
segment := segments.At(0)
_, _ = w.WriteString(fmt.Sprintf(` data-source-position="%d"`, segment.Start))
}
if n.IsChecked { if n.IsChecked {
_, _ = w.WriteString(` checked=""`) _, _ = w.WriteString(` checked=""`)
} }

View file

@ -173,6 +173,9 @@ func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer)
} }
buf = giteautil.NormalizeEOL(buf) buf = giteautil.NormalizeEOL(buf)
// Preserve original length.
bufWithMetadataLength := len(buf)
rc := &RenderConfig{ rc := &RenderConfig{
Meta: "table", Meta: "table",
Icon: "table", Icon: "table",
@ -180,6 +183,12 @@ func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer)
} }
buf, _ = ExtractMetadataBytes(buf, rc) buf, _ = ExtractMetadataBytes(buf, rc)
metaLength := bufWithMetadataLength - len(buf)
if metaLength < 0 {
metaLength = 0
}
rc.metaLength = metaLength
pc.Set(renderConfigKey, rc) pc.Set(renderConfigKey, rc)
if err := converter.Convert(buf, lw, parser.WithContext(pc)); err != nil { if err := converter.Convert(buf, lw, parser.WithContext(pc)); err != nil {

View file

@ -519,3 +519,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`,
`<table>
<thead>
<tr>
<th>foo</th>
</tr>
</thead>
<tbody>
<tr>
<td>bar</td>
</tr>
</tbody>
</table>
<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)
}
}

View file

@ -18,6 +18,9 @@ type RenderConfig struct {
TOC bool TOC bool
Lang string Lang string
yamlNode *yaml.Node yamlNode *yaml.Node
// Used internally. Cannot be controlled by frontmatter.
metaLength int
} }
// UnmarshalYAML implement yaml.v3 UnmarshalYAML // UnmarshalYAML implement yaml.v3 UnmarshalYAML

View file

@ -29,6 +29,14 @@ export function initMarkupTasklist() {
const encoder = new TextEncoder(); const encoder = new TextEncoder();
const buffer = encoder.encode(oldContent); 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); buffer.set(encoder.encode(checkboxCharacter), position);
const newContent = new TextDecoder().decode(buffer); const newContent = new TextDecoder().decode(buffer);