Files
paseo/packages/app/src/assistant-selection-copy/markup.ts
T
paseo-bot[bot] 950e1f8f41 Keep an image's alt text and source when copying a selection from an assistant reply (#6181)
* fix(app): copy an assistant image as its Markdown, with alt text

Selecting across an image in an assistant reply copied the rendered img,
which has no alt text and, for data and workspace images, points at a
preview URL. The image frame now carries its Markdown source and alt text
the same way other Markdown elements declare their copy form.

Closes #6163

* fix(app): copy an image selected on its own as its Markdown

A selection inside the rendered image left the image's copy attributes
on the frame outside the range, so nothing was copied as Markdown.

* test(app): name the image-only selection in the copy spec

---------

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-10-06 02:07:01 +02:00

90 lines
3.4 KiB
TypeScript

export const MARKDOWN_COPY_TAG_ATTRIBUTE = "data-paseo-markdown-tag";
export const MARKDOWN_COPY_IGNORE_ATTRIBUTE = "data-paseo-markdown-ignore";
export const MARKDOWN_COPY_LIST_MARKER_ATTRIBUTE = "data-paseo-markdown-list-marker";
export const MARKDOWN_COPY_UNWRAP_ATTRIBUTE = "data-paseo-markdown-unwrap";
export const MARKDOWN_COPY_LIST_START_ATTRIBUTE = "data-paseo-markdown-list-start";
export const MARKDOWN_COPY_LANGUAGE_ATTRIBUTE = "data-paseo-markdown-language";
export const MARKDOWN_COPY_ALIGN_ATTRIBUTE = "data-paseo-markdown-align";
export const MARKDOWN_COPY_SRC_ATTRIBUTE = "data-paseo-markdown-src";
export const MARKDOWN_COPY_ALT_ATTRIBUTE = "data-paseo-markdown-alt";
/**
* Trailing line breaks, with any indentation that followed the last one.
*
* Both ways of copying code strip these, for the same reason: pasting a trailing
* newline into a terminal runs the last line. A fence body always ends in one, and
* ends in several when the author left blank lines before the closing fence; a
* selection picks one up whenever it overshoots the end of a rendered line.
*/
export const TRAILING_CODE_LINE_BREAKS = /(\r?\n[ \t]*)+$/;
export const markdownCopyDataSet = {
blockquote: { paseoMarkdownTag: "blockquote" },
br: { paseoMarkdownTag: "br" },
code: { paseoMarkdownTag: "code" },
h1: { paseoMarkdownTag: "h1" },
h2: { paseoMarkdownTag: "h2" },
h3: { paseoMarkdownTag: "h3" },
h4: { paseoMarkdownTag: "h4" },
h5: { paseoMarkdownTag: "h5" },
h6: { paseoMarkdownTag: "h6" },
hr: { paseoMarkdownTag: "hr" },
ignore: { paseoMarkdownIgnore: "true" },
li: { paseoMarkdownTag: "li" },
listMarker: { paseoMarkdownIgnore: "true", paseoMarkdownListMarker: "true" },
ol: { paseoMarkdownTag: "ol" },
p: { paseoMarkdownTag: "p" },
pre: { paseoMarkdownTag: "pre" },
s: { paseoMarkdownTag: "s" },
strong: { paseoMarkdownTag: "strong" },
em: { paseoMarkdownTag: "em" },
table: { paseoMarkdownTag: "table" },
tbody: { paseoMarkdownTag: "tbody" },
td: { paseoMarkdownTag: "td" },
th: { paseoMarkdownTag: "th" },
thead: { paseoMarkdownTag: "thead" },
tr: { paseoMarkdownTag: "tr" },
ul: { paseoMarkdownTag: "ul" },
unwrap: { paseoMarkdownUnwrap: "true" },
} as const;
export type MarkdownCopyInlineTag = "br" | "code" | "em" | "s" | "strong";
export function markdownCopyOrderedListDataSet(start: unknown) {
return {
...markdownCopyDataSet.ol,
paseoMarkdownListStart: String(start ?? 1),
} as const;
}
export function markdownCopyCodeBlockDataSet(language: string | null | undefined) {
const fenceLanguage = language?.trim().split(/\s+/)[0];
return {
...markdownCopyDataSet.pre,
...(fenceLanguage ? { paseoMarkdownLanguage: fenceLanguage } : {}),
} as const;
}
/**
* An image copies as the Markdown it came from. The rendered `img` points at a preview
* URL for data and workspace images, and carries no alt text of its own.
*/
export function markdownCopyImageDataSet(source: string, alt: string | undefined) {
return {
paseoMarkdownTag: "img",
paseoMarkdownSrc: source,
...(alt ? { paseoMarkdownAlt: alt } : {}),
} as const;
}
export function markdownCopyTableCellDataSet(tag: "td" | "th", style: unknown) {
const alignment =
typeof style === "string"
? style.match(/(?:^|;)\s*text-align\s*:\s*(left|right|center)/i)?.[1]
: null;
return {
...markdownCopyDataSet[tag],
...(alignment ? { paseoMarkdownAlign: alignment.toLowerCase() } : {}),
} as const;
}