From 9ef289e7f47de48396241e5aed3d756f37e14e4f Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 15 Oct 2026 04:49:56 +0200 Subject: [PATCH] fix(app): allow additive task lists and admonitions in native prose --- docs/native-text-selection.md | 4 ++-- .../components/markdown-prose/prepare.test.ts | 24 +++++++++++-------- .../src/components/markdown-prose/prepare.ts | 14 ++++++++++- .../markdown-prose/renderer.native.tsx | 2 +- 4 files changed, 30 insertions(+), 14 deletions(-) diff --git a/docs/native-text-selection.md b/docs/native-text-selection.md index ce7014a36c..e18dbaca46 100644 --- a/docs/native-text-selection.md +++ b/docs/native-text-selection.md @@ -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. diff --git a/packages/app/src/components/markdown-prose/prepare.test.ts b/packages/app/src/components/markdown-prose/prepare.test.ts index 96da775850..7f2d882845 100644 --- a/packages/app/src/components/markdown-prose/prepare.test.ts +++ b/packages/app/src/components/markdown-prose/prepare.test.ts @@ -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)); +}); diff --git a/packages/app/src/components/markdown-prose/prepare.ts b/packages/app/src/components/markdown-prose/prepare.ts index 41132c7054..ba6d417bf6 100644 --- a/packages/app/src/components/markdown-prose/prepare.ts +++ b/packages/app/src/components/markdown-prose/prepare.ts @@ -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( diff --git a/packages/app/src/components/markdown-prose/renderer.native.tsx b/packages/app/src/components/markdown-prose/renderer.native.tsx index 3172b05844..1196f5a735 100644 --- a/packages/app/src/components/markdown-prose/renderer.native.tsx +++ b/packages/app/src/components/markdown-prose/renderer.native.tsx @@ -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);