diff --git a/src/extensions/yfm/Checkbox/Checkbox.test.ts b/src/extensions/yfm/Checkbox/Checkbox.test.ts
index cfbae01e..b31446c7 100644
--- a/src/extensions/yfm/Checkbox/Checkbox.test.ts
+++ b/src/extensions/yfm/Checkbox/Checkbox.test.ts
@@ -23,6 +23,7 @@ const {
const {
doc,
+ p,
b,
checkbox,
cbInput,
@@ -125,4 +126,70 @@ describe('Checkbox extension', () => {
[fixPastePlugin()],
);
});
+
+ it('should parse dom with multiple checkboxes without id', () => {
+ parseDOM(
+ schema,
+ `
+
+
+
+
+`,
+ doc(
+ checkbox(cbInput(), cbLabel('First checkbox')),
+ checkbox(cbInput({checked: 'true'}), cbLabel('Second checkbox')),
+ ),
+ [fixPastePlugin()],
+ );
+ });
+
+ it('should create empty label when next sibling is not a label', () => {
+ parseDOM(
+ schema,
+ `Not a label`,
+ doc(checkbox(cbInput(), cbLabel()), p('Not a label')),
+ [fixPastePlugin()],
+ );
+ });
+
+ it('should parse multiple checkboxes wrapped in div.checkbox', () => {
+ parseDOM(
+ schema,
+ `
+
+
+
+
+
+
+
+
`,
+ doc(
+ checkbox(cbInput(), cbLabel('Task 1')),
+ checkbox(cbInput({checked: 'true'}), cbLabel('Task 2')),
+ ),
+ [fixPastePlugin()],
+ );
+ });
+
+ it('should parse checkboxes with special characters in id', () => {
+ parseDOM(
+ schema,
+ `
+
+
+
+
+
+
+
+
`,
+ doc(
+ checkbox(cbInput(), cbLabel('Task with invalid ID')),
+ checkbox(cbInput({checked: 'true'}), cbLabel('Task with valid ID')),
+ ),
+ [fixPastePlugin()],
+ );
+ });
});
diff --git a/src/extensions/yfm/Checkbox/CheckboxSpecs/schema.ts b/src/extensions/yfm/Checkbox/CheckboxSpecs/schema.ts
index e34e8a81..d395a4fe 100644
--- a/src/extensions/yfm/Checkbox/CheckboxSpecs/schema.ts
+++ b/src/extensions/yfm/Checkbox/CheckboxSpecs/schema.ts
@@ -1,4 +1,4 @@
-import {Fragment, type NodeSpec} from 'prosemirror-model';
+import {Fragment, type NodeSpec, type Schema} from 'prosemirror-model';
import type {PlaceholderOptions} from '../../../../utils/placeholder';
@@ -26,36 +26,30 @@ export const getSchemaSpecs = (
tag: 'div.checkbox',
priority: 100,
getContent(node, schema) {
- const input = (node as HTMLElement).querySelector(
- 'input[type=checkbox]',
- );
- const label = (node as HTMLElement).querySelector(
- 'label[for]',
- );
+ if (node instanceof HTMLElement) {
+ const input = node.querySelector('input[type=checkbox]');
- const checked = input?.checked ? 'true' : null;
- const text = label?.textContent;
-
- return Fragment.from([
- checkboxInputType(schema).create({[CheckboxAttr.Checked]: checked}),
- checkboxLabelType(schema).create(null, text ? schema.text(text) : null),
- ]);
+ if (input && input instanceof HTMLInputElement) {
+ const label = findLabelForInput(input);
+ return createCheckboxFragment(
+ schema,
+ input.checked,
+ label?.textContent,
+ );
+ }
+ }
+ return Fragment.empty;
},
},
{
tag: 'input[type=checkbox]',
priority: 50,
- getContent(node, schema) {
- const id = (node as HTMLElement).id;
- const checked = (node as HTMLInputElement).checked ? 'true' : null;
- const text = node.parentNode?.querySelector(
- `label[for=${id}]`,
- )?.textContent;
-
- return Fragment.from([
- checkboxInputType(schema).create({[CheckboxAttr.Checked]: checked}),
- checkboxLabelType(schema).create(null, text ? schema.text(text) : null),
- ]);
+ getContent(input, schema) {
+ if (input instanceof HTMLInputElement) {
+ const label = findLabelForInput(input);
+ return createCheckboxFragment(schema, input.checked, label?.textContent);
+ }
+ return Fragment.empty;
},
},
],
@@ -94,7 +88,7 @@ export const getSchemaSpecs = (
{
// input handled by checkbox node parse rule
// ignore label
- tag: 'input[type=checkbox] ~ label[for]',
+ tag: 'input[type=checkbox] ~ label',
ignore: true,
consuming: true,
},
@@ -119,3 +113,24 @@ export const getSchemaSpecs = (
complex: 'leaf',
},
});
+
+// fallback for invalid HTML (input without id + label without for)
+function findNextSiblingLabel(element: HTMLInputElement): HTMLLabelElement | null {
+ const nextSibling = element.nextElementSibling;
+ return nextSibling instanceof HTMLLabelElement ? nextSibling : null;
+}
+
+function findLabelForInput(element: HTMLInputElement): HTMLLabelElement | null {
+ return element.labels?.[0] || findNextSiblingLabel(element);
+}
+
+function createCheckboxFragment(
+ schema: Schema,
+ checked: boolean | null,
+ labelText: string | null | undefined,
+): Fragment {
+ return Fragment.from([
+ checkboxInputType(schema).create({[CheckboxAttr.Checked]: checked ? 'true' : null}),
+ checkboxLabelType(schema).create(null, labelText ? schema.text(labelText) : null),
+ ]);
+}