Skip to content

Tasklists don't work if checkbox is a direct child of a list item #80

Closed
@Mr0grog

Description

@Mr0grog

Initial checklist

Affected packages and versions

has-util-to-mdast@10.0.0

Link to runnable example

No response

Steps to reproduce

Tasklists (lists where each item starts with a checkbox) are parsed correctly if the leading checkbox <input> element in an <li> node is nested in a <p> node, but not if the <input> is a direct child of the <li>. I’m not sure why this is a requirement (it’s certainly not in the HTML spec: https://html.spec.whatwg.org/multipage/grouping-content.html#the-li-element), so I assume it’s a bug. (But I appreciate also handling this common case where things are in a paragraph in the list item, which is best practice HTML.)

You can see where this explicit check for a <p> is implemented in lib/handlers/li.js:

export function li(state, node) {
const head = node.children[0]
/** @type {boolean | null} */
let checked = null
/** @type {Element | undefined} */
let clone
// Check if this node starts with a checkbox.
if (head && head.type === 'element' && head.tagName === 'p') {
const checkbox = head.children[0]
if (
checkbox &&
checkbox.type === 'element' &&
checkbox.tagName === 'input' &&
checkbox.properties &&
(checkbox.properties.type === 'checkbox' ||
checkbox.properties.type === 'radio')
) {
checked = Boolean(checkbox.properties.checked)
clone = {
...node,
children: [
{...head, children: head.children.slice(1)},
...node.children.slice(1)
]
}
}

I used this script to test:

import {inspect} from 'node:util';
import {toMdast} from 'hast-util-to-mdast';

const mdast = toMdast(hastTree);
console.log(inspect(mdast, false, 100, true));

Expected behavior

I expected that this script:

import {inspect} from 'node:util';
import {toMdast} from 'hast-util-to-mdast';

const mdast = toMdast({
  type: 'root',
  children: [
    {
      type: 'element',
      tagName: 'ul',
      children: [
        {
          type: 'element',
          tagName: 'li',
          children: [
            {
              type: 'element',
              tagName: 'input',
              properties: { type: 'checkbox', checked: true }
            },
            {
              type: 'text',
              value: 'Checked'
            }
          ]
        },
        {
          type: 'element',
          tagName: 'li',
          children: [
            {
              type: 'element',
              tagName: 'input',
              properties: { type: 'checkbox', checked: false }
            },
            {
              type: 'text',
              value: 'Unhecked'
            }
          ]
        }
      ]
    }
  ]
});

console.log(inspect(mdast, false, 100, true));

…to output:

{
  type: 'root',
  children: [
    {
      type: 'list',
      ordered: false,
      start: null,
      spread: false,
      children: [
        {
          type: 'listItem',
          spread: false,
          checked: true,
          children: [
            {
              type: 'paragraph',
              children: [ { type: 'text', value: 'Checked' } ]
            }
          ]
        },
        {
          type: 'listItem',
          spread: false,
          checked: false,
          children: [
            {
              type: 'paragraph',
              children: [ { type: 'text', value: 'Unhecked' } ]
            }
          ]
        }
      ]
    }
  ]
}

…which corresponds to this Markdown:

- [x] Checked
- [ ] Unchecked

Actual behavior

Instead, the above script outputs:

{
  type: 'root',
  children: [
    {
      type: 'list',
      ordered: false,
      start: null,
      spread: false,
      children: [
        {
          type: 'listItem',
          spread: false,
          checked: null,
          children: [
            {
              type: 'paragraph',
              children: [ { type: 'text', value: '[x]Checked' } ]
            }
          ]
        },
        {
          type: 'listItem',
          spread: false,
          checked: null,
          children: [
            {
              type: 'paragraph',
              children: [ { type: 'text', value: '[ ]Unhecked' } ]
            }
          ]
        }
      ]
    }
  ]
}

…which corresponds to this Markdown:

- \[x]Checked
- \[ ]Unchecked

Instead, to get the expected output, you. need to do:

import {inspect} from 'node:util';
import {toMdast} from 'hast-util-to-mdast';

const mdast = toMdast({
  type: 'root',
  children: [
    {
      type: 'element',
      tagName: 'ul',
      children: [
        {
          type: 'element',
          tagName: 'li',
          children: [
            {
              type: 'element',
              tagName: 'p',
              children: [
                {
                  type: 'element',
                  tagName: 'input',
                  properties: { type: 'checkbox', checked: true }
                },
                {
                  type: 'text',
                  value: 'Checked'
                }
              ]
            }
          ]
        },
        {
          type: 'element',
          tagName: 'li',
          children: [
            {
              type: 'element',
              tagName: 'p',
              children: [
                {
                  type: 'element',
                  tagName: 'input',
                  properties: { type: 'checkbox', checked: false }
                },
                {
                  type: 'text',
                  value: 'Unhecked'
                }
              ]
            }
          ]
        }
      ]
    }
  ]
});

console.log(inspect(mdast, false, 100, true));

Affected runtime and version

node>=16

Affected package manager and version

npm@9.5.1

Affected OS and version

macOS 13.5

Build and bundle tools

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions