mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-11-08 18:04:14 +01:00
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.
This commit is contained in:
parent
419804fd4d
commit
f62cd2f473
6 changed files with 66 additions and 7 deletions
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -177,6 +177,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -457,12 +462,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=""`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -178,6 +178,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: renderMetaModeFromString(string(ctx.RenderMetaAs)),
|
Meta: renderMetaModeFromString(string(ctx.RenderMetaAs)),
|
||||||
Icon: "table",
|
Icon: "table",
|
||||||
|
@ -185,6 +188,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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -20,6 +20,9 @@ type RenderConfig struct {
|
||||||
TOC string // "false": hide, "side"/empty: in sidebar, "main"/"true": in main view
|
TOC string // "false": hide, "side"/empty: in sidebar, "main"/"true": in main view
|
||||||
Lang string
|
Lang string
|
||||||
yamlNode *yaml.Node
|
yamlNode *yaml.Node
|
||||||
|
|
||||||
|
// Used internally. Cannot be controlled by frontmatter.
|
||||||
|
metaLength int
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderMetaModeFromString(s string) markup.RenderMetaMode {
|
func renderMetaModeFromString(s string) markup.RenderMetaMode {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue