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";...@@ -4,16 +4,13 @@ import type { MarkdownNode } from "react-native-nitro-markdown/headless";
44
5import { CopyTextButton } from "./CopyTextButton";5import { CopyTextButton } from "./CopyTextButton";
6import { MarkdownTextPrimitive } from "./MarkdownTextPrimitive";6import { MarkdownTextPrimitive } from "./MarkdownTextPrimitive";
7import {7import { nativeMarkdownDocumentRuns, nativeMarkdownListItemBlocks } from "./nativeMarkdownText";
8 nativeMarkdownDocumentRuns,
9 nativeMarkdownListItemBlocks,
10 nativeMarkdownTextRuns,
11} from "./nativeMarkdownText";
12import { NativeMarkdownSelectableText } from "./NativeMarkdownSelectableText.ios";8import { NativeMarkdownSelectableText } from "./NativeMarkdownSelectableText.ios";
13import type {9import type {
14 MarkdownCodeHighlighter,10 MarkdownCodeHighlighter,
15 MarkdownHighlightedToken,11 MarkdownHighlightedToken,
16 NativeMarkdownTextStyle,12 NativeMarkdownTextStyle,
13 SelectableMarkdownSkill,
17} from "./SelectableMarkdownText.types";14} from "./SelectableMarkdownText.types";
1815
19type HighlightedCode = ReadonlyArray<ReadonlyArray<MarkdownHighlightedToken>>;16type HighlightedCode = ReadonlyArray<ReadonlyArray<MarkdownHighlightedToken>>;
...@@ -48,12 +45,13 @@ function documentFor(node: MarkdownNode): MarkdownNode {...@@ -48,12 +45,13 @@ function documentFor(node: MarkdownNode): MarkdownNode {
4845
49function SelectableNode(props: {46function SelectableNode(props: {
50 readonly node: MarkdownNode;47 readonly node: MarkdownNode;
48 readonly skills: ReadonlyArray<SelectableMarkdownSkill>;
51 readonly textStyle: NativeMarkdownTextStyle;49 readonly textStyle: NativeMarkdownTextStyle;
52 readonly onLinkPress?: (href: string) => void;50 readonly onLinkPress?: (href: string) => void;
53}) {51}) {
54 return (52 return (
55 <NativeMarkdownSelectableText53 <NativeMarkdownSelectableText
56 runs={nativeMarkdownDocumentRuns(documentFor(props.node))}54 runs={nativeMarkdownDocumentRuns(documentFor(props.node), props.skills)}
57 textStyle={props.textStyle}55 textStyle={props.textStyle}
58 onLinkPress={props.onLinkPress}56 onLinkPress={props.onLinkPress}
59 />57 />
...@@ -322,6 +320,7 @@ function collectTableRows(node: MarkdownNode): MarkdownNode[] {...@@ -322,6 +320,7 @@ function collectTableRows(node: MarkdownNode): MarkdownNode[] {
322320
323function NativeTable(props: {321function NativeTable(props: {
324 readonly node: MarkdownNode;322 readonly node: MarkdownNode;
323 readonly skills: ReadonlyArray<SelectableMarkdownSkill>;
325 readonly textStyle: NativeMarkdownTextStyle;324 readonly textStyle: NativeMarkdownTextStyle;
326 readonly onLinkPress?: (href: string) => void;325 readonly onLinkPress?: (href: string) => void;
327}) {326}) {
...@@ -359,7 +358,7 @@ function NativeTable(props: {...@@ -359,7 +358,7 @@ function NativeTable(props: {
359 }}358 }}
360 >359 >
361 <NativeMarkdownSelectableText360 <NativeMarkdownSelectableText
362 runs={nativeMarkdownTextRuns(cell).map((run) =>361 runs={nativeMarkdownDocumentRuns(documentFor(cell), props.skills).map((run) =>
363 rowIndex === 0 || cell.isHeader ? { ...run, bold: true } : run,362 rowIndex === 0 || cell.isHeader ? { ...run, bold: true } : run,
364 )}363 )}
365 textStyle={props.textStyle}364 textStyle={props.textStyle}
...@@ -376,6 +375,7 @@ function NativeTable(props: {...@@ -376,6 +375,7 @@ function NativeTable(props: {
376375
377function NativeMarkdownImage(props: {376function NativeMarkdownImage(props: {
378 readonly node: MarkdownNode;377 readonly node: MarkdownNode;
378 readonly skills: ReadonlyArray<SelectableMarkdownSkill>;
379 readonly textStyle: NativeMarkdownTextStyle;379 readonly textStyle: NativeMarkdownTextStyle;
380 readonly onLinkPress?: (href: string) => void;380 readonly onLinkPress?: (href: string) => void;
381}) {381}) {
...@@ -384,6 +384,7 @@ function NativeMarkdownImage(props: {...@@ -384,6 +384,7 @@ function NativeMarkdownImage(props: {
384 return (384 return (
385 <SelectableNode385 <SelectableNode
386 node={props.node}386 node={props.node}
387 skills={props.skills}
387 textStyle={props.textStyle}388 textStyle={props.textStyle}
388 onLinkPress={props.onLinkPress}389 onLinkPress={props.onLinkPress}
389 />390 />
...@@ -445,6 +446,7 @@ function inlineGroups(nodes: ReadonlyArray<MarkdownNode>): MarkdownNode[] {...@@ -445,6 +446,7 @@ function inlineGroups(nodes: ReadonlyArray<MarkdownNode>): MarkdownNode[] {
445446
446function NativeMixedParagraph(props: {447function NativeMixedParagraph(props: {
447 readonly node: MarkdownNode;448 readonly node: MarkdownNode;
449 readonly skills: ReadonlyArray<SelectableMarkdownSkill>;
448 readonly textStyle: NativeMarkdownTextStyle;450 readonly textStyle: NativeMarkdownTextStyle;
449 readonly onLinkPress?: (href: string) => void;451 readonly onLinkPress?: (href: string) => void;
450}) {452}) {
...@@ -455,6 +457,7 @@ function NativeMixedParagraph(props: {...@@ -455,6 +457,7 @@ function NativeMixedParagraph(props: {
455 <NativeMarkdownImage457 <NativeMarkdownImage
456 key={nodeKey(child, index)}458 key={nodeKey(child, index)}
457 node={child}459 node={child}
460 skills={props.skills}
458 textStyle={props.textStyle}461 textStyle={props.textStyle}
459 onLinkPress={props.onLinkPress}462 onLinkPress={props.onLinkPress}
460 />463 />
...@@ -462,6 +465,7 @@ function NativeMixedParagraph(props: {...@@ -462,6 +465,7 @@ function NativeMixedParagraph(props: {
462 <SelectableNode465 <SelectableNode
463 key={nodeKey(child, index)}466 key={nodeKey(child, index)}
464 node={child}467 node={child}
468 skills={props.skills}
465 textStyle={props.textStyle}469 textStyle={props.textStyle}
466 onLinkPress={props.onLinkPress}470 onLinkPress={props.onLinkPress}
467 />471 />
...@@ -473,6 +477,7 @@ function NativeMixedParagraph(props: {...@@ -473,6 +477,7 @@ function NativeMixedParagraph(props: {
473477
474function NativeList(props: {478function NativeList(props: {
475 readonly node: MarkdownNode;479 readonly node: MarkdownNode;
480 readonly skills: ReadonlyArray<SelectableMarkdownSkill>;
476 readonly textStyle: NativeMarkdownTextStyle;481 readonly textStyle: NativeMarkdownTextStyle;
477 readonly highlightCode: MarkdownCodeHighlighter;482 readonly highlightCode: MarkdownCodeHighlighter;
478 readonly onLinkPress?: (href: string) => void;483 readonly onLinkPress?: (href: string) => void;
...@@ -534,6 +539,7 @@ function NativeList(props: {...@@ -534,6 +539,7 @@ function NativeList(props: {
534 <NativeMarkdownBlock539 <NativeMarkdownBlock
535 key={nodeKey(child, childIndex)}540 key={nodeKey(child, childIndex)}
536 node={child}541 node={child}
542 skills={props.skills}
537 textStyle={props.textStyle}543 textStyle={props.textStyle}
538 highlightCode={props.highlightCode}544 highlightCode={props.highlightCode}
539 onLinkPress={props.onLinkPress}545 onLinkPress={props.onLinkPress}
...@@ -551,6 +557,7 @@ function NativeList(props: {...@@ -551,6 +557,7 @@ function NativeList(props: {
551557
552export function NativeMarkdownBlock(props: {558export function NativeMarkdownBlock(props: {
553 readonly node: MarkdownNode;559 readonly node: MarkdownNode;
560 readonly skills: ReadonlyArray<SelectableMarkdownSkill>;
554 readonly textStyle: NativeMarkdownTextStyle;561 readonly textStyle: NativeMarkdownTextStyle;
555 readonly highlightCode: MarkdownCodeHighlighter;562 readonly highlightCode: MarkdownCodeHighlighter;
556 readonly onLinkPress?: (href: string) => void;563 readonly onLinkPress?: (href: string) => void;
...@@ -566,6 +573,7 @@ export function NativeMarkdownBlock(props: {...@@ -566,6 +573,7 @@ export function NativeMarkdownBlock(props: {
566 <NativeMarkdownBlock573 <NativeMarkdownBlock
567 key={nodeKey(child, index)}574 key={nodeKey(child, index)}
568 node={child}575 node={child}
576 skills={props.skills}
569 textStyle={props.textStyle}577 textStyle={props.textStyle}
570 highlightCode={props.highlightCode}578 highlightCode={props.highlightCode}
571 onLinkPress={props.onLinkPress}579 onLinkPress={props.onLinkPress}
...@@ -587,6 +595,7 @@ export function NativeMarkdownBlock(props: {...@@ -587,6 +595,7 @@ export function NativeMarkdownBlock(props: {
587 return (595 return (
588 <NativeTable596 <NativeTable
589 node={props.node}597 node={props.node}
598 skills={props.skills}
590 textStyle={props.textStyle}599 textStyle={props.textStyle}
591 onLinkPress={props.onLinkPress}600 onLinkPress={props.onLinkPress}
592 />601 />
...@@ -595,6 +604,7 @@ export function NativeMarkdownBlock(props: {...@@ -595,6 +604,7 @@ export function NativeMarkdownBlock(props: {
595 return (604 return (
596 <NativeMarkdownImage605 <NativeMarkdownImage
597 node={props.node}606 node={props.node}
607 skills={props.skills}
598 textStyle={props.textStyle}608 textStyle={props.textStyle}
599 onLinkPress={props.onLinkPress}609 onLinkPress={props.onLinkPress}
600 />610 />
...@@ -624,6 +634,7 @@ export function NativeMarkdownBlock(props: {...@@ -624,6 +634,7 @@ export function NativeMarkdownBlock(props: {
624 <NativeMarkdownBlock634 <NativeMarkdownBlock
625 key={nodeKey(child, index)}635 key={nodeKey(child, index)}
626 node={child}636 node={child}
637 skills={props.skills}
627 textStyle={props.textStyle}638 textStyle={props.textStyle}
628 highlightCode={props.highlightCode}639 highlightCode={props.highlightCode}
629 onLinkPress={props.onLinkPress}640 onLinkPress={props.onLinkPress}
...@@ -637,6 +648,7 @@ export function NativeMarkdownBlock(props: {...@@ -637,6 +648,7 @@ export function NativeMarkdownBlock(props: {
637 return (648 return (
638 <NativeList649 <NativeList
639 node={props.node}650 node={props.node}
651 skills={props.skills}
640 textStyle={props.textStyle}652 textStyle={props.textStyle}
641 highlightCode={props.highlightCode}653 highlightCode={props.highlightCode}
642 onLinkPress={props.onLinkPress}654 onLinkPress={props.onLinkPress}
...@@ -647,12 +659,14 @@ export function NativeMarkdownBlock(props: {...@@ -647,12 +659,14 @@ export function NativeMarkdownBlock(props: {
647 return (props.node.children ?? []).some((child) => child.type === "image") ? (659 return (props.node.children ?? []).some((child) => child.type === "image") ? (
648 <NativeMixedParagraph660 <NativeMixedParagraph
649 node={props.node}661 node={props.node}
662 skills={props.skills}
650 textStyle={props.textStyle}663 textStyle={props.textStyle}
651 onLinkPress={props.onLinkPress}664 onLinkPress={props.onLinkPress}
652 />665 />
653 ) : (666 ) : (
654 <SelectableNode667 <SelectableNode
655 node={props.node}668 node={props.node}
669 skills={props.skills}
656 textStyle={props.textStyle}670 textStyle={props.textStyle}
657 onLinkPress={props.onLinkPress}671 onLinkPress={props.onLinkPress}
658 />672 />
...@@ -673,6 +687,7 @@ export function NativeMarkdownBlock(props: {...@@ -673,6 +687,7 @@ export function NativeMarkdownBlock(props: {
673 >687 >
674 <SelectableNode688 <SelectableNode
675 node={props.node}689 node={props.node}
690 skills={props.skills}
676 textStyle={props.textStyle}691 textStyle={props.textStyle}
677 onLinkPress={props.onLinkPress}692 onLinkPress={props.onLinkPress}
678 />693 />
...@@ -690,6 +705,7 @@ export function NativeMarkdownBlock(props: {...@@ -690,6 +705,7 @@ export function NativeMarkdownBlock(props: {
690 <NativeMarkdownBlock705 <NativeMarkdownBlock
691 key={nodeKey(child, index)}706 key={nodeKey(child, index)}
692 node={child}707 node={child}
708 skills={props.skills}
693 textStyle={props.textStyle}709 textStyle={props.textStyle}
694 highlightCode={props.highlightCode}710 highlightCode={props.highlightCode}
695 onLinkPress={props.onLinkPress}711 onLinkPress={props.onLinkPress}
...@@ -703,6 +719,7 @@ export function NativeMarkdownBlock(props: {...@@ -703,6 +719,7 @@ export function NativeMarkdownBlock(props: {
703 return (719 return (
704 <SelectableNode720 <SelectableNode
705 node={props.node}721 node={props.node}
722 skills={props.skills}
706 textStyle={props.textStyle}723 textStyle={props.textStyle}
707 onLinkPress={props.onLinkPress}724 onLinkPress={props.onLinkPress}
708 />725 />
apps/mobile/modules/t3-markdown-text/src/SelectableMarkdownText.ios.tsx+1
...@@ -69,6 +69,7 @@ export function SelectableMarkdownText({...@@ -69,6 +69,7 @@ export function SelectableMarkdownText({
69 chunk.kind === "rich" ? (69 chunk.kind === "rich" ? (
70 <NativeMarkdownBlock70 <NativeMarkdownBlock
71 node={chunk.node}71 node={chunk.node}
72 skills={skills}
72 textStyle={textStyle}73 textStyle={textStyle}
73 highlightCode={highlightCode}74 highlightCode={highlightCode}
74 onLinkPress={onLinkPress}75 onLinkPress={onLinkPress}
apps/mobile/modules/t3-markdown-text/src/nativeMarkdownText.ts+1
...@@ -661,6 +661,7 @@ function appendDocumentBlock(...@@ -661,6 +661,7 @@ function appendDocumentBlock(
661function containsRichBlock(node: MarkdownNode): boolean {661function containsRichBlock(node: MarkdownNode): boolean {
662 if (662 if (
663 node.type === "code_block" ||663 node.type === "code_block" ||
664 node.type === "blockquote" ||
664 node.type === "table" ||665 node.type === "table" ||
665 node.type === "image" ||666 node.type === "image" ||
666 node.type === "horizontal_rule" ||667 node.type === "horizontal_rule" ||
apps/mobile/src/lib/nativeMarkdownText.test.ts+53-1
...@@ -173,6 +173,25 @@ describe("nativeMarkdownDocumentRuns", () => {...@@ -173,6 +173,25 @@ describe("nativeMarkdownDocumentRuns", () => {
173 ]);173 ]);
174 });174 });
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
176 it("leaves unknown skill-like text unchanged", () => {195 it("leaves unknown skill-like text unchanged", () => {
177 const node: MarkdownNode = {196 const node: MarkdownNode = {
178 type: "document",197 type: "document",
...@@ -328,7 +347,7 @@ describe("nativeMarkdownDocumentRuns", () => {...@@ -328,7 +347,7 @@ describe("nativeMarkdownDocumentRuns", () => {
328 ]);347 ]);
329 });348 });
330349
331 it("includes quotes and fenced code in the same selectable string", () => {350 it("preserves quotes and fenced code in document runs", () => {
332 const node: MarkdownNode = {351 const node: MarkdownNode = {
333 type: "document",352 type: "document",
334 children: [353 children: [
...@@ -414,6 +433,39 @@ describe("nativeMarkdownListItemBlocks", () => {...@@ -414,6 +433,39 @@ describe("nativeMarkdownListItemBlocks", () => {
414});433});
415434
416describe("nativeMarkdownDocumentChunks", () => {435describe("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
417 it("keeps headings and plain lists in one selectable document", () => {469 it("keeps headings and plain lists in one selectable document", () => {
418 const document: MarkdownNode = {470 const document: MarkdownNode = {
419 type: "document",471 type: "document",