authorgravatar for chris@xenforo.comChris Deeming <chris@xenforo.com> 2026-08-13 19:00:03+01:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-13 20:00:03+02:00
logfd51561b4e2de1893cb7eb4069937256d702572c
tree46fcb9465e1d196d6ea42441abfb4b50eca76adb
parent9e201941aaa9cfece3e0ffaa4cc24bbe880d1be4
signature Signed by PGP key B5690EEEBB952194

fix(mobile): extend blockquotes across wrapped lines (#6482)


4 files changed, 79 insertions(+), 8 deletions(-)

apps/mobile/modules/t3-markdown-text/src/NativeMarkdownBlock.ios.tsx+24-7
......@@ -4,16 +4,13 @@ import type { MarkdownNode } from "react-native-nitro-markdown/headless";
44
55import { CopyTextButton } from "./CopyTextButton";
66import { MarkdownTextPrimitive } from "./MarkdownTextPrimitive";
7import {
8 nativeMarkdownDocumentRuns,
9 nativeMarkdownListItemBlocks,
10 nativeMarkdownTextRuns,
11} from "./nativeMarkdownText";
7import { nativeMarkdownDocumentRuns, nativeMarkdownListItemBlocks } from "./nativeMarkdownText";
128import { NativeMarkdownSelectableText } from "./NativeMarkdownSelectableText.ios";
139import type {
1410 MarkdownCodeHighlighter,
1511 MarkdownHighlightedToken,
1612 NativeMarkdownTextStyle,
13 SelectableMarkdownSkill,
1714} from "./SelectableMarkdownText.types";
1815
1916type HighlightedCode = ReadonlyArray<ReadonlyArray<MarkdownHighlightedToken>>;
......@@ -48,12 +45,13 @@ function documentFor(node: MarkdownNode): MarkdownNode {
4845
4946function SelectableNode(props: {
5047 readonly node: MarkdownNode;
48 readonly skills: ReadonlyArray<SelectableMarkdownSkill>;
5149 readonly textStyle: NativeMarkdownTextStyle;
5250 readonly onLinkPress?: (href: string) => void;
5351}) {
5452 return (
5553 <NativeMarkdownSelectableText
56 runs={nativeMarkdownDocumentRuns(documentFor(props.node))}
54 runs={nativeMarkdownDocumentRuns(documentFor(props.node), props.skills)}
5755 textStyle={props.textStyle}
5856 onLinkPress={props.onLinkPress}
5957 />
......@@ -322,6 +320,7 @@ function collectTableRows(node: MarkdownNode): MarkdownNode[] {
322320
323321function NativeTable(props: {
324322 readonly node: MarkdownNode;
323 readonly skills: ReadonlyArray<SelectableMarkdownSkill>;
325324 readonly textStyle: NativeMarkdownTextStyle;
326325 readonly onLinkPress?: (href: string) => void;
327326}) {
......@@ -359,7 +358,7 @@ function NativeTable(props: {
359358 }}
360359 >
361360 <NativeMarkdownSelectableText
362 runs={nativeMarkdownTextRuns(cell).map((run) =>
361 runs={nativeMarkdownDocumentRuns(documentFor(cell), props.skills).map((run) =>
363362 rowIndex === 0 || cell.isHeader ? { ...run, bold: true } : run,
364363 )}
365364 textStyle={props.textStyle}
......@@ -376,6 +375,7 @@ function NativeTable(props: {
376375
377376function NativeMarkdownImage(props: {
378377 readonly node: MarkdownNode;
378 readonly skills: ReadonlyArray<SelectableMarkdownSkill>;
379379 readonly textStyle: NativeMarkdownTextStyle;
380380 readonly onLinkPress?: (href: string) => void;
381381}) {
......@@ -384,6 +384,7 @@ function NativeMarkdownImage(props: {
384384 return (
385385 <SelectableNode
386386 node={props.node}
387 skills={props.skills}
387388 textStyle={props.textStyle}
388389 onLinkPress={props.onLinkPress}
389390 />
......@@ -445,6 +446,7 @@ function inlineGroups(nodes: ReadonlyArray<MarkdownNode>): MarkdownNode[] {
445446
446447function NativeMixedParagraph(props: {
447448 readonly node: MarkdownNode;
449 readonly skills: ReadonlyArray<SelectableMarkdownSkill>;
448450 readonly textStyle: NativeMarkdownTextStyle;
449451 readonly onLinkPress?: (href: string) => void;
450452}) {
......@@ -455,6 +457,7 @@ function NativeMixedParagraph(props: {
455457 <NativeMarkdownImage
456458 key={nodeKey(child, index)}
457459 node={child}
460 skills={props.skills}
458461 textStyle={props.textStyle}
459462 onLinkPress={props.onLinkPress}
460463 />
......@@ -462,6 +465,7 @@ function NativeMixedParagraph(props: {
462465 <SelectableNode
463466 key={nodeKey(child, index)}
464467 node={child}
468 skills={props.skills}
465469 textStyle={props.textStyle}
466470 onLinkPress={props.onLinkPress}
467471 />
......@@ -473,6 +477,7 @@ function NativeMixedParagraph(props: {
473477
474478function NativeList(props: {
475479 readonly node: MarkdownNode;
480 readonly skills: ReadonlyArray<SelectableMarkdownSkill>;
476481 readonly textStyle: NativeMarkdownTextStyle;
477482 readonly highlightCode: MarkdownCodeHighlighter;
478483 readonly onLinkPress?: (href: string) => void;
......@@ -534,6 +539,7 @@ function NativeList(props: {
534539 <NativeMarkdownBlock
535540 key={nodeKey(child, childIndex)}
536541 node={child}
542 skills={props.skills}
537543 textStyle={props.textStyle}
538544 highlightCode={props.highlightCode}
539545 onLinkPress={props.onLinkPress}
......@@ -551,6 +557,7 @@ function NativeList(props: {
551557
552558export function NativeMarkdownBlock(props: {
553559 readonly node: MarkdownNode;
560 readonly skills: ReadonlyArray<SelectableMarkdownSkill>;
554561 readonly textStyle: NativeMarkdownTextStyle;
555562 readonly highlightCode: MarkdownCodeHighlighter;
556563 readonly onLinkPress?: (href: string) => void;
......@@ -566,6 +573,7 @@ export function NativeMarkdownBlock(props: {
566573 <NativeMarkdownBlock
567574 key={nodeKey(child, index)}
568575 node={child}
576 skills={props.skills}
569577 textStyle={props.textStyle}
570578 highlightCode={props.highlightCode}
571579 onLinkPress={props.onLinkPress}
......@@ -587,6 +595,7 @@ export function NativeMarkdownBlock(props: {
587595 return (
588596 <NativeTable
589597 node={props.node}
598 skills={props.skills}
590599 textStyle={props.textStyle}
591600 onLinkPress={props.onLinkPress}
592601 />
......@@ -595,6 +604,7 @@ export function NativeMarkdownBlock(props: {
595604 return (
596605 <NativeMarkdownImage
597606 node={props.node}
607 skills={props.skills}
598608 textStyle={props.textStyle}
599609 onLinkPress={props.onLinkPress}
600610 />
......@@ -624,6 +634,7 @@ export function NativeMarkdownBlock(props: {
624634 <NativeMarkdownBlock
625635 key={nodeKey(child, index)}
626636 node={child}
637 skills={props.skills}
627638 textStyle={props.textStyle}
628639 highlightCode={props.highlightCode}
629640 onLinkPress={props.onLinkPress}
......@@ -637,6 +648,7 @@ export function NativeMarkdownBlock(props: {
637648 return (
638649 <NativeList
639650 node={props.node}
651 skills={props.skills}
640652 textStyle={props.textStyle}
641653 highlightCode={props.highlightCode}
642654 onLinkPress={props.onLinkPress}
......@@ -647,12 +659,14 @@ export function NativeMarkdownBlock(props: {
647659 return (props.node.children ?? []).some((child) => child.type === "image") ? (
648660 <NativeMixedParagraph
649661 node={props.node}
662 skills={props.skills}
650663 textStyle={props.textStyle}
651664 onLinkPress={props.onLinkPress}
652665 />
653666 ) : (
654667 <SelectableNode
655668 node={props.node}
669 skills={props.skills}
656670 textStyle={props.textStyle}
657671 onLinkPress={props.onLinkPress}
658672 />
......@@ -673,6 +687,7 @@ export function NativeMarkdownBlock(props: {
673687 >
674688 <SelectableNode
675689 node={props.node}
690 skills={props.skills}
676691 textStyle={props.textStyle}
677692 onLinkPress={props.onLinkPress}
678693 />
......@@ -690,6 +705,7 @@ export function NativeMarkdownBlock(props: {
690705 <NativeMarkdownBlock
691706 key={nodeKey(child, index)}
692707 node={child}
708 skills={props.skills}
693709 textStyle={props.textStyle}
694710 highlightCode={props.highlightCode}
695711 onLinkPress={props.onLinkPress}
......@@ -703,6 +719,7 @@ export function NativeMarkdownBlock(props: {
703719 return (
704720 <SelectableNode
705721 node={props.node}
722 skills={props.skills}
706723 textStyle={props.textStyle}
707724 onLinkPress={props.onLinkPress}
708725 />
apps/mobile/modules/t3-markdown-text/src/SelectableMarkdownText.ios.tsx+1
......@@ -69,6 +69,7 @@ export function SelectableMarkdownText({
6969 chunk.kind === "rich" ? (
7070 <NativeMarkdownBlock
7171 node={chunk.node}
72 skills={skills}
7273 textStyle={textStyle}
7374 highlightCode={highlightCode}
7475 onLinkPress={onLinkPress}
apps/mobile/modules/t3-markdown-text/src/nativeMarkdownText.ts+1
......@@ -661,6 +661,7 @@ function appendDocumentBlock(
661661function containsRichBlock(node: MarkdownNode): boolean {
662662 if (
663663 node.type === "code_block" ||
664 node.type === "blockquote" ||
664665 node.type === "table" ||
665666 node.type === "image" ||
666667 node.type === "horizontal_rule" ||
apps/mobile/src/lib/nativeMarkdownText.test.ts+53-1
......@@ -173,6 +173,25 @@ describe("nativeMarkdownDocumentRuns", () => {
173173 ]);
174174 });
175175
176 it("decorates known skill references inside blockquotes", () => {
177 const node: MarkdownNode = {
178 type: "blockquote",
179 children: [
180 {
181 type: "paragraph",
182 children: [{ type: "text", content: "Use $ui for this." }],
183 },
184 ],
185 };
186
187 expect(nativeMarkdownDocumentRuns(node, [{ name: "ui", displayName: "UI" }])).toContainEqual({
188 text: "$ui",
189 role: "body",
190 skillName: "ui",
191 skillLabel: "UI",
192 });
193 });
194
176195 it("leaves unknown skill-like text unchanged", () => {
177196 const node: MarkdownNode = {
178197 type: "document",
......@@ -328,7 +347,7 @@ describe("nativeMarkdownDocumentRuns", () => {
328347 ]);
329348 });
330349
331 it("includes quotes and fenced code in the same selectable string", () => {
350 it("preserves quotes and fenced code in document runs", () => {
332351 const node: MarkdownNode = {
333352 type: "document",
334353 children: [
......@@ -414,6 +433,39 @@ describe("nativeMarkdownListItemBlocks", () => {
414433});
415434
416435describe("nativeMarkdownDocumentChunks", () => {
436 it("renders plain blockquotes as rich blocks so their marker spans wrapped lines", () => {
437 const blockquote: MarkdownNode = {
438 type: "blockquote",
439 beg: 0,
440 end: 120,
441 children: [
442 {
443 type: "paragraph",
444 children: [
445 {
446 type: "text",
447 content:
448 "Persistent random per-result keys are the strongest design, even when this text wraps.",
449 },
450 ],
451 },
452 ],
453 };
454
455 expect(
456 nativeMarkdownDocumentChunks({
457 type: "document",
458 children: [blockquote],
459 }),
460 ).toEqual([
461 {
462 kind: "rich",
463 key: "rich:blockquote:0:120",
464 node: blockquote,
465 },
466 ]);
467 });
468
417469 it("keeps headings and plain lists in one selectable document", () => {
418470 const document: MarkdownNode = {
419471 type: "document",