moyf_easy-copy/src/copyMetadata.ts
William Nurmi 04402c8298 fix: block link display text covers the whole multi-line block
A soft-wrapped multi-line item is ONE block in Obsidian (the block id sits
at the end of its last line and the cache span covers every line), but the
auto display text was built from a single line: the range's first line when
inserting a new id, the id-bearing last line when copying an existing one.
Multi-line blocks therefore got truncated aliases.

- new EasyCopy.getBlockText(): walks up to the block's first line (bullet
  or paragraph start, also from a continuation line under the cursor),
  extends through the soft-wrapped continuation lines, strips per-line
  trailing block ids, and joins with newlines
- both copy paths pass the full block text; extractBlockDisplayText joins
  the lines with single spaces (blank lines dropped) before applying the
  existing word/char limits
- the trailing-id strip is now ^[a-zA-Z0-9_-]+$ per line instead of the
  greedy /\^.*$/, so a mid-line caret (e.g. 2^10) survives
- firstLine renamed blockText through the pipeline; 5 new tests
2026-06-12 11:42:28 +03:00

116 lines
2.6 KiB
TypeScript

import { extractBlockDisplayText, sanitizeHeadingForLink, stripWikiBrackets } from './linkBuilder';
export interface CopyMetadata {
clipboardText: string;
sourceFilePath: string;
subpath: string;
alias: string;
isEmbed: boolean;
timestamp: number;
}
export interface BuildBlockCopyMetadataInput {
clipboardText: string;
sourceFilePath: string;
blockId: string;
useBrief: boolean;
/** 块的完整文本(可多行,行尾块 ID 会被清理) */
blockText: string;
autoBlockDisplayText: boolean;
autoEmbedBlockLink: boolean;
blockDisplayWordLimit: number;
blockDisplayCharLimit: number;
}
export function buildBlockCopyMetadata(input: BuildBlockCopyMetadataInput): CopyMetadata {
const {
clipboardText,
sourceFilePath,
blockId,
useBrief,
blockText,
autoBlockDisplayText,
autoEmbedBlockLink,
blockDisplayWordLimit,
blockDisplayCharLimit,
} = input;
let alias = blockId;
if (useBrief && blockText) {
alias = extractBlockDisplayText(blockText, blockId, blockDisplayWordLimit, blockDisplayCharLimit);
}
if (!autoBlockDisplayText) {
alias = '';
}
return {
clipboardText,
sourceFilePath,
subpath: `#^${blockId}`,
alias,
isEmbed: autoEmbedBlockLink,
timestamp: Date.now(),
};
}
export interface BuildHeadingCopyMetadataInput {
clipboardText: string;
sourceFilePath: string;
heading: string;
filename: string;
frontmatterTitle?: string;
useHeadingAsDisplayText: boolean;
headingLinkSeparator: string;
isNoteLink: boolean;
}
export function buildHeadingCopyMetadata(input: BuildHeadingCopyMetadataInput): CopyMetadata {
const {
clipboardText,
sourceFilePath,
heading: rawHeading,
filename,
frontmatterTitle,
useHeadingAsDisplayText,
headingLinkSeparator,
isNoteLink,
} = input;
const heading = stripWikiBrackets(rawHeading);
let alias = heading;
if (!useHeadingAsDisplayText) {
const separator = headingLinkSeparator || '#';
const filenameOrTitle = frontmatterTitle || filename;
alias = `${filenameOrTitle}${separator}${heading}`;
}
if (isNoteLink && filename === heading) {
alias = '';
}
return {
clipboardText,
sourceFilePath,
subpath: isNoteLink ? '' : `#${sanitizeHeadingForLink(heading)}`,
alias,
isEmbed: false,
timestamp: Date.now(),
};
}
export interface BuildFileCopyMetadataInput {
clipboardText: string;
sourceFilePath: string;
displayText?: string;
}
export function buildFileCopyMetadata(input: BuildFileCopyMetadataInput): CopyMetadata {
return {
clipboardText: input.clipboardText,
sourceFilePath: input.sourceFilePath,
subpath: '',
alias: input.displayText || '',
isEmbed: false,
timestamp: Date.now(),
};
}