From 183cbf99f4a3997a3e6104a32613490192d5999f Mon Sep 17 00:00:00 2001 From: Rob Brackett Date: Sat, 30 Sep 2023 10:07:47 -0700 Subject: [PATCH] Add support for task list checkboxes outside `p` Closes GH-80. Closes GH-81. Reviewed-by: Titus Wormer --- lib/handlers/li.js | 98 ++++++++++++++++++++++++------------- test/fixtures/ol/index.html | 10 ++++ test/fixtures/ol/index.md | 8 +++ test/fixtures/ul/index.html | 10 ++++ test/fixtures/ul/index.md | 8 +++ 5 files changed, 101 insertions(+), 33 deletions(-) diff --git a/lib/handlers/li.js b/lib/handlers/li.js index 3d48465..7528d85 100644 --- a/lib/handlers/li.js +++ b/lib/handlers/li.js @@ -17,43 +17,17 @@ import {phrasing} from 'hast-util-phrasing' * mdast node. */ 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) - ] - } - } - } + // If the list item starts with a checkbox, remove the checkbox and mark the + // list item as a GFM task list item. + const {cleanNode, checkbox} = extractLeadingCheckbox(node) + const checked = checkbox && Boolean(checkbox.properties.checked) - if (!clone) clone = node - - const spread = spreadout(clone) - const children = state.toFlow(state.all(clone)) + const spread = spreadout(cleanNode) + const children = state.toFlow(state.all(cleanNode)) /** @type {ListItem} */ const result = {type: 'listItem', spread, checked, children} - state.patch(clone, result) + state.patch(cleanNode, result) return result } @@ -99,3 +73,61 @@ function spreadout(node) { return false } + +/** + * If the first bit of content in an element is a checkbox, create a copy of + * the element that does not include the checkbox and return the cleaned up + * copy alongside the checkbox that was removed. If there was no leading + * checkbox, this returns the original element unaltered (not a copy). + * + * This detects trees like: + * `
  • Text
  • ` + * And returns a tree like: + * `
  • Text
  • ` + * + * Or with nesting: + * `
  • Text

  • ` + * Which returns a tree like: + * `
  • Text

  • ` + * + * @param {Readonly} node + * @returns {{cleanNode: Element, checkbox: Element | null}} + */ +function extractLeadingCheckbox(node) { + const head = node.children[0] + + if ( + head && + head.type === 'element' && + head.tagName === 'input' && + head.properties && + (head.properties.type === 'checkbox' || head.properties.type === 'radio') + ) { + return { + cleanNode: {...node, children: node.children.slice(1)}, + checkbox: head + } + } + + // The checkbox may be nested in another element. If the first element has + // children, look for a leading checkbox inside it. + // + // NOTE: this only handles nesting in `

    ` elements, which is most common. + // It's possible a leading checkbox might be nested in other types of flow or + // phrasing elements (and *deeply* nested, which is not possible with `

    `). + // Limiting things to `

    ` elements keeps this simpler for now. + if (head && head.type === 'element' && head.tagName === 'p') { + const {cleanNode: cleanHead, checkbox} = extractLeadingCheckbox(head) + if (checkbox) { + return { + cleanNode: { + ...node, + children: [cleanHead, ...node.children.slice(1)] + }, + checkbox + } + } + } + + return {cleanNode: node, checkbox: null} +} diff --git a/test/fixtures/ol/index.html b/test/fixtures/ol/index.html index 4e6476d..9511261 100644 --- a/test/fixtures/ol/index.html +++ b/test/fixtures/ol/index.html @@ -59,4 +59,14 @@

  • Echo

  • Foxtrot

  • Golf

  • +
  • +

    + Hotel +

    +
  • +
  • India
  • +
  • + Juliet +
  • +
  • diff --git a/test/fixtures/ol/index.md b/test/fixtures/ol/index.md index fdc6e10..94c108a 100644 --- a/test/fixtures/ol/index.md +++ b/test/fixtures/ol/index.md @@ -57,3 +57,11 @@ Quuux. 5. [ ] **Foxtrot** 6. [ ] **Golf** + +7. [ ] Hotel + +8. [ ] India + +9. [ ] Juliet + +10. diff --git a/test/fixtures/ul/index.html b/test/fixtures/ul/index.html index 1a5cb62..5ae1bd0 100644 --- a/test/fixtures/ul/index.html +++ b/test/fixtures/ul/index.html @@ -59,4 +59,14 @@
  • Echo

  • Foxtrot

  • Golf

  • +
  • +

    + Hotel +

    +
  • +
  • India
  • +
  • + Juliet +
  • +
  • diff --git a/test/fixtures/ul/index.md b/test/fixtures/ul/index.md index c71e3f1..a268381 100644 --- a/test/fixtures/ul/index.md +++ b/test/fixtures/ul/index.md @@ -57,3 +57,11 @@ Quuux. * [ ] **Foxtrot** * [ ] **Golf** + +* [ ] Hotel + +* [ ] India + +* [ ] Juliet + +*