|
1 | 1 | import type { MarkdownNode } from "react-native-nitro-markdown/headless"; |
| 2 | +import { isMarkdownFileLinkLabel } from "@t3tools/client-runtime/markdown-links"; |
2 | 3 | import { collectComposerInlineTokens } from "@t3tools/shared/composerInlineTokens"; |
3 | 4 | import { imageMimeType } from "@t3tools/shared/image"; |
4 | 5 | import { isWindowsAbsolutePath } from "@t3tools/shared/path"; |
@@ -109,19 +110,34 @@ export function nativeMarkdownContextCopyRanges( |
109 | 110 | }>, |
110 | 111 | ) { |
111 | 112 | let offset = 0; |
112 | | - return runs.flatMap(({ run, text, inlineImageLength }) => { |
| 113 | + const ranges: Array<{ start: number; end: number; text: string }> = []; |
| 114 | + for (const [index, { run, text, inlineImageLength }] of runs.entries()) { |
113 | 115 | const start = offset; |
114 | 116 | offset += text.length + inlineImageLength; |
115 | 117 | const reference = parseComposerContextHref(run.href ?? ""); |
116 | 118 | const source = reference |
117 | 119 | ? formatComposerContextReference({ ...reference, label: run.text }) |
118 | 120 | : run.skillName |
119 | 121 | ? `$${run.skillName}` |
120 | | - : run.fileIcon && run.href |
121 | | - ? (run.sourceText ?? `[${run.text}](<${run.href}>)`) |
122 | | - : null; |
123 | | - return source === null ? [] : [{ start, end: offset, text: source }]; |
124 | | - }); |
| 122 | + : run.sourceText !== undefined |
| 123 | + ? run.sourceText |
| 124 | + : run.fileIcon && run.href |
| 125 | + ? `[${run.text}](<${run.href}>)` |
| 126 | + : null; |
| 127 | + if (source === null) continue; |
| 128 | + const previous = ranges.at(-1); |
| 129 | + if ( |
| 130 | + run.sourceText !== undefined && |
| 131 | + !runs[index - 1]?.run.fileIcon && |
| 132 | + previous?.end === start && |
| 133 | + previous.text === source |
| 134 | + ) { |
| 135 | + previous.end = offset; |
| 136 | + } else { |
| 137 | + ranges.push({ start, end: offset, text: source }); |
| 138 | + } |
| 139 | + } |
| 140 | + return ranges; |
125 | 141 | } |
126 | 142 |
|
127 | 143 | import type { SelectableMarkdownSkill } from "./SelectableMarkdownText.types"; |
@@ -181,6 +197,7 @@ interface RunContext { |
181 | 197 | readonly href?: string; |
182 | 198 | readonly externalHost?: string; |
183 | 199 | readonly fileIcon?: MarkdownFileIcon; |
| 200 | + readonly sourceText?: string; |
184 | 201 | readonly role?: NativeMarkdownTextRun["role"]; |
185 | 202 | readonly headingLevel?: number; |
186 | 203 | readonly depth?: number; |
@@ -268,6 +285,7 @@ function sameRunStyle(left: NativeMarkdownTextRun, right: NativeMarkdownTextRun) |
268 | 285 | left.href === right.href && |
269 | 286 | left.externalHost === right.externalHost && |
270 | 287 | left.fileIcon === right.fileIcon && |
| 288 | + left.sourceText === right.sourceText && |
271 | 289 | left.skillName === right.skillName && |
272 | 290 | left.skillLabel === right.skillLabel && |
273 | 291 | left.role === right.role && |
@@ -298,6 +316,7 @@ function appendRun( |
298 | 316 | ...(context.href ? { href: context.href } : {}), |
299 | 317 | ...(context.externalHost ? { externalHost: context.externalHost } : {}), |
300 | 318 | ...(context.fileIcon ? { fileIcon: context.fileIcon } : {}), |
| 319 | + ...(context.sourceText !== undefined ? { sourceText: context.sourceText } : {}), |
301 | 320 | ...(context.role ? { role: context.role } : {}), |
302 | 321 | ...(context.headingLevel ? { headingLevel: context.headingLevel } : {}), |
303 | 322 | ...(context.depth ? { depth: context.depth } : {}), |
@@ -432,6 +451,36 @@ function nodeTextContent(node: MarkdownNode): string { |
432 | 451 | return (node.children ?? []).map(nodeTextContent).join(""); |
433 | 452 | } |
434 | 453 |
|
| 454 | +function fileLinkLabelMarkdown(node: MarkdownNode): string { |
| 455 | + const children = (node.children ?? []).map(fileLinkLabelMarkdown).join(""); |
| 456 | + switch (node.type) { |
| 457 | + case "bold": |
| 458 | + return `**${children}**`; |
| 459 | + case "italic": |
| 460 | + return `*${children}*`; |
| 461 | + case "strikethrough": |
| 462 | + return `~~${children}~~`; |
| 463 | + case "code_inline": { |
| 464 | + const content = nodeTextContent(node); |
| 465 | + const fence = "`".repeat( |
| 466 | + Math.max(0, ...(content.match(/`+/g) ?? []).map((run) => run.length)) + 1, |
| 467 | + ); |
| 468 | + const padding = /^`|`$|^ .* $/.test(content) && content.trim().length > 0 ? " " : ""; |
| 469 | + return `${fence}${padding}${content}${padding}${fence}`; |
| 470 | + } |
| 471 | + case "soft_break": |
| 472 | + return "\n"; |
| 473 | + case "line_break": |
| 474 | + return " \n"; |
| 475 | + case "text": |
| 476 | + return textNodeContent(nodeTextContent(node)).replace(/[\\[\]*_`~]/g, "\\$&"); |
| 477 | + case "image": |
| 478 | + return `![${(node.alt ?? "").replace(/[\\[\]]/g, "\\$&")}](<${node.href ?? ""}>)`; |
| 479 | + default: |
| 480 | + return node.content ?? children; |
| 481 | + } |
| 482 | +} |
| 483 | + |
435 | 484 | function appendNode( |
436 | 485 | runs: NativeMarkdownTextRun[], |
437 | 486 | node: MarkdownNode, |
@@ -482,6 +531,23 @@ function appendNode( |
482 | 531 | } |
483 | 532 | const presentation = resolveMarkdownLinkPresentation(node.href ?? ""); |
484 | 533 | if (presentation.kind === "file") { |
| 534 | + const label = textNodeContent(nodeTextContent(node)); |
| 535 | + if (!isMarkdownFileLinkLabel(label, presentation.href)) { |
| 536 | + const fileRuns: NativeMarkdownTextRun[] = []; |
| 537 | + const fileContext = { |
| 538 | + ...context, |
| 539 | + href: presentation.href, |
| 540 | + sourceText: `[${fileLinkLabelMarkdown(node)}](<${presentation.href}>)`, |
| 541 | + }; |
| 542 | + appendChildren(fileRuns, node, fileContext); |
| 543 | + appendRun(fileRuns, " ", fileContext); |
| 544 | + appendRun(fileRuns, presentation.label, { |
| 545 | + ...fileContext, |
| 546 | + fileIcon: presentation.icon, |
| 547 | + }); |
| 548 | + runs.push(...fileRuns); |
| 549 | + return runs; |
| 550 | + } |
485 | 551 | return appendRun(runs, presentation.label, { |
486 | 552 | ...context, |
487 | 553 | href: presentation.href, |
|
0 commit comments