mirror of
https://github.com/stablyai/orca.git
synced 2026-11-04 11:07:42 +00:00
* fix(editor): preserve sibling tasks beneath ordered list items * fix(editor): preserve wrapped inline markup in task items * fix(editor): keep nested task continuations valid and editable * fix(editor): keep literal task markers out of mixed checklists * fix(editor): apply review fixes to task-list parsing stack - Trim synthetic test matrices to the behavioral cases - Regenerate pnpm-lock (drops a stale github-slugger importer entry) - Assert the plain bullet's node type under an ordered parent * fix(editor): keep one-column-deeper ordered tasks as sibling checkboxes Dedent an ordered item's task block by the first task's column instead of keeping its leading whitespace. A task one column deeper stays a sibling checkbox, and text at or within the first task's column stays nested as before. Strip leftover paragraph indent from task children. --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
313 lines
14 KiB
Diff
313 lines
14 KiB
Diff
diff --git a/dist/index.cjs b/dist/index.cjs
|
||
--- a/dist/index.cjs
|
||
+++ b/dist/index.cjs
|
||
@@ -245,10 +245,9 @@
|
||
];
|
||
const ALPHA_NUMERALS = "abcdefghijklmnopqrstuvwxyz";
|
||
/**
|
||
-* Marker segment for ordered list lines: numeric, roman, or 1–2 letter alpha.
|
||
-* Roman is matched before alpha so "iii" is roman; invalid romans like "aa" fall through to alpha.
|
||
+* Orca patch: ordered-list markers are digits only, as in CommonMark, so "Dr." and "A." stay prose.
|
||
*/
|
||
-const ORDERED_LIST_MARKER_PATTERN = String.raw`\d+|[ivxlcdmIVXLCDM]+|${"[a-zA-Z]{1,2}"}`;
|
||
+const ORDERED_LIST_MARKER_PATTERN = String.raw`\d+`;
|
||
/**
|
||
* Convert a number to lowercase roman numerals.
|
||
* @example toRoman(1) // 'i'
|
||
@@ -897,6 +896,26 @@
|
||
};
|
||
}
|
||
/**
|
||
+* Orca patch: marker columns are counted without the delimiter, so trim() left later sibling tasks indented and
|
||
+* parseIndentedBlocks cut into them. Dedent by the first task; a task one column deeper is a sibling, and other
|
||
+* text at or within the first task's column stays nested under the task above.
|
||
+*/
|
||
+function dedentOrderedItemTaskBlock(blockLines) {
|
||
+ const isTaskLine = (line) => /^\s*[-+*]\s+\[[ xX]\][^\S\r\n]/.test(line);
|
||
+ const leadingWidth = (line) => line.length - line.trimStart().length;
|
||
+ const firstIndex = blockLines.findIndex((line) => line.trim() !== "");
|
||
+ if (firstIndex === -1 || !isTaskLine(blockLines[firstIndex])) return null;
|
||
+ const firstIndent = leadingWidth(blockLines[firstIndex]);
|
||
+ return blockLines.slice(firstIndex).map((line) => {
|
||
+ if (line.trim() === "") return "";
|
||
+ const indent = leadingWidth(line);
|
||
+ const depth = indent - firstIndent;
|
||
+ if (isTaskLine(line)) return depth === 1 ? line.trimStart() : line.slice(Math.min(indent, firstIndent));
|
||
+ if (depth > 0) return depth === 1 ? ` ${line.slice(firstIndent)}` : line.slice(firstIndent);
|
||
+ return indent > 0 ? ` ${line.trimStart()}` : line;
|
||
+ }).join("\n").trimEnd();
|
||
+}
|
||
+/**
|
||
* Recursively builds a nested structure from a flat array of list items
|
||
* based on their indentation levels. Creates proper markdown tokens with
|
||
* nested lists where appropriate.
|
||
@@ -920,7 +939,7 @@
|
||
raw: mainText,
|
||
tokens: lexer.inlineTokens(mainText)
|
||
});
|
||
- const additionalContent = blockLines.join("\n").trim();
|
||
+ const additionalContent = dedentOrderedItemTaskBlock(blockLines) ?? blockLines.join("\n").trim();
|
||
if (additionalContent) {
|
||
const blockTokens = lexer.blockTokens(additionalContent);
|
||
tokens.push(...blockTokens);
|
||
diff --git a/dist/index.js b/dist/index.js
|
||
--- a/dist/index.js
|
||
+++ b/dist/index.js
|
||
@@ -233,10 +233,9 @@
|
||
];
|
||
const ALPHA_NUMERALS = "abcdefghijklmnopqrstuvwxyz";
|
||
/**
|
||
-* Marker segment for ordered list lines: numeric, roman, or 1–2 letter alpha.
|
||
-* Roman is matched before alpha so "iii" is roman; invalid romans like "aa" fall through to alpha.
|
||
+* Orca patch: ordered-list markers are digits only, as in CommonMark, so "Dr." and "A." stay prose.
|
||
*/
|
||
-const ORDERED_LIST_MARKER_PATTERN = String.raw`\d+|[ivxlcdmIVXLCDM]+|${"[a-zA-Z]{1,2}"}`;
|
||
+const ORDERED_LIST_MARKER_PATTERN = String.raw`\d+`;
|
||
/**
|
||
* Convert a number to lowercase roman numerals.
|
||
* @example toRoman(1) // 'i'
|
||
@@ -885,6 +884,26 @@
|
||
};
|
||
}
|
||
/**
|
||
+* Orca patch: marker columns are counted without the delimiter, so trim() left later sibling tasks indented and
|
||
+* parseIndentedBlocks cut into them. Dedent by the first task; a task one column deeper is a sibling, and other
|
||
+* text at or within the first task's column stays nested under the task above.
|
||
+*/
|
||
+function dedentOrderedItemTaskBlock(blockLines) {
|
||
+ const isTaskLine = (line) => /^\s*[-+*]\s+\[[ xX]\][^\S\r\n]/.test(line);
|
||
+ const leadingWidth = (line) => line.length - line.trimStart().length;
|
||
+ const firstIndex = blockLines.findIndex((line) => line.trim() !== "");
|
||
+ if (firstIndex === -1 || !isTaskLine(blockLines[firstIndex])) return null;
|
||
+ const firstIndent = leadingWidth(blockLines[firstIndex]);
|
||
+ return blockLines.slice(firstIndex).map((line) => {
|
||
+ if (line.trim() === "") return "";
|
||
+ const indent = leadingWidth(line);
|
||
+ const depth = indent - firstIndent;
|
||
+ if (isTaskLine(line)) return depth === 1 ? line.trimStart() : line.slice(Math.min(indent, firstIndent));
|
||
+ if (depth > 0) return depth === 1 ? ` ${line.slice(firstIndent)}` : line.slice(firstIndent);
|
||
+ return indent > 0 ? ` ${line.trimStart()}` : line;
|
||
+ }).join("\n").trimEnd();
|
||
+}
|
||
+/**
|
||
* Recursively builds a nested structure from a flat array of list items
|
||
* based on their indentation levels. Creates proper markdown tokens with
|
||
* nested lists where appropriate.
|
||
@@ -908,7 +927,7 @@
|
||
raw: mainText,
|
||
tokens: lexer.inlineTokens(mainText)
|
||
});
|
||
- const additionalContent = blockLines.join("\n").trim();
|
||
+ const additionalContent = dedentOrderedItemTaskBlock(blockLines) ?? blockLines.join("\n").trim();
|
||
if (additionalContent) {
|
||
const blockTokens = lexer.blockTokens(additionalContent);
|
||
tokens.push(...blockTokens);
|
||
diff --git a/dist/kit/index.cjs b/dist/kit/index.cjs
|
||
--- a/dist/kit/index.cjs
|
||
+++ b/dist/kit/index.cjs
|
||
@@ -233,10 +233,9 @@
|
||
];
|
||
const ALPHA_NUMERALS = "abcdefghijklmnopqrstuvwxyz";
|
||
/**
|
||
-* Marker segment for ordered list lines: numeric, roman, or 1–2 letter alpha.
|
||
-* Roman is matched before alpha so "iii" is roman; invalid romans like "aa" fall through to alpha.
|
||
+* Orca patch: ordered-list markers are digits only, as in CommonMark, so "Dr." and "A." stay prose.
|
||
*/
|
||
-const ORDERED_LIST_MARKER_PATTERN = String.raw`\d+|[ivxlcdmIVXLCDM]+|${"[a-zA-Z]{1,2}"}`;
|
||
+const ORDERED_LIST_MARKER_PATTERN = String.raw`\d+`;
|
||
/**
|
||
* Convert a number to lowercase roman numerals.
|
||
* @example toRoman(1) // 'i'
|
||
@@ -839,6 +838,26 @@
|
||
};
|
||
}
|
||
/**
|
||
+* Orca patch: marker columns are counted without the delimiter, so trim() left later sibling tasks indented and
|
||
+* parseIndentedBlocks cut into them. Dedent by the first task; a task one column deeper is a sibling, and other
|
||
+* text at or within the first task's column stays nested under the task above.
|
||
+*/
|
||
+function dedentOrderedItemTaskBlock(blockLines) {
|
||
+ const isTaskLine = (line) => /^\s*[-+*]\s+\[[ xX]\][^\S\r\n]/.test(line);
|
||
+ const leadingWidth = (line) => line.length - line.trimStart().length;
|
||
+ const firstIndex = blockLines.findIndex((line) => line.trim() !== "");
|
||
+ if (firstIndex === -1 || !isTaskLine(blockLines[firstIndex])) return null;
|
||
+ const firstIndent = leadingWidth(blockLines[firstIndex]);
|
||
+ return blockLines.slice(firstIndex).map((line) => {
|
||
+ if (line.trim() === "") return "";
|
||
+ const indent = leadingWidth(line);
|
||
+ const depth = indent - firstIndent;
|
||
+ if (isTaskLine(line)) return depth === 1 ? line.trimStart() : line.slice(Math.min(indent, firstIndent));
|
||
+ if (depth > 0) return depth === 1 ? ` ${line.slice(firstIndent)}` : line.slice(firstIndent);
|
||
+ return indent > 0 ? ` ${line.trimStart()}` : line;
|
||
+ }).join("\n").trimEnd();
|
||
+}
|
||
+/**
|
||
* Recursively builds a nested structure from a flat array of list items
|
||
* based on their indentation levels. Creates proper markdown tokens with
|
||
* nested lists where appropriate.
|
||
@@ -862,7 +881,7 @@
|
||
raw: mainText,
|
||
tokens: lexer.inlineTokens(mainText)
|
||
});
|
||
- const additionalContent = blockLines.join("\n").trim();
|
||
+ const additionalContent = dedentOrderedItemTaskBlock(blockLines) ?? blockLines.join("\n").trim();
|
||
if (additionalContent) {
|
||
const blockTokens = lexer.blockTokens(additionalContent);
|
||
tokens.push(...blockTokens);
|
||
diff --git a/dist/kit/index.js b/dist/kit/index.js
|
||
--- a/dist/kit/index.js
|
||
+++ b/dist/kit/index.js
|
||
@@ -232,10 +232,9 @@
|
||
];
|
||
const ALPHA_NUMERALS = "abcdefghijklmnopqrstuvwxyz";
|
||
/**
|
||
-* Marker segment for ordered list lines: numeric, roman, or 1–2 letter alpha.
|
||
-* Roman is matched before alpha so "iii" is roman; invalid romans like "aa" fall through to alpha.
|
||
+* Orca patch: ordered-list markers are digits only, as in CommonMark, so "Dr." and "A." stay prose.
|
||
*/
|
||
-const ORDERED_LIST_MARKER_PATTERN = String.raw`\d+|[ivxlcdmIVXLCDM]+|${"[a-zA-Z]{1,2}"}`;
|
||
+const ORDERED_LIST_MARKER_PATTERN = String.raw`\d+`;
|
||
/**
|
||
* Convert a number to lowercase roman numerals.
|
||
* @example toRoman(1) // 'i'
|
||
@@ -838,6 +837,26 @@
|
||
};
|
||
}
|
||
/**
|
||
+* Orca patch: marker columns are counted without the delimiter, so trim() left later sibling tasks indented and
|
||
+* parseIndentedBlocks cut into them. Dedent by the first task; a task one column deeper is a sibling, and other
|
||
+* text at or within the first task's column stays nested under the task above.
|
||
+*/
|
||
+function dedentOrderedItemTaskBlock(blockLines) {
|
||
+ const isTaskLine = (line) => /^\s*[-+*]\s+\[[ xX]\][^\S\r\n]/.test(line);
|
||
+ const leadingWidth = (line) => line.length - line.trimStart().length;
|
||
+ const firstIndex = blockLines.findIndex((line) => line.trim() !== "");
|
||
+ if (firstIndex === -1 || !isTaskLine(blockLines[firstIndex])) return null;
|
||
+ const firstIndent = leadingWidth(blockLines[firstIndex]);
|
||
+ return blockLines.slice(firstIndex).map((line) => {
|
||
+ if (line.trim() === "") return "";
|
||
+ const indent = leadingWidth(line);
|
||
+ const depth = indent - firstIndent;
|
||
+ if (isTaskLine(line)) return depth === 1 ? line.trimStart() : line.slice(Math.min(indent, firstIndent));
|
||
+ if (depth > 0) return depth === 1 ? ` ${line.slice(firstIndent)}` : line.slice(firstIndent);
|
||
+ return indent > 0 ? ` ${line.trimStart()}` : line;
|
||
+ }).join("\n").trimEnd();
|
||
+}
|
||
+/**
|
||
* Recursively builds a nested structure from a flat array of list items
|
||
* based on their indentation levels. Creates proper markdown tokens with
|
||
* nested lists where appropriate.
|
||
@@ -861,7 +880,7 @@
|
||
raw: mainText,
|
||
tokens: lexer.inlineTokens(mainText)
|
||
});
|
||
- const additionalContent = blockLines.join("\n").trim();
|
||
+ const additionalContent = dedentOrderedItemTaskBlock(blockLines) ?? blockLines.join("\n").trim();
|
||
if (additionalContent) {
|
||
const blockTokens = lexer.blockTokens(additionalContent);
|
||
tokens.push(...blockTokens);
|
||
diff --git a/dist/ordered-list/index.cjs b/dist/ordered-list/index.cjs
|
||
--- a/dist/ordered-list/index.cjs
|
||
+++ b/dist/ordered-list/index.cjs
|
||
@@ -19,10 +19,9 @@
|
||
];
|
||
const ALPHA_NUMERALS = "abcdefghijklmnopqrstuvwxyz";
|
||
/**
|
||
-* Marker segment for ordered list lines: numeric, roman, or 1–2 letter alpha.
|
||
-* Roman is matched before alpha so "iii" is roman; invalid romans like "aa" fall through to alpha.
|
||
+* Orca patch: ordered-list markers are digits only, as in CommonMark, so "Dr." and "A." stay prose.
|
||
*/
|
||
-const ORDERED_LIST_MARKER_PATTERN = String.raw`\d+|[ivxlcdmIVXLCDM]+|${"[a-zA-Z]{1,2}"}`;
|
||
+const ORDERED_LIST_MARKER_PATTERN = String.raw`\d+`;
|
||
/**
|
||
* Convert a number to lowercase roman numerals.
|
||
* @example toRoman(1) // 'i'
|
||
@@ -333,6 +332,26 @@
|
||
};
|
||
}
|
||
/**
|
||
+* Orca patch: marker columns are counted without the delimiter, so trim() left later sibling tasks indented and
|
||
+* parseIndentedBlocks cut into them. Dedent by the first task; a task one column deeper is a sibling, and other
|
||
+* text at or within the first task's column stays nested under the task above.
|
||
+*/
|
||
+function dedentOrderedItemTaskBlock(blockLines) {
|
||
+ const isTaskLine = (line) => /^\s*[-+*]\s+\[[ xX]\][^\S\r\n]/.test(line);
|
||
+ const leadingWidth = (line) => line.length - line.trimStart().length;
|
||
+ const firstIndex = blockLines.findIndex((line) => line.trim() !== "");
|
||
+ if (firstIndex === -1 || !isTaskLine(blockLines[firstIndex])) return null;
|
||
+ const firstIndent = leadingWidth(blockLines[firstIndex]);
|
||
+ return blockLines.slice(firstIndex).map((line) => {
|
||
+ if (line.trim() === "") return "";
|
||
+ const indent = leadingWidth(line);
|
||
+ const depth = indent - firstIndent;
|
||
+ if (isTaskLine(line)) return depth === 1 ? line.trimStart() : line.slice(Math.min(indent, firstIndent));
|
||
+ if (depth > 0) return depth === 1 ? ` ${line.slice(firstIndent)}` : line.slice(firstIndent);
|
||
+ return indent > 0 ? ` ${line.trimStart()}` : line;
|
||
+ }).join("\n").trimEnd();
|
||
+}
|
||
+/**
|
||
* Recursively builds a nested structure from a flat array of list items
|
||
* based on their indentation levels. Creates proper markdown tokens with
|
||
* nested lists where appropriate.
|
||
@@ -356,7 +375,7 @@
|
||
raw: mainText,
|
||
tokens: lexer.inlineTokens(mainText)
|
||
});
|
||
- const additionalContent = blockLines.join("\n").trim();
|
||
+ const additionalContent = dedentOrderedItemTaskBlock(blockLines) ?? blockLines.join("\n").trim();
|
||
if (additionalContent) {
|
||
const blockTokens = lexer.blockTokens(additionalContent);
|
||
tokens.push(...blockTokens);
|
||
diff --git a/dist/ordered-list/index.js b/dist/ordered-list/index.js
|
||
--- a/dist/ordered-list/index.js
|
||
+++ b/dist/ordered-list/index.js
|
||
@@ -18,10 +18,9 @@
|
||
];
|
||
const ALPHA_NUMERALS = "abcdefghijklmnopqrstuvwxyz";
|
||
/**
|
||
-* Marker segment for ordered list lines: numeric, roman, or 1–2 letter alpha.
|
||
-* Roman is matched before alpha so "iii" is roman; invalid romans like "aa" fall through to alpha.
|
||
+* Orca patch: ordered-list markers are digits only, as in CommonMark, so "Dr." and "A." stay prose.
|
||
*/
|
||
-const ORDERED_LIST_MARKER_PATTERN = String.raw`\d+|[ivxlcdmIVXLCDM]+|${"[a-zA-Z]{1,2}"}`;
|
||
+const ORDERED_LIST_MARKER_PATTERN = String.raw`\d+`;
|
||
/**
|
||
* Convert a number to lowercase roman numerals.
|
||
* @example toRoman(1) // 'i'
|
||
@@ -332,6 +331,26 @@
|
||
};
|
||
}
|
||
/**
|
||
+* Orca patch: marker columns are counted without the delimiter, so trim() left later sibling tasks indented and
|
||
+* parseIndentedBlocks cut into them. Dedent by the first task; a task one column deeper is a sibling, and other
|
||
+* text at or within the first task's column stays nested under the task above.
|
||
+*/
|
||
+function dedentOrderedItemTaskBlock(blockLines) {
|
||
+ const isTaskLine = (line) => /^\s*[-+*]\s+\[[ xX]\][^\S\r\n]/.test(line);
|
||
+ const leadingWidth = (line) => line.length - line.trimStart().length;
|
||
+ const firstIndex = blockLines.findIndex((line) => line.trim() !== "");
|
||
+ if (firstIndex === -1 || !isTaskLine(blockLines[firstIndex])) return null;
|
||
+ const firstIndent = leadingWidth(blockLines[firstIndex]);
|
||
+ return blockLines.slice(firstIndex).map((line) => {
|
||
+ if (line.trim() === "") return "";
|
||
+ const indent = leadingWidth(line);
|
||
+ const depth = indent - firstIndent;
|
||
+ if (isTaskLine(line)) return depth === 1 ? line.trimStart() : line.slice(Math.min(indent, firstIndent));
|
||
+ if (depth > 0) return depth === 1 ? ` ${line.slice(firstIndent)}` : line.slice(firstIndent);
|
||
+ return indent > 0 ? ` ${line.trimStart()}` : line;
|
||
+ }).join("\n").trimEnd();
|
||
+}
|
||
+/**
|
||
* Recursively builds a nested structure from a flat array of list items
|
||
* based on their indentation levels. Creates proper markdown tokens with
|
||
* nested lists where appropriate.
|
||
@@ -355,7 +374,7 @@
|
||
raw: mainText,
|
||
tokens: lexer.inlineTokens(mainText)
|
||
});
|
||
- const additionalContent = blockLines.join("\n").trim();
|
||
+ const additionalContent = dedentOrderedItemTaskBlock(blockLines) ?? blockLines.join("\n").trim();
|
||
if (additionalContent) {
|
||
const blockTokens = lexer.blockTokens(additionalContent);
|
||
tokens.push(...blockTokens);
|