obsidian-img2other/main.js
wsb1224 71a7dec6aa 1. 支持markdown和富文本编辑器
2. 完善代码块和图片复制粘贴
2026-04-09 17:42:37 +08:00

4173 lines
147 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* eslint-disable */
const { Plugin, Notice, TFile, Modal, Setting, MarkdownView, MarkdownRenderer, PluginSettingTab } = require('obsidian');
let electronClipboard = null;
try {
electronClipboard = require('electron').clipboard;
} catch (e) {
electronClipboard = null;
}
/* ========== 用户配置 ========== */
const DEFAULT_USER_CONFIG = {
currentPlatform: 'feishu',
imageMaxWidth: 1000,
imageQuality: 0.92,
displayMaxWidth: 800,
imageFormat: 'auto',
platformImageStrategies: {
wechat: 'balanced',
feishu: 'quality',
zhihu: 'balanced',
yuque: 'quality'
},
platformThemes: {
wechat: 'official',
feishu: 'minimal',
zhihu: 'knowledge',
yuque: 'soft'
},
customThemes: {
custom: {
name: '我的主题',
fontFamily: "'PingFang SC', 'Microsoft YaHei', sans-serif",
textColor: '#24303f',
mutedColor: '#64748b',
backgroundColor: '#ffffff',
accentColor: '#2563eb',
linkColor: '#2563eb',
blockquoteBorder: '#60a5fa',
blockquoteBackground: '#eff6ff',
codeBackground: '#f8fafc',
codeColor: '#1e293b',
headingColor: '#0f172a',
borderColor: '#dbe2ea',
borderRadius: '8px',
paragraphSpacing: '16px',
lineHeight: '1.75',
headingDivider: true,
tableHeaderFill: true,
imageRounded: true,
codeBlockBorder: true
}
},
customThemeEditorKey: 'custom',
customThemeSearchQuery: '',
customThemeOrder: ['custom'],
themePreviewPlatform: 'wechat',
themeListShowUsedOnly: false,
themeListUsedFirst: true,
themeListShowRecommendedOnly: false,
themeManagerListCollapsed: false,
lastAppliedRecommendedTheme: null,
themeManagerSidebarWidth: 280,
platformThemeOverrides: {
wechat: {},
feishu: {},
zhihu: {},
yuque: {}
},
compressGif: false,
enableCodeHighlight: true,
enableThemeStyles: true,
enableFrontmatter: false,
enableProgressNotice: true,
enablePreview: true,
parallelImageProcessing: true,
maxParallelImages: 5,
themeStylesCache: true,
includeFrontmatter: false,
clipboardFormats: ['text/html', 'text/plain'],
};
let USER_CONFIG = { ...DEFAULT_USER_CONFIG };
/* ========== 图片优化配置 ========== */
const IMAGE_CONFIG = {
MAX_WIDTH: 1000,
QUALITY: 0.85,
DISPLAY_MAX_WIDTH: 800,
FORMAT: 'jpeg',
COMPRESS_GIF: false
};
/* ========== 缓存管理 ========== */
const Cache = {
themeStyles: null,
themeType: null,
clear() {
this.themeStyles = null;
this.themeType = null;
}
};
/* ========== 平台预设配置 ========== */
const PLATFORM_PRESETS = {
wechat: {
name: '微信公众号',
imageMaxWidth: 900,
imageQuality: 0.9,
displayMaxWidth: '100%',
imageFormat: 'auto',
compressGif: false,
enableCodeHighlight: true,
enableThemeStyles: false,
enableProgressNotice: true,
description: '适合微信公众号图片宽度900px质量0.85',
compatibilityNotes: [
'代码块使用截图效果最佳',
'表格可能显示异常',
'样式已优化为内联样式'
],
styleStrategy: 'inline',
styleMode: 'inline-only',
preserveThemeLevel: 'medium',
supports: {
styleTag: false,
className: false,
table: 'partial',
codeBlock: 'partial',
math: 'weak'
},
transforms: {
flattenCallout: true,
simplifyTable: true,
simplifyNestedBlocks: true
},
previewFrame: {
background: '#f5f5f5',
padding: '20px 14px',
contentBackground: '#ffffff',
contentShadow: '0 8px 24px rgba(0,0,0,0.06)',
contentBorder: '1px solid #ebeef5'
},
maxImageWidth: 900,
fontSize: '16px',
lineHeight: '1.75'
},
feishu: {
name: '飞书云文档',
imageMaxWidth: 1000,
imageQuality: 0.92,
displayMaxWidth: 800,
imageFormat: 'auto',
compressGif: false,
enableCodeHighlight: true,
enableThemeStyles: true,
enableProgressNotice: true,
description: '适合飞书文档图片宽度1000px质量0.85',
compatibilityNotes: [
'支持完整的样式和代码高亮',
'图片支持拖拽调整大小',
'推荐使用此预设'
],
styleStrategy: 'css',
styleMode: 'inline-preferred',
preserveThemeLevel: 'high',
supports: {
styleTag: true,
className: true,
table: 'good',
codeBlock: 'good',
math: 'medium'
},
transforms: {
flattenCallout: false,
simplifyTable: false,
simplifyNestedBlocks: false
},
previewFrame: {
background: '#eef2f7',
padding: '24px',
contentBackground: '#ffffff',
contentShadow: '0 10px 28px rgba(15,23,42,0.08)',
contentBorder: '1px solid #d8dee9'
},
maxImageWidth: 1000,
fontSize: '16px',
lineHeight: '1.6'
},
zhihu: {
name: '知乎',
imageMaxWidth: 800,
imageQuality: 0.9,
displayMaxWidth: 600,
imageFormat: 'auto',
compressGif: false,
enableCodeHighlight: true,
enableThemeStyles: true,
enableProgressNotice: true,
description: '适合知乎回答图片宽度800px质量0.8',
compatibilityNotes: [
'代码高亮支持良好',
'表格样式基本兼容',
'图片支持点击查看大图'
],
styleStrategy: 'css',
styleMode: 'inline-preferred',
preserveThemeLevel: 'medium',
supports: {
styleTag: true,
className: true,
table: 'partial',
codeBlock: 'partial',
math: 'weak'
},
transforms: {
flattenCallout: false,
simplifyTable: true,
simplifyNestedBlocks: true
},
previewFrame: {
background: '#f6f6f6',
padding: '28px 18px',
contentBackground: '#fffdf8',
contentShadow: '0 12px 30px rgba(120, 113, 108, 0.08)',
contentBorder: '1px solid #eee7da'
},
maxImageWidth: 800,
fontSize: '16px',
lineHeight: '1.6'
},
yuque: {
name: '语雀',
imageMaxWidth: 900,
imageQuality: 0.9,
displayMaxWidth: 700,
imageFormat: 'auto',
compressGif: false,
enableCodeHighlight: true,
enableThemeStyles: true,
enableProgressNotice: true,
description: '适合语雀文档图片宽度900px质量0.8',
compatibilityNotes: [
'样式支持完整',
'代码高亮效果佳',
'推荐使用'
],
styleStrategy: 'css',
styleMode: 'inline-preferred',
preserveThemeLevel: 'high',
supports: {
styleTag: true,
className: true,
table: 'good',
codeBlock: 'good',
math: 'medium'
},
transforms: {
flattenCallout: false,
simplifyTable: false,
simplifyNestedBlocks: false
},
previewFrame: {
background: '#f7f8fb',
padding: '24px 18px',
contentBackground: '#fcfcfd',
contentShadow: '0 10px 26px rgba(99,102,241,0.06)',
contentBorder: '1px solid #e6e8f0'
},
maxImageWidth: 900,
fontSize: '16px',
lineHeight: '1.6'
}
};
const PLATFORM_THEMES = {
official: {
name: '官方简洁',
fontFamily: "'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif",
textColor: '#222222',
mutedColor: '#57606a',
backgroundColor: '#ffffff',
accentColor: '#07c160',
linkColor: '#576b95',
blockquoteBorder: '#07c160',
blockquoteBackground: '#f6fffb',
codeBackground: '#f6f8fa',
codeColor: '#1f2328',
headingColor: '#111111',
borderColor: '#d0d7de',
borderRadius: '6px',
paragraphSpacing: '16px',
lineHeight: '1.75',
headingDivider: true,
tableHeaderFill: true,
imageRounded: true,
codeBlockBorder: true
},
minimal: {
name: '极简专业',
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif",
textColor: '#1f2328',
mutedColor: '#59636e',
backgroundColor: '#ffffff',
accentColor: '#2f80ed',
linkColor: '#0969da',
blockquoteBorder: '#2f80ed',
blockquoteBackground: '#f6f8fa',
codeBackground: '#f6f8fa',
codeColor: '#24292f',
headingColor: '#111827',
borderColor: '#d8dee4',
borderRadius: '8px',
paragraphSpacing: '16px',
lineHeight: '1.7',
headingDivider: true,
tableHeaderFill: true,
imageRounded: true,
codeBlockBorder: true
},
knowledge: {
name: '知识专栏',
fontFamily: "'Noto Serif SC', 'Source Han Serif SC', Georgia, serif",
textColor: '#2d2d2d',
mutedColor: '#666666',
backgroundColor: '#fffdf8',
accentColor: '#d97706',
linkColor: '#b45309',
blockquoteBorder: '#f59e0b',
blockquoteBackground: '#fff7ed',
codeBackground: '#f8fafc',
codeColor: '#1e293b',
headingColor: '#1f2937',
borderColor: '#e5dccf',
borderRadius: '8px',
paragraphSpacing: '18px',
lineHeight: '1.85',
headingDivider: false,
tableHeaderFill: true,
imageRounded: false,
codeBlockBorder: false
},
soft: {
name: '柔和文档',
fontFamily: "'Inter', 'PingFang SC', 'Microsoft YaHei', sans-serif",
textColor: '#24303f',
mutedColor: '#64748b',
backgroundColor: '#fcfcfd',
accentColor: '#7c3aed',
linkColor: '#6d28d9',
blockquoteBorder: '#8b5cf6',
blockquoteBackground: '#f5f3ff',
codeBackground: '#f8fafc',
codeColor: '#1e293b',
headingColor: '#0f172a',
borderColor: '#e2e8f0',
borderRadius: '10px',
paragraphSpacing: '16px',
lineHeight: '1.75',
headingDivider: true,
tableHeaderFill: true,
imageRounded: true,
codeBlockBorder: true
},
'wechat-business': {
name: '公众号商务风',
fontFamily: "'PingFang SC', 'Microsoft YaHei', sans-serif",
textColor: '#1f1f1f',
mutedColor: '#5f6368',
backgroundColor: '#ffffff',
accentColor: '#0f766e',
linkColor: '#0f766e',
blockquoteBorder: '#14b8a6',
blockquoteBackground: '#f0fdfa',
codeBackground: '#f8fafc',
codeColor: '#0f172a',
headingColor: '#111827',
borderColor: '#dbe4ea',
borderRadius: '4px',
paragraphSpacing: '18px',
lineHeight: '1.8',
headingDivider: true,
tableHeaderFill: false,
imageRounded: false,
codeBlockBorder: false
},
'wechat-minimal': {
name: '公众号极简风',
fontFamily: "'PingFang SC', 'Helvetica Neue', sans-serif",
textColor: '#262626',
mutedColor: '#737373',
backgroundColor: '#ffffff',
accentColor: '#3b82f6',
linkColor: '#2563eb',
blockquoteBorder: '#93c5fd',
blockquoteBackground: '#eff6ff',
codeBackground: '#f5f7fb',
codeColor: '#1f2937',
headingColor: '#111827',
borderColor: '#e5e7eb',
borderRadius: '2px',
paragraphSpacing: '16px',
lineHeight: '1.75',
headingDivider: false,
tableHeaderFill: false,
imageRounded: false,
codeBlockBorder: false
},
'zhihu-column': {
name: '知乎长文风',
fontFamily: "'Noto Serif SC', Georgia, serif",
textColor: '#2b2b2b',
mutedColor: '#646464',
backgroundColor: '#fffefb',
accentColor: '#2563eb',
linkColor: '#1d4ed8',
blockquoteBorder: '#60a5fa',
blockquoteBackground: '#f8fbff',
codeBackground: '#f8fafc',
codeColor: '#111827',
headingColor: '#1f2937',
borderColor: '#e6e8ec',
borderRadius: '6px',
paragraphSpacing: '20px',
lineHeight: '1.9',
headingDivider: false,
tableHeaderFill: true,
imageRounded: false,
codeBlockBorder: false
},
'feishu-team': {
name: '飞书团队文档风',
fontFamily: "Inter, 'PingFang SC', 'Segoe UI', sans-serif",
textColor: '#172033',
mutedColor: '#5b6475',
backgroundColor: '#ffffff',
accentColor: '#1456f0',
linkColor: '#1456f0',
blockquoteBorder: '#7aa2ff',
blockquoteBackground: '#f5f8ff',
codeBackground: '#f3f6fb',
codeColor: '#172033',
headingColor: '#111827',
borderColor: '#dbe2ea',
borderRadius: '8px',
paragraphSpacing: '16px',
lineHeight: '1.7',
headingDivider: true,
tableHeaderFill: true,
imageRounded: true,
codeBlockBorder: true
},
'yuque-notes': {
name: '语雀知识库风',
fontFamily: "'Inter', 'PingFang SC', sans-serif",
textColor: '#243042',
mutedColor: '#64748b',
backgroundColor: '#fcfcfe',
accentColor: '#7c3aed',
linkColor: '#6d28d9',
blockquoteBorder: '#a78bfa',
blockquoteBackground: '#f5f3ff',
codeBackground: '#f8fafc',
codeColor: '#0f172a',
headingColor: '#111827',
borderColor: '#e5e7ef',
borderRadius: '10px',
paragraphSpacing: '17px',
lineHeight: '1.78',
headingDivider: true,
tableHeaderFill: true,
imageRounded: true,
codeBlockBorder: true
}
};
const PLATFORM_THEME_RECOMMENDATIONS = {
wechat: ['wechat-business', 'official', 'wechat-minimal'],
feishu: ['feishu-team', 'minimal'],
zhihu: ['zhihu-column', 'knowledge'],
yuque: ['yuque-notes', 'soft']
};
const REMOVE_SELECTORS = [
'.collapse-indicator',
'.internal-query-header',
'.copy-code-button',
'.markdown-embed-link',
'.mod-header',
'.metadata-container',
'.metadata-properties',
'.inline-title',
'.cm-widgetBuffer',
'.markdown-reading-view .print',
'script',
'style'
];
const BASE_STYLE_PROPS = [
'color',
'background-color',
'font-size',
'font-family',
'font-weight',
'font-style',
'line-height',
'text-align',
'text-decoration',
'letter-spacing',
'margin-top',
'margin-bottom',
'margin-left',
'margin-right',
'padding-top',
'padding-bottom',
'padding-left',
'padding-right',
'border-top',
'border-right',
'border-bottom',
'border-left',
'border-radius',
'display',
'width',
'max-width',
'min-width',
'white-space',
'overflow-x',
'list-style-type'
];
const TAG_STYLE_PROPS = {
p: ['margin-top', 'margin-bottom', 'line-height', 'text-align'],
li: ['margin-top', 'margin-bottom', 'line-height'],
blockquote: ['color', 'background-color', 'border-left', 'padding-left', 'margin-top', 'margin-bottom'],
pre: ['background-color', 'color', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left', 'border-radius', 'overflow-x', 'white-space'],
code: ['background-color', 'color', 'font-family', 'font-size', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left', 'border-radius', 'white-space'],
table: ['width', 'border-collapse', 'margin-top', 'margin-bottom'],
th: ['border-top', 'border-right', 'border-bottom', 'border-left', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left', 'background-color', 'text-align'],
td: ['border-top', 'border-right', 'border-bottom', 'border-left', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left', 'background-color', 'text-align'],
img: ['display', 'width', 'max-width', 'height', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left', 'border-radius'],
h1: ['color', 'font-size', 'font-weight', 'line-height', 'margin-top', 'margin-bottom'],
h2: ['color', 'font-size', 'font-weight', 'line-height', 'margin-top', 'margin-bottom'],
h3: ['color', 'font-size', 'font-weight', 'line-height', 'margin-top', 'margin-bottom'],
h4: ['color', 'font-size', 'font-weight', 'line-height', 'margin-top', 'margin-bottom'],
h5: ['color', 'font-size', 'font-weight', 'line-height', 'margin-top', 'margin-bottom'],
h6: ['color', 'font-size', 'font-weight', 'line-height', 'margin-top', 'margin-bottom']
};
/* ========== 应用平台预设 ========== */
function applyPlatformPreset(platformKey) {
const preset = PLATFORM_PRESETS[platformKey];
if (!preset) {
console.warn('[bulk-copy] 未找到平台预设:', platformKey);
return;
}
USER_CONFIG.currentPlatform = platformKey;
USER_CONFIG.imageMaxWidth = preset.imageMaxWidth;
USER_CONFIG.imageQuality = preset.imageQuality;
USER_CONFIG.displayMaxWidth = preset.displayMaxWidth;
USER_CONFIG.imageFormat = preset.imageFormat;
USER_CONFIG.compressGif = preset.compressGif;
USER_CONFIG.enableCodeHighlight = preset.enableCodeHighlight;
USER_CONFIG.enableThemeStyles = preset.enableThemeStyles;
USER_CONFIG.enableProgressNotice = preset.enableProgressNotice;
IMAGE_CONFIG.MAX_WIDTH = preset.maxImageWidth || preset.imageMaxWidth;
console.log('[bulk-copy] 已应用平台预设:', preset.name);
new Notice(`✅ 已切换到 ${preset.name} 预设`);
}
function syncImageConfigFromUserConfig() {
IMAGE_CONFIG.MAX_WIDTH = USER_CONFIG.imageMaxWidth;
IMAGE_CONFIG.QUALITY = USER_CONFIG.imageQuality;
IMAGE_CONFIG.DISPLAY_MAX_WIDTH = USER_CONFIG.displayMaxWidth;
IMAGE_CONFIG.FORMAT = USER_CONFIG.imageFormat;
IMAGE_CONFIG.COMPRESS_GIF = USER_CONFIG.compressGif;
}
function getCurrentImageStrategy(platformKey = USER_CONFIG.currentPlatform) {
return USER_CONFIG.platformImageStrategies?.[platformKey] || 'quality';
}
function getImageStrategyConfig(platformKey = USER_CONFIG.currentPlatform) {
switch (getCurrentImageStrategy(platformKey)) {
case 'size':
return {
keepPng: false,
smallImageThreshold: 150 * 1024,
qualityBoost: -0.12,
resizeThresholdBoost: -200
};
case 'balanced':
return {
keepPng: true,
smallImageThreshold: 280 * 1024,
qualityBoost: -0.04,
resizeThresholdBoost: 0
};
case 'quality':
default:
return {
keepPng: true,
smallImageThreshold: 500 * 1024,
qualityBoost: 0,
resizeThresholdBoost: 200
};
}
}
function getImageStrategyLabel(platformKey = USER_CONFIG.currentPlatform) {
switch (getCurrentImageStrategy(platformKey)) {
case 'size':
return '体积优先';
case 'balanced':
return '平衡';
case 'quality':
default:
return '高清优先';
}
}
function getCurrentPlatformTheme(platformKey = USER_CONFIG.currentPlatform) {
return USER_CONFIG.platformThemes?.[platformKey] || 'minimal';
}
function getAllThemeOptions() {
return {
...PLATFORM_THEMES,
...(USER_CONFIG.customThemes || {})
};
}
function getEditableCustomThemeKey() {
return USER_CONFIG.customThemeEditorKey || 'custom';
}
function getEditableCustomTheme() {
const customThemes = USER_CONFIG.customThemes || DEFAULT_USER_CONFIG.customThemes;
return customThemes[getEditableCustomThemeKey()] || customThemes.custom || DEFAULT_USER_CONFIG.customThemes.custom;
}
function getOrderedCustomThemeKeys() {
const customThemes = USER_CONFIG.customThemes || DEFAULT_USER_CONFIG.customThemes;
const allKeys = Object.keys(customThemes);
const configuredOrder = USER_CONFIG.customThemeOrder || DEFAULT_USER_CONFIG.customThemeOrder || [];
const ordered = configuredOrder.filter(key => allKeys.includes(key));
const missing = allKeys.filter(key => !ordered.includes(key));
return [...ordered, ...missing];
}
function getPlatformThemeConfig(platformKey = USER_CONFIG.currentPlatform) {
const allThemes = getAllThemeOptions();
const baseTheme = allThemes[getCurrentPlatformTheme(platformKey)] || allThemes.minimal || PLATFORM_THEMES.minimal;
const overrides = USER_CONFIG.platformThemeOverrides?.[platformKey] || {};
return {
...baseTheme,
...overrides
};
}
function getPlatformThemeLabel(platformKey = USER_CONFIG.currentPlatform) {
return getPlatformThemeConfig(platformKey).name;
}
function getRecommendedThemeKeys(platformKey) {
return PLATFORM_THEME_RECOMMENDATIONS[platformKey] || [];
}
function isRecommendedTheme(platformKey, themeKey) {
return getRecommendedThemeKeys(platformKey).includes(themeKey);
}
function getRecommendedThemeLabels(platformKey) {
return getRecommendedThemeKeys(platformKey)
.map(themeKey => getAllThemeOptions()[themeKey]?.name)
.filter(Boolean);
}
function updatePlatformThemeOverride(platformKey, key, value) {
USER_CONFIG.platformThemeOverrides = {
...DEFAULT_USER_CONFIG.platformThemeOverrides,
...(USER_CONFIG.platformThemeOverrides || {}),
[platformKey]: {
...(USER_CONFIG.platformThemeOverrides?.[platformKey] || {}),
[key]: value
}
};
}
function resetPlatformToRecommendedTheme(platformKey) {
const recommendedThemeKey = getRecommendedThemeKeys(platformKey)[0] || DEFAULT_USER_CONFIG.platformThemes[platformKey] || 'official';
USER_CONFIG.platformThemes = {
...DEFAULT_USER_CONFIG.platformThemes,
...(USER_CONFIG.platformThemes || {}),
[platformKey]: recommendedThemeKey
};
USER_CONFIG.platformThemeOverrides = {
...DEFAULT_USER_CONFIG.platformThemeOverrides,
...(USER_CONFIG.platformThemeOverrides || {}),
[platformKey]: {}
};
}
function validateSingleThemeImport(parsed) {
if (!parsed || typeof parsed !== 'object') {
return 'JSON 顶层必须是对象';
}
if (!parsed.theme || typeof parsed.theme !== 'object') {
return '缺少 theme 对象';
}
if (parsed.key && typeof parsed.key !== 'string') {
return 'key 必须是字符串';
}
return null;
}
function validateThemeCollectionImport(parsed) {
if (!parsed || typeof parsed !== 'object') {
return 'JSON 顶层必须是对象';
}
if (!parsed.customThemes || typeof parsed.customThemes !== 'object') {
return '缺少 customThemes 对象';
}
if (parsed.customThemeOrder && !Array.isArray(parsed.customThemeOrder)) {
return 'customThemeOrder 必须是数组';
}
if (parsed.customThemeEditorKey && typeof parsed.customThemeEditorKey !== 'string') {
return 'customThemeEditorKey 必须是字符串';
}
return null;
}
function getThemeDetailDiffs(currentTheme, compareTheme) {
const items = [
['headingDivider', '标题分隔线'],
['tableHeaderFill', '表头底色'],
['imageRounded', '图片圆角'],
['codeBlockBorder', '代码块边框']
];
return items
.filter(([key]) => !!currentTheme && !!compareTheme && currentTheme[key] !== compareTheme[key])
.map(([key, label]) => `${label}: 当前${currentTheme[key] ? '开启' : '关闭'} / 推荐${compareTheme[key] ? '开启' : '关闭'}`);
}
function getThemeColorDiffs(currentTheme, compareTheme) {
const items = [
['accentColor', '强调色'],
['backgroundColor', '背景色'],
['codeBackground', '代码背景'],
['headingColor', '标题颜色']
];
return items
.filter(([key]) => !!currentTheme && !!compareTheme && currentTheme[key] !== compareTheme[key])
.map(([key, label]) => `${label}: 当前 ${currentTheme[key] || '-'} / 推荐 ${compareTheme[key] || '-'}`);
}
function applyCurrentPlatformConfig() {
const preset = PLATFORM_PRESETS[USER_CONFIG.currentPlatform];
if (!preset) {
syncImageConfigFromUserConfig();
return;
}
USER_CONFIG.platformImageStrategies = {
...DEFAULT_USER_CONFIG.platformImageStrategies,
...(USER_CONFIG.platformImageStrategies || {})
};
USER_CONFIG = {
...USER_CONFIG,
imageMaxWidth: USER_CONFIG.imageMaxWidth ?? preset.imageMaxWidth,
imageQuality: USER_CONFIG.imageQuality ?? preset.imageQuality,
displayMaxWidth: USER_CONFIG.displayMaxWidth ?? preset.displayMaxWidth,
imageFormat: USER_CONFIG.imageFormat || preset.imageFormat,
compressGif: typeof USER_CONFIG.compressGif === 'boolean' ? USER_CONFIG.compressGif : preset.compressGif,
enableCodeHighlight: typeof USER_CONFIG.enableCodeHighlight === 'boolean' ? USER_CONFIG.enableCodeHighlight : preset.enableCodeHighlight,
enableThemeStyles: typeof USER_CONFIG.enableThemeStyles === 'boolean' ? USER_CONFIG.enableThemeStyles : preset.enableThemeStyles,
enableProgressNotice: typeof USER_CONFIG.enableProgressNotice === 'boolean' ? USER_CONFIG.enableProgressNotice : preset.enableProgressNotice
};
syncImageConfigFromUserConfig();
}
/* ========== 安全大图转 base64 ========== */
async function buf2base64(buf) {
return new Promise((resolve, reject) => {
try {
const bytes = new Uint8Array(buf);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
resolve(btoa(binary));
} catch (e) {
reject(e);
}
});
}
/* ========== 图片压缩和尺寸限制 ========== */
async function compressImage(buf, ext, maxWidth = IMAGE_CONFIG.MAX_WIDTH, quality = IMAGE_CONFIG.QUALITY) {
return new Promise((resolve) => {
try {
const normalizedExt = (ext || '').toLowerCase();
const mime = getMime(normalizedExt);
const blob = new Blob([buf], { type: mime });
const img = new Image();
const url = URL.createObjectURL(blob);
const timeout = setTimeout(() => {
URL.revokeObjectURL(url);
resolve({ data: buf, needsConversion: false });
}, 30000);
img.onload = () => {
clearTimeout(timeout);
URL.revokeObjectURL(url);
try {
let width = img.width;
let height = img.height;
if (width > maxWidth) {
height = Math.round((height * maxWidth) / width);
width = maxWidth;
}
if (width === img.width) {
resolve({ data: buf, needsConversion: false, wasResized: false, mime });
return;
}
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.drawImage(img, 0, 0, width, height);
const preservePng = normalizedExt === 'png' && IMAGE_CONFIG.FORMAT === 'auto';
const outputMime = preservePng ? 'image/png' : 'image/jpeg';
const outputQuality = outputMime === 'image/png' ? undefined : quality;
canvas.toBlob(
(compressedBlob) => {
if (!compressedBlob) {
resolve({ data: buf, needsConversion: false, wasResized: false, mime });
return;
}
const reader = new FileReader();
reader.onload = () => {
resolve({
data: reader.result,
needsConversion: true,
mime: outputMime,
wasResized: true
});
};
reader.onerror = () => resolve({ data: buf, needsConversion: false, wasResized: false, mime });
reader.readAsArrayBuffer(compressedBlob);
},
outputMime,
outputQuality
);
} else {
resolve({ data: buf, needsConversion: false, wasResized: false, mime });
}
} catch (e) {
resolve({ data: buf, needsConversion: false, wasResized: false, mime });
}
};
img.onerror = () => {
clearTimeout(timeout);
URL.revokeObjectURL(url);
resolve({ data: buf, needsConversion: false, wasResized: false, mime });
};
img.src = url;
} catch (err) {
resolve({ data: buf, needsConversion: false, wasResized: false, mime: getMime(ext || '') });
}
});
}
/* ========== mime 映射 ========== */
const getMime = ext => ({
png: 'image/png',
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
gif: 'image/gif',
svg: 'image/svg+xml',
webp: 'image/webp',
bmp: 'image/bmp'
}[ext.toLowerCase()] || 'application/octet-stream');
function shouldKeepOriginalImage(ext, mime, buf, maxWidth, platformKey = USER_CONFIG.currentPlatform) {
const normalizedExt = (ext || '').toLowerCase();
const strategy = getImageStrategyConfig(platformKey);
if (!mime.startsWith('image/')) {
return true;
}
if (normalizedExt === 'png' && IMAGE_CONFIG.FORMAT === 'auto' && strategy.keepPng) {
return true;
}
if ((normalizedExt === 'jpg' || normalizedExt === 'jpeg' || normalizedExt === 'webp') && buf && buf.byteLength < strategy.smallImageThreshold) {
return true;
}
if (maxWidth >= 1200 + strategy.resizeThresholdBoost) {
return true;
}
return false;
}
/* ========== 单张图内嵌 ========== */
async function inlineImage(imgName, alt, context) {
const { plugin, baseFolder, assetsFolder } = context;
const candidates = [
`${assetsFolder}/${imgName}`,
`${baseFolder}/${imgName}`,
imgName.startsWith('/') ? imgName.slice(1) : imgName
];
let imgFile = candidates
.map(p => {
try {
return plugin.app.vault.getAbstractFileByPath(p);
} catch (e) {
return null;
}
})
.find(f => f instanceof TFile);
if (!imgFile) {
try {
const all = plugin.app.vault.getFiles();
imgFile = all.find(f => f.name === imgName);
} catch (e) {
// ignore
}
}
if (!imgFile) {
console.warn('[bulk-copy] 未找到图片文件', imgName);
return `![${alt}](${imgName})`;
}
try {
const buf = await plugin.app.vault.readBinary(imgFile);
const ext = imgFile.extension;
const mime = getMime(ext);
if (ext && ext.toLowerCase() === 'svg') {
try {
const svgContent = await plugin.app.vault.read(imgFile);
const base64Svg = btoa(unescape(encodeURIComponent(svgContent)));
const currentPreset = PLATFORM_PRESETS[USER_CONFIG.currentPlatform];
const maxWidth = currentPreset?.maxImageWidth || IMAGE_CONFIG.DISPLAY_MAX_WIDTH;
return `<img src="data:${mime};base64,${base64Svg}" alt="${alt}" style="max-width: ${maxWidth}px; width: 100%; height: auto; display: block; margin: 15px auto;" />`;
} catch (e) {
return `![${alt}](${imgName})`;
}
}
if (ext && ext.toLowerCase() === 'gif') {
try {
const data = await buf2base64(buf);
const currentPreset = PLATFORM_PRESETS[USER_CONFIG.currentPlatform];
const maxWidth = currentPreset?.maxImageWidth || IMAGE_CONFIG.DISPLAY_MAX_WIDTH;
return `<img src="data:${mime};base64,${data}" alt="${alt}" style="max-width: ${maxWidth}px; width: 100%; height: auto; display: block; margin: 15px auto;" />`;
} catch (e) {
return `![${alt}](${imgName})`;
}
}
const currentPlatform = USER_CONFIG.currentPlatform;
const currentPreset = PLATFORM_PRESETS[currentPlatform];
const maxWidth = currentPreset?.maxImageWidth || IMAGE_CONFIG.MAX_WIDTH;
const strategy = getImageStrategyConfig(currentPlatform);
const baseQuality = currentPreset?.imageQuality || IMAGE_CONFIG.QUALITY;
const quality = Math.max(0.55, Math.min(0.98, baseQuality + strategy.qualityBoost));
const shouldKeepOriginal = shouldKeepOriginalImage(ext, mime, buf, maxWidth, currentPlatform);
const compressed = shouldKeepOriginal
? { data: buf, needsConversion: false, wasResized: false, mime }
: await compressImage(buf, ext, maxWidth, quality);
let finalMime = mime;
let finalData;
if (compressed && compressed.needsConversion) {
finalMime = compressed.mime;
finalData = await buf2base64(compressed.data);
} else if (compressed && compressed.data) {
finalData = await buf2base64(compressed.data);
} else {
finalData = await buf2base64(buf);
}
if (!finalData) {
return `![${alt}](${imgName})`;
}
const displayWidth = currentPreset?.displayMaxWidth || IMAGE_CONFIG.DISPLAY_MAX_WIDTH;
const imgStyle = `max-width: ${typeof displayWidth === 'number' ? `${displayWidth}px` : displayWidth}; width: 100%; height: auto; display: block; margin: 15px auto; border-radius: 4px;`;
const altText = alt ? alt.replace(/"/g, '&quot;') : '';
return `<img src="data:${finalMime};base64,${finalData}" alt="${altText}" style="${imgStyle}" />`;
} catch (e) {
console.error('[bulk-copy] 图片处理失败:', imgName, e);
return `![${alt}](${imgName})`;
}
}
/* ========== Markdown 转 HTML简化版 ========== */
function markdownToHtml(md) {
if (!md) return '';
let html = md;
// 转义 HTML 特殊字符
const escapeHtml = (text) => {
if (typeof text !== 'string') return '';
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
};
// 代码块(先处理,避免内部语法被转换)
const codeBlocks = [];
html = html.replace(/```[\s\S]*?```/g, (match) => {
const id = codeBlocks.length;
const code = match.replace(/```/g, '').trim();
codeBlocks.push(`<pre style="background: #f6f8fa; padding: 16px; border-radius: 6px; overflow-x: auto;"><code style="font-family: monospace;">${escapeHtml(code)}</code></pre>`);
return `\n<!--CODEBLOCK${id}-->\n`;
});
// 行内代码
const inlineCodes = [];
html = html.replace(/`([^`\n]+)`/g, (match, code) => {
const id = inlineCodes.length;
inlineCodes.push(`<code style="background: rgba(175,184,193,0.2); padding: 2px 4px; border-radius: 3px; font-family: monospace;">${escapeHtml(code)}</code>`);
return `<!--INLINECODE${id}-->`;
});
// Wiki 链接 [[link]]
html = html.replace(/\[\[([^\]|]+?)(?:\|([^\]]+))?\]\]/g, (match, link, text) => {
const display = text || link;
return `<a href="${link}" target="_blank">${display}</a>`;
});
// 标签 #tag
html = html.replace(/(?<!\w)#(\w+)/g, (match, tag) => {
return `<span style="background-color: rgba(0, 123, 255, 0.1); color: #007bff; padding: 2px 6px; border-radius: 4px; font-size: 0.85em;">#${tag}</span>`;
});
// 图片
html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => {
return `<img src="${src}" alt="${alt}" style="max-width: 100%; height: auto; display: block; margin: 15px auto;">`;
});
// Wiki 图片 ![[img]]
html = html.replace(/!\[\[([^\]|]+?)(?:\|[^\]]+)?\]\]/g, (match, img) => {
const name = img.split('|')[0].trim();
return `<img src="${name}" alt="${name}" style="max-width: 100%; height: auto; display: block; margin: 15px auto;">`;
});
// 标题
html = html.replace(/^######\s+(.+)$/gm, '<h6>$1</h6>');
html = html.replace(/^#####\s+(.+)$/gm, '<h5>$1</h5>');
html = html.replace(/^####\s+(.+)$/gm, '<h4>$1</h4>');
html = html.replace(/^###\s+(.+)$/gm, '<h3>$1</h3>');
html = html.replace(/^##\s+(.+)$/gm, '<h2>$1</h2>');
html = html.replace(/^#\s+(.+)$/gm, '<h1>$1</h1>');
// 粗体
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
html = html.replace(/__(.+?)__/g, '<strong>$1</strong>');
// 粗斜体
html = html.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>');
html = html.replace(/___(.+?)___/g, '<strong><em>$1</em></strong>');
// 斜体
html = html.replace(/\*([^\s*][^*]*?)\*/g, '<em>$1</em>');
html = html.replace(/\b_([^\s_][^_]*?)_\b/g, '<em>$1</em>');
// 删除线
html = html.replace(/~~(.+?)~~/g, '<del>$1</del>');
// 高亮
html = html.replace(/==(.+?)==/g, '<mark>$1</mark>');
// 引用
html = html.replace(/^>\s+(.+)$/gm, '<blockquote style="border-left: 4px solid #ddd; padding: 10px 15px; margin: 15px 0; color: #666;">$1</blockquote>');
// 分割线
html = html.replace(/^(---|\*\*\*|___)$/gm, '<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">');
// 列表(先处理无序列表)
html = html.replace(/^[\*\-\+]\s+(.+)$/gm, '<li>$1</li>');
// 处理有序列表
html = html.replace(/^\d+\.\s+(.+)$/gm, '<li>$1</li>');
// 任务列表
html = html.replace(/^[\*\-\+]\s+\[x\]\s+(.+)$/gm, '<li><input type="checkbox" checked disabled style="margin-right: 8px;"> $1</li>');
html = html.replace(/^[\*\-\+]\s+\[\s\]\s+(.+)$/gm, '<li><input type="checkbox" disabled style="margin-right: 8px;"> $1</li>');
html = html.replace(/^\d+\.\s+\[x\]\s+(.+)$/gm, '<li><input type="checkbox" checked disabled style="margin-right: 8px;"> $1</li>');
html = html.replace(/^\d+\.\s+\[\s\]\s+(.+)$/gm, '<li><input type="checkbox" disabled style="margin-right: 8px;"> $1</li>');
// 链接
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
// 表格
html = html.replace(/^\|(.+)\|[ \t]*$\n^\|[-:\s|]+\|[ \t]*$\n((?:^\|.+\|[ \t]*$\n?)*)/gm, (match, header, body) => {
const headers = header.split('|').map(h => h.trim()).filter(h => h);
const headerHtml = `<tr>${headers.map(h => `<th style="border: 1px solid #ddd; padding: 8px 12px; background: #f6f8fa; font-weight: 600;">${h}</th>`).join('')}</tr>`;
const rows = body.trim().split('\n').map(row => {
const cells = row.split('|').map(c => c.trim()).filter(c => c);
return `<tr>${cells.map(c => `<td style="border: 1px solid #ddd; padding: 8px 12px;">${c}</td>`).join('')}</tr>`;
}).join('');
return `<table style="border-collapse: collapse; width: 100%; margin: 15px 0;"><thead>${headerHtml}</thead><tbody>${rows}</tbody></table>`;
});
// 关键修复:处理换行
// 将单个换行符转换为 <br>,保留段落结构
const lines = html.split('\n');
const processed = [];
let inParagraph = false;
let paragraphContent = [];
for (const line of lines) {
const trimmed = line.trim();
// 检查是否是块级元素或占位符或空行
const isBlockElement = trimmed.match(/^<(h[1-6]|ul|ol|li|blockquote|pre|hr|table|\/table|\/tbody|\/thead|div|\/div|section|article)/) ||
trimmed.match(/^<!--CODEBLOCK|INLINECODE/) ||
trimmed === '';
if (isBlockElement) {
// 如果在段落中,先结束段落
if (inParagraph && paragraphContent.length > 0) {
// 段落内容用 <br> 连接(保留 Obsidian 的换行)
processed.push(`<p style="margin: 15px 0; line-height: 1.6;">${paragraphContent.join('<br>')}</p>`);
paragraphContent = [];
inParagraph = false;
}
// 如果不是空行,添加块级元素
if (trimmed !== '') {
processed.push(line);
}
} else {
// 普通文本行
if (!inParagraph) {
inParagraph = true;
}
paragraphContent.push(line);
}
}
// 处理末尾的段落
if (inParagraph && paragraphContent.length > 0) {
processed.push(`<p style="margin: 15px 0; line-height: 1.6;">${paragraphContent.join('<br>')}</p>`);
}
html = processed.join('\n');
// 还原占位符
codeBlocks.forEach((code, i) => {
html = html.replace(new RegExp(`<!--CODEBLOCK${i}-->`, 'g'), code);
});
inlineCodes.forEach((code, i) => {
html = html.replace(new RegExp(`<!--INLINECODE${i}-->`, 'g'), code);
});
return html;
}
/* ========== 替换图片为 HTML ========== */
async function replaceImagesWithHtml(md, context) {
if (!md) return { html: md, stats: { count: 0, totalSize: '0KB' } };
const wikiMatches = [];
const wikiRegex = /!\[\[([^\]|]+?)(?:\|[^\]]+)?\]\]/g;
let match;
while ((match = wikiRegex.exec(md)) !== null) {
const name = match[1].split('|')[0].trim();
const altMatch = match[1].match(/\|(.+)$/);
const alt = altMatch ? altMatch[1] : '';
wikiMatches.push({ name, alt, original: match[0] });
}
const standardMatches = [];
const standardRegex = /!\[([^\]]*)\]\(([^)]+)\)/g;
while ((match = standardRegex.exec(md)) !== null) {
const alt = match[1];
const src = decodeURIComponent(match[2]).trim();
standardMatches.push({ name: src, alt, original: match[0] });
}
if (wikiMatches.length === 0 && standardMatches.length === 0) {
return { html: md, stats: { count: 0, totalSize: '0KB' } };
}
if (USER_CONFIG.enableProgressNotice) {
const totalImages = wikiMatches.length + standardMatches.length;
new Notice(`📷 发现 ${totalImages} 张图片,正在处理...`);
}
const processAll = async (matches) => {
const results = [];
for (let i = 0; i < matches.length; i++) {
if (USER_CONFIG.enableProgressNotice && i > 0 && i % 3 === 0) {
new Notice(`正在处理图片 ${i + 1}/${matches.length}...`);
}
try {
const result = await inlineImage(matches[i].name, matches[i].alt, context);
results.push(result);
} catch (e) {
console.error('[bulk-copy] 图片处理失败:', matches[i].name, e);
results.push(matches[i].original);
}
}
return results;
};
const wikiResults = await processAll(wikiMatches);
const standardResults = await processAll(standardMatches);
let result = md;
wikiMatches.forEach((match, index) => {
result = result.replace(match.original, wikiResults[index] || match.original);
});
standardMatches.forEach((match, index) => {
result = result.replace(match.original, standardResults[index] || match.original);
});
const totalImages = wikiMatches.length + standardMatches.length;
const totalSize = totalImages * 150; // KB per image (estimated)
const formattedSize = totalSize > 1024
? `${(totalSize / 1024).toFixed(1)}MB`
: `${totalSize}KB`;
return { html: result, stats: { count: totalImages, totalSize: formattedSize } };
}
function resolveActiveMarkdownView(plugin) {
const activeView = plugin.app.workspace.getActiveViewOfType?.(MarkdownView);
if (!activeView) {
return null;
}
const mode = activeView.getMode?.() || (activeView.currentMode ? activeView.currentMode.type : 'unknown');
return {
view: activeView,
mode
};
}
function getPreviewRoot(view, mode) {
if (!view || !view.containerEl) {
return null;
}
const normalizedMode = (mode || '').toLowerCase();
if (normalizedMode !== 'preview' && normalizedMode !== 'reading') {
return null;
}
return view.containerEl.querySelector('.markdown-reading-view .markdown-preview-view, .markdown-reading-view .markdown-rendered, .markdown-preview-view.markdown-rendered');
}
function cloneExportRoot(root, options) {
if (!root) {
return null;
}
const clone = root.cloneNode(true);
if (!options || options.mode !== 'selection') {
return clone;
}
return filterSelectionFromDom(clone, options.selectedText || '');
}
function filterSelectionFromDom(root, selectedText) {
if (!root || !selectedText) {
return root;
}
const normalizedSelected = selectedText.replace(/\s+/g, ' ').trim();
if (!normalizedSelected) {
return root;
}
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, null);
const matches = [];
while (walker.nextNode()) {
const node = walker.currentNode;
const text = (node.textContent || '').replace(/\s+/g, ' ').trim();
if (text && (text === normalizedSelected || text.includes(normalizedSelected))) {
matches.push(node);
}
}
if (matches.length === 0) {
return root;
}
const wrapper = document.createElement('div');
matches.forEach(node => {
if (node.childNodes.length === 1 && node.firstChild && node.firstChild.nodeType === Node.TEXT_NODE) {
wrapper.appendChild(node.cloneNode(true));
}
});
return wrapper.childNodes.length > 0 ? wrapper : root;
}
function sanitizeObsidianDom(root) {
if (!root) {
return root;
}
REMOVE_SELECTORS.forEach(selector => {
root.querySelectorAll(selector).forEach(node => node.remove());
});
root.querySelectorAll('[contenteditable="true"], .is-collapsed').forEach(node => {
node.removeAttribute('contenteditable');
node.classList.remove('is-collapsed');
});
root.querySelectorAll('*').forEach(node => {
Array.from(node.attributes).forEach(attr => {
if (attr.name.startsWith('data-') || attr.name === 'spellcheck' || attr.name === 'dir') {
node.removeAttribute(attr.name);
}
});
});
return root;
}
function buildStylePolicy(platformPreset) {
return {
props: BASE_STYLE_PROPS,
tagProps: TAG_STYLE_PROPS,
allowClassName: !!platformPreset?.supports?.className,
preserveTheme: platformPreset?.preserveThemeLevel !== 'none'
};
}
function inlineNodeStyle(targetNode, sourceNode, stylePolicy) {
if (!(targetNode instanceof HTMLElement) || !(sourceNode instanceof HTMLElement)) {
return;
}
const computed = window.getComputedStyle(sourceNode);
const tagName = targetNode.tagName.toLowerCase();
const propSet = new Set([...(stylePolicy.props || []), ...((stylePolicy.tagProps && stylePolicy.tagProps[tagName]) || [])]);
propSet.forEach(prop => {
const value = computed.getPropertyValue(prop);
if (!value) {
return;
}
const trimmed = value.trim();
if (!trimmed || trimmed === 'none' || trimmed === 'normal' || trimmed === 'rgba(0, 0, 0, 0)') {
return;
}
targetNode.style.setProperty(prop, trimmed);
});
if (!stylePolicy.allowClassName) {
targetNode.removeAttribute('class');
}
}
function inlineStylesFromSource(clonedRoot, sourceRoot, platformPreset) {
if (!clonedRoot || !sourceRoot) {
return clonedRoot;
}
const stylePolicy = buildStylePolicy(platformPreset);
const clonedNodes = [clonedRoot, ...clonedRoot.querySelectorAll('*')];
const sourceNodes = [sourceRoot, ...sourceRoot.querySelectorAll('*')];
const total = Math.min(clonedNodes.length, sourceNodes.length);
for (let i = 0; i < total; i++) {
inlineNodeStyle(clonedNodes[i], sourceNodes[i], stylePolicy);
}
return clonedRoot;
}
function captureThemeTokens() {
if (Cache.themeStyles) {
return Cache.themeStyles;
}
const computed = window.getComputedStyle(document.body);
Cache.themeStyles = {
textNormal: computed.getPropertyValue('--text-normal').trim() || computed.color,
textMuted: computed.getPropertyValue('--text-muted').trim(),
backgroundPrimary: computed.getPropertyValue('--background-primary').trim() || '#ffffff',
backgroundSecondary: computed.getPropertyValue('--background-secondary').trim(),
borderColor: computed.getPropertyValue('--background-modifier-border').trim() || '#d0d7de',
accentColor: computed.getPropertyValue('--interactive-accent').trim() || '#2f80ed',
codeBackground: computed.getPropertyValue('--code-background').trim() || computed.getPropertyValue('--background-secondary').trim(),
blockquoteBorder: computed.getPropertyValue('--blockquote-border-color').trim() || computed.getPropertyValue('--background-modifier-border').trim()
};
return Cache.themeStyles;
}
function transformCallouts(root, platformPreset, themeStyle) {
if (!platformPreset?.transforms?.flattenCallout) {
return;
}
root.querySelectorAll('.callout').forEach(callout => {
const title = callout.querySelector('.callout-title');
const content = callout.querySelector('.callout-content');
const wrapper = document.createElement('blockquote');
wrapper.style.borderLeft = `4px solid ${themeStyle.blockquoteBorder || themeStyle.accentColor || '#2f80ed'}`;
wrapper.style.backgroundColor = themeStyle.blockquoteBackground || themeStyle.backgroundColor || '#f6f8fa';
wrapper.style.padding = '12px 16px';
wrapper.style.margin = '16px 0';
wrapper.style.borderRadius = themeStyle.borderRadius || '6px';
if (title) {
const heading = document.createElement('p');
heading.textContent = title.textContent || '';
heading.style.fontWeight = '600';
heading.style.margin = '0 0 8px 0';
wrapper.appendChild(heading);
}
if (content) {
Array.from(content.childNodes).forEach(node => wrapper.appendChild(node.cloneNode(true)));
} else {
Array.from(callout.childNodes).forEach(node => wrapper.appendChild(node.cloneNode(true)));
}
callout.replaceWith(wrapper);
});
}
function transformCodeBlocks(root, platformPreset, themeStyle) {
root.querySelectorAll('pre').forEach(pre => {
pre.style.backgroundColor = themeStyle.codeBackground || '#f6f8fa';
pre.style.color = themeStyle.codeColor || pre.style.color;
pre.style.borderRadius = pre.style.borderRadius || '6px';
pre.style.padding = pre.style.padding || '12px 16px';
pre.style.overflowX = 'auto';
pre.style.border = themeStyle.codeBlockBorder ? `1px solid ${themeStyle.borderColor || '#d0d7de'}` : 'none';
if (platformPreset?.supports?.codeBlock === 'partial') {
pre.querySelectorAll('[class]').forEach(node => node.removeAttribute('class'));
}
if (platformPreset?.name === '微信公众号') {
pre.style.backgroundColor = themeStyle.codeBackground || '#f7f7f7';
pre.style.borderLeft = `4px solid ${themeStyle.accentColor || '#07c160'}`;
pre.style.borderTop = 'none';
pre.style.borderRight = 'none';
pre.style.borderBottom = 'none';
pre.style.borderRadius = '0 8px 8px 0';
pre.style.whiteSpace = 'pre-wrap';
pre.style.wordBreak = 'break-word';
pre.style.fontSize = '13px';
pre.querySelectorAll('code').forEach(code => {
code.removeAttribute('class');
code.style.backgroundColor = 'transparent';
code.style.padding = '0';
code.style.borderRadius = '0';
code.style.fontSize = '13px';
code.style.color = themeStyle.codeColor || '#1f2937';
});
}
});
}
function transformTables(root, platformPreset, themeStyle) {
root.querySelectorAll('table').forEach(table => {
table.style.width = '100%';
table.style.borderCollapse = 'collapse';
table.querySelectorAll('th, td').forEach(cell => {
cell.style.border = `1px solid ${themeStyle.borderColor || '#d0d7de'}`;
cell.style.padding = '8px 12px';
});
table.querySelectorAll('th').forEach(cell => {
cell.style.backgroundColor = themeStyle.tableHeaderFill ? (themeStyle.codeBackground || '#f6f8fa') : 'transparent';
cell.style.color = themeStyle.headingColor || cell.style.color;
});
if (platformPreset?.transforms?.simplifyTable) {
table.removeAttribute('class');
}
});
}
function transformTaskLists(root, platformPreset, themeStyle) {
root.querySelectorAll('li.task-list-item').forEach(item => {
const checkbox = item.querySelector('input[type="checkbox"]');
if (!checkbox) {
return;
}
const checked = checkbox.checked;
checkbox.remove();
const marker = document.createElement('span');
marker.textContent = checked ? '☑ ' : '☐ ';
marker.style.color = themeStyle.accentColor || themeStyle.textColor || '#333';
marker.style.fontWeight = '600';
if (checked) {
item.style.opacity = '0.8';
item.style.textDecoration = 'line-through';
} else {
item.style.opacity = '1';
item.style.textDecoration = 'none';
}
item.insertBefore(marker, item.firstChild);
});
}
function transformNestedLists(root, platformPreset, themeStyle) {
root.querySelectorAll('ul ul, ul ol, ol ul, ol ol').forEach(list => {
list.style.marginTop = '6px';
list.style.marginBottom = '6px';
list.style.paddingLeft = '1.2em';
});
if (platformPreset?.name === '微信公众号') {
root.querySelectorAll('li ul, li ol').forEach(list => {
list.style.borderLeft = `2px solid ${themeStyle.borderColor || '#d0d7de'}`;
list.style.paddingLeft = '12px';
});
}
}
function transformFootnotes(root, platformPreset, themeStyle) {
root.querySelectorAll('.footnotes, section.footnotes').forEach(section => {
const title = document.createElement('p');
title.textContent = '注释';
title.style.fontWeight = '700';
title.style.margin = '20px 0 10px';
section.insertBefore(title, section.firstChild);
});
root.querySelectorAll('sup a.footnote-link, a.footnote-backref').forEach(link => {
link.style.color = themeStyle.linkColor || themeStyle.accentColor || '#576b95';
link.style.textDecoration = 'none';
});
}
function transformNestedBlockquotes(root, platformPreset, themeStyle) {
root.querySelectorAll('blockquote blockquote').forEach(blockquote => {
blockquote.style.margin = '10px 0 0';
blockquote.style.backgroundColor = themeStyle.backgroundColor || '#fff';
blockquote.style.borderLeft = `3px solid ${themeStyle.borderColor || '#d0d7de'}`;
blockquote.style.opacity = '0.95';
});
if (platformPreset?.name === '微信公众号') {
root.querySelectorAll('blockquote blockquote blockquote').forEach(blockquote => {
const text = document.createElement('p');
text.textContent = blockquote.textContent?.trim() || '';
text.style.margin = '8px 0 0';
blockquote.replaceWith(text);
});
}
}
function transformEmbeds(root, platformPreset, themeStyle) {
root.querySelectorAll('.internal-embed, .markdown-embed').forEach(embed => {
const replacement = document.createElement('blockquote');
replacement.style.borderLeft = `4px solid ${themeStyle.blockquoteBorder || themeStyle.accentColor || '#999'}`;
replacement.style.backgroundColor = themeStyle.blockquoteBackground || '#f8f8f8';
replacement.style.padding = '12px 16px';
replacement.style.margin = '16px 0';
replacement.style.borderRadius = themeStyle.borderRadius || '6px';
const image = embed.querySelector('img');
if (image) {
const title = document.createElement('p');
title.textContent = '图片嵌入';
title.style.fontWeight = '700';
title.style.margin = '0 0 8px';
replacement.appendChild(title);
replacement.appendChild(image.cloneNode(true));
const caption = document.createElement('p');
caption.textContent = image.getAttribute('alt') || '嵌入图片说明';
caption.style.margin = '10px 0 0';
caption.style.fontSize = '13px';
caption.style.color = themeStyle.mutedColor || '#666';
replacement.appendChild(caption);
} else {
const title = document.createElement('p');
title.textContent = '内容嵌入';
title.style.fontWeight = '700';
title.style.margin = '0 0 8px';
replacement.appendChild(title);
const body = document.createElement('p');
body.textContent = embed.textContent?.trim() || '嵌入内容';
body.style.margin = '0';
replacement.appendChild(body);
}
embed.replaceWith(replacement);
});
}
function transformMathBlocks(root, platformPreset, themeStyle) {
root.querySelectorAll('.math, .katex, mjx-container').forEach(node => {
const replacement = document.createElement('div');
replacement.style.backgroundColor = themeStyle.codeBackground || '#f6f8fa';
replacement.style.color = themeStyle.codeColor || '#333';
replacement.style.padding = '12px 16px';
replacement.style.borderRadius = themeStyle.borderRadius || '6px';
replacement.style.margin = '16px 0';
replacement.style.borderLeft = `4px solid ${themeStyle.accentColor || '#07c160'}`;
const title = document.createElement('p');
title.textContent = '公式';
title.style.margin = '0 0 8px';
title.style.fontWeight = '700';
replacement.appendChild(title);
const body = document.createElement('pre');
body.textContent = node.textContent?.trim() || '数学公式';
body.style.margin = '0';
body.style.whiteSpace = 'pre-wrap';
body.style.background = 'transparent';
body.style.border = 'none';
body.style.padding = '0';
replacement.appendChild(body);
node.replaceWith(replacement);
});
}
function transformHorizontalRules(root, platformPreset, themeStyle) {
root.querySelectorAll('hr').forEach(node => {
node.style.border = 'none';
node.style.borderTop = `1px solid ${themeStyle.borderColor || '#d0d7de'}`;
node.style.margin = '24px 0';
});
}
function transformWechatTablesToCards(root, platformPreset, themeStyle) {
if (platformPreset?.name !== '微信公众号') {
return;
}
root.querySelectorAll('table').forEach(table => {
const wrapper = document.createElement('div');
wrapper.style.margin = '16px 0';
wrapper.style.border = `1px solid ${themeStyle.borderColor || '#d0d7de'}`;
wrapper.style.borderRadius = themeStyle.borderRadius || '8px';
wrapper.style.overflow = 'hidden';
wrapper.style.backgroundColor = themeStyle.backgroundColor || '#fff';
const header = document.createElement('div');
header.textContent = '表格内容';
header.style.padding = '10px 12px';
header.style.fontWeight = '700';
header.style.backgroundColor = themeStyle.codeBackground || '#f6f8fa';
header.style.borderBottom = `1px solid ${themeStyle.borderColor || '#d0d7de'}`;
wrapper.appendChild(header);
const rows = Array.from(table.querySelectorAll('tr'));
rows.forEach((row, rowIndex) => {
const rowCard = document.createElement('div');
rowCard.style.display = 'block';
rowCard.style.padding = '10px 12px';
rowCard.style.borderBottom = rowIndex < rows.length - 1 ? `1px solid ${themeStyle.borderColor || '#d0d7de'}` : 'none';
rowCard.style.backgroundColor = rowIndex % 2 === 0 ? (themeStyle.backgroundColor || '#fff') : (themeStyle.blockquoteBackground || '#fafafa');
const rowTitle = document.createElement('p');
rowTitle.textContent = rowIndex === 0 ? '表头' : `${rowIndex}`;
rowTitle.style.margin = '0 0 6px';
rowTitle.style.fontWeight = '600';
rowTitle.style.color = themeStyle.headingColor || '#111';
rowCard.appendChild(rowTitle);
const cells = Array.from(row.children);
const compactTwoColumn = rowIndex > 0 && cells.length === 2;
if (compactTwoColumn) {
const compact = document.createElement('p');
compact.style.margin = '4px 0';
compact.style.lineHeight = themeStyle.lineHeight || '1.7';
compact.innerHTML = `<strong>${cells[0].textContent?.trim() || '字段'}</strong>${cells[1].textContent?.trim() || ''}`;
rowCard.appendChild(compact);
} else {
cells.forEach((cell, cellIndex) => {
const line = document.createElement('p');
line.style.margin = '4px 0';
line.style.lineHeight = themeStyle.lineHeight || '1.7';
const labelPrefix = rowIndex === 0
? `字段 ${cellIndex + 1}`
: `${rows[0]?.children[cellIndex]?.textContent?.trim() || `${cellIndex + 1}`}`;
line.innerHTML = `<strong>${labelPrefix}</strong>${cell.textContent?.trim() || ''}`;
rowCard.appendChild(line);
});
}
wrapper.appendChild(rowCard);
});
table.replaceWith(wrapper);
});
}
function transformWechatSpecificBlocks(root, platformPreset, themeStyle) {
if (platformPreset?.name !== '微信公众号') {
return;
}
transformTaskLists(root, platformPreset, themeStyle);
transformNestedLists(root, platformPreset, themeStyle);
transformFootnotes(root, platformPreset, themeStyle);
transformNestedBlockquotes(root, platformPreset, themeStyle);
transformEmbeds(root, platformPreset, themeStyle);
transformMathBlocks(root, platformPreset, themeStyle);
transformHorizontalRules(root, platformPreset, themeStyle);
transformWechatTablesToCards(root, platformPreset, themeStyle);
}
function normalizeLinks(root) {
root.querySelectorAll('a').forEach(link => {
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener noreferrer');
const href = link.getAttribute('href') || '';
if (href.startsWith('app://') || href.startsWith('obsidian://')) {
link.removeAttribute('href');
}
});
}
function applyThemeToRoot(root, theme) {
if (!root || !theme) {
return root;
}
root.style.fontFamily = theme.fontFamily;
root.style.color = theme.textColor;
root.style.backgroundColor = theme.backgroundColor;
root.style.lineHeight = theme.lineHeight;
root.querySelectorAll('p').forEach(node => {
node.style.color = theme.textColor;
node.style.margin = `${theme.paragraphSpacing} 0`;
node.style.lineHeight = theme.lineHeight;
});
root.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach(node => {
node.style.color = theme.headingColor;
node.style.fontFamily = theme.fontFamily;
node.style.fontWeight = '700';
node.style.lineHeight = '1.35';
node.style.marginTop = '1.4em';
node.style.marginBottom = '0.7em';
});
root.querySelectorAll('h1, h2').forEach(node => {
node.style.paddingBottom = theme.headingDivider ? '0.25em' : '0';
node.style.borderBottom = theme.headingDivider ? `1px solid ${theme.borderColor}` : 'none';
});
root.querySelectorAll('h1').forEach(node => {
node.style.fontSize = '2em';
});
root.querySelectorAll('h2').forEach(node => {
node.style.fontSize = '1.6em';
});
root.querySelectorAll('h3').forEach(node => {
node.style.fontSize = '1.3em';
});
root.querySelectorAll('h4, h5, h6').forEach(node => {
node.style.fontSize = '1.05em';
});
root.querySelectorAll('a').forEach(node => {
node.style.color = theme.linkColor;
node.style.textDecoration = 'none';
});
root.querySelectorAll('ul, ol').forEach(node => {
node.style.paddingLeft = '1.6em';
node.style.margin = `${theme.paragraphSpacing} 0`;
});
root.querySelectorAll('li').forEach(node => {
node.style.margin = '0.35em 0';
node.style.color = theme.textColor;
node.style.lineHeight = theme.lineHeight;
});
root.querySelectorAll('blockquote').forEach(node => {
node.style.borderLeft = `4px solid ${theme.blockquoteBorder}`;
node.style.backgroundColor = theme.blockquoteBackground;
node.style.padding = '12px 16px';
node.style.borderRadius = theme.borderRadius;
node.style.color = theme.mutedColor;
});
root.querySelectorAll('pre').forEach(node => {
node.style.backgroundColor = theme.codeBackground;
node.style.color = theme.codeColor;
node.style.borderRadius = theme.borderRadius;
node.style.border = theme.codeBlockBorder ? `1px solid ${theme.borderColor}` : 'none';
node.style.padding = '14px 16px';
node.style.margin = `${theme.paragraphSpacing} 0`;
node.style.overflowX = 'auto';
});
root.querySelectorAll('code').forEach(node => {
node.style.backgroundColor = theme.codeBackground;
node.style.color = theme.codeColor;
node.style.borderRadius = '4px';
node.style.padding = '0.15em 0.35em';
node.style.fontSize = '0.92em';
});
root.querySelectorAll('table').forEach(node => {
node.style.borderCollapse = 'collapse';
node.style.border = `1px solid ${theme.borderColor}`;
node.style.width = '100%';
node.style.margin = `${theme.paragraphSpacing} 0`;
node.style.overflow = 'hidden';
node.style.borderRadius = theme.borderRadius;
});
root.querySelectorAll('th, td').forEach(node => {
node.style.border = `1px solid ${theme.borderColor}`;
node.style.padding = '10px 12px';
});
root.querySelectorAll('th').forEach(node => {
node.style.backgroundColor = theme.tableHeaderFill ? theme.codeBackground : 'transparent';
node.style.color = theme.headingColor;
node.style.fontWeight = '600';
});
root.querySelectorAll('img').forEach(node => {
node.style.borderRadius = theme.imageRounded ? theme.borderRadius : '0';
node.style.margin = `${theme.paragraphSpacing} auto`;
node.style.display = 'block';
});
root.querySelectorAll('span, strong, em, del, mark').forEach(node => {
if (!node.style.color || node.style.color === 'inherit') {
node.style.color = theme.textColor;
}
node.style.fontFamily = theme.fontFamily;
});
return root;
}
function collectExportWarnings(root, platformPreset, context) {
const warnings = [];
if (root.querySelector('.math, .katex, mjx-container')) {
warnings.push(`检测到数学公式,${platformPreset?.name || '当前平台'} 可能无法完整保留公式样式`);
}
if (root.querySelector('table') && platformPreset?.supports?.table === 'partial') {
warnings.push(`检测到表格,${platformPreset?.name || '当前平台'} 可能会简化表格样式`);
}
if (root.querySelector('pre') && platformPreset?.supports?.codeBlock === 'partial') {
warnings.push(`检测到代码块,${platformPreset?.name || '当前平台'} 可能无法完整保留语法高亮`);
}
if (root.querySelector('.callout') && platformPreset?.transforms?.flattenCallout) {
warnings.push(`检测到 callout已为 ${platformPreset?.name || '当前平台'} 自动降级为普通卡片样式`);
}
if (platformPreset?.name === '微信公众号') {
if (root.querySelector('li.task-list-item')) {
warnings.push('检测到任务列表,微信公众号已降级为符号清单样式');
}
if (root.querySelector('ul ul, ul ol, ol ul, ol ol')) {
warnings.push('检测到嵌套列表,微信公众号已简化缩进与层级表现');
}
if (root.querySelector('blockquote blockquote')) {
warnings.push('检测到多层引用,微信公众号已压平为更稳定的引用结构');
}
if (root.querySelector('table')) {
warnings.push('检测到表格,微信公众号已降级为卡片式字段展示');
}
if (root.querySelector('.internal-embed, .markdown-embed')) {
warnings.push('检测到嵌入块,微信公众号已按图片嵌入或内容嵌入降级展示');
}
if (root.querySelector('.footnotes, section.footnotes')) {
warnings.push('检测到脚注,微信公众号已降级为普通注释区块');
}
}
return warnings;
}
function adaptDomForPlatform(root, platformPreset, context) {
const themeStyle = context?.platformTheme || captureThemeTokens();
transformCallouts(root, platformPreset, themeStyle);
transformCodeBlocks(root, platformPreset, themeStyle);
transformTables(root, platformPreset, themeStyle);
transformWechatSpecificBlocks(root, platformPreset, themeStyle);
normalizeLinks(root, platformPreset, context);
return root;
}
async function resolveImageSource(imgEl, context) {
const src = imgEl.getAttribute('src') || '';
if (!src || src.startsWith('data:')) {
return { kind: 'passthrough', src };
}
if (/^https?:\/\//i.test(src)) {
return { kind: 'remote', src };
}
const alt = imgEl.getAttribute('alt') || '';
// 处理 Obsidian 预览 DOM 中的内部资源 URLapp://local/...
// 以及 blob: URL提取文件名后在 vault 中查找
if (src.startsWith('app://') || src.startsWith('blob:')) {
let filename = '';
if (src.startsWith('app://')) {
// app://local/绝对路径/文件名.ext?参数 → 取最后一段去掉查询参数
const pathPart = src.replace(/^app:\/\/[^/]+/, ''); // 去掉 app://local
const lastSegment = pathPart.split('/').pop() || '';
filename = decodeURIComponent(lastSegment.split('?')[0]);
}
// blob: URL 无法提取文件名,尝试用 alt 作为备选
if (!filename && alt) {
filename = alt;
}
if (filename) {
const html = await inlineImage(filename, alt, context);
return { kind: 'vault', html };
}
return { kind: 'passthrough', src };
}
const decoded = decodeURIComponent(src).replace(/^\.\//, '');
const html = await inlineImage(decoded, alt, context);
return { kind: 'vault', html };
}
async function replaceImageSrcWithBase64(imgEl, context) {
const resolved = await resolveImageSource(imgEl, context);
if (resolved.kind === 'passthrough' || resolved.kind === 'remote') {
return resolved.kind;
}
const wrapper = document.createElement('div');
wrapper.innerHTML = resolved.html;
const newImg = wrapper.querySelector('img');
if (newImg) {
imgEl.replaceWith(newImg);
return 'inlined';
}
// resolved.html 可能是 Markdown 回退文本(找不到文件时 inlineImage 返回 ![alt](name)
// 记录一下原始 src 便于排查
console.warn('[bulk-copy] 图片内联失败,无法在 vault 中找到文件。原始 src:', imgEl.getAttribute('src'), '解析结果:', resolved.html);
return 'failed';
}
async function inlineImagesInDom(root, context) {
const images = Array.from(root.querySelectorAll('img'));
let inlined = 0;
for (const img of images) {
try {
const result = await replaceImageSrcWithBase64(img, context);
if (result === 'inlined') {
inlined++;
}
} catch (e) {
console.error('[bulk-copy] DOM 图片内联失败:', e);
}
}
return { count: inlined, totalSize: `${inlined * 150}KB` };
}
async function buildOffscreenRenderedDom(plugin, markdown, context) {
if (!markdown || !MarkdownRenderer || typeof MarkdownRenderer.render !== 'function') {
return null;
}
const container = document.createElement('div');
container.addClass('markdown-rendered');
try {
await MarkdownRenderer.render(plugin.app, markdown, container, context.file?.path || '', plugin);
sanitizeObsidianDom(container);
return container;
} catch (e) {
console.error('[bulk-copy] 离屏渲染失败:', e);
return null;
}
}
function buildClipboardPayload(exportResult) {
const payload = {};
if (USER_CONFIG.clipboardFormats.includes('text/plain')) {
payload['text/plain'] = new Blob([exportResult.plainText], { type: 'text/plain' });
}
if (USER_CONFIG.clipboardFormats.includes('text/html')) {
payload['text/html'] = new Blob([exportResult.html], { type: 'text/html' });
}
return payload;
}
function legacyCopyToClipboard(html, text) {
return new Promise((resolve, reject) => {
const listener = (event) => {
event.preventDefault();
try {
if (event.clipboardData) {
if (html) {
event.clipboardData.setData('text/html', html);
}
event.clipboardData.setData('text/plain', text || '');
resolve();
return;
}
} catch (error) {
reject(error);
}
reject(new Error('无法访问 clipboardData'));
};
document.addEventListener('copy', listener, { once: true });
try {
const successful = document.execCommand('copy');
if (!successful) {
document.removeEventListener('copy', listener);
reject(new Error('execCommand(copy) 返回失败'));
}
} catch (error) {
document.removeEventListener('copy', listener);
reject(error);
}
});
}
function writeWithElectronClipboard(html, text) {
if (!electronClipboard) {
return false;
}
try {
const wrappedHtml = html
? `<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>${html}</body></html>`
: '';
electronClipboard.write({
html: wrappedHtml,
text: text || ''
});
console.log('[bulk-copy] 已使用 electron clipboard 写入系统剪贴板');
return true;
} catch (error) {
console.warn('[bulk-copy] electron clipboard 写入失败:', error);
return false;
}
}
async function writeClipboardPayload(payload, exportResult) {
const fallbackText = exportResult.plainText || '';
const fallbackHtml = exportResult.html || '';
if (navigator.clipboard && window.ClipboardItem) {
try {
const clipboardItem = new ClipboardItem(payload);
await navigator.clipboard.write([clipboardItem]);
console.log('[bulk-copy] 已使用 ClipboardItem 写入剪贴板');
return;
} catch (error) {
console.warn('[bulk-copy] ClipboardItem 写入失败,尝试回退:', error);
}
}
if (writeWithElectronClipboard(fallbackHtml, fallbackText)) {
return;
}
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
try {
await navigator.clipboard.writeText(fallbackText);
console.log('[bulk-copy] 已使用 writeText 写入纯文本剪贴板');
return;
} catch (error) {
console.warn('[bulk-copy] writeText 纯文本写入失败,尝试 execCommand 回退:', error);
}
}
try {
await legacyCopyToClipboard(fallbackHtml, fallbackText);
console.log('[bulk-copy] 已使用 execCommand 回退写入剪贴板');
return;
} catch (error) {
console.warn('[bulk-copy] execCommand 富文本复制失败:', error);
}
throw new Error('当前环境不支持可用的剪贴板写入方式');
}
async function buildExportDom(plugin, options, context) {
const resolvedView = resolveActiveMarkdownView(plugin);
const previewRoot = getPreviewRoot(resolvedView && resolvedView.view, resolvedView && resolvedView.mode);
if (previewRoot && options.mode !== 'selection') {
console.log('[bulk-copy] 使用阅读视图预览 DOM 导出', { mode: resolvedView && resolvedView.mode });
const clonedRoot = cloneExportRoot(previewRoot, options);
sanitizeObsidianDom(clonedRoot);
inlineStylesFromSource(clonedRoot, previewRoot, PLATFORM_PRESETS[options.platform]);
adaptDomForPlatform(clonedRoot, PLATFORM_PRESETS[options.platform], context);
const imageStats = await inlineImagesInDom(clonedRoot, context);
const warnings = collectExportWarnings(clonedRoot, PLATFORM_PRESETS[options.platform], context);
return {
root: clonedRoot,
meta: {
source: 'preview-dom',
imageCount: imageStats.count,
warnings
}
};
}
const markdown = options.selectedText || await plugin.app.vault.read(context.file);
const offscreenRoot = await buildOffscreenRenderedDom(plugin, markdown, context);
if (offscreenRoot) {
console.log('[bulk-copy] 使用离屏渲染导出', { mode: resolvedView && resolvedView.mode });
inlineStylesFromSource(offscreenRoot, offscreenRoot, PLATFORM_PRESETS[options.platform]);
adaptDomForPlatform(offscreenRoot, PLATFORM_PRESETS[options.platform], context);
const imageStats = await inlineImagesInDom(offscreenRoot, context);
const warnings = collectExportWarnings(offscreenRoot, PLATFORM_PRESETS[options.platform], context);
return {
root: offscreenRoot,
meta: {
source: 'offscreen-render',
imageCount: imageStats.count,
warnings
}
};
}
return null;
}
async function exportContentForClipboard(plugin, options) {
const file = plugin.app.workspace.getActiveFile();
const platformPreset = PLATFORM_PRESETS[options.platform] || PLATFORM_PRESETS.feishu;
const platformTheme = getPlatformThemeConfig(options.platform);
console.log('[bulk-copy] 当前导出主题', {
platform: options.platform,
themeKey: getCurrentPlatformTheme(options.platform),
themeName: platformTheme.name
});
const baseFolder = file.parent?.path || '';
const assetsFolder = `${baseFolder}/${file.basename}.assets`;
const context = { plugin, baseFolder, assetsFolder, file, platformPreset, platformTheme };
const domResult = await buildExportDom(plugin, options, context);
if (domResult && domResult.root) {
const container = document.createElement('div');
container.style.maxWidth = typeof platformPreset.displayMaxWidth === 'number'
? `${platformPreset.displayMaxWidth}px`
: String(platformPreset.displayMaxWidth || '800px');
container.style.margin = '0 auto';
container.style.fontSize = platformPreset.fontSize || '16px';
container.style.lineHeight = platformTheme.lineHeight || platformPreset.lineHeight || '1.6';
container.style.fontFamily = platformTheme.fontFamily;
container.style.color = platformTheme.textColor;
container.style.backgroundColor = platformTheme.backgroundColor;
applyThemeToRoot(domResult.root, platformTheme);
container.appendChild(domResult.root);
return {
html: container.outerHTML,
plainText: options.selectedText || domResult.root.textContent || '',
meta: {
...domResult.meta,
theme: getCurrentPlatformTheme(options.platform)
}
};
}
let md = options.selectedText;
if (!md) {
md = await plugin.app.vault.read(file);
}
const imageResult = await replaceImagesWithHtml(md, context);
const htmlContent = markdownToHtml(imageResult.html);
const wrappedHtml = `<div style="font-family: ${platformTheme.fontFamily}; font-size: ${platformPreset.fontSize || '16px'}; line-height: ${platformTheme.lineHeight || platformPreset.lineHeight || '1.6'}; color: ${platformTheme.textColor}; background-color: ${platformTheme.backgroundColor}; max-width: ${typeof platformPreset.displayMaxWidth === 'number' ? `${platformPreset.displayMaxWidth}px` : platformPreset.displayMaxWidth}; margin: 0 auto;">${htmlContent}</div>`;
return {
html: wrappedHtml,
plainText: md,
meta: {
source: 'markdown-fallback',
imageCount: imageResult.stats.count,
warnings: ['当前未检测到可用预览 DOM已回退到 Markdown 转 HTML 路径'],
theme: getCurrentPlatformTheme(options.platform)
}
};
}
/* ========== 平台选择 Modal ========== */
class PlatformSelectorModal extends Modal {
constructor(app, callback) {
super(app);
this.callback = callback;
this.selectedPlatform = USER_CONFIG.currentPlatform || 'feishu';
}
onOpen() {
const { contentEl } = this;
contentEl.empty();
contentEl.addClass('platform-selector-modal');
const header = contentEl.createEl('div', { cls: 'modal-header' });
header.createEl('h2', { text: '📋 选择目标平台' });
header.createEl('p', { text: '选择你要复制到的平台,将自动应用最佳样式' });
const platformContainer = contentEl.createEl('div', { cls: 'platform-options' });
Object.keys(PLATFORM_PRESETS).forEach(platformKey => {
const preset = PLATFORM_PRESETS[platformKey];
const strategyLabel = getImageStrategyLabel(platformKey);
const themeLabel = getPlatformThemeLabel(platformKey);
const recommendedThemeLabels = getRecommendedThemeLabels(platformKey);
const qualityLabel = typeof preset.imageQuality === 'number' ? preset.imageQuality.toFixed(2) : String(preset.imageQuality || '-');
const widthLabel = typeof preset.imageMaxWidth === 'number' ? `${preset.imageMaxWidth}px` : String(preset.imageMaxWidth || '-');
const option = platformContainer.createEl('div', {
cls: 'platform-option'
});
const icons = { wechat: '💬', feishu: '📝', zhihu: '💡', yuque: '📚' };
option.innerHTML = `
<div class="option-content">
<div class="platform-icon">${icons[platformKey] || '📱'}</div>
<div class="platform-info">
<div class="platform-name">${preset.name}</div>
<div class="platform-desc">${preset.description}</div>
<div class="platform-meta-row">
<span class="platform-theme">${themeLabel}</span>
</div>
<div class="platform-meta-row muted">
<span class="platform-recommended-theme">推荐 ${recommendedThemeLabels.join(' / ') || '无'}</span>
</div>
</div>
<div class="check-icon">✓</div>
</div>
`;
option.title = `图片策略: ${strategyLabel} | 压缩质量: ${qualityLabel} | 最大宽度: ${widthLabel}`;
// 设置初始选中状态
if (this.selectedPlatform === platformKey) {
option.classList.add('selected');
}
// 点击事件:直接选择并关闭
option.addEventListener('click', async () => {
this.selectedPlatform = platformKey;
option.addClass('selected');
try {
new Notice(`⏳ 正在复制到 ${PLATFORM_PRESETS[platformKey].name}...`);
await this.callback(platformKey);
} finally {
this.close();
}
});
});
// 应用样式
this.applyStyles(contentEl);
}
applyStyles(container) {
const style = document.createElement('style');
style.innerHTML = `
.platform-selector-modal {
max-width: 560px;
}
.platform-selector-modal .modal-header {
text-align: center;
margin-bottom: 14px;
padding-bottom: 10px;
border-bottom: 1px solid var(--background-modifier-border);
}
.platform-selector-modal .modal-header h2 {
margin-top: 0;
margin-bottom: 6px;
font-size: 1.1rem;
}
.platform-selector-modal .modal-header p {
color: var(--text-muted);
margin: 0;
}
.platform-options {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
margin-bottom: 8px;
}
.platform-option {
padding: 10px 12px;
border: 1px solid var(--background-modifier-border);
border-radius: 10px;
cursor: pointer;
transition: all 0.2s ease;
background: var(--background-primary);
}
.platform-option:hover {
border-color: var(--interactive-accent);
background: var(--background-secondary);
}
.platform-option.selected {
border-color: var(--interactive-accent);
background: rgba(var(--interactive-accent-rgb), 0.1);
}
.platform-option .option-content {
display: flex;
align-items: flex-start;
gap: 8px;
}
.platform-option .platform-icon {
font-size: 22px;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
background: var(--background-secondary);
border-radius: 8px;
}
.platform-option .platform-info {
flex: 1;
}
.platform-option .platform-name {
font-weight: 600;
font-size: 0.92rem;
margin-bottom: 2px;
color: var(--text-normal);
}
.platform-option .platform-desc {
font-size: 0.76rem;
color: var(--text-muted);
line-height: 1.45;
}
.platform-option .platform-meta-row {
display: flex;
flex-wrap: wrap;
gap: 4px 8px;
margin-top: 4px;
}
.platform-option .platform-meta-row.muted {
color: var(--text-muted);
}
.platform-option .platform-strategy {
font-size: 0.78rem;
color: var(--interactive-accent);
}
.platform-option .platform-theme {
font-size: 0.75rem;
color: var(--text-normal);
padding: 2px 6px;
border-radius: 999px;
background: var(--background-secondary);
}
.platform-option .platform-recommended-theme {
font-size: 0.72rem;
}
.platform-option .platform-metrics {
font-size: 0.75rem;
}
.platform-option .check-icon {
font-size: 18px;
color: var(--interactive-accent);
font-weight: bold;
opacity: 0;
transition: opacity 0.2s;
}
@media (max-width: 640px) {
.platform-selector-modal {
max-width: 92vw;
}
.platform-options {
grid-template-columns: 1fr;
}
}
.platform-option.selected .check-icon {
opacity: 1;
}
`;
container.appendChild(style);
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}
class ThemeJsonModal extends Modal {
constructor(app, options) {
super(app);
this.options = options;
}
onOpen() {
const { contentEl } = this;
contentEl.empty();
contentEl.addClass('bulk-copy-theme-json-modal');
contentEl.createEl('h2', { text: this.options.title || '主题 JSON' });
if (this.options.description) {
contentEl.createEl('p', { text: this.options.description, cls: 'setting-item-description' });
}
const textarea = contentEl.createEl('textarea');
textarea.style.width = '100%';
textarea.style.minHeight = '320px';
textarea.style.marginTop = '12px';
textarea.style.fontFamily = 'monospace';
textarea.style.fontSize = '13px';
textarea.style.lineHeight = '1.5';
textarea.value = this.options.initialValue || '';
if (this.options.readOnly) {
textarea.readOnly = true;
}
const actions = contentEl.createDiv();
actions.style.display = 'flex';
actions.style.gap = '10px';
actions.style.marginTop = '14px';
if (this.options.readOnly) {
const copyButton = actions.createEl('button', { text: '复制到剪贴板' });
copyButton.addEventListener('click', async () => {
await navigator.clipboard.writeText(textarea.value);
new Notice('✅ JSON 已复制到剪贴板');
});
} else {
const importButton = actions.createEl('button', { text: '确认导入' });
importButton.addEventListener('click', async () => {
await this.options.onConfirm(textarea.value);
this.close();
});
}
const closeButton = actions.createEl('button', { text: '关闭' });
closeButton.addEventListener('click', () => this.close());
}
onClose() {
this.contentEl.empty();
}
}
class ThemeManagerModal extends Modal {
constructor(app, plugin, onChange) {
super(app);
this.plugin = plugin;
this.onChange = onChange;
this.activeTab = 'basic';
}
onOpen() {
const { contentEl } = this;
this.modalEl.style.width = 'min(1280px, 92vw)';
this.modalEl.style.maxWidth = '92vw';
this.modalEl.style.height = 'min(860px, 88vh)';
this.modalEl.style.maxHeight = '88vh';
this.modalEl.style.resize = 'both';
this.modalEl.style.overflow = 'hidden';
if (this.contentEl && this.contentEl.parentElement) {
this.contentEl.parentElement.style.height = '100%';
}
contentEl.empty();
contentEl.addClass('bulk-copy-theme-manager-modal');
const style = document.createElement('style');
style.innerHTML = `
.bulk-copy-theme-manager-modal {
height: 100%;
overflow: auto;
}
.bulk-copy-theme-manager-layout {
display: grid;
grid-template-columns: 280px 1fr;
gap: 18px;
margin-top: 16px;
min-height: calc(100% - 72px);
}
.bulk-copy-theme-manager-layout.collapsed {
grid-template-columns: 1fr;
}
.bulk-copy-theme-list {
border: 1px solid var(--background-modifier-border);
border-radius: 14px;
background: var(--background-secondary);
padding: 10px;
overflow: auto;
}
.bulk-copy-theme-sidebar-wrap {
position: relative;
min-width: 180px;
}
.bulk-copy-theme-resizer {
position: absolute;
top: 0;
right: -9px;
width: 10px;
height: 100%;
cursor: col-resize;
z-index: 2;
}
.bulk-copy-theme-list-item {
padding: 10px 12px;
border-radius: 10px;
cursor: pointer;
margin-bottom: 8px;
border: 1px solid transparent;
}
.bulk-copy-theme-list-item:hover {
background: var(--background-primary);
}
.bulk-copy-theme-list-item.active {
background: rgba(var(--interactive-accent-rgb), 0.12);
border-color: var(--interactive-accent);
}
.bulk-copy-theme-list-name {
font-weight: 600;
}
.bulk-copy-theme-list-meta {
font-size: 12px;
color: var(--text-muted);
margin-top: 4px;
}
.bulk-copy-theme-badge {
display: inline-block;
margin-top: 6px;
padding: 2px 8px;
border-radius: 999px;
font-size: 11px;
color: var(--interactive-accent);
background: rgba(var(--interactive-accent-rgb), 0.1);
cursor: pointer;
}
.bulk-copy-theme-swatches {
display: flex;
gap: 6px;
margin-top: 8px;
}
.bulk-copy-theme-swatch {
width: 14px;
height: 14px;
border-radius: 999px;
border: 1px solid rgba(0, 0, 0, 0.08);
}
.bulk-copy-theme-editor-panel {
border: 1px solid var(--background-modifier-border);
border-radius: 14px;
background: var(--background-primary);
padding: 14px 16px;
overflow: auto;
}
.bulk-copy-theme-tabs {
display: flex;
gap: 8px;
margin-bottom: 14px;
flex-wrap: wrap;
}
.bulk-copy-theme-tab {
padding: 8px 12px;
border-radius: 10px;
border: 1px solid var(--background-modifier-border);
background: var(--background-secondary);
cursor: pointer;
}
.bulk-copy-theme-tab.active {
border-color: var(--interactive-accent);
color: var(--interactive-accent);
background: rgba(var(--interactive-accent-rgb), 0.08);
}
.bulk-copy-preview-compare-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 14px;
margin-top: 12px;
}
.bulk-copy-preview-compare-card {
padding: 12px;
border: 1px solid var(--background-modifier-border);
border-radius: 12px;
background: var(--background-primary);
}
.bulk-copy-preview-compare-card h4 {
margin: 0 0 8px;
}
.bulk-copy-preview-compare-note {
margin: 0 0 10px;
color: var(--text-muted);
font-size: 12px;
}
`;
contentEl.appendChild(style);
contentEl.createEl('h2', { text: '主题模板管理器' });
contentEl.createEl('p', { text: '集中管理自定义主题模板:搜索、切换、复制、排序、导入导出与编辑。', cls: 'setting-item-description' });
const toolbar = contentEl.createDiv();
toolbar.style.display = 'flex';
toolbar.style.justifyContent = 'flex-end';
toolbar.style.marginTop = '8px';
const toggleButton = toolbar.createEl('button', { text: USER_CONFIG.themeManagerListCollapsed ? '展开左侧列表' : '折叠左侧列表' });
toggleButton.addEventListener('click', async () => {
USER_CONFIG.themeManagerListCollapsed = !USER_CONFIG.themeManagerListCollapsed;
await this.plugin.saveUserConfig();
this.onOpen();
});
const layout = contentEl.createDiv({ cls: `bulk-copy-theme-manager-layout${USER_CONFIG.themeManagerListCollapsed ? ' collapsed' : ''}` });
layout.style.gridTemplateColumns = USER_CONFIG.themeManagerListCollapsed ? '1fr' : `${USER_CONFIG.themeManagerSidebarWidth || 280}px 1fr`;
const sidebarWrap = layout.createDiv({ cls: 'bulk-copy-theme-sidebar-wrap' });
sidebarWrap.style.width = `${USER_CONFIG.themeManagerSidebarWidth || 280}px`;
const listPane = sidebarWrap.createDiv({ cls: 'bulk-copy-theme-list' });
const editorPane = layout.createDiv({ cls: 'bulk-copy-theme-editor-panel' });
if (USER_CONFIG.themeManagerListCollapsed) {
sidebarWrap.style.display = 'none';
} else {
const resizer = sidebarWrap.createDiv({ cls: 'bulk-copy-theme-resizer' });
resizer.addEventListener('mousedown', (event) => {
event.preventDefault();
const startX = event.clientX;
const startWidth = USER_CONFIG.themeManagerSidebarWidth || 280;
const onMouseMove = (moveEvent) => {
const nextWidth = Math.max(200, Math.min(420, startWidth + (moveEvent.clientX - startX)));
USER_CONFIG.themeManagerSidebarWidth = nextWidth;
layout.style.gridTemplateColumns = `${nextWidth}px 1fr`;
sidebarWrap.style.width = `${nextWidth}px`;
};
const onMouseUp = async () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
await this.plugin.saveUserConfig();
};
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
});
}
const customThemes = USER_CONFIG.customThemes || DEFAULT_USER_CONFIG.customThemes;
new Setting(listPane)
.setName('仅看使用中')
.addToggle(toggle => {
toggle.setValue(!!USER_CONFIG.themeListShowUsedOnly);
toggle.onChange(async value => {
USER_CONFIG.themeListShowUsedOnly = value;
await this.plugin.saveUserConfig();
this.onOpen();
});
});
new Setting(listPane)
.setName('使用中优先')
.addToggle(toggle => {
toggle.setValue(!!USER_CONFIG.themeListUsedFirst);
toggle.onChange(async value => {
USER_CONFIG.themeListUsedFirst = value;
await this.plugin.saveUserConfig();
this.onOpen();
});
});
new Setting(listPane)
.setName('仅看当前平台推荐')
.setDesc(`${PLATFORM_PRESETS[USER_CONFIG.themePreviewPlatform || 'wechat']?.name || '当前平台'} 的推荐主题过滤`)
.addToggle(toggle => {
toggle.setValue(!!USER_CONFIG.themeListShowRecommendedOnly);
toggle.onChange(async value => {
USER_CONFIG.themeListShowRecommendedOnly = value;
await this.plugin.saveUserConfig();
this.onOpen();
});
});
let themeKeys = getOrderedCustomThemeKeys();
if (USER_CONFIG.themeListShowUsedOnly) {
themeKeys = themeKeys.filter(themeKey => Object.keys(PLATFORM_PRESETS).some(platformKey => USER_CONFIG.platformThemes?.[platformKey] === themeKey));
}
if (USER_CONFIG.themeListShowRecommendedOnly) {
const recommendedKeys = new Set(getRecommendedThemeKeys(USER_CONFIG.themePreviewPlatform || 'wechat'));
themeKeys = themeKeys.filter(themeKey => recommendedKeys.has(themeKey));
}
if (USER_CONFIG.themeListUsedFirst) {
themeKeys = [...themeKeys].sort((a, b) => {
const aUsed = Object.keys(PLATFORM_PRESETS).some(platformKey => USER_CONFIG.platformThemes?.[platformKey] === a);
const bUsed = Object.keys(PLATFORM_PRESETS).some(platformKey => USER_CONFIG.platformThemes?.[platformKey] === b);
return Number(bUsed) - Number(aUsed);
});
}
themeKeys.forEach(themeKey => {
const theme = customThemes[themeKey] || DEFAULT_USER_CONFIG.customThemes.custom;
const usedBy = Object.keys(PLATFORM_PRESETS).filter(platformKey => USER_CONFIG.platformThemes?.[platformKey] === themeKey);
const recommendedBy = usedBy.filter(platformKey => getRecommendedThemeKeys(platformKey)[0] === themeKey);
const item = listPane.createDiv({ cls: `bulk-copy-theme-list-item${themeKey === getEditableCustomThemeKey() ? ' active' : ''}` });
item.createDiv({ text: customThemes[themeKey]?.name || themeKey, cls: 'bulk-copy-theme-list-name' });
item.createDiv({ text: themeKey, cls: 'bulk-copy-theme-list-meta' });
item.createDiv({
text: usedBy.length > 0
? `使用平台: ${usedBy.map(platformKey => PLATFORM_PRESETS[platformKey].name).join(' / ')}`
: '使用平台: 未绑定',
cls: 'bulk-copy-theme-list-meta'
});
if (recommendedBy.length > 0) {
const badge = item.createDiv({
text: `推荐中: ${recommendedBy.map(platformKey => PLATFORM_PRESETS[platformKey].name).join(' / ')}`,
cls: 'bulk-copy-theme-badge'
});
badge.title = '点击切换到对应平台预览';
badge.addEventListener('click', async (event) => {
event.stopPropagation();
const firstPlatform = recommendedBy[0];
USER_CONFIG.themePreviewPlatform = firstPlatform;
await this.plugin.saveUserConfig();
new Notice(`已切换到 ${PLATFORM_PRESETS[firstPlatform]?.name || firstPlatform} 预览,可继续查看该平台配置效果`);
this.onOpen();
});
}
const swatches = item.createDiv({ cls: 'bulk-copy-theme-swatches' });
[theme.accentColor, theme.backgroundColor, theme.codeBackground].forEach(color => {
const swatch = swatches.createDiv({ cls: 'bulk-copy-theme-swatch' });
swatch.style.backgroundColor = color || '#cccccc';
swatch.title = color || '#cccccc';
});
item.addEventListener('click', async () => {
USER_CONFIG.customThemeEditorKey = themeKey;
await this.plugin.saveUserConfig();
this.onOpen();
});
});
const tabs = editorPane.createDiv({ cls: 'bulk-copy-theme-tabs' });
[
['basic', '基础'],
['color', '颜色'],
['details', '细节'],
['preview', '预览'],
['manage', '导入导出']
].forEach(([key, label]) => {
const tab = tabs.createDiv({ cls: `bulk-copy-theme-tab${this.activeTab === key ? ' active' : ''}`, text: label });
tab.addEventListener('click', () => {
this.activeTab = key;
this.onOpen();
});
});
const visibleTabs = this.activeTab === 'basic'
? ['basic']
: this.activeTab === 'color'
? ['color']
: this.activeTab === 'details'
? ['details']
: this.activeTab === 'preview'
? ['preview']
: ['manage'];
this.plugin.settingTab.renderCustomThemeEditor(editorPane, { visibleTabs });
}
onClose() {
if (typeof this.onChange === 'function') {
this.onChange();
}
this.contentEl.empty();
}
}
class BulkCopySettingTab extends PluginSettingTab {
constructor(app, plugin) {
super(app, plugin);
this.plugin = plugin;
}
createSection(containerEl, title, description) {
const section = containerEl.createDiv({ cls: 'bulk-copy-settings-section' });
section.createEl('h3', { text: title, cls: 'bulk-copy-settings-section-title' });
if (description) {
section.createEl('p', { text: description, cls: 'bulk-copy-settings-section-desc' });
}
return section;
}
renderSettingsStyles(containerEl) {
const style = document.createElement('style');
style.innerHTML = `
.bulk-copy-settings-root {
padding-bottom: 28px;
}
.bulk-copy-settings-hero {
padding: 18px 20px;
margin-bottom: 18px;
border: 1px solid var(--background-modifier-border);
border-radius: 14px;
background: linear-gradient(180deg, var(--background-primary), var(--background-secondary));
}
.bulk-copy-settings-hero h2 {
margin: 0 0 6px;
}
.bulk-copy-settings-hero p {
margin: 0;
color: var(--text-muted);
line-height: 1.6;
}
.bulk-copy-settings-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 12px;
margin: 16px 0 0;
}
.bulk-copy-settings-stat {
padding: 12px 14px;
border-radius: 12px;
background: var(--background-primary-alt, var(--background-primary));
border: 1px solid var(--background-modifier-border);
}
.bulk-copy-settings-stat-label {
font-size: 12px;
color: var(--text-muted);
margin-bottom: 4px;
}
.bulk-copy-settings-stat-value {
font-size: 14px;
font-weight: 600;
color: var(--text-normal);
}
.bulk-copy-settings-section {
margin: 20px 0;
padding: 16px 18px 6px;
border: 1px solid var(--background-modifier-border);
border-radius: 14px;
background: var(--background-primary);
}
.bulk-copy-settings-section-title {
margin: 0 0 6px;
font-size: 16px;
}
.bulk-copy-settings-section-desc {
margin: 0 0 14px;
color: var(--text-muted);
line-height: 1.6;
}
.bulk-copy-platform-section {
margin: 12px 0 16px;
border: 1px solid var(--background-modifier-border);
border-radius: 12px;
overflow: hidden;
background: var(--background-primary-alt, var(--background-primary));
}
.bulk-copy-platform-section summary {
cursor: pointer;
padding: 14px 16px;
font-weight: 600;
list-style: none;
background: var(--background-secondary);
}
.bulk-copy-platform-section summary::-webkit-details-marker {
display: none;
}
.bulk-copy-platform-section[open] summary {
border-bottom: 1px solid var(--background-modifier-border);
}
.bulk-copy-platform-section .setting-item,
.bulk-copy-settings-section .setting-item {
padding-top: 12px;
padding-bottom: 12px;
}
.bulk-copy-platform-section h3,
.bulk-copy-settings-section h3 {
margin-top: 14px;
}
.bulk-copy-subtle-note {
margin: 2px 0 10px;
color: var(--text-muted);
font-size: 13px;
line-height: 1.5;
}
.bulk-copy-theme-manager-box {
margin-top: 10px;
padding: 14px 16px;
border: 1px dashed var(--background-modifier-border);
border-radius: 12px;
background: var(--background-secondary);
}
.bulk-copy-theme-manager-box details summary {
cursor: pointer;
font-weight: 600;
list-style: none;
}
.bulk-copy-theme-manager-box details summary::-webkit-details-marker {
display: none;
}
`;
containerEl.appendChild(style);
}
renderThemeDetailSettings(containerEl, platformKey, title, description) {
const theme = getPlatformThemeConfig(platformKey);
containerEl.createEl('h3', { text: title });
if (description) {
containerEl.createEl('p', { text: description, cls: 'setting-item-description' });
}
new Setting(containerEl)
.setName('标题分隔线')
.setDesc('控制 H1/H2 是否显示下边线')
.addToggle(toggle => {
toggle.setValue(!!theme.headingDivider);
toggle.onChange(async value => {
updatePlatformThemeOverride(platformKey, 'headingDivider', value);
await this.plugin.saveUserConfig();
});
});
new Setting(containerEl)
.setName('表头底色')
.setDesc('控制表格表头是否使用底色强调')
.addToggle(toggle => {
toggle.setValue(!!theme.tableHeaderFill);
toggle.onChange(async value => {
updatePlatformThemeOverride(platformKey, 'tableHeaderFill', value);
await this.plugin.saveUserConfig();
});
});
new Setting(containerEl)
.setName('图片圆角')
.setDesc('控制导出图片是否保留圆角')
.addToggle(toggle => {
toggle.setValue(!!theme.imageRounded);
toggle.onChange(async value => {
updatePlatformThemeOverride(platformKey, 'imageRounded', value);
await this.plugin.saveUserConfig();
});
});
new Setting(containerEl)
.setName('代码块边框')
.setDesc('控制代码块是否显示边框')
.addToggle(toggle => {
toggle.setValue(!!theme.codeBlockBorder);
toggle.onChange(async value => {
updatePlatformThemeOverride(platformKey, 'codeBlockBorder', value);
await this.plugin.saveUserConfig();
});
});
}
renderThemePreview(containerEl, platformKey) {
const theme = getPlatformThemeConfig(platformKey);
const preset = PLATFORM_PRESETS[platformKey];
const frame = preset.previewFrame || {};
const outer = containerEl.createDiv({ cls: 'bulk-copy-theme-preview-frame' });
outer.style.background = frame.background || '#f5f5f5';
outer.style.padding = frame.padding || '20px';
outer.style.margin = '12px 0 20px';
outer.style.borderRadius = '14px';
const preview = outer.createDiv({ cls: 'bulk-copy-theme-preview' });
preview.style.padding = '16px 18px';
preview.style.maxWidth = typeof preset.displayMaxWidth === 'number'
? `${preset.displayMaxWidth}px`
: String(preset.displayMaxWidth || '800px');
preview.style.margin = '0 auto';
preview.style.border = frame.contentBorder || `1px solid ${theme.borderColor}`;
preview.style.boxShadow = frame.contentShadow || 'none';
preview.style.borderRadius = theme.borderRadius;
preview.style.backgroundColor = frame.contentBackground || theme.backgroundColor;
preview.style.color = theme.textColor;
preview.style.fontFamily = theme.fontFamily;
preview.innerHTML = `
<h2>示例标题</h2>
<p>这是一段用于预览导出效果的正文内容,展示当前平台主题的字体、颜色、间距和整体观感,并包含 <code>行内代码</code> 与 <a href="#">链接样式</a>。</p>
<h3>二级内容分组</h3>
<p>下面的区块用于观察更复杂的富文本结构在当前主题下的表现。</p>
<blockquote>这是一段引用示例,用于观察边框、背景和文本色。</blockquote>
<ul>
<li>无序列表第一项</li>
<li>无序列表第二项,包含 <code>inline code</code></li>
</ul>
<ol>
<li>有序列表第一项</li>
<li>有序列表第二项</li>
</ol>
<pre><code>const message = 'theme preview';</code></pre>
<table>
<thead>
<tr><th>字段</th><th>说明</th></tr>
</thead>
<tbody>
<tr><td>主题</td><td>${theme.name}</td></tr>
<tr><td>平台</td><td>${PLATFORM_PRESETS[platformKey].name}</td></tr>
</tbody>
</table>
<img alt="preview" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0nNzIwJyBoZWlnaHQ9JzM2MCcgdmlld0JveD0nMCAwIDcyMCAzNjAnIHhtbG5zPSdodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2Zyc+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSdnJyB4MT0nMCcgeTE9JzAnIHgyPScxJyB5Mj0nMSc+PHN0b3Agb2Zmc2V0PScwJScgc3RvcC1jb2xvcj0nI2UzZThmZicvPjxzdG9wIG9mZnNldD0nMTAwJScgc3RvcC1jb2xvcj0nI2QxZmFlNScvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHdpZHRoPSc3MjAnIGhlaWdodD0nMzYwJyByeD0nMjQnIGZpbGw9J3VybCgjZyknLz48cmVjdCB4PSc2MCcgeT0nNjAnIHdpZHRoPSc2MDAnIGhlaWdodD0nMTgwJyByeD0nMTYnIGZpbGw9JyNmZmZmZmYnIG9wYWNpdHk9JzAuODUnLz48dGV4dCB4PSc5MCcgeT0nMTMwJyBmb250LXNpemU9JzM2JyBmaWxsPScjMWYyOTM3JyBmb250LWZhbWlseT0nQXJpYWwsIHNhbnMtc2VyaWYnPuWbvueJh+inhOiIoumihuWfnyDpooTonIh+</dGV4dD48dGV4dCB4PSc5MCcgeT0nMTgwJyBmb250LXNpemU9JzIwJyBmaWxsPScjNjQ3NDhiJyBmb250LWZhbWlseT0nQXJpYWwsIHNhbnMtc2VyaWYnPuWPr+eUqOS6juinguWvn+aWh+eroOeCueOAgeS7o+eggeWdl+WSjOWbvueJh+WxleekuuOAguWImO+8mTwvdGV4dD48Y2lyY2xlIGN4PSc1ODAnIGN5PScyODAnIHI9JzQ4JyBmaWxsPScjOTMzM2VhJyBvcGFjaXR5PScwLjE1Jy8+PGNpcmNsZSBjeD0nNjIwJyBjeT0nMjQwJyByPScyOCcgZmlsbD0nI2Y1OWUwYicgb3BhY2l0eT0nMC4yNScvPjwvc3ZnPg==" />
<p>图片说明文字:用于观察图片上下间距、圆角和说明段落是否协调。</p>
`;
applyThemeToRoot(preview, theme);
}
addThemeOptions(dropdown, platformKey) {
const allThemes = getAllThemeOptions();
const builtInKeys = Object.keys(PLATFORM_THEMES);
const customKeys = getOrderedCustomThemeKeys();
[...builtInKeys, ...customKeys].forEach(themeKey => {
const label = isRecommendedTheme(platformKey, themeKey)
? `${allThemes[themeKey].name} (推荐)`
: allThemes[themeKey].name;
dropdown.addOption(themeKey, label);
});
}
renderCustomThemeEditor(containerEl, options = {}) {
const editorKey = getEditableCustomThemeKey();
const customThemes = USER_CONFIG.customThemes || DEFAULT_USER_CONFIG.customThemes;
const customTheme = getEditableCustomTheme();
const searchQuery = String(USER_CONFIG.customThemeSearchQuery || '').trim().toLowerCase();
const filteredThemeKeys = getOrderedCustomThemeKeys().filter(themeKey => {
const name = String(customThemes[themeKey]?.name || themeKey).toLowerCase();
return !searchQuery || name.includes(searchQuery) || themeKey.toLowerCase().includes(searchQuery);
});
const visibleTabs = options.visibleTabs || ['basic', 'color', 'manage'];
const headerBox = containerEl.createDiv({ cls: 'bulk-copy-settings-section' });
headerBox.createEl('h3', { text: customTheme.name || '我的主题', cls: 'bulk-copy-settings-section-title' });
headerBox.createEl('p', { text: `主题 Key: ${editorKey}`, cls: 'bulk-copy-subtle-note' });
headerBox.createEl('p', { text: '可以维护多个自定义主题模板,并分配给任意平台。建议先调整字体、标题颜色和背景色,再微调代码背景与圆角。', cls: 'setting-item-description' });
if (visibleTabs.includes('preview')) {
const previewSection = containerEl.createDiv({ cls: 'bulk-copy-settings-section' });
previewSection.createEl('h3', { text: '主题预览', cls: 'bulk-copy-settings-section-title' });
previewSection.createEl('p', { text: '当前主题可在不同平台容器下进行预览,不会影响全局默认平台。', cls: 'bulk-copy-subtle-note' });
new Setting(previewSection)
.setName('预览平台容器')
.setDesc('仅用于主题管理器中的预览切换')
.addDropdown(dropdown => {
Object.keys(PLATFORM_PRESETS).forEach(platformKey => {
dropdown.addOption(platformKey, PLATFORM_PRESETS[platformKey].name);
});
dropdown.setValue(USER_CONFIG.themePreviewPlatform || 'wechat');
dropdown.onChange(async value => {
USER_CONFIG.themePreviewPlatform = value;
await this.plugin.saveUserConfig();
this.display();
});
});
const previewTheme = getPlatformThemeConfig(USER_CONFIG.themePreviewPlatform || 'wechat');
const statusGrid = previewSection.createDiv({ cls: 'bulk-copy-settings-grid' });
[
['标题分隔线', previewTheme.headingDivider ? '开启' : '关闭'],
['表头底色', previewTheme.tableHeaderFill ? '开启' : '关闭'],
['图片圆角', previewTheme.imageRounded ? '开启' : '关闭'],
['代码块边框', previewTheme.codeBlockBorder ? '开启' : '关闭']
].forEach(([label, value]) => {
const stat = statusGrid.createDiv({ cls: 'bulk-copy-settings-stat' });
stat.createDiv({ text: label, cls: 'bulk-copy-settings-stat-label' });
stat.createDiv({ text: value, cls: 'bulk-copy-settings-stat-value' });
});
const compareGrid = previewSection.createDiv({ cls: 'bulk-copy-preview-compare-grid' });
const currentCard = compareGrid.createDiv({ cls: 'bulk-copy-preview-compare-card' });
currentCard.createEl('h4', { text: '当前主题' });
currentCard.createEl('p', { text: customTheme.name || '我的主题', cls: 'bulk-copy-preview-compare-note' });
this.renderThemePreview(currentCard, USER_CONFIG.themePreviewPlatform || 'wechat');
const recommendedKey = getRecommendedThemeKeys(USER_CONFIG.themePreviewPlatform || 'wechat')[0];
if (recommendedKey) {
const recommendedTheme = getAllThemeOptions()[recommendedKey];
const diffs = getThemeDetailDiffs(customTheme, recommendedTheme);
const colorDiffs = getThemeColorDiffs(customTheme, recommendedTheme);
if (diffs.length > 0) {
const diffSection = previewSection.createDiv({ cls: 'bulk-copy-settings-section' });
diffSection.createEl('h3', { text: '差异提示', cls: 'bulk-copy-settings-section-title' });
diffs.forEach(text => diffSection.createEl('p', { text, cls: 'bulk-copy-subtle-note' }));
}
if (colorDiffs.length > 0) {
const colorDiffSection = previewSection.createDiv({ cls: 'bulk-copy-settings-section' });
colorDiffSection.createEl('h3', { text: '颜色差异', cls: 'bulk-copy-settings-section-title' });
colorDiffs.forEach(text => colorDiffSection.createEl('p', { text, cls: 'bulk-copy-subtle-note' }));
}
const compareSection = compareGrid.createDiv({ cls: 'bulk-copy-preview-compare-card' });
compareSection.createEl('h4', { text: '推荐主题' });
compareSection.createEl('p', { text: recommendedTheme?.name || recommendedKey, cls: 'bulk-copy-preview-compare-note' });
const actionRow = compareSection.createDiv();
actionRow.style.marginBottom = '10px';
const applyButton = actionRow.createEl('button', { text: '应用到当前预览平台' });
applyButton.addEventListener('click', async () => {
const previewPlatform = USER_CONFIG.themePreviewPlatform || 'wechat';
USER_CONFIG.lastAppliedRecommendedTheme = {
platformKey: previewPlatform,
previousThemeKey: USER_CONFIG.platformThemes?.[previewPlatform],
previousOverrides: USER_CONFIG.platformThemeOverrides?.[previewPlatform] || {}
};
USER_CONFIG.platformThemes = {
...DEFAULT_USER_CONFIG.platformThemes,
...(USER_CONFIG.platformThemes || {}),
[previewPlatform]: recommendedKey
};
USER_CONFIG.platformThemeOverrides = {
...DEFAULT_USER_CONFIG.platformThemeOverrides,
...(USER_CONFIG.platformThemeOverrides || {}),
[previewPlatform]: {}
};
await this.plugin.saveUserConfig();
new Notice(`✅ 已将推荐主题应用到 ${PLATFORM_PRESETS[previewPlatform]?.name || previewPlatform}`);
this.display();
});
if (USER_CONFIG.lastAppliedRecommendedTheme && USER_CONFIG.lastAppliedRecommendedTheme.platformKey === (USER_CONFIG.themePreviewPlatform || 'wechat')) {
const undoButton = actionRow.createEl('button', { text: '撤销刚才应用' });
undoButton.style.marginLeft = '8px';
undoButton.addEventListener('click', async () => {
const snapshot = USER_CONFIG.lastAppliedRecommendedTheme;
USER_CONFIG.platformThemes = {
...DEFAULT_USER_CONFIG.platformThemes,
...(USER_CONFIG.platformThemes || {}),
[snapshot.platformKey]: snapshot.previousThemeKey || DEFAULT_USER_CONFIG.platformThemes[snapshot.platformKey]
};
USER_CONFIG.platformThemeOverrides = {
...DEFAULT_USER_CONFIG.platformThemeOverrides,
...(USER_CONFIG.platformThemeOverrides || {}),
[snapshot.platformKey]: snapshot.previousOverrides || {}
};
USER_CONFIG.lastAppliedRecommendedTheme = null;
await this.plugin.saveUserConfig();
new Notice(`✅ 已撤销 ${PLATFORM_PRESETS[snapshot.platformKey]?.name || snapshot.platformKey} 的推荐主题应用`);
this.display();
});
}
const previewThemeKeyBackup = USER_CONFIG.platformThemes?.[USER_CONFIG.themePreviewPlatform || 'wechat'];
USER_CONFIG.platformThemes = {
...USER_CONFIG.platformThemes,
[USER_CONFIG.themePreviewPlatform || 'wechat']: recommendedKey
};
this.renderThemePreview(compareSection, USER_CONFIG.themePreviewPlatform || 'wechat');
USER_CONFIG.platformThemes = {
...USER_CONFIG.platformThemes,
[USER_CONFIG.themePreviewPlatform || 'wechat']: previewThemeKeyBackup
};
}
return;
}
if (visibleTabs.includes('details')) {
const detailSection = containerEl.createDiv({ cls: 'bulk-copy-settings-section' });
detailSection.createEl('h3', { text: '主题细节', cls: 'bulk-copy-settings-section-title' });
detailSection.createEl('p', { text: '这些开关属于当前自定义主题本身,会影响不同平台绑定该主题后的基础表现。', cls: 'bulk-copy-subtle-note' });
new Setting(detailSection)
.setName('标题分隔线')
.setDesc('控制 H1/H2 是否显示下边线')
.addToggle(toggle => {
toggle.setValue(!!customTheme.headingDivider);
toggle.onChange(async value => {
await updateCustomThemeField('headingDivider', value);
this.display();
});
});
new Setting(detailSection)
.setName('表头底色')
.setDesc('控制表格表头是否显示底色')
.addToggle(toggle => {
toggle.setValue(!!customTheme.tableHeaderFill);
toggle.onChange(async value => {
await updateCustomThemeField('tableHeaderFill', value);
this.display();
});
});
new Setting(detailSection)
.setName('图片圆角')
.setDesc('控制图片是否保留圆角')
.addToggle(toggle => {
toggle.setValue(!!customTheme.imageRounded);
toggle.onChange(async value => {
await updateCustomThemeField('imageRounded', value);
this.display();
});
});
new Setting(detailSection)
.setName('代码块边框')
.setDesc('控制代码块是否显示边框')
.addToggle(toggle => {
toggle.setValue(!!customTheme.codeBlockBorder);
toggle.onChange(async value => {
await updateCustomThemeField('codeBlockBorder', value);
this.display();
});
});
return;
}
const searchSection = containerEl.createDiv({ cls: 'bulk-copy-settings-section' });
searchSection.createEl('h3', { text: '搜索与切换', cls: 'bulk-copy-settings-section-title' });
if (visibleTabs.includes('manage')) {
new Setting(searchSection)
.setName('主题搜索')
.setDesc('按名称快速筛选自定义主题模板')
.addText(text => {
text.setPlaceholder('输入主题名称');
text.setValue(String(USER_CONFIG.customThemeSearchQuery || ''));
text.onChange(async value => {
USER_CONFIG.customThemeSearchQuery = value;
await this.plugin.saveUserConfig();
this.display();
});
});
}
const updateCustomThemeField = async (key, value) => {
USER_CONFIG.customThemes = {
...DEFAULT_USER_CONFIG.customThemes,
...(USER_CONFIG.customThemes || {}),
[editorKey]: {
...DEFAULT_USER_CONFIG.customThemes.custom,
...(USER_CONFIG.customThemes?.[editorKey] || {}),
[key]: value
}
};
await this.plugin.saveUserConfig();
};
if (visibleTabs.includes('manage')) {
new Setting(searchSection)
.setName('当前编辑主题')
.setDesc('选择一个自定义主题进行编辑')
.addDropdown(dropdown => {
filteredThemeKeys.forEach(themeKey => {
dropdown.addOption(themeKey, customThemes[themeKey].name || themeKey);
});
dropdown.setValue(filteredThemeKeys.includes(editorKey) ? editorKey : (filteredThemeKeys[0] || editorKey));
dropdown.onChange(async value => {
USER_CONFIG.customThemeEditorKey = value;
await this.plugin.saveUserConfig();
this.display();
});
});
}
const manageSection = containerEl.createDiv({ cls: 'bulk-copy-settings-section' });
manageSection.createEl('h3', { text: '模板管理', cls: 'bulk-copy-settings-section-title' });
if (visibleTabs.includes('manage')) {
new Setting(manageSection)
.setName('主题模板管理')
.setDesc('新增、复制、排序、导出或导入自定义主题模板')
.addButton(button => {
button.setButtonText('新增主题');
button.onClick(async () => {
const newKey = `custom-${Date.now()}`;
USER_CONFIG.customThemes = {
...DEFAULT_USER_CONFIG.customThemes,
...(USER_CONFIG.customThemes || {}),
[newKey]: {
...DEFAULT_USER_CONFIG.customThemes.custom,
name: `我的主题 ${Object.keys(USER_CONFIG.customThemes || {}).length + 1}`
}
};
USER_CONFIG.customThemeOrder = [...getOrderedCustomThemeKeys(), newKey];
USER_CONFIG.customThemeEditorKey = newKey;
await this.plugin.saveUserConfig();
this.display();
});
})
.addButton(button => {
button.setButtonText('复制当前主题');
button.onClick(async () => {
const newKey = `custom-${Date.now()}`;
USER_CONFIG.customThemes = {
...DEFAULT_USER_CONFIG.customThemes,
...(USER_CONFIG.customThemes || {}),
[newKey]: {
...customTheme,
name: `${customTheme.name || '我的主题'} 副本`
}
};
USER_CONFIG.customThemeOrder = [...getOrderedCustomThemeKeys(), newKey];
USER_CONFIG.customThemeEditorKey = newKey;
await this.plugin.saveUserConfig();
this.display();
});
})
.addButton(button => {
button.setButtonText('上移');
button.onClick(async () => {
const order = [...getOrderedCustomThemeKeys()];
const index = order.indexOf(editorKey);
if (index > 0) {
[order[index - 1], order[index]] = [order[index], order[index - 1]];
USER_CONFIG.customThemeOrder = order;
await this.plugin.saveUserConfig();
this.display();
}
});
})
.addButton(button => {
button.setButtonText('下移');
button.onClick(async () => {
const order = [...getOrderedCustomThemeKeys()];
const index = order.indexOf(editorKey);
if (index >= 0 && index < order.length - 1) {
[order[index + 1], order[index]] = [order[index], order[index + 1]];
USER_CONFIG.customThemeOrder = order;
await this.plugin.saveUserConfig();
this.display();
}
});
})
.addButton(button => {
button.setButtonText('导出主题');
button.onClick(() => {
const exportPayload = JSON.stringify({ key: editorKey, theme: customTheme }, null, 2);
new ThemeJsonModal(this.app, {
title: '导出当前主题',
description: '复制下面的 JSON可在其他环境中导入该主题。',
initialValue: exportPayload,
readOnly: true
}).open();
});
})
.addButton(button => {
button.setButtonText('导出全部');
button.onClick(() => {
const exportPayload = JSON.stringify({
customThemes: USER_CONFIG.customThemes || DEFAULT_USER_CONFIG.customThemes,
customThemeOrder: getOrderedCustomThemeKeys(),
customThemeEditorKey: getEditableCustomThemeKey()
}, null, 2);
new ThemeJsonModal(this.app, {
title: '导出全部自定义主题',
description: '复制下面的 JSON可完整备份当前的自定义主题库。',
initialValue: exportPayload,
readOnly: true
}).open();
});
})
.addButton(button => {
button.setButtonText('导入主题');
button.onClick(() => {
new ThemeJsonModal(this.app, {
title: '导入当前主题',
description: '粘贴单个主题 JSON 后确认导入。',
initialValue: '',
readOnly: false,
onConfirm: async (raw) => {
try {
const parsed = JSON.parse(raw);
const validationError = validateSingleThemeImport(parsed);
if (validationError) {
new Notice(`❌ 主题导入失败:${validationError}`);
return;
}
const importedKey = parsed.key || `custom-${Date.now()}`;
const importedTheme = {
...DEFAULT_USER_CONFIG.customThemes.custom,
...(parsed.theme || {})
};
USER_CONFIG.customThemes = {
...DEFAULT_USER_CONFIG.customThemes,
...(USER_CONFIG.customThemes || {}),
[importedKey]: importedTheme
};
USER_CONFIG.customThemeOrder = [...getOrderedCustomThemeKeys().filter(key => key !== importedKey), importedKey];
USER_CONFIG.customThemeEditorKey = importedKey;
await this.plugin.saveUserConfig();
this.display();
new Notice('✅ 主题导入成功');
} catch (error) {
new Notice('❌ 主题导入失败JSON 格式无效');
}
}
}).open();
});
})
.addButton(button => {
button.setButtonText('导入全部');
button.onClick(() => {
new ThemeJsonModal(this.app, {
title: '导入全部自定义主题',
description: '粘贴完整主题集合 JSON 后确认导入。',
initialValue: '',
readOnly: false,
onConfirm: async (raw) => {
try {
const parsed = JSON.parse(raw);
const validationError = validateThemeCollectionImport(parsed);
if (validationError) {
new Notice(`❌ 全部主题导入失败:${validationError}`);
return;
}
const importedThemes = parsed.customThemes || {};
const importedOrder = Array.isArray(parsed.customThemeOrder) ? parsed.customThemeOrder : Object.keys(importedThemes);
const importedEditorKey = parsed.customThemeEditorKey || importedOrder[0] || 'custom';
USER_CONFIG.customThemes = Object.keys(importedThemes).length > 0
? importedThemes
: { ...DEFAULT_USER_CONFIG.customThemes };
USER_CONFIG.customThemeOrder = importedOrder.length > 0
? importedOrder
: [...DEFAULT_USER_CONFIG.customThemeOrder];
USER_CONFIG.customThemeEditorKey = importedEditorKey;
await this.plugin.saveUserConfig();
this.display();
new Notice('✅ 全部自定义主题导入成功');
} catch (error) {
new Notice('❌ 全部主题导入失败JSON 格式无效');
}
}
}).open();
});
})
.addButton(button => {
button.setButtonText('删除当前主题');
button.setWarning();
button.setDisabled(editorKey === 'custom' && Object.keys(customThemes).length <= 1);
button.onClick(async () => {
const themeEntries = { ...(USER_CONFIG.customThemes || {}) };
delete themeEntries[editorKey];
const boundPlatforms = Object.keys(USER_CONFIG.platformThemes || {}).filter(platformKey => USER_CONFIG.platformThemes[platformKey] === editorKey);
if (boundPlatforms.length > 0) {
const confirmed = window.confirm(`当前主题正被以下平台使用:${boundPlatforms.map(platformKey => PLATFORM_PRESETS[platformKey]?.name || platformKey).join(' / ')}。删除后这些平台将恢复到默认推荐主题,是否继续?`);
if (!confirmed) {
return;
}
}
Object.keys(USER_CONFIG.platformThemes || {}).forEach(platformKey => {
if (USER_CONFIG.platformThemes[platformKey] === editorKey) {
USER_CONFIG.platformThemes[platformKey] = DEFAULT_USER_CONFIG.platformThemes[platformKey] || 'official';
}
});
USER_CONFIG.customThemes = Object.keys(themeEntries).length > 0
? themeEntries
: { ...DEFAULT_USER_CONFIG.customThemes };
USER_CONFIG.customThemeOrder = getOrderedCustomThemeKeys().filter(key => key !== editorKey);
if (USER_CONFIG.customThemeOrder.length === 0) {
USER_CONFIG.customThemeOrder = [...DEFAULT_USER_CONFIG.customThemeOrder];
}
USER_CONFIG.customThemeEditorKey = Object.keys(USER_CONFIG.customThemes)[0];
await this.plugin.saveUserConfig();
this.display();
});
});
}
const isHexColor = (value) => /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(String(value || '').trim());
const fontPresets = {
system: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif",
pingfang: "'PingFang SC', 'Microsoft YaHei', sans-serif",
serif: "'Noto Serif SC', Georgia, serif",
inter: "Inter, 'PingFang SC', 'Segoe UI', sans-serif"
};
const basicSection = containerEl.createDiv({ cls: 'bulk-copy-settings-section' });
basicSection.createEl('h3', { text: '基础样式', cls: 'bulk-copy-settings-section-title' });
if (visibleTabs.includes('basic')) {
new Setting(basicSection)
.setName('主题名称')
.setDesc('显示在平台主题下拉中的名称')
.addText(text => {
text.setValue(String(customTheme.name || '我的主题'));
text.onChange(async value => {
await updateCustomThemeField('name', value || '我的主题');
});
});
new Setting(basicSection)
.setName('字体预设')
.setDesc('选择常用字体组合,可继续在下方自定义字体文本')
.addDropdown(dropdown => {
dropdown.addOption('system', '系统无衬线');
dropdown.addOption('pingfang', '苹方/雅黑');
dropdown.addOption('serif', '中文衬线');
dropdown.addOption('inter', 'Inter 文档风');
const matchedKey = Object.keys(fontPresets).find(key => fontPresets[key] === customTheme.fontFamily) || 'pingfang';
dropdown.setValue(matchedKey);
dropdown.onChange(async value => {
await updateCustomThemeField('fontFamily', fontPresets[value] || fontPresets.pingfang);
this.display();
});
});
new Setting(basicSection)
.setName('字体族')
.setDesc('支持直接输入完整 font-family 字符串')
.addText(text => {
text.setValue(String(customTheme.fontFamily || ''));
text.onChange(async value => {
await updateCustomThemeField('fontFamily', value);
});
});
new Setting(basicSection)
.setName('圆角')
.setDesc('例如 0px、6px、8px、12px')
.addDropdown(dropdown => {
['0px', '4px', '6px', '8px', '10px', '12px', '16px'].forEach(value => {
dropdown.addOption(value, value);
});
dropdown.setValue(String(customTheme.borderRadius || '8px'));
dropdown.onChange(async value => {
await updateCustomThemeField('borderRadius', value);
});
})
.addText(text => {
text.setPlaceholder('8px');
text.setValue(String(customTheme.borderRadius || ''));
text.onChange(async value => {
await updateCustomThemeField('borderRadius', value);
});
});
new Setting(basicSection)
.setName('行高')
.setDesc('推荐 1.6 - 1.9')
.addDropdown(dropdown => {
['1.6', '1.7', '1.75', '1.8', '1.85', '1.9'].forEach(value => {
dropdown.addOption(value, value);
});
dropdown.setValue(String(customTheme.lineHeight || '1.75'));
dropdown.onChange(async value => {
await updateCustomThemeField('lineHeight', value);
});
})
.addText(text => {
text.setPlaceholder('1.75');
text.setValue(String(customTheme.lineHeight || ''));
text.onChange(async value => {
await updateCustomThemeField('lineHeight', value);
});
});
}
const colorSection = containerEl.createDiv({ cls: 'bulk-copy-settings-section' });
colorSection.createEl('h3', { text: '颜色设置', cls: 'bulk-copy-settings-section-title' });
colorSection.createEl('p', { text: '支持点击色块选择颜色,也可以直接输入十六进制色值。', cls: 'bulk-copy-subtle-note' });
if (visibleTabs.includes('color')) {
[
['textColor', '正文颜色'],
['headingColor', '标题颜色'],
['backgroundColor', '背景颜色'],
['linkColor', '链接颜色'],
['blockquoteBackground', '引用背景'],
['codeBackground', '代码背景'],
['borderColor', '边框颜色']
].forEach(([key, label]) => {
new Setting(colorSection)
.setName(label)
.setDesc(`自定义主题的 ${label}`)
.addColorPicker(color => {
color.setValue(isHexColor(customTheme[key]) ? customTheme[key] : '#000000');
color.onChange(async value => {
await updateCustomThemeField(key, value);
});
})
.addText(text => {
text.setPlaceholder('#000000');
text.setValue(String(customTheme[key] || ''));
text.onChange(async value => {
if (!value || isHexColor(value)) {
await updateCustomThemeField(key, value);
}
});
});
});
}
}
createPlatformSection(containerEl, platformKey) {
const section = containerEl.createEl('details', { cls: 'bulk-copy-platform-section' });
if (platformKey === USER_CONFIG.currentPlatform) {
section.open = true;
}
const summary = section.createEl('summary');
summary.textContent = `${PLATFORM_PRESETS[platformKey].name} 配置`;
new Setting(section)
.setName('图片导出策略')
.setDesc('设置该平台复制时使用的图片导出策略')
.addDropdown(dropdown => {
dropdown.addOption('quality', '高清优先');
dropdown.addOption('balanced', '平衡');
dropdown.addOption('size', '体积优先');
dropdown.setValue(getCurrentImageStrategy(platformKey));
dropdown.onChange(async value => {
USER_CONFIG.platformImageStrategies = {
...DEFAULT_USER_CONFIG.platformImageStrategies,
...(USER_CONFIG.platformImageStrategies || {}),
[platformKey]: value
};
await this.plugin.saveUserConfig();
});
});
new Setting(section)
.setName('平台主题')
.setDesc('设置该平台导出时使用的主题样式')
.addDropdown(dropdown => {
this.addThemeOptions(dropdown, platformKey);
dropdown.setValue(getCurrentPlatformTheme(platformKey));
dropdown.onChange(async value => {
USER_CONFIG.platformThemes = {
...DEFAULT_USER_CONFIG.platformThemes,
...(USER_CONFIG.platformThemes || {}),
[platformKey]: value
};
await this.plugin.saveUserConfig();
this.display();
});
});
new Setting(section)
.setName('恢复推荐主题')
.setDesc(`一键恢复到 ${getRecommendedThemeLabels(platformKey).join(' / ') || '平台推荐主题'},并清空该平台的主题细节覆盖`)
.addButton(button => {
button.setButtonText('恢复推荐');
button.onClick(async () => {
resetPlatformToRecommendedTheme(platformKey);
await this.plugin.saveUserConfig();
this.display();
});
});
this.renderThemeDetailSettings(section, platformKey, '主题细节', '单独调整该平台导出时的标题、表格、图片和代码块表现。');
this.renderThemePreview(section, platformKey);
}
display() {
const { containerEl } = this;
containerEl.empty();
containerEl.addClass('bulk-copy-settings-root');
this.renderSettingsStyles(containerEl);
const hero = containerEl.createDiv({ cls: 'bulk-copy-settings-hero' });
hero.createEl('h2', { text: 'Bulk Copy with Images 设置' });
hero.createEl('p', { text: '按平台管理复制主题、图片策略和导出细节。推荐先确定平台,再调整主题与局部风格。' });
const stats = hero.createDiv({ cls: 'bulk-copy-settings-grid' });
[
['当前默认平台', PLATFORM_PRESETS[USER_CONFIG.currentPlatform]?.name || USER_CONFIG.currentPlatform],
['当前主题', getPlatformThemeLabel(USER_CONFIG.currentPlatform)],
['图片策略', getImageStrategyLabel(USER_CONFIG.currentPlatform)]
].forEach(([label, value]) => {
const stat = stats.createDiv({ cls: 'bulk-copy-settings-stat' });
stat.createDiv({ text: label, cls: 'bulk-copy-settings-stat-label' });
stat.createDiv({ text: value, cls: 'bulk-copy-settings-stat-value' });
});
const basicSection = this.createSection(containerEl, '基础配置', '设置默认平台和基础图片参数。');
new Setting(basicSection)
.setName('默认平台')
.setDesc('快速复制命令会直接使用这里设置的平台预设')
.addDropdown(dropdown => {
Object.keys(PLATFORM_PRESETS).forEach(key => {
dropdown.addOption(key, PLATFORM_PRESETS[key].name);
});
dropdown.setValue(USER_CONFIG.currentPlatform);
dropdown.onChange(async value => {
applyPlatformPreset(value);
await this.plugin.saveUserConfig();
this.display();
});
});
new Setting(basicSection)
.setName('图片最大宽度')
.setDesc('超过此宽度的图片会被等比压缩')
.addText(text => {
text.setPlaceholder('1000');
text.setValue(String(USER_CONFIG.imageMaxWidth));
text.onChange(async value => {
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed <= 0) {
return;
}
USER_CONFIG.imageMaxWidth = parsed;
syncImageConfigFromUserConfig();
await this.plugin.saveUserConfig();
});
});
new Setting(basicSection)
.setName('图片压缩质量')
.setDesc('范围 0.1 - 1.0,值越高越清晰,体积也越大')
.addText(text => {
text.setPlaceholder('0.92');
text.setValue(String(USER_CONFIG.imageQuality));
text.onChange(async value => {
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed <= 0 || parsed > 1) {
return;
}
USER_CONFIG.imageQuality = parsed;
syncImageConfigFromUserConfig();
await this.plugin.saveUserConfig();
});
});
const currentPlatformSection = this.createSection(containerEl, '当前平台快捷配置', '快速调整当前默认平台的主题和图片策略。');
new Setting(currentPlatformSection)
.setName('图片导出策略')
.setDesc('为当前默认平台设置图片导出策略')
.addDropdown(dropdown => {
dropdown.addOption('quality', '高清优先');
dropdown.addOption('balanced', '平衡');
dropdown.addOption('size', '体积优先');
dropdown.setValue(getCurrentImageStrategy(USER_CONFIG.currentPlatform));
dropdown.onChange(async value => {
USER_CONFIG.platformImageStrategies = {
...DEFAULT_USER_CONFIG.platformImageStrategies,
...(USER_CONFIG.platformImageStrategies || {}),
[USER_CONFIG.currentPlatform]: value
};
await this.plugin.saveUserConfig();
this.display();
});
});
new Setting(currentPlatformSection)
.setName('平台主题')
.setDesc('为当前默认平台设置导出主题样式')
.addDropdown(dropdown => {
this.addThemeOptions(dropdown, USER_CONFIG.currentPlatform);
dropdown.setValue(getCurrentPlatformTheme(USER_CONFIG.currentPlatform));
dropdown.onChange(async value => {
USER_CONFIG.platformThemes = {
...DEFAULT_USER_CONFIG.platformThemes,
...(USER_CONFIG.platformThemes || {}),
[USER_CONFIG.currentPlatform]: value
};
await this.plugin.saveUserConfig();
this.display();
});
});
this.renderThemeDetailSettings(
currentPlatformSection,
USER_CONFIG.currentPlatform,
'当前平台主题细节',
'这里的细节设置会应用到当前默认平台。'
);
const customThemeSection = this.createSection(containerEl, '自定义主题', '自定义主题模板已迁移到专门的管理器中,便于集中编辑和维护。');
new Setting(customThemeSection)
.setName('主题模板管理器')
.setDesc('打开独立管理器,集中处理搜索、排序、导入导出和主题编辑')
.addButton(button => {
button.setButtonText('打开管理器');
button.setCta();
button.onClick(() => {
new ThemeManagerModal(this.app, this.plugin, () => this.display()).open();
});
});
const allPlatformsSection = this.createSection(containerEl, '各平台配置', '每个平台都可以独立设置图片策略、主题、细节参数,并查看预览。');
Object.keys(PLATFORM_PRESETS).forEach(platformKey => {
this.createPlatformSection(allPlatformsSection, platformKey);
});
const behaviorSection = this.createSection(containerEl, '行为偏好', '控制复制时的辅助行为与提示。');
new Setting(behaviorSection)
.setName('启用主题样式保留')
.setDesc('导出时尽量保留当前 Obsidian 主题的视觉效果')
.addToggle(toggle => {
toggle.setValue(!!USER_CONFIG.enableThemeStyles);
toggle.onChange(async value => {
USER_CONFIG.enableThemeStyles = value;
await this.plugin.saveUserConfig();
});
});
new Setting(behaviorSection)
.setName('显示进度提示')
.setDesc('处理多张图片时显示通知提示')
.addToggle(toggle => {
toggle.setValue(!!USER_CONFIG.enableProgressNotice);
toggle.onChange(async value => {
USER_CONFIG.enableProgressNotice = value;
await this.plugin.saveUserConfig();
});
});
}
}
/* ========== 复制内容函数 ========== */
async function copyContentWithImages(plugin, content, isFullFile) {
const startTime = performance.now();
try {
const file = plugin.app.workspace.getActiveFile();
if (!file) {
new Notice('❌ 请先打开一篇 Markdown 笔记');
return;
}
console.log('[bulk-copy] 🚀 开始复制内容...');
const exportResult = await exportContentForClipboard(plugin, {
mode: isFullFile ? 'full' : 'selection',
selectedText: content || '',
platform: USER_CONFIG.currentPlatform,
includeImages: true,
preserveTheme: USER_CONFIG.enableThemeStyles
});
// 写入剪贴板
try {
const payload = buildClipboardPayload(exportResult);
if (Object.keys(payload).length === 0) {
throw new Error('没有可用的剪贴板格式');
}
await writeClipboardPayload(payload, exportResult);
console.log('[bulk-copy] ✅ 复制成功', exportResult.meta);
let sourceLabel = 'Markdown 兜底';
if (exportResult.meta && exportResult.meta.source === 'preview-dom') {
sourceLabel = '预览 DOM';
} else if (exportResult.meta && exportResult.meta.source === 'offscreen-render') {
sourceLabel = '离屏渲染';
}
const strategyLabel = getImageStrategyLabel(USER_CONFIG.currentPlatform);
const themeLabel = getPlatformThemeLabel(USER_CONFIG.currentPlatform);
const imageCount = exportResult.meta?.imageCount ?? 0;
new Notice(`✅ 已复制内容(来源: ${sourceLabel},主题: ${themeLabel},图片策略: ${strategyLabel},图片数: ${imageCount}`);
if (exportResult.meta && exportResult.meta.warnings && exportResult.meta.warnings.length > 0) {
exportResult.meta.warnings.forEach(warning => new Notice(`⚠️ ${warning}`));
}
} catch (err) {
console.error('[bulk-copy] 复制失败:', err);
new Notice('❌ 复制失败: ' + err.message);
}
const endTime = performance.now();
const duration = ((endTime - startTime) / 1000).toFixed(2);
console.log(`[bulk-copy] ✅ 复制完成,耗时: ${duration}`);
} catch (err) {
console.error('[bulk-copy] 复制过程发生错误:', err);
new Notice('❌ 复制失败: ' + err.message);
}
}
/* ========== 快速复制 ========== */
async function quickCopyActiveFileWithImages(plugin) {
const file = plugin.app.workspace.getActiveFile();
if (!file) {
new Notice('❌ 请先打开一篇 Markdown 笔记');
return;
}
await copyContentWithImages(plugin, null, true);
}
/* ========== 复制全文(带平台选择) ========== */
async function copyActiveFileWithImages(plugin) {
const file = plugin.app.workspace.getActiveFile();
if (!file) {
new Notice('❌ 请先打开一篇 Markdown 笔记');
return;
}
new PlatformSelectorModal(plugin.app, async (selectedPlatform) => {
applyPlatformPreset(selectedPlatform);
await copyContentWithImages(plugin, null, true);
}).open();
}
/* ========== 复制选定内容(带平台选择) ========== */
async function copySelectedContentWithImages(plugin) {
const file = plugin.app.workspace.getActiveFile();
if (!file) {
new Notice('❌ 请先打开一篇 Markdown 笔记');
return;
}
const activeEditor = plugin.app.workspace.activeEditor;
if (!activeEditor || !activeEditor.editor) {
new Notice('❌ 无法获取编辑器实例');
return;
}
const editor = activeEditor.editor;
const selectedText = editor.getSelection();
if (!selectedText || selectedText.trim() === '') {
new Notice('❌ 请先选择要复制的文本内容');
return;
}
new PlatformSelectorModal(plugin.app, async (selectedPlatform) => {
applyPlatformPreset(selectedPlatform);
await copyContentWithImages(plugin, selectedText, false);
}).open();
}
/* ========== 快速复制选定内容 ========== */
async function quickCopySelectedContentWithImages(plugin) {
const file = plugin.app.workspace.getActiveFile();
if (!file) {
new Notice('❌ 请先打开一篇 Markdown 笔记');
return;
}
const activeEditor = plugin.app.workspace.activeEditor;
if (!activeEditor || !activeEditor.editor) {
new Notice('❌ 无法获取编辑器实例');
return;
}
const editor = activeEditor.editor;
const selectedText = editor.getSelection();
if (!selectedText || selectedText.trim() === '') {
new Notice('❌ 请先选择要复制的文本内容');
return;
}
await copyContentWithImages(plugin, selectedText, false);
}
/* ========== 插件入口 ========== */
module.exports = class BulkCopyImagesPlugin extends Plugin {
async onload() {
console.log('[bulk-copy] Bulk Copy with Images 插件已加载');
await this.loadUserConfig();
new Notice('✅ Bulk Copy with Images 插件已加载');
// 主命令:复制全文(带平台选择)
this.addCommand({
id: 'bulk-copy-with-images',
name: '复制全文(选择平台)',
hotkeys: [{ modifiers: ['Ctrl', 'Shift'], key: 'c' }],
callback: () => copyActiveFileWithImages(this)
});
// 主命令:复制选定内容(带平台选择)
this.addCommand({
id: 'bulk-copy-selected-with-images',
name: '复制选定内容(选择平台)',
hotkeys: [{ modifiers: ['Ctrl', 'Shift'], key: 'v' }],
callback: () => copySelectedContentWithImages(this)
});
// 快速命令:复制全文(使用默认平台)
this.addCommand({
id: 'quick-copy-with-images',
name: '快速复制全文(使用默认平台)',
hotkeys: [{ modifiers: ['Ctrl', 'Alt'], key: 'c' }],
callback: () => quickCopyActiveFileWithImages(this)
});
// 快速命令:复制选定内容(使用默认平台)
this.addCommand({
id: 'quick-copy-selected-with-images',
name: '快速复制选定内容(使用默认平台)',
hotkeys: [{ modifiers: ['Ctrl', 'Alt'], key: 'v' }],
callback: () => quickCopySelectedContentWithImages(this)
});
// 平台选择命令
Object.keys(PLATFORM_PRESETS).forEach(platformKey => {
const preset = PLATFORM_PRESETS[platformKey];
this.addCommand({
id: `switch-platform-${platformKey}`,
name: `切换默认平台: ${preset.name}`,
callback: async () => {
applyPlatformPreset(platformKey);
await this.saveUserConfig();
}
});
});
this.settingTab = new BulkCopySettingTab(this.app, this);
this.addSettingTab(this.settingTab);
// 监听主题切换
this.registerEvent(
this.app.workspace.on('css-change', () => {
Cache.clear();
console.log('[bulk-copy] 主题已切换,清除样式缓存');
})
);
}
async loadUserConfig() {
const savedConfig = await this.loadData();
USER_CONFIG = {
...DEFAULT_USER_CONFIG,
...(savedConfig || {})
};
USER_CONFIG.platformImageStrategies = {
...DEFAULT_USER_CONFIG.platformImageStrategies,
...(savedConfig?.platformImageStrategies || {})
};
USER_CONFIG.platformThemes = {
...DEFAULT_USER_CONFIG.platformThemes,
...(savedConfig?.platformThemes || {})
};
USER_CONFIG.customThemes = {
...DEFAULT_USER_CONFIG.customThemes,
...(savedConfig?.customThemes || {})
};
USER_CONFIG.customThemeEditorKey = savedConfig?.customThemeEditorKey || DEFAULT_USER_CONFIG.customThemeEditorKey;
USER_CONFIG.customThemeSearchQuery = savedConfig?.customThemeSearchQuery || DEFAULT_USER_CONFIG.customThemeSearchQuery;
USER_CONFIG.customThemeOrder = savedConfig?.customThemeOrder || DEFAULT_USER_CONFIG.customThemeOrder;
USER_CONFIG.themePreviewPlatform = savedConfig?.themePreviewPlatform || DEFAULT_USER_CONFIG.themePreviewPlatform;
USER_CONFIG.themeListShowUsedOnly = typeof savedConfig?.themeListShowUsedOnly === 'boolean' ? savedConfig.themeListShowUsedOnly : DEFAULT_USER_CONFIG.themeListShowUsedOnly;
USER_CONFIG.themeListUsedFirst = typeof savedConfig?.themeListUsedFirst === 'boolean' ? savedConfig.themeListUsedFirst : DEFAULT_USER_CONFIG.themeListUsedFirst;
USER_CONFIG.themeListShowRecommendedOnly = typeof savedConfig?.themeListShowRecommendedOnly === 'boolean' ? savedConfig.themeListShowRecommendedOnly : DEFAULT_USER_CONFIG.themeListShowRecommendedOnly;
USER_CONFIG.themeManagerListCollapsed = typeof savedConfig?.themeManagerListCollapsed === 'boolean' ? savedConfig.themeManagerListCollapsed : DEFAULT_USER_CONFIG.themeManagerListCollapsed;
USER_CONFIG.lastAppliedRecommendedTheme = savedConfig?.lastAppliedRecommendedTheme || DEFAULT_USER_CONFIG.lastAppliedRecommendedTheme;
USER_CONFIG.themeManagerSidebarWidth = savedConfig?.themeManagerSidebarWidth || DEFAULT_USER_CONFIG.themeManagerSidebarWidth;
USER_CONFIG.platformThemeOverrides = {
...DEFAULT_USER_CONFIG.platformThemeOverrides,
...(savedConfig?.platformThemeOverrides || {})
};
applyCurrentPlatformConfig();
}
async saveUserConfig() {
syncImageConfigFromUserConfig();
await this.saveData(USER_CONFIG);
}
onunload() {
console.log('[bulk-copy] Bulk Copy with Images 插件已卸载');
Cache.clear();
}
};