mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 16:40:24 +08: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:
		@@ -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);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user