Files
outline/shared/editor/rules/checkboxes.ts
T
Tom Moor adbffc0734 chore: clear mechanical lint warnings (Phase 1) (#12198)
* chore: clear mechanical lint warnings

Drops 44 oxlint warnings (559 → 515) by fixing easy mechanical rules
across the codebase: no-useless-escape, no-duplicate-type-constituents,
no-redundant-type-constituents, no-unused-expressions,
no-meaningless-void-operator, require-array-sort-compare, await-thenable.

* chore: drop callback parameter from useCallback deps

The `open` argument is a parameter of the callback, not a closed-over
variable, so it doesn't belong in the deps array.

* chore: promote cleared lint rules to errors

Promotes the rules cleared in this PR from warn to error so future
violations fail the lint:

- no-unused-expressions
- typescript/await-thenable
- typescript/no-duplicate-type-constituents
- typescript/no-meaningless-void-operator
- typescript/require-array-sort-compare

Removes the override that suppressed no-useless-escape on source
files (the global rule is already error) and fixes the 21 escape
violations that this exposed in regex character classes and template
literals.

* chore: address PR review feedback

- usePinnedDocuments: simplify UrlId to plain string instead of the
  intersection trick.
- PlantUML embed: move - to end of character class so it's a literal
  hyphen rather than a range operator.
- checkboxes: type token params as Token | undefined to match the
  actual call sites that pass tokens[index - 2] etc.
2026-04-28 20:00:03 -04:00

135 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { Token } from "markdown-it";
import type MarkdownIt from "markdown-it";
const CHECKBOX_REGEX = /\[(X|\s|_|-)\]\s(.*)?/i;
function matches(token: Token | undefined) {
return token && token.content.match(CHECKBOX_REGEX);
}
function isInline(token: Token | undefined): boolean {
return !!token && token.type === "inline";
}
function isParagraph(token: Token | undefined): boolean {
return !!token && token.type === "paragraph_open";
}
function isListItem(token: Token | undefined): boolean {
// Only match list_item_open, not checkbox_item_open - items that are already
// checkbox_item_open have been processed (e.g., by the tables rule for
// checkboxes in table cells) and should not be processed again.
return !!token && token.type === "list_item_open";
}
function looksLikeChecklist(tokens: Token[], index: number) {
return (
isInline(tokens[index]) &&
isListItem(tokens[index - 2]) &&
isParagraph(tokens[index - 1]) &&
matches(tokens[index])
);
}
export default function markdownItCheckbox(md: MarkdownIt): void {
function render(tokens: Token[], idx: number) {
const token = tokens[idx];
const checked = !!token.attrGet("checked");
if (token.nesting === 1) {
// opening tag
return `<li class="checkbox-list-item"><span class="checkbox ${
checked ? "checked" : ""
}">${checked ? "[x]" : "[ ]"}</span>`;
} else {
// closing tag
return "</li>\n";
}
}
md.renderer.rules.checkbox_item_open = render;
md.renderer.rules.checkbox_item_close = render;
// insert a new rule after the "inline" rules are parsed
md.core.ruler.after("inline", "checkboxes", (state) => {
const tokens = state.tokens;
// work backwards through the tokens and find text that looks like a checkbox
for (let i = tokens.length - 1; i > 0; i--) {
const matchesChecklist = looksLikeChecklist(tokens, i);
if (matchesChecklist) {
const value = matchesChecklist[1];
const checked = value.toLowerCase() === "x";
// convert surrounding list tokens
if (tokens[i - 3].type === "bullet_list_open") {
tokens[i - 3].type = "checkbox_list_open";
}
if (tokens[i + 3].type === "bullet_list_close") {
tokens[i + 3].type = "checkbox_list_close";
}
// remove [ ] [x] from list item label must use the content from the
// child for escaped characters to be unescaped correctly.
const tokenChildren = tokens[i].children;
if (tokenChildren && tokenChildren[0].type === "text") {
const contentMatches = tokenChildren[0].content.match(CHECKBOX_REGEX);
if (contentMatches) {
const label = contentMatches[2];
tokens[i].content = label;
tokenChildren[0].content = label;
}
}
// open list item and ensure checked state is transferred
tokens[i - 2].type = "checkbox_item_open";
if (checked === true) {
tokens[i - 2].attrs = [["checked", "true"]];
}
// close the list item
let j = i;
while (
tokens[j] &&
tokens[j].type !== "list_item_close" &&
tokens[j].type !== "checkbox_item_close"
) {
j++;
}
if (tokens[j]) {
tokens[j].type = "checkbox_item_close";
}
}
}
// Second pass: convert any remaining direct child list_item tokens inside
// a checkbox_list to checkbox_item so they aren't silently dropped by the
// Prosemirror schema which requires checkbox_item+ children.
const checkboxListOpenLevels: number[] = [];
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (token.type === "checkbox_list_open") {
checkboxListOpenLevels.push(token.level);
} else if (token.type === "checkbox_list_close") {
checkboxListOpenLevels.pop();
} else if (checkboxListOpenLevels.length > 0) {
const checkboxListOpenLevel =
checkboxListOpenLevels[checkboxListOpenLevels.length - 1];
const isDirectChild = token.level === checkboxListOpenLevel + 1;
if (isDirectChild && token.type === "list_item_open") {
token.type = "checkbox_item_open";
} else if (isDirectChild && token.type === "list_item_close") {
token.type = "checkbox_item_close";
}
}
}
return false;
});
}