fix(app): allow additive task lists and admonitions in native prose

This commit is contained in:
Mohamed Boudra
2026-10-15 15:32:02 +02:00
parent ae649cf7ec
commit 9ef289e7f4
4 changed files with 30 additions and 14 deletions
+2 -2
View File
@@ -15,11 +15,11 @@ commonmark: strike=0 tasks=0 quotes=1 spoilers=1
github: strike=1 tasks=1 quotes=1 spoilers=1
```
Paseo's existing parser does not implement task lists. Escape task markers during inline preparation so `[ ]` and `[x]` remain literal text after a bullet, in both streaming and settled replies. Leave task toggling disabled.
GitHub flavor renders `[ ]` and `[x]` list items as checkboxes. Toggling is disabled because changes would not persist. Preserve task markers in both streaming and settled replies.
## Compatibility constraints
- Disable `latexMath`. Dollar-delimited text must stay literal. Disable admonitions too; they would reinterpret existing quote content.
- Disable `latexMath`. Dollar-delimited text must stay literal. Enable admonitions for `[!NOTE]`-style quotes; they add formatting without hiding the quote content. Ordinary quotes keep the existing blockquote style mapping.
- Spoilers have no public parser switch in 1.1.1. Prepare inline text with escaped pipes so `||text||` stays visible. Preserve pipes inside code and link destinations.
- `lineHeight` is a minimum. Use the Markdown theme's line height; font metrics can still increase it.
- Strikethrough color is iOS-only. Android uses the surrounding text color.
@@ -91,13 +91,17 @@ it.each([false, true])(
},
);
it.each(["- [ ] Task", "- [x] Done", "1. [X] Done", "- parent\n - [ ] Nested task"])(
"keeps task markers literal through streaming and settlement: %s",
(text) => {
const settled = prepareProse(text, noFiles);
const streaming = prepareProse(text, noFiles, true);
expect(settled).toMatch(/\\\[/);
expect(streaming).toBe(settled);
expect(parser.render(settled)).toBe(parser.render(text));
},
);
it.each([
"- [ ] Task",
"- [x] Done",
"1. [X] Done",
"- parent\n - [ ] Nested task",
"> [!NOTE]\n> Keep this note",
"> Ordinary quote",
])("preserves native block markers through streaming and settlement: %s", (text) => {
const settled = prepareProse(text, noFiles);
const streaming = prepareProse(text, noFiles, true);
expect(settled).toBe(text);
expect(streaming).toBe(settled);
expect(parser.render(settled)).toBe(parser.render(text));
});
@@ -60,6 +60,15 @@ function renderInline(tokens: Token[], canResolveCode: (text: string) => boolean
.join("");
}
function preserveNativeBlockMarker(source: string, rendered: string): string {
const marker = source.match(
/^\[(?:[ xX]|!(?:NOTE|TIP|IMPORTANT|WARNING|CAUTION))\](?=\s|$)/,
)?.[0];
if (!marker) return rendered;
const escaped = escapeText(marker);
return rendered.startsWith(escaped) ? marker + rendered.slice(escaped.length) : rendered;
}
/** Keep the existing inline dialect, including literal pipes and tappable inline paths. */
export function prepareProse(
text: string,
@@ -88,7 +97,10 @@ export function prepareProse(
if (child.type === "text") child.content = child.content.toUpperCase();
}
}
const rendered = renderInline(token.children, canResolveCode)
const rendered = preserveNativeBlockMarker(
token.content,
renderInline(token.children, canResolveCode),
)
.replace(/ $/, " ")
.split("\n");
const replacement = rendered.map(
@@ -9,7 +9,7 @@ import type { MarkdownProseProps } from "./props";
import { inlineCodeLinkContent, prepareProse } from "./prepare";
const ThemedMarkdown = withUnistyles(EnrichedMarkdownText);
const MD4C_FLAGS = { latexMath: false, hardSoftBreaks: true, admonitions: false };
const MD4C_FLAGS = { latexMath: false, hardSoftBreaks: true, admonitions: true };
function proseStyleMapping(theme: Theme): { markdownStyle: MarkdownStyle } {
const styles = createMarkdownStyles(theme);