This commit is contained in:
William Nurmi 2026-06-12 13:37:16 +03:00 committed by GitHub
commit 8cb6ca4d8a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 183 additions and 29 deletions

View file

@ -11,7 +11,7 @@ describe('buildBlockCopyMetadata', () => {
sourceFilePath: 'notes/note.md',
blockId: 'abc123',
useBrief: false,
firstLine: '',
blockText: '',
autoBlockDisplayText: true,
autoEmbedBlockLink: false,
blockDisplayWordLimit: 5,
@ -34,11 +34,11 @@ describe('buildBlockCopyMetadata', () => {
expect(meta.alias).toBe('abc123');
});
it('uses extracted display text when useBrief + firstLine are set', () => {
it('uses extracted display text when useBrief + blockText are set', () => {
const meta = buildBlockCopyMetadata({
...base,
useBrief: true,
firstLine: 'The quick brown fox jumps over the lazy dog',
blockText: 'The quick brown fox jumps over the lazy dog',
blockDisplayWordLimit: 4,
});
// extractBlockDisplayText 会截断到单词数上限
@ -46,8 +46,8 @@ describe('buildBlockCopyMetadata', () => {
expect(meta.alias.length).toBeGreaterThan(0);
});
it('keeps blockId alias when useBrief is true but firstLine is empty', () => {
const meta = buildBlockCopyMetadata({ ...base, useBrief: true, firstLine: '' });
it('keeps blockId alias when useBrief is true but blockText is empty', () => {
const meta = buildBlockCopyMetadata({ ...base, useBrief: true, blockText: '' });
expect(meta.alias).toBe('abc123');
});
@ -60,7 +60,7 @@ describe('buildBlockCopyMetadata', () => {
const meta = buildBlockCopyMetadata({
...base,
useBrief: true,
firstLine: 'Some long first line of content here',
blockText: 'Some long first line of content here',
autoBlockDisplayText: false,
});
expect(meta.alias).toBe('');

View file

@ -14,7 +14,8 @@ export interface BuildBlockCopyMetadataInput {
sourceFilePath: string;
blockId: string;
useBrief: boolean;
firstLine: string;
/** 块的完整文本(可多行,行尾块 ID 会被清理) */
blockText: string;
autoBlockDisplayText: boolean;
autoEmbedBlockLink: boolean;
blockDisplayWordLimit: number;
@ -27,7 +28,7 @@ export function buildBlockCopyMetadata(input: BuildBlockCopyMetadataInput): Copy
sourceFilePath,
blockId,
useBrief,
firstLine,
blockText,
autoBlockDisplayText,
autoEmbedBlockLink,
blockDisplayWordLimit,
@ -35,8 +36,8 @@ export function buildBlockCopyMetadata(input: BuildBlockCopyMetadataInput): Copy
} = input;
let alias = blockId;
if (useBrief && firstLine) {
alias = extractBlockDisplayText(firstLine, blockId, blockDisplayWordLimit, blockDisplayCharLimit);
if (useBrief && blockText) {
alias = extractBlockDisplayText(blockText, blockId, blockDisplayWordLimit, blockDisplayCharLimit);
}
if (!autoBlockDisplayText) {
alias = '';

View file

@ -42,7 +42,8 @@ export type TranslationKey =
| 'block-id-insert-position' | 'block-id-insert-position-desc'
| 'block-id-end-of-block' | 'block-id-next-line' | 'block-id-next-line-with-gap'
| 'modal-block-id' | 'modal-block-id-desc'
| 'auto-block-display-text' | 'auto-block-display-text-desc'
| 'auto-block-display-text' | 'auto-block-display-text-desc'
| 'block-display-full-block' | 'block-display-full-block-desc'
| 'block-display-word-limit' | 'block-display-word-limit-desc'
| 'block-display-char-limit' | 'block-display-char-limit-desc'
| 'generate-current-block-link-auto' | 'generate-current-block-link-manual'
@ -136,6 +137,8 @@ export const translations: Record<Language, Record<TranslationKey, string>> = {
'enable-link-desc': 'Enable copying link like [linktitle](linkurl) - the plugin will copy the title or the URL of the link based on the current cursor position.',
'auto-block-display-text': 'Generate display text for block links',
'auto-block-display-text-desc': 'If enabled, display text will be automatically added to generated block ID links',
'block-display-full-block': 'Use the whole block as display text',
'block-display-full-block-desc': 'If enabled, the display text is taken from the whole (multi-line) block: soft-wrapped continuation lines are joined with spaces. If disabled, only a single line is used (legacy behavior). Tables, code blocks and math blocks fall back to the block ID.',
'block-display-word-limit': 'Block Display Text: Word limit for English-like languages',
'block-display-word-limit-desc': 'Maximum number of words to show in block display text for space-separated languages (e.g., English "this is a sentence")',
'block-display-char-limit': 'Block Display Text: Character limit for CJK-like languages',
@ -280,6 +283,8 @@ export const translations: Record<Language, Record<TranslationKey, string>> = {
'frontmatter-key-desc': '用于显示文本的笔记属性名默认title',
'auto-block-display-text': '生成块链接的显示文本',
'auto-block-display-text-desc': '启用后会自动为生成的块ID链接添加显示文本',
'block-display-full-block': '显示文本取整个块',
'block-display-full-block-desc': '启用后,显示文本取自整个(多行)块:软换行的后续行会用空格拼接。关闭时仅使用单独一行(旧版行为)。表格、代码块和数学块会回退到块 ID。',
'block-display-word-limit': '块显示文本:英语类语言的单词数限制',
'block-display-word-limit-desc': '使用空格分隔的语言(如英语 "this is a sentence")在块显示文本中显示的最大单词数',
'block-display-char-limit': '块显示文本CJK 类语言的字符数限制',
@ -299,6 +304,8 @@ export const translations: Record<Language, Record<TranslationKey, string>> = {
'add-extra-commands-desc': '啟用後,會在命令面板中新增「複製當前筆記鏈接」和「生成並複製當前塊鏈接」命令',
'auto-block-display-text': '生成塊連結的顯示文本',
'auto-block-display-text-desc': '啟用後會自動為生成的塊ID連結添加顯示文本',
'block-display-full-block': '顯示文本取整個塊',
'block-display-full-block-desc': '啟用後,顯示文本取自整個(多行)塊:軟換行的後續行會用空格拼接。關閉時僅使用單獨一行(舊版行為)。表格、代碼塊和數學塊會回退到塊 ID。',
'block-display-word-limit': '塊顯示文本:英語類語言的單詞數限制',
'block-display-word-limit-desc': '空格分隔語言(如 "this is a sentence")在塊顯示文本中顯示的最大單詞數',
'block-display-char-limit': '塊顯示文本CJK 類語言的字符數限制',

View file

@ -765,6 +765,45 @@ describe('extractBlockDisplayText', () => {
});
});
describe('multi-line blocks', () => {
it('joins soft-wrapped continuation lines with spaces', () => {
const result = extractBlockDisplayText('- Evolutionary values\n zeroth existence', 'fallback', 99, 99);
expect(result).toBe('Evolutionary values zeroth existence');
});
it('strips a trailing block id from any line of the block', () => {
const result = extractBlockDisplayText('- first part ^abc123\n second part ^def-456', 'fallback', 99, 99);
expect(result).toBe('first part second part');
});
it('drops blank lines when joining', () => {
const result = extractBlockDisplayText('first paragraph\n\n second paragraph', 'fallback', 99, 99);
expect(result).toBe('first paragraph second paragraph');
});
it('still applies the word limit to the joined text', () => {
const result = extractBlockDisplayText('one two\n three four five', 'fallback', 3, 5);
expect(result).toBe('one two three');
});
it('keeps a mid-line caret intact', () => {
const result = extractBlockDisplayText('value is 2^10 here', 'fallback', 99, 99);
expect(result).toBe('value is 2^10 here');
});
it('keeps an end-of-line caret expression intact (block ids need a separator)', () => {
const result = extractBlockDisplayText('first line ^abc123\n the result is 2^10', 'fallback', 99, 99);
expect(result).toBe('first line the result is 2^10');
});
it('strips pipes that would break the wiki link alias', () => {
const result = extractBlockDisplayText('| col1 | col2 |', 'fallback', 99, 99);
expect(result).not.toContain('|');
expect(result).toContain('col1');
expect(result).toContain('col2');
});
});
describe('English text', () => {
it('extracts first 3 words by default', () => {
const result = extractBlockDisplayText('The quick brown fox jumps', 'fallback', 3, 5);
@ -892,7 +931,7 @@ describe('buildBlockLink', () => {
const defaults = {
filename: 'MyNote',
useBrief: true,
firstLine: 'The quick brown fox',
blockText: 'The quick brown fox',
linkFormat: LinkFormat.WIKILINK,
autoBlockDisplayText: true,
autoEmbedBlockLink: false,
@ -957,11 +996,11 @@ describe('buildBlockLink', () => {
});
});
it('uses blockId as display when firstLine is empty', () => {
it('uses blockId as display when blockText is empty', () => {
const result = buildBlockLink({
...defaults,
blockId: 'abc123',
firstLine: '',
blockText: '',
});
expect(result).toBe('[[MyNote#^abc123|abc123]]');
});

View file

@ -181,15 +181,26 @@ export function buildHeadingLink(options: BuildHeadingLinkOptions): BuildHeading
// --- 块显示文本 ---
export function extractBlockDisplayText(
firstLine: string,
blockText: string,
blockId: string,
wordLimit: number,
charLimit: number,
): string {
let text = firstLine;
// 先去掉结尾的 ^ 及其后面的内容(如果有的话)
text = text.replace(/\^.*\s*$/, '');
text = text.trim().replace(/- \[.\]\s+/, '').replace('- ', '').replace(/=|\*|\[|\]|\(|\)|`|>\s+/g, '');
// 逐行清理:去掉行尾的块 ID 和列表/任务前缀,再把多行块合并为一行。
// 软换行的多行内容属于同一个 block块 ID 在最后一行末尾),
// 所以显示文本要包含整个块,换行替换为空格。
let text = blockText
.split('\n')
.map((line) => line
// 块 ID 前必须有空白(或独占一行)才是真正的 ID——行尾的 2^10 这类不算
.replace(/(?:^|\s+)\^[a-zA-Z0-9_-]+\s*$/, '')
.trim()
.replace(/^- \[.\]\s+/, '')
.replace(/^- /, ''))
.filter((line) => line.length > 0)
.join(' ');
// | 会截断 wiki 链接的别名(也可能撑破表格),所以和其他语法字符一起去掉
text = text.replace(/=|\*|\[|\]|\(|\)|`|\||>\s+/g, '');
if (!text) return blockId;
@ -227,7 +238,8 @@ export interface BuildBlockLinkOptions {
blockId: string;
filename: string;
useBrief: boolean;
firstLine: string;
/** 块的完整文本(可多行,行尾块 ID 会被清理) */
blockText: string;
linkFormat: LinkFormat;
autoBlockDisplayText: boolean;
autoEmbedBlockLink: boolean;
@ -237,14 +249,14 @@ export interface BuildBlockLinkOptions {
export function buildBlockLink(options: BuildBlockLinkOptions): string {
const {
blockId, filename, useBrief, firstLine,
blockId, filename, useBrief, blockText,
linkFormat, autoBlockDisplayText, autoEmbedBlockLink,
blockDisplayWordLimit, blockDisplayCharLimit,
} = options;
let displayText = blockId;
if (useBrief && firstLine) {
displayText = extractBlockDisplayText(firstLine, blockId, blockDisplayWordLimit, blockDisplayCharLimit);
if (useBrief && blockText) {
displayText = extractBlockDisplayText(blockText, blockId, blockDisplayWordLimit, blockDisplayCharLimit);
}
let link: string;

View file

@ -327,6 +327,71 @@ export default class EasyCopy extends Plugin {
return { start, end };
}
/**
* `- ` / `* ` / `+ ` / `1. ` / `1) ` block
* trim
*/
private isListItemStart(trimmedLine: string): boolean {
return /^(?:[-*+]|\d+[.)])\s/.test(trimmedLine);
}
/**
* block block
*
* ID extractBlockDisplayText
*
* block
* //退
* ID
*/
private getBlockText(editor: Editor, cursorLine: number): string {
// 独立成行的块 ID与内容隔一个空行属于上面那个 block——
// 先跳回上面的 block 再展开,别名才能取到真正的块内容
if (
cursorLine >= 2 &&
/^\^[a-zA-Z0-9_-]+$/.test(editor.getLine(cursorLine).trim()) &&
editor.getLine(cursorLine - 1).trim() === '' &&
editor.getLine(cursorLine - 2).trim() !== ''
) {
cursorLine -= 2;
}
let start = cursorLine;
while (start > 0) {
const line = editor.getLine(start).trim();
if (line === '' || this.isListItemStart(line) || line.startsWith('#')) {
break; // 本行自己就是 block 的开头
}
const prev = editor.getLine(start - 1).trim();
if (prev === '' || prev.startsWith('#')) {
break; // 上一行是空行或标题:本行是段落开头
}
start--;
}
let end = start;
// 标题自成一个单行block不向下延伸
const startIsHeading = editor.getLine(start).trim().startsWith('#');
while (
!startIsHeading &&
end < editor.lineCount() - 1 &&
this.isContinuousText(editor.getLine(end + 1)) &&
!this.isListItemStart(editor.getLine(end + 1).trim())
) {
end++;
}
const lines: string[] = [];
for (let i = start; i <= end; i++) {
const line = editor.getLine(i);
const trimmed = line.trim();
if (trimmed.startsWith('|') || trimmed.startsWith('```') || trimmed.startsWith('$$')) {
return '';
}
// 块 ID 前必须有空白(或独占一行)才是真正的 ID——
// 行尾的 2^10 这类插入语不能被当作块 ID 去掉
lines.push(line.replace(/(?:^|\s+)\^[a-zA-Z0-9_-]+\s*$/, ''));
}
return lines.join('\n');
}
/*
* Block ID+
*/
@ -334,6 +399,7 @@ export default class EasyCopy extends Plugin {
const cursor = editor.getCursor();
const { end } = this.detectBlockRange(editor, cursor.line);
let lastLine = editor.getLine(end);
let lastLineNo = end;
// 检查下两行是否为单独的块ID行
if (end <= editor.lineCount() - 2) {
@ -343,6 +409,7 @@ export default class EasyCopy extends Plugin {
// 判断该行是否为合法的 block ID 行:前面可有空格,必须以 ^ 开头,后面只能是 block id不允许有其他字符或空格
if (/^\s*\^[a-zA-Z0-9_-]+$/.test(possibleBlockIdLine)) {
lastLine = possibleBlockIdLine;
lastLineNo = end + 2;
}
}
}
@ -353,6 +420,7 @@ export default class EasyCopy extends Plugin {
return {
type: ContextType.BLOCKID,
curLine: lastLine,
line: lastLineNo,
match: match[1],
range: [lastLine.lastIndexOf('^'), lastLine.length]
};
@ -600,7 +668,14 @@ export default class EasyCopy extends Plugin {
switch (contextType.type) {
case ContextType.BLOCKID:
this.copyBlockLink(contextType.match!, filename, true, contextType.curLine);
// 显示文本来源:设置开启时取整个 block 的文本(多行块的别名才完整),
// 否则保持旧行为,仅用检测到块 ID 的那一行。
// 从块 ID 实际所在的行(而不是光标行)展开,确保别名和链接
// 指向同一个 block——检测范围可能跨越列表项。
this.copyBlockLink(contextType.match!, filename, true,
this.settings.blockDisplayFullBlock
? this.getBlockText(editor, contextType.line ?? editor.getCursor().line)
: contextType.curLine);
return;
case ContextType.BOLD:
void navigator.clipboard.writeText(contextType.match!);
@ -697,12 +772,12 @@ export default class EasyCopy extends Plugin {
/**
*
*/
private copyBlockLink(content: string, filename: string, useBrief: boolean, firstLine=''): void {
private copyBlockLink(content: string, filename: string, useBrief: boolean, blockText=''): void {
const blockIdLink = buildBlockLink({
blockId: content,
filename,
useBrief,
firstLine,
blockText,
linkFormat: this.getEffectiveLinkFormat(),
autoBlockDisplayText: this.settings.autoBlockDisplayText,
autoEmbedBlockLink: this.settings.autoEmbedBlockLink,
@ -720,7 +795,7 @@ export default class EasyCopy extends Plugin {
sourceFilePath: blockFile.path,
blockId: content,
useBrief,
firstLine,
blockText,
autoBlockDisplayText: this.settings.autoBlockDisplayText,
autoEmbedBlockLink: this.settings.autoEmbedBlockLink,
blockDisplayWordLimit: this.settings.blockDisplayWordLimit,
@ -865,7 +940,13 @@ export default class EasyCopy extends Plugin {
// —— 新逻辑:定位 block段落末尾 ——
const cursor = editor.getCursor();
const { start, end } = this.detectBlockRange(editor, cursor.line);
const firstLine = editor.getLine(start);
// 在插入块 ID 之前取显示文本来源:设置开启时取整个 block 的文本
// (多行 → 一行),否则保持旧行为,仅用 block 的第一行。
// 从将要插入块 ID 的行end展开确保别名和链接指向同一个
// block——检测范围可能跨越列表项。
const blockText = this.settings.blockDisplayFullBlock
? this.getBlockText(editor, end)
: editor.getLine(start);
const lastLine = editor.getLine(end);
// 检查 block 最后一行末是否已有块ID
@ -904,7 +985,7 @@ export default class EasyCopy extends Plugin {
const useBrief = !isManual;
// 生成之后复制块ID链接
this.copyBlockLink(blockId, filename, useBrief, firstLine);
this.copyBlockLink(blockId, filename, useBrief, blockText);
}
}

View file

@ -303,6 +303,17 @@ export class EasyCopySettingTab extends PluginSettingTab {
// 新增:块显示文本限制设置,仅在启用 autoBlockDisplayText 时显示
if (this.plugin.settings.autoBlockDisplayText) {
blockIdGroup.addSetting(setting => setting
.setName(this.plugin.t('block-display-full-block'))
.setDesc(this.plugin.t('block-display-full-block-desc'))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.blockDisplayFullBlock)
.onChange( value => {
this.plugin.settings.blockDisplayFullBlock = value;
void this.plugin.saveSettings();
})
));
blockIdGroup.addSetting(setting => setting
.setName(this.plugin.t('block-display-word-limit'))
.setDesc(this.plugin.t('block-display-word-limit-desc'))

View file

@ -37,6 +37,7 @@ export enum ContextType {
export interface ContextData {
type: ContextType;
curLine: string;
line?: number; // curLine 所在的行号(目前仅 BLOCKID 上下文会设置)
match: string | null;
range: [number, number] | null;
}
@ -71,6 +72,7 @@ export interface EasyCopySettings {
allowManualBlockId: boolean; // 是否允许手动输入 Block ID
blockIdInsertPosition: BlockIdInsertPosition; // 块ID的插入位置
autoBlockDisplayText: boolean; // 自动为 Block 添加显示文本
blockDisplayFullBlock: boolean; // 显示文本取整个(多行)块;关闭时与旧版一致仅取单行
blockDisplayWordLimit: number; // Block 显示文本英文单词限制(按空格分隔)
blockDisplayCharLimit: number; // Block 显示文本字符限制(非英文语言)
enableDisplayNameRegex: boolean; // 是否启用正则替换显示名称
@ -108,6 +110,7 @@ export const DEFAULT_SETTINGS: EasyCopySettings = {
allowManualBlockId: false, // 默认关闭
blockIdInsertPosition: BlockIdInsertPosition.END_OF_BLOCK, // 默认在块末尾插入
autoBlockDisplayText: true,
blockDisplayFullBlock: false, // 默认关闭:保持旧的单行行为
blockDisplayWordLimit: 3, // 英文单词限制3个单词
blockDisplayCharLimit: 5, // 字符限制5个字符
enableDisplayNameRegex: false,