fix: handle task list items checked with uppercase X (#40334) (#40339)

Backport #40334 by @SudhanshuMatrix

Markdown renders `[X]` as a checked task like `[x]`, but parts of the
frontend only recognize lowercase `x`.

- Clicking a `[X]` checkbox now unchecks it instead of failing.
- Pressing Enter on a `- [X]` line in the editor continues the list with
`- [ ] ` instead of dropping the checkbox.

Assisted-by: Claude Code:claude-opus-5-5

Co-authored-by: Shudhanshu Singh <sudhanshuwriterblc@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
authored and GitHub committed 2026-11-12 11:56:50 +00:00
1 parent b24cd7839b
commit 78698abee0
4 files changed
+8 -7

No files matched your search

@@ -199,5 +199,5 @@ test('EditorMarkdown', () => {
testInput('- [ ]', '- [ ]\n- ');
testInput('- [ ] foo', '- [ ] foo\n- [ ] ');
testInput('* [x] foo', '* [x] foo\n* [ ] ');
testInput('1. [x] foo', '1. [x] foo\n2. [ ] ');
testInput('1. [X] foo', '1. [X] foo\n2. [ ] ');
});
+3 -3
View File
@@ -148,9 +148,9 @@ export function markdownHandleIndention(tvs: TextareaValueSelection): MarkdownHa
lineContent = lineContent.slice(indention.length);
if (linesBuf.inlinePos <= indention.length) return unhandled; // if cursor is at the indention, do nothing, let the browser handle it
// parse the prefixes: "1. ", "- ", "* ", there could also be " [ ] " or " [x] " for task lists
// parse the prefixes: "1. ", "- ", "* ", there could also be " [ ] ", " [x] " or " [X] " for task lists
// there must be a space after the prefix because none of "1.foo" / "-foo" is a list item
const prefixMatch = /^([0-9]+\.|[-*])(\s\[([ x])\])?\s/.exec(lineContent);
const prefixMatch = /^([0-9]+\.|[-*])(\s\[([ xX])\])?\s/.exec(lineContent);
let prefix = '';
if (prefixMatch) {
prefix = prefixMatch[0];
@@ -168,7 +168,7 @@ export function markdownHandleIndention(tvs: TextareaValueSelection): MarkdownHa
// start a new line with the same indention
let newPrefix = prefix;
if (/^\d+\./.test(prefix)) newPrefix = `1. ${newPrefix.slice(newPrefix.indexOf('.') + 2)}`;
newPrefix = newPrefix.replace('[x]', '[ ]');
newPrefix = newPrefix.replace(/\[[xX]\]/, '[ ]');
const inlinePos = linesBuf.inlinePos;
linesBuf.lines[linesBuf.posLineIndex] = line.substring(0, inlinePos);
+1
View File
@@ -3,6 +3,7 @@ import {toggleTasklistCheckbox} from './tasklist.ts';
test('toggleTasklistCheckbox', () => {
expect(toggleTasklistCheckbox('- [ ] task', 3, true)).toEqual('- [x] task');
expect(toggleTasklistCheckbox('- [x] task', 3, false)).toEqual('- [ ] task');
expect(toggleTasklistCheckbox('- [X] task', 3, false)).toEqual('- [ ] task');
expect(toggleTasklistCheckbox('- [ ] task', 0, true)).toBeNull();
expect(toggleTasklistCheckbox('- [ ] task', 99, true)).toBeNull();
expect(toggleTasklistCheckbox('😀 - [ ] task', 8, true)).toEqual('😀 - [x] task');
+3 -3
View File
@@ -5,14 +5,14 @@ const preventListener = (e: Event) => e.preventDefault();
/**
* Toggle a task list checkbox in markdown content.
* `position` is the byte offset of the space or `x` character inside `[ ]`.
* `position` is the byte offset of the space, `x` or `X` character inside `[ ]`.
* Returns the updated content, or null if the position is invalid.
*/
export function toggleTasklistCheckbox(content: string, position: number, checked: boolean): string | null {
const buffer = new TextEncoder().encode(content);
// Indexes may fall off the ends and return undefined.
if (buffer[position - 1] !== '['.charCodeAt(0) ||
buffer[position] !== ' '.charCodeAt(0) && buffer[position] !== 'x'.charCodeAt(0) ||
!' xX'.includes(String.fromCharCode(buffer[position])) ||
buffer[position + 1] !== ']'.charCodeAt(0)) {
return null;
}
@@ -49,7 +49,7 @@ export function initMarkupTasklist(elMarkup: HTMLElement): void {
if (newContent === null) {
// 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}`);
throw new Error(`Expected position to be space, x or X and surrounded by brackets, but it's not: position=${position}`);
}
if (newContent === oldContent) {