From 77ca18122fb2e3f487388c0988e8c763844e5625 Mon Sep 17 00:00:00 2001
From: Evgeny Steps to follow
+
+ 1. Left-Click on the extension
+ 2. Click on the gear icon to open the configuration menu
+ 3. Scroll down to "Obsidian integration" section and turn "enable obsidian integration" on.
+ 4. Fill out the form below (Obsidian vault name and Obsidian folder name.)
+ 5. Right-click on the extension and open the menu
+ 6. In "MarkDownload - Markdown Web Clipper", select "Send Tab to Obsidian"
+
+
` are preserved in code blocks (thanks @mnaumov !)
-- Update user guide link in README to point to GitHub
-- Added keyboard shortcuts to copy selection / current tab to obsidian (user-definable in browsers that support that) (thanks @legolasdimir and @likeablob !)
-- Select multiple tabs (hold crtl/cmd) then copy all tab urls as a markdown link list via keyboard shortcut or context menu (thanks @romanPrignon !)
-- Allow users to include custom text such like `{date:YYYY-MM-DD}/`` in their Obsidian Folder Name setting (thanks @likeablob !)
-- Fixed a small typo in the user guide (thanks @devon-research !)
-- Fix for missing headings on sites like Substack (thanks @eactisgrosso !)
-- Add support for websites using MathJax 3 (thanks @LeLocTai !)
-- Moved previous version history into [CHANGELOG.md](./CHANGELOG.md)
+## 3.4.0
+- Fixed extra spaces in titles which could cause issues (thanks @rickdoesdev !)
+- Fixed an issue with image paths in some circumstances (thanks @rickdoesdev !)
+- Added parametersizations for "mixed-kebab" and "mixed_snake" which retain original casing but replace spaces (thanks @NSHenry !)
+ - Also added a special "obsidian-cal" parameterization which is the same as "mixed-kebab" with duplicate `-` removed for additional compatibility with the Obsidian Consistent Attachment Location plugin (thanks @NSHenry !)
+- Added lowecase and uppercase options to parameterizations (thanks @redxtech !)
+- Updated Turndown to v7.1.3 (thanks @WeHat !)
+- Updated Readability to v0.5.0 (thanks @WeHat !)
+- Fixed some issues with code block parsing and formatting (thanks @WeHat !)
+- Fixed an issue with some sites missing a proper title (thanks @WeHat !)
+- Fixed an issue with bad base urls causing issues with links in certain circumstances (thanks @WeHat !)
+- Fixed an issue with readability removing everything in certain circumstances (thanks @WeHat !)
+- Send properly configured title to the Obsidian integration (thanks @nekton39 !)
+- Updates to the README (thanks @2gn and @eugenesvk !)
> Previous version history is recorded in [CHANGELOG.md](./CHANGELOG.md)
diff --git a/src/manifest.json b/src/manifest.json
index 31b63706..19a581cc 100644
--- a/src/manifest.json
+++ b/src/manifest.json
@@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "MarkDownload - Markdown Web Clipper",
- "version": "3.3.1",
+ "version": "3.4.0",
"author": "Gordon Pedsersen",
"description": "This extension works like a web clipper, but it downloads articles in markdown format.",
"icons": {
diff --git a/xcode/MarkDownload - Markdown Web Clipper/MarkDownload - Markdown Web Clipper.xcodeproj/project.pbxproj b/xcode/MarkDownload - Markdown Web Clipper/MarkDownload - Markdown Web Clipper.xcodeproj/project.pbxproj
index 6dbeea6c..b6ee2564 100644
--- a/xcode/MarkDownload - Markdown Web Clipper/MarkDownload - Markdown Web Clipper.xcodeproj/project.pbxproj
+++ b/xcode/MarkDownload - Markdown Web Clipper/MarkDownload - Markdown Web Clipper.xcodeproj/project.pbxproj
@@ -467,7 +467,7 @@
CODE_SIGN_ENTITLEMENTS = "MarkDownload - Markdown Web Clipper/MarkDownload___Markdown_Web_Clipper.entitlements";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
- CURRENT_PROJECT_VERSION = 4;
+ CURRENT_PROJECT_VERSION = 4.0.1;
DEVELOPMENT_TEAM = 4JYKNQ5WRZ;
INFOPLIST_FILE = "MarkDownload - Markdown Web Clipper/Info.plist";
LD_RUNPATH_SEARCH_PATHS = (
@@ -475,7 +475,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
- MARKETING_VERSION = 3.2.0;
+ MARKETING_VERSION = 3.4.0;
PRODUCT_BUNDLE_IDENTIFIER = au.death.MarkDownload;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
@@ -491,7 +491,7 @@
CODE_SIGN_ENTITLEMENTS = "MarkDownload - Markdown Web Clipper/MarkDownload___Markdown_Web_Clipper.entitlements";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
- CURRENT_PROJECT_VERSION = 4;
+ CURRENT_PROJECT_VERSION = 4.0.1;
DEVELOPMENT_TEAM = 4JYKNQ5WRZ;
INFOPLIST_FILE = "MarkDownload - Markdown Web Clipper/Info.plist";
LD_RUNPATH_SEARCH_PATHS = (
@@ -499,7 +499,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
- MARKETING_VERSION = 3.2.0;
+ MARKETING_VERSION = 3.4.0;
PRODUCT_BUNDLE_IDENTIFIER = au.death.MarkDownload;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
From 1adab2a89de2e3011b209b08b04ba3860baceccc Mon Sep 17 00:00:00 2001
From: TianleLin <940428567qq@gmail.com>
Date: Wed, 10 Sep 2025 15:31:52 +0800
Subject: [PATCH 21/29] fix v3
---
UPGRADE_NOTES.md | 101 +++++
src/background/service-worker.js | 668 +++++++++++++++++++++++++++++
src/contentScript/contentScript.js | 497 ++++++++++++++++++++-
src/contentScript/pageContext.js | 26 +-
src/manifest.json | 51 ++-
5 files changed, 1311 insertions(+), 32 deletions(-)
create mode 100644 UPGRADE_NOTES.md
create mode 100644 src/background/service-worker.js
diff --git a/UPGRADE_NOTES.md b/UPGRADE_NOTES.md
new file mode 100644
index 00000000..da611208
--- /dev/null
+++ b/UPGRADE_NOTES.md
@@ -0,0 +1,101 @@
+# MarkDownload Manifest V3 升级说明
+
+## 已完成的修改
+
+### 1. Manifest.json 升级
+- 从 Manifest V2 升级到 Manifest V3
+- 更新权限:将 `` 移动到 `host_permissions`
+- 将 `browser_action` 更改为 `action`
+- 将 `background.scripts` 更改为 `background.service_worker`
+- 添加了 `scripting` 权限以支持动态脚本注入
+- 添加了 `content_scripts` 声明以自动注入内容脚本
+- **修复**: 移除了 `options_ui` 中不兼容的 `chrome_style` 和 `browser_style` 选项
+- **修复**: 将命令 `_execute_browser_action` 改为 `_execute_action`
+
+### 2. Background Script 转换为 Service Worker
+- 创建了新的 `src/background/service-worker.js` 文件
+- 将所有依赖脚本使用 `importScripts()` 导入
+- 更新了所有 Chrome API 调用以适配 Manifest V3
+- 使用 `chrome.scripting.executeScript()` 替代 `chrome.tabs.executeScript()`
+- 添加了详细的调试日志用于问题排查
+
+### 3. 内容脚本改进
+- 添加了详细的调试日志
+- 改进了DOM处理和错误处理
+- 增强了剪贴板操作的兼容性
+- 添加了更好的浏览器API兼容性检查
+
+### 4. 调试功能
+- 在所有关键函数中添加了console.log输出
+- 添加了错误处理和回退机制
+- 提供了详细的操作状态信息
+
+## 测试建议
+
+### 1. 加载扩展
+1. 打开 Chrome 浏览器
+2. 导航到 `chrome://extensions/`
+3. 启用"开发者模式"
+4. 点击"加载已解压的扩展程序"
+5. 选择 `src` 目录
+
+### 2. 测试功能
+1. **基本测试**:
+ - 右键点击页面,检查是否出现 MarkDownload 上下文菜单
+ - 点击扩展图标,检查弹出窗口是否正常显示
+
+2. **核心功能测试**:
+ - 测试"Download Tab As Markdown"
+ - 测试"Copy Tab As Markdown"
+ - 测试选中文本后的"Copy Selection As Markdown"
+
+3. **调试检查**:
+ - 打开 Chrome 开发者工具
+ - 检查 Console 标签页中的 MarkDownload 日志输出
+ - Service Worker 日志可在扩展管理页面的"Service Worker"链接中查看
+
+### 3. 常见问题排查
+
+#### 如果扩展无法加载:
+1. 检查 manifest.json 语法是否正确
+2. 确保所有引用的文件都存在
+3. 查看扩展管理页面的错误信息
+
+**已修复的常见错误:**
+- ✅ "The chrome_style option cannot be used with manifest version 3" - 已从 `options_ui` 中移除 `chrome_style` 和 `browser_style`
+- ✅ "_execute_browser_action command not recognized" - 已更改为 `_execute_action`
+
+#### 如果功能不工作:
+1. 检查 Service Worker 是否运行(在扩展管理页面)
+2. 查看浏览器控制台的错误信息
+3. 检查 MarkDownload 的调试日志
+
+#### 如果权限问题:
+1. 确保网站允许执行脚本
+2. 检查是否在受限页面(如 chrome://)上使用
+3. 验证 host_permissions 是否正确设置
+
+## 技术细节
+
+### Service Worker 变化
+- 不再是持久的背景页面,而是事件驱动的
+- 需要使用 `importScripts()` 导入依赖
+- API 调用从 `browser.*` 改为 `chrome.*`
+
+### 内容脚本注入
+- 从动态注入改为声明式注入
+- 使用 `chrome.scripting.executeScript()` 进行必要时的动态注入
+- 改进了脚本可用性检查
+
+### 权限模型
+- `` 权限移至 `host_permissions`
+- 添加了 `scripting` 权限支持动态脚本执行
+
+## 已知限制
+- 在某些受保护的页面(如 chrome:// 页面)上无法工作
+- Service Worker 在空闲时会被暂停,但会在需要时自动恢复
+
+## 版本兼容性
+- 支持 Chrome 88+ (Manifest V3 最低要求)
+- 向后兼容原有功能
+- 保持与 Firefox 的兼容性(通过 browser-polyfill)
diff --git a/src/background/service-worker.js b/src/background/service-worker.js
new file mode 100644
index 00000000..88ff5a6e
--- /dev/null
+++ b/src/background/service-worker.js
@@ -0,0 +1,668 @@
+// MarkDownload Service Worker for Manifest V3
+// Debug logging enabled
+console.log('MarkDownload Service Worker starting...');
+
+// Import all required scripts for service worker
+try {
+ importScripts(
+ '../browser-polyfill.min.js',
+ 'apache-mime-types.js',
+ 'moment.min.js',
+ 'turndown.js',
+ 'turndown-plugin-gfm.js',
+ 'Readability.js'
+ );
+ console.log('MarkDownload: All scripts imported successfully');
+} catch (e) {
+ console.error('MarkDownload: Failed to import scripts:', e);
+}
+
+// Default options (from default-options.js)
+const defaultOptions = {
+ headingStyle: "atx",
+ hr: "___",
+ bulletListMarker: "-",
+ codeBlockStyle: "fenced",
+ fence: "```",
+ emDelimiter: "_",
+ strongDelimiter: "**",
+ linkStyle: "inlined",
+ linkReferenceStyle: "full",
+ imageStyle: "markdown",
+ imageRefStyle: "inlined",
+ frontmatter: "---\ncreated: {date:YYYY-MM-DDTHH:mm:ss} (UTC {date:Z})\ntags: [{keywords}]\nsource: {baseURI}\nauthor: {byline}\n---\n\n# {pageTitle}\n\n> ## Excerpt\n> {excerpt}\n\n---",
+ backmatter: "",
+ title: "{pageTitle}",
+ includeTemplate: false,
+ saveAs: false,
+ downloadImages: false,
+ imagePrefix: '{pageTitle}/',
+ mdClipsFolder: null,
+ disallowedChars: '[]#^',
+ downloadMode: 'downloadsApi',
+ turndownEscape: true,
+ contextMenus: true,
+ obsidianIntegration: false,
+ obsidianVault: "",
+ obsidianFolder: "",
+};
+
+// Function to get options from storage
+async function getOptions() {
+ let options = defaultOptions;
+ try {
+ const result = await chrome.storage.sync.get(defaultOptions);
+ options = { ...defaultOptions, ...result };
+ } catch (err) {
+ console.error('MarkDownload: Failed to get options:', err);
+ }
+ if (!chrome.downloads) options.downloadMode = 'contentLink';
+ return options;
+}
+
+// Service Worker Event Listeners
+chrome.runtime.onStartup.addListener(() => {
+ console.log('MarkDownload: Extension startup');
+ createMenus().catch(e => console.error('MarkDownload: Failed to create menus on startup:', e));
+});
+
+chrome.runtime.onInstalled.addListener((details) => {
+ console.log('MarkDownload: Extension installed/updated:', details.reason);
+ createMenus().catch(e => console.error('MarkDownload: Failed to create menus on install:', e));
+});
+
+// Keep service worker alive
+chrome.runtime.onSuspend.addListener(() => {
+ console.log('MarkDownload: Service Worker is being suspended');
+});
+
+chrome.runtime.onSuspendCanceled.addListener(() => {
+ console.log('MarkDownload: Service Worker suspension cancelled');
+});
+
+// Log platform info for debugging
+chrome.runtime.getPlatformInfo().then(platformInfo => {
+ console.log('MarkDownload Platform Info:', platformInfo);
+
+ // Try to get browser info if available
+ if (chrome.runtime.getBrowserInfo) {
+ chrome.runtime.getBrowserInfo().then(browserInfo => {
+ console.log('MarkDownload Browser Info:', browserInfo);
+ }).catch(e => console.warn('MarkDownload: Could not get browser info:', e));
+ }
+}).catch(e => {
+ console.warn('MarkDownload: Could not get platform info:', e);
+});
+
+// Initialize TurndownService prototype
+if (typeof TurndownService !== 'undefined') {
+ TurndownService.prototype.defaultEscape = TurndownService.prototype.escape;
+ console.log('MarkDownload: TurndownService initialized');
+}
+
+// Core helper functions
+function cleanAttribute(attribute) {
+ return attribute ? attribute.replace(/(\n+\s*)+/g, '\n') : '';
+}
+
+function validateUri(href, baseURI) {
+ try {
+ new URL(href);
+ } catch {
+ const baseUri = new URL(baseURI);
+ if (href.startsWith('/')) {
+ href = baseUri.origin + href;
+ } else {
+ href = baseUri.href + (baseUri.href.endsWith('/') ? '' : '') + href;
+ }
+ }
+ return href;
+}
+
+// Helper functions moved to content script
+
+// Image processing functions moved to content script
+
+// Text replacement functions moved to content script
+
+// Polyfill for replaceAll if needed
+if (!String.prototype.replaceAll) {
+ String.prototype.replaceAll = function(str, newStr) {
+ if (Object.prototype.toString.call(str).toLowerCase() === '[object regexp]') {
+ return this.replace(str, newStr);
+ }
+ return this.replace(new RegExp(str, 'g'), newStr);
+ };
+}
+
+// Context menu creation function
+async function createMenus() {
+ console.log('MarkDownload: Creating context menus');
+ const options = await getOptions();
+
+ try {
+ await chrome.contextMenus.removeAll();
+ } catch (e) {
+ console.warn('MarkDownload: Failed to remove existing menus:', e);
+ }
+
+ if (options.contextMenus) {
+ const menuItems = [
+ {
+ id: "download-markdown-alltabs",
+ title: "Download All Tabs as Markdown",
+ contexts: ["all"]
+ },
+ {
+ id: "separator-0",
+ type: "separator",
+ contexts: ["all"]
+ },
+ {
+ id: "download-markdown-selection",
+ title: "Download Selection As Markdown",
+ contexts: ["selection"]
+ },
+ {
+ id: "download-markdown-all",
+ title: "Download Tab As Markdown",
+ contexts: ["all"]
+ },
+ {
+ id: "separator-1",
+ type: "separator",
+ contexts: ["all"]
+ },
+ {
+ id: "copy-markdown-selection",
+ title: "Copy Selection As Markdown",
+ contexts: ["selection"]
+ },
+ {
+ id: "copy-markdown-link",
+ title: "Copy Link As Markdown",
+ contexts: ["link"]
+ },
+ {
+ id: "copy-markdown-image",
+ title: "Copy Image As Markdown",
+ contexts: ["image"]
+ },
+ {
+ id: "copy-markdown-all",
+ title: "Copy Tab As Markdown",
+ contexts: ["all"]
+ },
+ {
+ id: "copy-tab-as-markdown-link",
+ title: "Copy Tab URL as Markdown Link",
+ contexts: ["all"]
+ },
+ {
+ id: "separator-2",
+ type: "separator",
+ contexts: ["all"]
+ },
+ {
+ id: "toggle-includeTemplate",
+ type: "checkbox",
+ title: "Include front/back template",
+ contexts: ["all"],
+ checked: options.includeTemplate
+ },
+ {
+ id: "toggle-downloadImages",
+ type: "checkbox",
+ title: "Download Images",
+ contexts: ["all"],
+ checked: options.downloadImages
+ }
+ ];
+
+ if (options.obsidianIntegration) {
+ menuItems.splice(-2, 0,
+ {
+ id: "copy-markdown-obsidian",
+ title: "Send Text selection to Obsidian",
+ contexts: ["selection"]
+ },
+ {
+ id: "copy-markdown-obsall",
+ title: "Send Tab to Obsidian",
+ contexts: ["all"]
+ },
+ {
+ id: "separator-3",
+ type: "separator",
+ contexts: ["all"]
+ }
+ );
+ }
+
+ for (const item of menuItems) {
+ try {
+ await chrome.contextMenus.create(item);
+ console.log('MarkDownload: Created menu item:', item.id, '- Title:', item.title);
+ } catch (e) {
+ console.warn('MarkDownload: Failed to create menu item:', item.id, e);
+ }
+ }
+ console.log('MarkDownload: Context menus created successfully');
+
+ // Test: List all created menus for debugging
+ try {
+ const result = await new Promise((resolve) => {
+ chrome.contextMenus.query({}, resolve);
+ });
+ console.log('MarkDownload: Verified menus exist:', result?.length || 'query not supported');
+ } catch (e) {
+ console.log('MarkDownload: Menu verification not available');
+ }
+ }
+}
+
+// Main Turndown conversion function
+// Turndown conversion function moved to content script
+
+// Message handling from content scripts and popup
+chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
+ console.log('MarkDownload: Received message:', message.type, 'from tab:', sender.tab?.id);
+
+ // Handle message asynchronously
+ (async () => {
+ try {
+ await handleMessage(message, sender, sendResponse);
+ } catch (error) {
+ console.error('MarkDownload: Message handling error:', error);
+ sendResponse({ error: error.message });
+ }
+ })();
+
+ return true; // Indicate async response
+});
+
+// Handle messages from content scripts and popup
+async function handleMessage(message, sender, sendResponse) {
+ try {
+ if (message.type === "clip") {
+ console.log('MarkDownload: Processing clip message');
+
+ if (!sender.tab?.id) {
+ sendResponse({ error: 'No valid tab ID' });
+ return;
+ }
+
+ // Use complete processing in content script (including markdown conversion)
+ const result = await getCompleteMarkdownFromContent(sender.tab.id, message.clipSelection);
+
+ if (!result) {
+ sendResponse({ error: 'Failed to process complete article' });
+ return;
+ }
+
+ // Format folder
+ const mdClipsFolder = await formatMdClipsFolder(result.article);
+
+ // Send response back
+ sendResponse({
+ type: "display.md",
+ markdown: result.markdown,
+ article: result.article,
+ imageList: result.imageList,
+ mdClipsFolder: mdClipsFolder
+ });
+ } else if (message.type === "download") {
+ console.log('MarkDownload: Processing download message');
+ await downloadMarkdown(message.markdown, message.title, sender.tab.id, message.imageList, message.mdClipsFolder);
+ sendResponse({ success: true });
+ } else {
+ sendResponse({ error: 'Unknown message type: ' + message.type });
+ }
+ } catch (error) {
+ console.error('MarkDownload: Error handling message:', error);
+ sendResponse({ error: error.message });
+ }
+}
+
+// Context menu click handlers
+chrome.contextMenus.onClicked.addListener(async (info, tab) => {
+ console.log('MarkDownload: Context menu clicked:', info.menuItemId, 'on tab:', tab.id);
+ console.log('MarkDownload: Full info object:', info);
+ console.log('MarkDownload: Tab object:', tab);
+
+ try {
+ if (info.menuItemId.startsWith("copy-markdown")) {
+ console.log('MarkDownload: Routing to copyMarkdownFromContext');
+ await copyMarkdownFromContext(info, tab);
+ } else if (info.menuItemId === "download-markdown-alltabs") {
+ console.log('MarkDownload: Routing to downloadMarkdownForAllTabs');
+ await downloadMarkdownForAllTabs(info);
+ } else if (info.menuItemId.startsWith("download-markdown")) {
+ console.log('MarkDownload: Routing to downloadMarkdownFromContext');
+ await downloadMarkdownFromContext(info, tab);
+ } else if (info.menuItemId.startsWith("copy-tab-as-markdown-link")) {
+ console.log('MarkDownload: Routing to copyTabAsMarkdownLink');
+ await copyTabAsMarkdownLink(tab);
+ } else if (info.menuItemId.startsWith("toggle-")) {
+ console.log('MarkDownload: Routing to toggleSetting');
+ await toggleSetting(info.menuItemId.split('-')[1]);
+ } else {
+ console.warn('MarkDownload: Unhandled menu item:', info.menuItemId);
+ }
+ console.log('MarkDownload: Context menu action completed for:', info.menuItemId);
+ } catch (error) {
+ console.error('MarkDownload: Context menu action failed:', info.menuItemId, error);
+ }
+});
+
+// Command handlers
+chrome.commands.onCommand.addListener((command) => {
+ console.log('MarkDownload: Command executed:', command);
+
+ chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
+ const tab = tabs[0];
+
+ if (command === "download_tab_as_markdown") {
+ downloadMarkdownFromContext({ menuItemId: "download-markdown-all" }, tab);
+ } else if (command === "copy_tab_as_markdown") {
+ copyMarkdownFromContext({ menuItemId: "copy-markdown-all" }, tab);
+ } else if (command === "copy_selection_as_markdown") {
+ copyMarkdownFromContext({ menuItemId: "copy-markdown-selection" }, tab);
+ } else if (command === "copy_tab_as_markdown_link") {
+ copyTabAsMarkdownLink(tab);
+ } else if (command === "copy_selection_to_obsidian") {
+ copyMarkdownFromContext({ menuItemId: "copy-markdown-obsidian" }, tab);
+ } else if (command === "copy_tab_to_obsidian") {
+ copyMarkdownFromContext({ menuItemId: "copy-markdown-obsall" }, tab);
+ }
+ });
+});
+
+// Helper function to execute scripts in tabs (Manifest V3 compatible)
+async function executeScript(tabId, func, args = []) {
+ try {
+ const result = await chrome.scripting.executeScript({
+ target: { tabId: tabId },
+ func: func,
+ args: args
+ });
+ return result[0]?.result;
+ } catch (error) {
+ console.error('MarkDownload: Failed to execute script:', error);
+ throw error;
+ }
+}
+
+// Helper function to inject content script if needed
+async function ensureContentScript(tabId) {
+ try {
+ const result = await executeScript(tabId, () => {
+ return typeof getSelectionAndDom === 'function';
+ });
+
+ if (!result) {
+ console.log('MarkDownload: Injecting content script into tab:', tabId);
+ await chrome.scripting.executeScript({
+ target: { tabId: tabId },
+ files: ['browser-polyfill.min.js', 'contentScript/contentScript.js']
+ });
+ }
+ } catch (error) {
+ console.error('MarkDownload: Failed to ensure content script:', error);
+ throw error;
+ }
+}
+
+// Function to get complete article and markdown from content script
+async function getCompleteMarkdownFromContent(tabId, selection = false) {
+ try {
+ console.log('MarkDownload: Getting complete markdown from tab:', tabId, 'selection:', selection);
+ await ensureContentScript(tabId);
+
+ // Get options from storage
+ const options = await getOptions();
+
+ // Get DOM content
+ const result = await executeScript(tabId, () => {
+ return getSelectionAndDom();
+ });
+
+ if (!result || !result.dom) {
+ console.error('MarkDownload: No DOM content received');
+ return null;
+ }
+
+ // Process complete article in content script (including markdown conversion)
+ const completeResult = await executeScript(tabId, (domString, options, selection) => {
+ return processCompleteArticle(domString, options, selection);
+ }, [result.dom, options, selection]);
+
+ if (!completeResult) {
+ console.error('MarkDownload: Failed to process complete article');
+ return null;
+ }
+
+ console.log('MarkDownload: Complete markdown processed successfully, title:', completeResult.title, 'markdown length:', completeResult.markdown.length);
+ return completeResult;
+ } catch (error) {
+ console.error('MarkDownload: Failed to get complete markdown from content:', error);
+ return null;
+ }
+}
+
+// Note: DOM processing has been moved to content script to avoid Service Worker limitations
+
+// DOM-dependent functions have been moved to content script
+
+async function formatMdClipsFolder(article) {
+ let options = await getOptions();
+ let mdClipsFolder = '';
+ if (options.mdClipsFolder && options.downloadMode == 'downloadsApi') {
+ mdClipsFolder = textReplace(options.mdClipsFolder, article, options.disallowedChars);
+ mdClipsFolder = mdClipsFolder.split('/').map(s => generateValidFileName(s, options.disallowedChars)).join('/');
+ if (!mdClipsFolder.endsWith('/')) mdClipsFolder += '/';
+ }
+ return mdClipsFolder;
+}
+
+// Download function
+async function downloadMarkdown(markdown, title, tabId, imageList = {}, mdClipsFolder = '') {
+ console.log('MarkDownload: Starting download process');
+ const options = await getOptions();
+
+ if (options.downloadMode == 'downloadsApi' && chrome.downloads) {
+ const url = URL.createObjectURL(new Blob([markdown], {
+ type: "text/markdown;charset=utf-8"
+ }));
+
+ try {
+ if (mdClipsFolder && !mdClipsFolder.endsWith('/')) mdClipsFolder += '/';
+
+ const id = await chrome.downloads.download({
+ url: url,
+ filename: mdClipsFolder + title + ".md",
+ saveAs: options.saveAs
+ });
+
+ // Handle download completion
+ const downloadListener = (delta) => {
+ if (delta.id === id && delta.state && delta.state.current == "complete") {
+ chrome.downloads.onChanged.removeListener(downloadListener);
+ URL.revokeObjectURL(url);
+ }
+ };
+ chrome.downloads.onChanged.addListener(downloadListener);
+ } catch (err) {
+ console.error("MarkDownload: Download failed", err);
+ }
+ } else {
+ // Fallback to content link method
+ try {
+ await ensureContentScript(tabId);
+ const filename = mdClipsFolder + generateValidFileName(title, options.disallowedChars) + ".md";
+ const base64Data = btoa(encodeURIComponent(markdown).replace(/%([0-9A-F]{2})/g, function (match, p1) {
+ return String.fromCharCode('0x' + p1);
+ }));
+
+ await executeScript(tabId, (filename, data) => {
+ const link = document.createElement('a');
+ link.download = filename;
+ link.href = `data:text/markdown;base64,${data}`;
+ link.click();
+ }, [filename, base64Data]);
+ } catch (error) {
+ console.error("MarkDownload: Content link download failed:", error);
+ }
+ }
+}
+
+// Context menu action handlers
+async function downloadMarkdownFromContext(info, tab) {
+ try {
+ console.log('MarkDownload: Download from context menu');
+
+ // Get complete markdown result from content script
+ const result = await getCompleteMarkdownFromContent(tab.id, info.menuItemId === "download-markdown-selection");
+ if (!result) {
+ console.error('MarkDownload: Failed to get complete markdown result');
+ return;
+ }
+
+ const mdClipsFolder = await formatMdClipsFolder(result.article);
+
+ await downloadMarkdown(result.markdown, result.title, tab.id, result.imageList, mdClipsFolder);
+ } catch (error) {
+ console.error('MarkDownload: Download from context failed:', error);
+ }
+}
+
+async function copyMarkdownFromContext(info, tab) {
+ try {
+ console.log('MarkDownload: Copy from context menu, menuItemId:', info.menuItemId);
+ await ensureContentScript(tab.id);
+
+ if (info.menuItemId === "copy-markdown-link") {
+ console.log('MarkDownload: Processing link copy');
+ const options = await getOptions();
+ options.frontmatter = options.backmatter = '';
+ const article = await getArticleFromContent(tab.id, false);
+ if (!article) {
+ console.error('MarkDownload: Failed to get article for link');
+ return;
+ }
+ const { markdown } = turndown(`${info.linkText || info.selectionText}`, { ...options, downloadImages: false }, article);
+ console.log('MarkDownload: Link markdown generated, length:', markdown.length);
+
+ await executeScript(tab.id, (text) => {
+ console.log('MarkDownload: Copying link to clipboard, length:', text.length);
+ navigator.clipboard.writeText(text).then(() => {
+ console.log('MarkDownload: Link copied to clipboard successfully');
+ }).catch(err => {
+ console.error('MarkDownload: Failed to copy link to clipboard:', err);
+ });
+ }, [markdown]);
+ } else if (info.menuItemId === "copy-markdown-image") {
+ console.log('MarkDownload: Processing image copy');
+ await executeScript(tab.id, (url) => {
+ console.log('MarkDownload: Copying image to clipboard:', url);
+ navigator.clipboard.writeText(``).then(() => {
+ console.log('MarkDownload: Image copied to clipboard successfully');
+ }).catch(err => {
+ console.error('MarkDownload: Failed to copy image to clipboard:', err);
+ });
+ }, [info.srcUrl]);
+ } else {
+ console.log('MarkDownload: Processing general copy, selection:', info.menuItemId === "copy-markdown-selection");
+
+ // Get complete markdown result (including conversion) from content script
+ const result = await getCompleteMarkdownFromContent(tab.id, info.menuItemId === "copy-markdown-selection");
+ if (!result) {
+ console.error('MarkDownload: Failed to get complete markdown result');
+ return;
+ }
+
+ console.log('MarkDownload: Complete result retrieved, title:', result.title, 'markdown length:', result.markdown?.length || 0);
+
+ if (!result.markdown) {
+ console.error('MarkDownload: No markdown content received');
+ return;
+ }
+
+ // Copy to clipboard via content script
+ await executeScript(tab.id, (markdown) => {
+ console.log('MarkDownload ContentScript: Copying markdown to clipboard, length:', markdown?.length);
+
+ if (!markdown) {
+ console.error('MarkDownload ContentScript: No markdown content to copy');
+ return false;
+ }
+
+ return copyToClipboard(markdown);
+ }, [result.markdown]);
+
+ console.log('MarkDownload: Copy operation completed');
+ }
+ } catch (error) {
+ console.error('MarkDownload: Copy from context failed:', error);
+ }
+}
+
+async function copyTabAsMarkdownLink(tab) {
+ try {
+ console.log('MarkDownload: Copy tab as markdown link');
+ const result = await getCompleteMarkdownFromContent(tab.id, false);
+ if (!result) {
+ console.error('MarkDownload: Failed to get article for link');
+ return;
+ }
+
+ await executeScript(tab.id, (title, url) => {
+ const linkMarkdown = `[${title}](${url})`;
+ console.log('MarkDownload ContentScript: Copying link to clipboard:', linkMarkdown);
+ return copyToClipboard(linkMarkdown);
+ }, [result.title, result.article.baseURI]);
+ } catch (error) {
+ console.error('MarkDownload: Copy tab link failed:', error);
+ }
+}
+
+async function downloadMarkdownForAllTabs(info) {
+ try {
+ console.log('MarkDownload: Download all tabs as markdown');
+ const tabs = await chrome.tabs.query({ currentWindow: true });
+
+ for (const tab of tabs) {
+ try {
+ await downloadMarkdownFromContext(info, tab);
+ } catch (e) {
+ console.warn('MarkDownload: Failed to download tab:', tab.id, e);
+ }
+ }
+ } catch (error) {
+ console.error('MarkDownload: Download all tabs failed:', error);
+ }
+}
+
+// Settings toggle
+async function toggleSetting(setting, options = null) {
+ if (options == null) {
+ options = await getOptions();
+ }
+
+ options[setting] = !options[setting];
+ await chrome.storage.sync.set(options);
+
+ if (setting === "includeTemplate") {
+ chrome.contextMenus.update("toggle-includeTemplate", {
+ checked: options.includeTemplate
+ });
+ }
+
+ if (setting === "downloadImages") {
+ chrome.contextMenus.update("toggle-downloadImages", {
+ checked: options.downloadImages
+ });
+ }
+}
+
+console.log('MarkDownload Service Worker fully loaded and ready');
diff --git a/src/contentScript/contentScript.js b/src/contentScript/contentScript.js
index 0a56afa7..5af2e1ea 100644
--- a/src/contentScript/contentScript.js
+++ b/src/contentScript/contentScript.js
@@ -1,15 +1,396 @@
function notifyExtension() {
// send a message that the content should be clipped
- browser.runtime.sendMessage({ type: "clip", dom: content});
+ console.log('MarkDownload ContentScript: Notifying extension');
+ const content = getHTMLOfDocument();
+ console.log('MarkDownload ContentScript: DOM content length:', content.length);
+
+ if (typeof browser === 'undefined' && typeof chrome !== 'undefined') {
+ window.browser = chrome;
+ }
+
+ browser.runtime.sendMessage({
+ type: "clip",
+ dom: content
+ }).then(response => {
+ console.log('MarkDownload ContentScript: Response received:', response);
+ }).catch(error => {
+ console.error('MarkDownload ContentScript: Error sending message:', error);
+ });
+}
+
+// 初始化 TurndownService
+if (typeof TurndownService !== 'undefined') {
+ TurndownService.prototype.defaultEscape = TurndownService.prototype.escape;
+ console.log('MarkDownload ContentScript: TurndownService initialized');
+}
+
+// Helper functions moved from service worker
+function cleanAttribute(attribute) {
+ return attribute ? attribute.replace(/(\n+\s*)+/g, '\n') : '';
+}
+
+function validateUri(href, baseURI) {
+ try {
+ new URL(href);
+ } catch {
+ const baseUri = new URL(baseURI);
+ if (href.startsWith('/')) {
+ href = baseUri.origin + href;
+ } else {
+ href = baseUri.href + (baseUri.href.endsWith('/') ? '' : '') + href;
+ }
+ }
+ return href;
+}
+
+function generateValidFileName(title, disallowedChars = null) {
+ if (!title) return title;
+ title = title + '';
+
+ var illegalRe = /[\/\?<>\\:\*\|":]/g;
+ var name = title.replace(illegalRe, "").replace(new RegExp('\u00A0', 'g'), ' ')
+ .replace(new RegExp(/\s+/, 'g'), ' ')
+ .trim();
+
+ if (disallowedChars) {
+ for (let c of disallowedChars) {
+ if (`[\\^$.|?*+()`.includes(c)) c = `\\${c}`;
+ name = name.replace(new RegExp(c, 'g'), '');
+ }
+ }
+
+ return name;
+}
+
+function getImageFilename(src, options, prependFilePath = true) {
+ const slashPos = src.lastIndexOf('/');
+ const queryPos = src.indexOf('?');
+ let filename = src.substring(slashPos + 1, queryPos > 0 ? queryPos : src.length);
+
+ let imagePrefix = (options.imagePrefix || '');
+
+ if (prependFilePath && options.title.includes('/')) {
+ imagePrefix = options.title.substring(0, options.title.lastIndexOf('/') + 1) + imagePrefix;
+ } else if (prependFilePath) {
+ imagePrefix = options.title + (imagePrefix.startsWith('/') ? '' : '/') + imagePrefix;
+ }
+
+ if (filename.includes(';base64,')) {
+ filename = 'image.' + filename.substring(0, filename.indexOf(';'));
+ }
+
+ let extension = filename.substring(filename.lastIndexOf('.'));
+ if (extension == filename) {
+ filename = filename + '.idunno';
+ }
+
+ filename = generateValidFileName(filename, options.disallowedChars);
+ return imagePrefix + filename;
+}
+
+function textReplace(string, article, disallowedChars = null) {
+ for (const key in article) {
+ if (article.hasOwnProperty(key) && key != "content") {
+ let s = (article[key] || '') + '';
+ if (s && disallowedChars) s = generateValidFileName(s, disallowedChars);
+
+ string = string.replace(new RegExp('{' + key + '}', 'g'), s)
+ .replace(new RegExp('{' + key + ':lower}', 'g'), s.toLowerCase())
+ .replace(new RegExp('{' + key + ':upper}', 'g'), s.toUpperCase())
+ .replace(new RegExp('{' + key + ':kebab}', 'g'), s.replace(/ /g, '-').toLowerCase())
+ .replace(new RegExp('{' + key + ':mixed-kebab}', 'g'), s.replace(/ /g, '-'))
+ .replace(new RegExp('{' + key + ':snake}', 'g'), s.replace(/ /g, '_').toLowerCase())
+ .replace(new RegExp('{' + key + ':mixed_snake}', 'g'), s.replace(/ /g, '_'))
+ .replace(new RegExp('{' + key + ':obsidian-cal}', 'g'), s.replace(/ /g, '-').replace(/-{2,}/g, "-"))
+ .replace(new RegExp('{' + key + ':camel}', 'g'), s.replace(/ ./g, (str) => str.trim().toUpperCase()).replace(/^./, (str) => str.toLowerCase()))
+ .replace(new RegExp('{' + key + ':pascal}', 'g'), s.replace(/ ./g, (str) => str.trim().toUpperCase()).replace(/^./, (str) => str.toUpperCase()));
+ }
+ }
+
+ // Replace date formats
+ const now = new Date();
+ const dateRegex = /{date:(.+?)}/g;
+ const matches = string.match(dateRegex);
+ if (matches && matches.forEach) {
+ matches.forEach(match => {
+ const format = match.substring(6, match.length - 1);
+ if (typeof moment !== 'undefined') {
+ const dateString = moment(now).format(format);
+ string = string.replaceAll(match, dateString);
+ } else {
+ string = string.replaceAll(match, now.toISOString());
+ }
+ });
+ }
+
+ // Replace keywords
+ const keywordRegex = /{keywords:?(.*)?}/g;
+ const keywordMatches = string.match(keywordRegex);
+ if (keywordMatches && keywordMatches.forEach) {
+ keywordMatches.forEach(match => {
+ let seperator = match.substring(10, match.length - 1);
+ try {
+ seperator = JSON.parse(JSON.stringify(seperator).replace(/\\\\/g, '\\'));
+ } catch { }
+ const keywordsString = (article.keywords || []).join(seperator);
+ string = string.replace(new RegExp(match.replace(/\\/g, '\\\\'), 'g'), keywordsString);
+ });
+ }
+
+ string = string.replace(/{(.*?)}/g, '');
+ return string;
+}
+
+// Main turndown function
+function turndownContent(content, options, article) {
+ console.log('MarkDownload ContentScript: Converting content to markdown');
+
+ if (options.turndownEscape) TurndownService.prototype.escape = TurndownService.prototype.defaultEscape;
+ else TurndownService.prototype.escape = s => s;
+
+ var turndownService = new TurndownService(options);
+ turndownService.use(turndownPluginGfm.gfm);
+ turndownService.keep(['iframe', 'sub', 'sup', 'u', 'ins', 'del', 'small', 'big']);
+
+ let imageList = {};
+
+ // Add image rule
+ turndownService.addRule('images', {
+ filter: function (node, tdopts) {
+ if (node.nodeName == 'IMG' && node.getAttribute('src')) {
+ let src = node.getAttribute('src');
+ node.setAttribute('src', validateUri(src, article.baseURI));
+
+ if (options.downloadImages) {
+ let imageFilename = getImageFilename(src, options, false);
+ if (!imageList[src] || imageList[src] != imageFilename) {
+ let i = 1;
+ while (Object.values(imageList).includes(imageFilename)) {
+ const parts = imageFilename.split('.');
+ if (i == 1) parts.splice(parts.length - 1, 0, i++);
+ else parts.splice(parts.length - 2, 1, i++);
+ imageFilename = parts.join('.');
+ }
+ imageList[src] = imageFilename;
+ }
+
+ const obsidianLink = options.imageStyle.startsWith("obsidian");
+ const localSrc = options.imageStyle === 'obsidian-nofolder'
+ ? imageFilename.substring(imageFilename.lastIndexOf('/') + 1)
+ : imageFilename.split('/').map(s => obsidianLink ? s : encodeURI(s)).join('/');
+
+ if(options.imageStyle != 'originalSource' && options.imageStyle != 'base64') {
+ node.setAttribute('src', localSrc);
+ }
+ return true;
+ } else return true;
+ }
+ return false;
+ },
+ replacement: function (content, node, tdopts) {
+ if (options.imageStyle == 'noImage') return '';
+ else if (options.imageStyle.startsWith('obsidian')) return `![[${node.getAttribute('src')}]]`;
+ else {
+ var alt = cleanAttribute(node.getAttribute('alt'));
+ var src = node.getAttribute('src') || '';
+ var title = cleanAttribute(node.getAttribute('title'));
+ var titlePart = title ? ' "' + title + '"' : '';
+ if (options.imageRefStyle == 'referenced') {
+ var id = this.references.length + 1;
+ this.references.push('[fig' + id + ']: ' + src + titlePart);
+ return '![' + alt + '][fig' + id + ']';
+ }
+ else return src ? '![' + alt + ']' + '(' + src + titlePart + ')' : '';
+ }
+ },
+ references: [],
+ append: function (options) {
+ var references = '';
+ if (this.references.length) {
+ references = '\n\n' + this.references.join('\n') + '\n\n';
+ this.references = [];
+ }
+ return references;
+ }
+ });
+
+ // Add link rule
+ turndownService.addRule('links', {
+ filter: (node, tdopts) => {
+ if (node.nodeName == 'A' && node.getAttribute('href')) {
+ const href = node.getAttribute('href');
+ node.setAttribute('href', validateUri(href, article.baseURI));
+ return options.linkStyle == 'stripLinks';
+ }
+ return false;
+ },
+ replacement: (content, node, tdopts) => content
+ });
+
+ // Add math rule
+ turndownService.addRule('mathjax', {
+ filter(node, options) {
+ return article.math && article.math.hasOwnProperty(node.id);
+ },
+ replacement(content, node, options) {
+ const math = article.math[node.id];
+ let tex = math.tex.trim().replaceAll('\xa0', '');
+
+ if (math.inline) {
+ tex = tex.replaceAll('\n', ' ');
+ return `$${tex}$`;
+ } else {
+ return `$$\n${tex}\n$$`;
+ }
+ }
+ });
+
+ let markdown = options.frontmatter + turndownService.turndown(content) + options.backmatter;
+
+ // Strip out non-printing special characters
+ markdown = markdown.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f\u00ad\u061c\u200b-\u200f\u2028\u2029\ufeff\ufff9-\ufffc]/g, '');
+
+ return { markdown: markdown, imageList: imageList };
+}
+
+// Complete article processing function
+function processCompleteArticle(domString, options, selection = false) {
+ console.log('MarkDownload ContentScript: Processing complete article');
+
+ try {
+ // Step 1: Process DOM to article
+ const article = processArticleFromDOM(domString);
+
+ if (!article) {
+ throw new Error('Failed to process article from DOM');
+ }
+
+ // Step 2: Apply selection if provided
+ const selectionData = getHTMLOfSelection();
+ if (selection && selectionData) {
+ article.content = selectionData;
+ }
+
+ // Step 3: Prepare options
+ if (options.includeTemplate) {
+ options.frontmatter = textReplace(options.frontmatter, article) + '\n';
+ options.backmatter = '\n' + textReplace(options.backmatter, article);
+ } else {
+ options.frontmatter = options.backmatter = '';
+ }
+
+ options.imagePrefix = textReplace(options.imagePrefix, article, options.disallowedChars)
+ .split('/').map(s => generateValidFileName(s, options.disallowedChars)).join('/');
+
+ // Step 4: Convert to markdown
+ const result = turndownContent(article.content, options, article);
+
+ // Step 5: Format title
+ let title = textReplace(options.title, article, options.disallowedChars + '/');
+ title = title.split('/').map(s => generateValidFileName(s, options.disallowedChars)).join('/');
+
+ console.log('MarkDownload ContentScript: Article processing complete, title:', title, 'markdown length:', result.markdown.length);
+
+ return {
+ markdown: result.markdown,
+ imageList: result.imageList,
+ title: title,
+ article: article
+ };
+ } catch (error) {
+ console.error('MarkDownload ContentScript: Error processing complete article:', error);
+ throw error;
+ }
+}
+
+// 新增:处理来自Service Worker的DOM处理请求
+function processArticleFromDOM(domString) {
+ console.log('MarkDownload ContentScript: Processing DOM to article');
+
+ try {
+ const parser = new DOMParser();
+ const dom = parser.parseFromString(domString, "text/html");
+
+ if (dom.documentElement.nodeName == "parsererror") {
+ console.error("MarkDownload ContentScript: DOM parsing error");
+ throw new Error("DOM parsing failed");
+ }
+
+ const math = {};
+
+ // 简化的数学公式处理
+ if (dom.body) {
+ dom.body.querySelectorAll('h1, h2, h3, h4, h5, h6')?.forEach(header => {
+ header.className = '';
+ });
+ dom.documentElement.removeAttribute('class');
+ }
+
+ // 使用 Readability 提取文章
+ const article = new Readability(dom).parse();
+
+ if (!article) {
+ throw new Error("Failed to parse article from DOM");
+ }
+
+ // 添加元数据
+ article.baseURI = dom.baseURI || window.location.href;
+ article.pageTitle = dom.title || document.title || 'Untitled';
+
+ try {
+ const url = new URL(article.baseURI);
+ article.hash = url.hash;
+ article.host = url.host;
+ article.origin = url.origin;
+ article.hostname = url.hostname;
+ article.pathname = url.pathname;
+ article.port = url.port;
+ article.protocol = url.protocol;
+ article.search = url.search;
+ } catch (e) {
+ console.warn('MarkDownload ContentScript: Failed to parse URL info:', e);
+ }
+
+ if (dom.head) {
+ article.keywords = dom.head.querySelector('meta[name="keywords"]')?.content?.split(',')?.map(s => s.trim());
+
+ dom.head.querySelectorAll('meta[name][content], meta[property][content]')?.forEach(meta => {
+ const key = (meta.getAttribute('name') || meta.getAttribute('property'));
+ const val = meta.getAttribute('content');
+ if (key && val && !article[key]) {
+ article[key] = val;
+ }
+ });
+ }
+
+ article.math = math;
+ console.log('MarkDownload ContentScript: Article processed successfully, title:', article.title);
+ return article;
+ } catch (error) {
+ console.error('MarkDownload ContentScript: Error processing DOM:', error);
+ throw error;
+ }
}
function getHTMLOfDocument() {
+ console.log('MarkDownload ContentScript: Starting document processing');
+ console.log('MarkDownload ContentScript: Page URL:', window.location.href);
+ console.log('MarkDownload ContentScript: Page title:', document.title);
+
+ try {
// make sure a title tag exists so that pageTitle is not empty and
- // a filename can be genarated.
+ // a filename can be generated.
+ if (!document.head) {
+ console.warn('MarkDownload ContentScript: No document head found, creating one');
+ const head = document.createElement('head');
+ document.documentElement.insertBefore(head, document.body);
+ }
+
if (document.head.getElementsByTagName('title').length == 0) {
+ console.log('MarkDownload ContentScript: Adding missing title tag');
let titleEl = document.createElement('title');
- // prepate a good default text (the text displayed in the window title)
- titleEl.innerText = document.title;
+ // prepare a good default text (the text displayed in the window title)
+ titleEl.innerText = document.title || 'Untitled';
document.head.append(titleEl);
}
@@ -22,6 +403,7 @@ function getHTMLOfDocument() {
if (baseEls.length > 0) {
baseEl = baseEls[0];
} else {
+ console.log('MarkDownload ContentScript: Adding base element');
baseEl = document.createElement('base');
document.head.append(baseEl);
}
@@ -34,14 +416,33 @@ function getHTMLOfDocument() {
let href = baseEl.getAttribute('href');
if (!href || !href.startsWith(window.location.origin)) {
+ console.log('MarkDownload ContentScript: Setting base href to:', window.location.href);
baseEl.setAttribute('href', window.location.href);
}
// remove the hidden content from the page
+ if (document.body) {
+ console.log('MarkDownload ContentScript: Removing hidden nodes');
removeHiddenNodes(document.body);
+ } else {
+ console.warn('MarkDownload ContentScript: No document body found');
+ }
// get the content of the page as a string
- return document.documentElement.outerHTML;
+ const htmlContent = document.documentElement.outerHTML;
+ console.log('MarkDownload ContentScript: Generated HTML length:', htmlContent.length);
+
+ // Basic validation
+ if (!htmlContent || htmlContent.length < 100) {
+ console.warn('MarkDownload ContentScript: Generated HTML seems too small');
+ }
+
+ return htmlContent;
+ } catch (error) {
+ console.error('MarkDownload ContentScript: Error processing document:', error);
+ // Return basic HTML as fallback
+ return `${document.title || 'Error'} Error processing page content.
`;
+ }
}
// code taken from here: https://www.reddit.com/r/javascript/comments/27bcao/anyone_have_a_method_for_finding_all_the_hidden/
@@ -97,24 +498,93 @@ function getHTMLOfSelection() {
}
function getSelectionAndDom() {
+ console.log('MarkDownload ContentScript: Getting selection and DOM');
+
+ try {
+ const selection = getHTMLOfSelection();
+ const dom = getHTMLOfDocument();
+
+ console.log('MarkDownload ContentScript: Selection length:', selection.length);
+ console.log('MarkDownload ContentScript: DOM length:', dom.length);
+
+ return {
+ selection: selection,
+ dom: dom
+ };
+ } catch (error) {
+ console.error('MarkDownload ContentScript: Error getting selection and DOM:', error);
return {
- selection: getHTMLOfSelection(),
+ selection: '',
dom: getHTMLOfDocument()
+ };
}
}
// This function must be called in a visible page, such as a browserAction popup
// or a content script. Calling it in a background page has no effect!
function copyToClipboard(text) {
- navigator.clipboard.writeText(text);
+ console.log('MarkDownload ContentScript: Copying to clipboard, length:', text.length);
+
+ if (navigator.clipboard && navigator.clipboard.writeText) {
+ navigator.clipboard.writeText(text).then(() => {
+ console.log('MarkDownload ContentScript: Successfully copied to clipboard');
+ }).catch(error => {
+ console.error('MarkDownload ContentScript: Failed to copy to clipboard:', error);
+
+ // Fallback method
+ try {
+ const textArea = document.createElement('textarea');
+ textArea.value = text;
+ textArea.style.position = 'fixed';
+ textArea.style.left = '-999999px';
+ textArea.style.top = '-999999px';
+ document.body.appendChild(textArea);
+ textArea.select();
+ textArea.setSelectionRange(0, 99999);
+ document.execCommand('copy');
+ document.body.removeChild(textArea);
+ console.log('MarkDownload ContentScript: Fallback copy successful');
+ } catch (fallbackError) {
+ console.error('MarkDownload ContentScript: Fallback copy failed:', fallbackError);
+ }
+ });
+ } else {
+ console.warn('MarkDownload ContentScript: Clipboard API not available, using fallback');
+ // Fallback method for older browsers
+ try {
+ const textArea = document.createElement('textarea');
+ textArea.value = text;
+ textArea.style.position = 'fixed';
+ textArea.style.left = '-999999px';
+ textArea.style.top = '-999999px';
+ document.body.appendChild(textArea);
+ textArea.select();
+ textArea.setSelectionRange(0, 99999);
+ document.execCommand('copy');
+ document.body.removeChild(textArea);
+ console.log('MarkDownload ContentScript: Fallback copy successful');
+ } catch (error) {
+ console.error('MarkDownload ContentScript: All copy methods failed:', error);
+ }
+ }
}
function downloadMarkdown(filename, text) {
+ console.log('MarkDownload ContentScript: Starting download:', filename, 'Data length:', text.length);
+
+ try {
let datauri = `data:text/markdown;base64,${text}`;
var link = document.createElement('a');
link.download = filename;
link.href = datauri;
+ link.style.display = 'none';
+ document.body.appendChild(link);
link.click();
+ document.body.removeChild(link);
+ console.log('MarkDownload ContentScript: Download triggered successfully');
+ } catch (error) {
+ console.error('MarkDownload ContentScript: Download failed:', error);
+ }
}
function downloadImage(filename, url) {
@@ -163,8 +633,21 @@ function downloadImage(filename, url) {
*/
}
+// Initialize content script with debugging
+console.log('MarkDownload ContentScript: Initializing on', window.location.href);
+
+// Ensure browser polyfill compatibility
+if (typeof browser === 'undefined' && typeof chrome !== 'undefined') {
+ window.browser = chrome;
+}
+
(function loadPageContextScript(){
+ try {
var s = document.createElement('script');
s.src = browser.runtime.getURL('contentScript/pageContext.js');
(document.head||document.documentElement).appendChild(s);
+ console.log('MarkDownload ContentScript: Page context script loaded');
+ } catch (error) {
+ console.error('MarkDownload ContentScript: Failed to load page context script:', error);
+ }
})()
diff --git a/src/contentScript/pageContext.js b/src/contentScript/pageContext.js
index 90525dbf..d7c008fe 100644
--- a/src/contentScript/pageContext.js
+++ b/src/contentScript/pageContext.js
@@ -1,11 +1,25 @@
function addLatexToMathJax3()
{
- if (!MathJax?.startup?.document?.math)
- return
+ // Check if MathJax exists and has the required properties
+ if (typeof MathJax === 'undefined' || !MathJax?.startup?.document?.math) {
+ console.log('MarkDownload PageContext: MathJax not available, skipping LaTeX processing');
+ return;
+ }
- for (math of MathJax.startup.document.math)
- {
- math.typesetRoot.setAttribute("markdownload-latex", math.math)
+ try {
+ console.log('MarkDownload PageContext: Processing MathJax LaTeX');
+ for (const math of MathJax.startup.document.math) {
+ if (math.typesetRoot && math.math) {
+ math.typesetRoot.setAttribute("markdownload-latex", math.math);
+ }
+ }
+ console.log('MarkDownload PageContext: MathJax LaTeX processing completed');
+ } catch (error) {
+ console.warn('MarkDownload PageContext: Error processing MathJax LaTeX:', error);
}
}
-addLatexToMathJax3()
+
+// Only run if we're in a proper browser context
+if (typeof window !== 'undefined') {
+ addLatexToMathJax3();
+}
diff --git a/src/manifest.json b/src/manifest.json
index 19a581cc..b61d2fa7 100644
--- a/src/manifest.json
+++ b/src/manifest.json
@@ -1,8 +1,8 @@
{
- "manifest_version": 2,
+ "manifest_version": 3,
"name": "MarkDownload - Markdown Web Clipper",
"version": "3.4.0",
- "author": "Gordon Pedsersen",
+ "author": "Gordon Pedsensen",
"description": "This extension works like a web clipper, but it downloads articles in markdown format.",
"icons": {
"16": "icons/favicon-16x16.png",
@@ -13,14 +13,17 @@
"512": "icons/favicon-512x512.png"
},
"permissions": [
- "",
"activeTab",
"downloads",
"storage",
"contextMenus",
- "clipboardWrite"
+ "clipboardWrite",
+ "scripting"
],
- "browser_action": {
+ "host_permissions": [
+ ""
+ ],
+ "action": {
"default_title": "MarkDownload",
"default_popup": "popup/popup.html",
"default_icon": {
@@ -31,26 +34,36 @@
}
},
"background": {
- "scripts": [
- "browser-polyfill.min.js",
- "background/apache-mime-types.js",
- "background/moment.min.js",
- "background/turndown.js",
- "background/turndown-plugin-gfm.js",
- "/background/Readability.js",
- "shared/context-menus.js",
- "shared/default-options.js",
- "background/background.js"
- ]
+ "service_worker": "background/service-worker.js"
},
+ "content_scripts": [
+ {
+ "matches": [""],
+ "js": [
+ "browser-polyfill.min.js",
+ "background/apache-mime-types.js",
+ "background/moment.min.js",
+ "background/turndown.js",
+ "background/turndown-plugin-gfm.js",
+ "background/Readability.js",
+ "shared/default-options.js",
+ "contentScript/contentScript.js"
+ ],
+ "run_at": "document_end"
+ }
+ ],
+ "web_accessible_resources": [
+ {
+ "resources": ["contentScript/pageContext.js"],
+ "matches": [""]
+ }
+ ],
"options_ui": {
"page": "options/options.html",
- "browser_style": false,
- "chrome_style": false,
"open_in_tab": false
},
"commands": {
- "_execute_browser_action": {
+ "_execute_action": {
"suggested_key": {
"default": "Alt+Shift+M"
}
From 83a9a1ac85493ff7ee38218d35b2142abd3a7f2a Mon Sep 17 00:00:00 2001
From: TianleLin <940428567qq@gmail.com>
Date: Wed, 10 Sep 2025 17:57:51 +0800
Subject: [PATCH 22/29] fix send to obsidian
---
src/background/service-worker.js | 154 ++++++++++++++++++++++++++++++-
1 file changed, 150 insertions(+), 4 deletions(-)
diff --git a/src/background/service-worker.js b/src/background/service-worker.js
index 88ff5a6e..88acd577 100644
--- a/src/background/service-worker.js
+++ b/src/background/service-worker.js
@@ -119,7 +119,80 @@ function validateUri(href, baseURI) {
return href;
}
-// Helper functions moved to content script
+// Helper functions for Obsidian integration
+function generateValidFileName(title, disallowedChars = null) {
+ if (!title) return title;
+ title = title + '';
+
+ var illegalRe = /[\/\?<>\\:\*\|":]/g;
+ var name = title.replace(illegalRe, "").replace(new RegExp('\u00A0', 'g'), ' ')
+ .replace(new RegExp(/\s+/, 'g'), ' ')
+ .trim();
+
+ if (disallowedChars) {
+ for (let c of disallowedChars) {
+ if (`[\\^$.|?*+()`.includes(c)) c = `\\${c}`;
+ name = name.replace(new RegExp(c, 'g'), '');
+ }
+ }
+
+ return name;
+}
+
+function textReplace(string, article, disallowedChars = null) {
+ for (const key in article) {
+ if (article.hasOwnProperty(key) && key != "content") {
+ let s = (article[key] || '') + '';
+ if (s && disallowedChars) s = generateValidFileName(s, disallowedChars);
+
+ string = string.replace(new RegExp('{' + key + '}', 'g'), s)
+ .replace(new RegExp('{' + key + ':lower}', 'g'), s.toLowerCase())
+ .replace(new RegExp('{' + key + ':upper}', 'g'), s.toUpperCase())
+ .replace(new RegExp('{' + key + ':kebab}', 'g'), s.replace(/ /g, '-').toLowerCase())
+ .replace(new RegExp('{' + key + ':mixed-kebab}', 'g'), s.replace(/ /g, '-'))
+ .replace(new RegExp('{' + key + ':snake}', 'g'), s.replace(/ /g, '_').toLowerCase())
+ .replace(new RegExp('{' + key + ':mixed_snake}', 'g'), s.replace(/ /g, '_'))
+ .replace(new RegExp('{' + key + ':obsidian-cal}', 'g'), s.replace(/ /g, '-').replace(/-{2,}/g, "-"))
+ .replace(new RegExp('{' + key + ':camel}', 'g'), s.replace(/ ./g, (str) => str.trim().toUpperCase()).replace(/^./, (str) => str.toLowerCase()))
+ .replace(new RegExp('{' + key + ':pascal}', 'g'), s.replace(/ ./g, (str) => str.trim().toUpperCase()).replace(/^./, (str) => str.toUpperCase()));
+ }
+ }
+
+ // Replace date formats
+ const now = new Date();
+ const dateRegex = /{date:(.+?)}/g;
+ const matches = string.match(dateRegex);
+ if (matches && matches.forEach) {
+ matches.forEach(match => {
+ const format = match.substring(6, match.length - 1);
+ if (typeof moment !== 'undefined') {
+ const dateString = moment(now).format(format);
+ string = string.replaceAll(match, dateString);
+ } else {
+ // Fallback if moment is not available
+ string = string.replaceAll(match, now.toISOString());
+ }
+ });
+ }
+
+ // Replace keywords
+ const keywordRegex = /{keywords:?(.*)?}/g;
+ const keywordMatches = string.match(keywordRegex);
+ if (keywordMatches && keywordMatches.forEach) {
+ keywordMatches.forEach(match => {
+ let seperator = match.substring(10, match.length - 1);
+ try {
+ seperator = JSON.parse(JSON.stringify(seperator).replace(/\\\\/g, '\\'));
+ } catch { }
+ const keywordsString = (article.keywords || []).join(seperator);
+ string = string.replace(new RegExp(match.replace(/\\/g, '\\\\'), 'g'), keywordsString);
+ });
+ }
+
+ // Replace anything left in curly braces
+ string = string.replace(/{(.*?)}/g, '');
+ return string;
+}
// Image processing functions moved to content script
@@ -331,7 +404,10 @@ chrome.contextMenus.onClicked.addListener(async (info, tab) => {
console.log('MarkDownload: Tab object:', tab);
try {
- if (info.menuItemId.startsWith("copy-markdown")) {
+ if (info.menuItemId === "copy-markdown-obsidian" || info.menuItemId === "copy-markdown-obsall") {
+ console.log('MarkDownload: Routing to sendToObsidian');
+ await sendToObsidian(info, tab);
+ } else if (info.menuItemId.startsWith("copy-markdown")) {
console.log('MarkDownload: Routing to copyMarkdownFromContext');
await copyMarkdownFromContext(info, tab);
} else if (info.menuItemId === "download-markdown-alltabs") {
@@ -371,9 +447,9 @@ chrome.commands.onCommand.addListener((command) => {
} else if (command === "copy_tab_as_markdown_link") {
copyTabAsMarkdownLink(tab);
} else if (command === "copy_selection_to_obsidian") {
- copyMarkdownFromContext({ menuItemId: "copy-markdown-obsidian" }, tab);
+ sendToObsidian({ menuItemId: "copy-markdown-obsidian" }, tab);
} else if (command === "copy_tab_to_obsidian") {
- copyMarkdownFromContext({ menuItemId: "copy-markdown-obsall" }, tab);
+ sendToObsidian({ menuItemId: "copy-markdown-obsall" }, tab);
}
});
});
@@ -665,4 +741,74 @@ async function toggleSetting(setting, options = null) {
}
}
+// Obsidian integration function (using clipboard method like original)
+async function sendToObsidian(info, tab) {
+ try {
+ console.log('MarkDownload: Sending to Obsidian, menuItemId:', info.menuItemId);
+
+ // Get complete markdown result from content script
+ const result = await getCompleteMarkdownFromContent(tab.id, info.menuItemId === "copy-markdown-obsidian");
+ if (!result) {
+ console.error('MarkDownload: Failed to get complete markdown result for Obsidian');
+ return;
+ }
+
+ console.log('MarkDownload: Obsidian result retrieved, title:', result.title, 'markdown length:', result.markdown?.length || 0);
+
+ if (!result.markdown) {
+ console.error('MarkDownload: No markdown content received for Obsidian');
+ return;
+ }
+
+ // Get Obsidian options
+ const options = await getOptions();
+ if (!options.obsidianIntegration) {
+ console.error('MarkDownload: Obsidian integration not enabled');
+ return;
+ }
+
+ // Step 1: Copy markdown to clipboard (like original implementation)
+ console.log('MarkDownload: Copying markdown to clipboard for Obsidian');
+ await executeScript(tab.id, (markdown) => {
+ console.log('MarkDownload ContentScript: Copying markdown to clipboard for Obsidian');
+ return copyToClipboard(markdown);
+ }, [result.markdown]);
+
+ // Step 2: Prepare Obsidian URI using clipboard method
+ const vault = options.obsidianVault || '';
+ const folder = options.obsidianFolder || '';
+
+ // Format folder like original implementation
+ let obsidianFolder = '';
+ if (folder) {
+ // Use text replacement and filename generation like original
+ obsidianFolder = result.article ?
+ textReplace(folder, result.article, options.disallowedChars) : folder;
+ obsidianFolder = obsidianFolder.split('/').map(s => generateValidFileName(s, options.disallowedChars)).join('/');
+ if (!obsidianFolder.endsWith('/')) obsidianFolder += '/';
+ }
+
+ // Generate filename like original
+ const filename = generateValidFileName(result.title, options.disallowedChars);
+ const filepath = obsidianFolder + filename;
+
+ // Create Obsidian URI using clipboard method (like original)
+ const obsidianUri = 'obsidian://advanced-uri?' +
+ 'vault=' + encodeURIComponent(vault) +
+ '&clipboard=true' +
+ '&mode=new' +
+ '&filepath=' + encodeURIComponent(filepath);
+
+ console.log('MarkDownload: Opening Obsidian URI (clipboard method):', obsidianUri);
+
+ // Open Obsidian URI using chrome.tabs.create (Manifest V3 compatible)
+ await chrome.tabs.create({ url: obsidianUri });
+ console.log('MarkDownload: Obsidian URI opened successfully');
+
+ console.log('MarkDownload: Obsidian operation completed');
+ } catch (error) {
+ console.error('MarkDownload: Send to Obsidian failed:', error);
+ }
+}
+
console.log('MarkDownload Service Worker fully loaded and ready');
From 0a24d161e357263fddf6824ee882d920e6ab1db2 Mon Sep 17 00:00:00 2001
From: TianleLin <940428567qq@gmail.com>
Date: Wed, 10 Sep 2025 18:00:25 +0800
Subject: [PATCH 23/29] fix send to obsidian
---
src/background/service-worker.js | 25 ++++++++++++++++++++++---
1 file changed, 22 insertions(+), 3 deletions(-)
diff --git a/src/background/service-worker.js b/src/background/service-worker.js
index 88acd577..647d2f88 100644
--- a/src/background/service-worker.js
+++ b/src/background/service-worker.js
@@ -801,9 +801,28 @@ async function sendToObsidian(info, tab) {
console.log('MarkDownload: Opening Obsidian URI (clipboard method):', obsidianUri);
- // Open Obsidian URI using chrome.tabs.create (Manifest V3 compatible)
- await chrome.tabs.create({ url: obsidianUri });
- console.log('MarkDownload: Obsidian URI opened successfully');
+ // Try to open Obsidian URI directly
+ try {
+ // Method 1: Try to update current tab (most direct)
+ await chrome.tabs.update(tab.id, { url: obsidianUri });
+ console.log('MarkDownload: Obsidian URI opened successfully via tab update');
+ } catch (error) {
+ console.log('MarkDownload: Tab update failed, trying alternative method:', error);
+
+ // Method 2: Fallback to creating new tab
+ try {
+ await chrome.tabs.create({ url: obsidianUri });
+ console.log('MarkDownload: Obsidian URI opened successfully via new tab');
+ } catch (error2) {
+ console.error('MarkDownload: All methods failed:', error2);
+
+ // Method 3: Final fallback - show notification with manual instructions
+ await executeScript(tab.id, (uri) => {
+ alert('MarkDownload: Content copied to clipboard!\n\nTo open in Obsidian:\n1. Open Obsidian\n2. Create new note\n3. Paste content (Ctrl+V)\n\nOr click OK to try opening Obsidian URI again.');
+ window.open(uri, '_blank');
+ }, [obsidianUri]);
+ }
+ }
console.log('MarkDownload: Obsidian operation completed');
} catch (error) {
From 0283e135bad7b4568c1571ee52a52bff0df1ef4d Mon Sep 17 00:00:00 2001
From: TianleLin <940428567qq@gmail.com>
Date: Wed, 10 Sep 2025 18:17:40 +0800
Subject: [PATCH 24/29] fix download
---
src/background/service-worker.js | 37 +++++-------
src/popup/popup.html | 2 +-
src/popup/popup.js | 100 +++++++++++++++----------------
3 files changed, 66 insertions(+), 73 deletions(-)
diff --git a/src/background/service-worker.js b/src/background/service-worker.js
index 647d2f88..f5c9df39 100644
--- a/src/background/service-worker.js
+++ b/src/background/service-worker.js
@@ -339,7 +339,7 @@ async function createMenus() {
// Message handling from content scripts and popup
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
- console.log('MarkDownload: Received message:', message.type, 'from tab:', sender.tab?.id);
+ console.log('MarkDownload: Received message:', message.type, 'from tab:', sender.tab?.id, 'message.tabId:', message?.tabId);
// Handle message asynchronously
(async () => {
@@ -360,13 +360,16 @@ async function handleMessage(message, sender, sendResponse) {
if (message.type === "clip") {
console.log('MarkDownload: Processing clip message');
- if (!sender.tab?.id) {
+ // Prefer tabId from message (popup), fallback to sender.tab.id
+ const tabId = message.tabId || sender.tab?.id;
+ if (!tabId) {
sendResponse({ error: 'No valid tab ID' });
return;
}
+ console.log('MarkDownload: Using tab ID for clip:', tabId);
// Use complete processing in content script (including markdown conversion)
- const result = await getCompleteMarkdownFromContent(sender.tab.id, message.clipSelection);
+ const result = await getCompleteMarkdownFromContent(tabId, message.clipSelection);
if (!result) {
sendResponse({ error: 'Failed to process complete article' });
@@ -376,17 +379,20 @@ async function handleMessage(message, sender, sendResponse) {
// Format folder
const mdClipsFolder = await formatMdClipsFolder(result.article);
- // Send response back
- sendResponse({
+ // Send response back (for request) and also broadcast for popup listeners
+ const responsePayload = {
type: "display.md",
markdown: result.markdown,
article: result.article,
imageList: result.imageList,
mdClipsFolder: mdClipsFolder
- });
+ };
+ try { sendResponse(responsePayload); } catch {}
+ try { chrome.runtime.sendMessage(responsePayload); } catch {}
} else if (message.type === "download") {
console.log('MarkDownload: Processing download message');
- await downloadMarkdown(message.markdown, message.title, sender.tab.id, message.imageList, message.mdClipsFolder);
+ const tabId = message.tabId || sender.tab?.id;
+ await downloadMarkdown(message.markdown, message.title, tabId, message.imageList, message.mdClipsFolder);
sendResponse({ success: true });
} else {
sendResponse({ error: 'Unknown message type: ' + message.type });
@@ -547,27 +553,16 @@ async function downloadMarkdown(markdown, title, tabId, imageList = {}, mdClipsF
const options = await getOptions();
if (options.downloadMode == 'downloadsApi' && chrome.downloads) {
- const url = URL.createObjectURL(new Blob([markdown], {
- type: "text/markdown;charset=utf-8"
- }));
+ // Use data URL directly; createObjectURL is not available in MV3 service worker
+ const dataUrl = 'data:text/markdown;charset=utf-8,' + encodeURIComponent(markdown);
try {
if (mdClipsFolder && !mdClipsFolder.endsWith('/')) mdClipsFolder += '/';
-
const id = await chrome.downloads.download({
- url: url,
+ url: dataUrl,
filename: mdClipsFolder + title + ".md",
saveAs: options.saveAs
});
-
- // Handle download completion
- const downloadListener = (delta) => {
- if (delta.id === id && delta.state && delta.state.current == "complete") {
- chrome.downloads.onChanged.removeListener(downloadListener);
- URL.revokeObjectURL(url);
- }
- };
- chrome.downloads.onChanged.addListener(downloadListener);
} catch (err) {
console.error("MarkDownload: Download failed", err);
}
diff --git a/src/popup/popup.html b/src/popup/popup.html
index 54eaaee4..62caac63 100644
--- a/src/popup/popup.html
+++ b/src/popup/popup.html
@@ -26,7 +26,7 @@
Download selected
-
+
diff --git a/src/popup/popup.js b/src/popup/popup.js
index 773e857e..6ae6a921 100644
--- a/src/popup/popup.js
+++ b/src/popup/popup.js
@@ -97,40 +97,45 @@ const showOrHideClipOption = selection => {
}
const clipSite = id => {
- return browser.tabs.executeScript(id, { code: "getSelectionAndDom()" })
- .then((result) => {
- if (result && result[0]) {
- showOrHideClipOption(result[0].selection);
- let message = {
- type: "clip",
- dom: result[0].dom,
- selection: result[0].selection
- }
- return browser.storage.sync.get(defaultOptions).then(options => {
- browser.runtime.sendMessage({
- ...message,
- ...options
- });
- }).catch(err => {
- console.error(err);
- showError(err)
- return browser.runtime.sendMessage({
- ...message,
- ...defaultOptions
- });
- }).catch(err => {
- console.error(err);
- showError(err)
- });
+ return chrome.scripting.executeScript({
+ target: { tabId: id },
+ func: () => {
+ return getSelectionAndDom();
+ }
+ }).then((result) => {
+ if (result && result[0] && result[0].result) {
+ showOrHideClipOption(result[0].result.selection);
+ let message = {
+ type: "clip",
+ dom: result[0].result.dom,
+ selection: result[0].result.selection,
+ tabId: id
}
- }).catch(err => {
- console.error(err);
- showError(err)
- });
+ return chrome.storage.sync.get(defaultOptions).then(options => {
+ chrome.runtime.sendMessage({
+ ...message,
+ ...options
+ });
+ }).catch(err => {
+ console.error(err);
+ showError(err)
+ return chrome.runtime.sendMessage({
+ ...message,
+ ...defaultOptions
+ });
+ }).catch(err => {
+ console.error(err);
+ showError(err)
+ });
+ }
+ }).catch(err => {
+ console.error(err);
+ showError(err)
+ });
}
-// inject the necessary scripts
-browser.storage.sync.get(defaultOptions).then(options => {
+// Initialize popup
+chrome.storage.sync.get(defaultOptions).then(options => {
checkInitialSettings(options);
document.getElementById("selected").addEventListener("click", (e) => {
@@ -150,49 +155,42 @@ browser.storage.sync.get(defaultOptions).then(options => {
toggleDownloadImages(options);
});
- return browser.tabs.query({
+ return chrome.tabs.query({
currentWindow: true,
active: true
});
}).then((tabs) => {
var id = tabs[0].id;
var url = tabs[0].url;
- browser.tabs.executeScript(id, {
- file: "/browser-polyfill.min.js"
- })
- .then(() => {
- return browser.tabs.executeScript(id, {
- file: "/contentScript/contentScript.js"
- });
- }).then( () => {
- console.info("Successfully injected MarkDownload content script");
- return clipSite(id);
- }).catch( (error) => {
- console.error(error);
- showError(error);
- });
+
+ // Content script is already injected via manifest, just call clipSite
+ console.info("MarkDownload popup: Content script should be available");
+ return clipSite(id);
+}).catch( (error) => {
+ console.error(error);
+ showError(error);
});
// listen for notifications from the background page
-browser.runtime.onMessage.addListener(notify);
+chrome.runtime.onMessage.addListener(notify);
//function to send the download message to the background page
function sendDownloadMessage(text) {
if (text != null) {
-
- return browser.tabs.query({
+ return chrome.tabs.query({
currentWindow: true,
active: true
}).then(tabs => {
+ const activeTab = tabs[0];
var message = {
type: "download",
markdown: text,
title: document.getElementById("title").value,
- tab: tabs[0],
+ tabId: activeTab?.id,
imageList: imageList,
mdClipsFolder: mdClipsFolder
};
- return browser.runtime.sendMessage(message);
+ return chrome.runtime.sendMessage(message);
});
}
}
From 83c6faef2af5815e9e853545716ed5e79c869285 Mon Sep 17 00:00:00 2001
From: TianleLin <940428567qq@gmail.com>
Date: Wed, 10 Sep 2025 18:44:04 +0800
Subject: [PATCH 25/29] fix download image and add copy button
---
src/background/service-worker.js | 40 +++++++++++++++++++++-
src/contentScript/contentScript.js | 5 ++-
src/options/options.js | 22 ++++++------
src/popup/popup.html | 1 +
src/popup/popup.js | 50 +++++++++++++++++++++++----
src/shared/context-menus.js | 54 +++++++++++++++---------------
src/shared/default-options.js | 4 +--
7 files changed, 127 insertions(+), 49 deletions(-)
diff --git a/src/background/service-worker.js b/src/background/service-worker.js
index f5c9df39..ee6afd36 100644
--- a/src/background/service-worker.js
+++ b/src/background/service-worker.js
@@ -550,27 +550,63 @@ async function formatMdClipsFolder(article) {
// Download function
async function downloadMarkdown(markdown, title, tabId, imageList = {}, mdClipsFolder = '') {
console.log('MarkDownload: Starting download process');
+ console.log('MarkDownload: Download params - title:', title, 'tabId:', tabId, 'imageList keys:', Object.keys(imageList).length, 'mdClipsFolder:', mdClipsFolder);
+
const options = await getOptions();
+ console.log('MarkDownload: Options - downloadMode:', options.downloadMode, 'downloadImages:', options.downloadImages, 'saveAs:', options.saveAs);
if (options.downloadMode == 'downloadsApi' && chrome.downloads) {
+ console.log('MarkDownload: Using downloads API');
// Use data URL directly; createObjectURL is not available in MV3 service worker
const dataUrl = 'data:text/markdown;charset=utf-8,' + encodeURIComponent(markdown);
+ console.log('MarkDownload: Data URL length:', dataUrl.length);
try {
if (mdClipsFolder && !mdClipsFolder.endsWith('/')) mdClipsFolder += '/';
+ const filename = mdClipsFolder + title + ".md";
+ console.log('MarkDownload: Downloading file:', filename);
+
const id = await chrome.downloads.download({
url: dataUrl,
- filename: mdClipsFolder + title + ".md",
+ filename: filename,
saveAs: options.saveAs
});
+ console.log('MarkDownload: Download started with ID:', id);
+
+ // Download images (if enabled)
+ if (options.downloadImages && Object.keys(imageList).length > 0) {
+ console.log('MarkDownload: Downloading images:', Object.keys(imageList).length);
+ // Get the relative path of the markdown file (if any) for image path
+ let destPath = mdClipsFolder + title.substring(0, title.lastIndexOf('/'));
+ if(destPath && !destPath.endsWith('/')) destPath += '/';
+
+ Object.entries(imageList).forEach(async ([src, filename]) => {
+ try {
+ // Start the download of the image
+ const imgId = await chrome.downloads.download({
+ url: src,
+ // Set a destination path (relative to md file)
+ filename: destPath ? destPath + filename : filename,
+ saveAs: false
+ });
+ console.log('MarkDownload: Image download started:', filename);
+ } catch (imgErr) {
+ console.error('MarkDownload: Image download failed:', src, imgErr);
+ }
+ });
+ }
} catch (err) {
console.error("MarkDownload: Download failed", err);
+ console.error("MarkDownload: Error details:", err.message, err.stack);
}
} else {
+ console.log('MarkDownload: Using content link fallback method');
// Fallback to content link method
try {
await ensureContentScript(tabId);
const filename = mdClipsFolder + generateValidFileName(title, options.disallowedChars) + ".md";
+ console.log('MarkDownload: Fallback filename:', filename);
+
const base64Data = btoa(encodeURIComponent(markdown).replace(/%([0-9A-F]{2})/g, function (match, p1) {
return String.fromCharCode('0x' + p1);
}));
@@ -581,8 +617,10 @@ async function downloadMarkdown(markdown, title, tabId, imageList = {}, mdClipsF
link.href = `data:text/markdown;base64,${data}`;
link.click();
}, [filename, base64Data]);
+ console.log('MarkDownload: Fallback download completed');
} catch (error) {
console.error("MarkDownload: Content link download failed:", error);
+ console.error("MarkDownload: Fallback error details:", error.message, error.stack);
}
}
}
diff --git a/src/contentScript/contentScript.js b/src/contentScript/contentScript.js
index 5af2e1ea..df13fa0a 100644
--- a/src/contentScript/contentScript.js
+++ b/src/contentScript/contentScript.js
@@ -159,9 +159,11 @@ function turndownContent(content, options, article) {
filter: function (node, tdopts) {
if (node.nodeName == 'IMG' && node.getAttribute('src')) {
let src = node.getAttribute('src');
+ console.log('MarkDownload ContentScript: Found image:', src);
node.setAttribute('src', validateUri(src, article.baseURI));
if (options.downloadImages) {
+ console.log('MarkDownload ContentScript: Download images enabled, processing image:', src);
let imageFilename = getImageFilename(src, options, false);
if (!imageList[src] || imageList[src] != imageFilename) {
let i = 1;
@@ -172,6 +174,7 @@ function turndownContent(content, options, article) {
imageFilename = parts.join('.');
}
imageList[src] = imageFilename;
+ console.log('MarkDownload ContentScript: Added image to list:', src, '->', imageFilename);
}
const obsidianLink = options.imageStyle.startsWith("obsidian");
@@ -289,7 +292,7 @@ function processCompleteArticle(domString, options, selection = false) {
let title = textReplace(options.title, article, options.disallowedChars + '/');
title = title.split('/').map(s => generateValidFileName(s, options.disallowedChars)).join('/');
- console.log('MarkDownload ContentScript: Article processing complete, title:', title, 'markdown length:', result.markdown.length);
+ console.log('MarkDownload ContentScript: Article processing complete, title:', title, 'markdown length:', result.markdown.length, 'images found:', Object.keys(result.imageList).length);
return {
markdown: result.markdown,
diff --git a/src/options/options.js b/src/options/options.js
index 24485e1f..eb839293 100644
--- a/src/options/options.js
+++ b/src/options/options.js
@@ -42,22 +42,22 @@ const saveOptions = e => {
const save = () => {
const spinner = document.getElementById("spinner");
spinner.style.display = "block";
- browser.storage.sync.set(options)
+ chrome.storage.sync.set(options)
.then(() => {
- browser.contextMenus.update("toggle-includeTemplate", {
+ chrome.contextMenus.update("toggle-includeTemplate", {
checked: options.includeTemplate
});
try {
- browser.contextMenus.update("tabtoggle-includeTemplate", {
+ chrome.contextMenus.update("tabtoggle-includeTemplate", {
checked: options.includeTemplate
});
} catch { }
- browser.contextMenus.update("toggle-downloadImages", {
+ chrome.contextMenus.update("toggle-downloadImages", {
checked: options.downloadImages
});
try {
- browser.contextMenus.update("tabtoggle-downloadImages", {
+ chrome.contextMenus.update("tabtoggle-downloadImages", {
checked: options.downloadImages
});
} catch { }
@@ -96,10 +96,10 @@ const setCurrentChoice = result => {
// if browser doesn't support the download api (i.e. Safari)
// we have to use contentLink download mode
- if (!browser.downloads) {
+ if (!chrome.downloads) {
options.downloadMode = 'contentLink';
document.querySelectorAll("[name='downloadMode']").forEach(el => el.disabled = true)
- document.querySelector('#downloadMode p').innerText = "The Downloas API is unavailable in this browser."
+ document.querySelector('#downloadMode p').innerText = "The Downloads API is unavailable in this browser."
}
const downloadImages = options.downloadImages && options.downloadMode == 'downloadsApi';
@@ -149,7 +149,7 @@ const restoreOptions = () => {
console.error(error);
}
- browser.storage.sync.get(defaultOptions).then(setCurrentChoice, onError);
+ chrome.storage.sync.get(defaultOptions).then(setCurrentChoice, onError);
}
function textareaInput(){
@@ -201,7 +201,7 @@ const inputChange = e => {
let lines = ev.target.result;
options = JSON.parse(lines);
setCurrentChoice(options);
- browser.contextMenus.removeAll()
+ chrome.contextMenus.removeAll()
createMenus()
save();
refereshElements();
@@ -214,7 +214,7 @@ const inputChange = e => {
if (key == "contextMenus") {
if (value) { createMenus() }
- else { browser.contextMenus.removeAll() }
+ else { chrome.contextMenus.removeAll() }
}
save();
@@ -240,7 +240,7 @@ const buttonClick = (e) => {
var d = new Date();
var datestring = d.getFullYear() + "-" + ("0" + (d.getMonth() + 1)).slice(-2) + "-" + ("0" + d.getDate()).slice(-2);
- browser.downloads.download({
+ chrome.downloads.download({
url: url,
saveAs: true,
filename: `MarkDownload-export-${datestring}.json`
diff --git a/src/popup/popup.html b/src/popup/popup.html
index 62caac63..07a21960 100644
--- a/src/popup/popup.html
+++ b/src/popup/popup.html
@@ -24,6 +24,7 @@
Download
Download selected
+ Copy
diff --git a/src/popup/popup.js b/src/popup/popup.js
index 6ae6a921..c19d0ad0 100644
--- a/src/popup/popup.js
+++ b/src/popup/popup.js
@@ -24,6 +24,7 @@ cm.on("cursorActivity", (cm) => {
});
document.getElementById("download").addEventListener("click", download);
document.getElementById("downloadSelection").addEventListener("click", downloadSelection);
+document.getElementById("copy").addEventListener("click", copyToClipboard);
const defaultOptions = {
includeTemplate: false,
@@ -48,7 +49,7 @@ const toggleClipSelection = options => {
options.clipSelection = !options.clipSelection;
document.querySelector("#selected").classList.toggle("checked");
document.querySelector("#document").classList.toggle("checked");
- browser.storage.sync.set(options).then(() => clipSite()).catch((error) => {
+ chrome.storage.sync.set(options).then(() => clipSite()).catch((error) => {
console.error(error);
});
}
@@ -56,12 +57,12 @@ const toggleClipSelection = options => {
const toggleIncludeTemplate = options => {
options.includeTemplate = !options.includeTemplate;
document.querySelector("#includeTemplate").classList.toggle("checked");
- browser.storage.sync.set(options).then(() => {
- browser.contextMenus.update("toggle-includeTemplate", {
+ chrome.storage.sync.set(options).then(() => {
+ chrome.contextMenus.update("toggle-includeTemplate", {
checked: options.includeTemplate
});
try {
- browser.contextMenus.update("tabtoggle-includeTemplate", {
+ chrome.contextMenus.update("tabtoggle-includeTemplate", {
checked: options.includeTemplate
});
} catch { }
@@ -74,12 +75,12 @@ const toggleIncludeTemplate = options => {
const toggleDownloadImages = options => {
options.downloadImages = !options.downloadImages;
document.querySelector("#downloadImages").classList.toggle("checked");
- browser.storage.sync.set(options).then(() => {
- browser.contextMenus.update("toggle-downloadImages", {
+ chrome.storage.sync.set(options).then(() => {
+ chrome.contextMenus.update("toggle-downloadImages", {
checked: options.downloadImages
});
try {
- browser.contextMenus.update("tabtoggle-downloadImages", {
+ chrome.contextMenus.update("tabtoggle-downloadImages", {
checked: options.downloadImages
});
} catch { }
@@ -210,6 +211,41 @@ async function downloadSelection(e) {
}
}
+// event handler for copy button
+async function copyToClipboard(e) {
+ e.preventDefault();
+ try {
+ const textToCopy = cm.getValue();
+ await navigator.clipboard.writeText(textToCopy);
+
+ // Show feedback to user
+ const copyButton = document.getElementById("copy");
+ const originalText = copyButton.textContent;
+ copyButton.textContent = "Copied!";
+ copyButton.style.backgroundColor = "#4CAF50";
+
+ // Reset button after 2 seconds
+ setTimeout(() => {
+ copyButton.textContent = originalText;
+ copyButton.style.backgroundColor = "";
+ }, 2000);
+
+ } catch (error) {
+ console.error('Failed to copy text: ', error);
+
+ // Fallback: show error feedback
+ const copyButton = document.getElementById("copy");
+ const originalText = copyButton.textContent;
+ copyButton.textContent = "Copy Failed";
+ copyButton.style.backgroundColor = "#f44336";
+
+ setTimeout(() => {
+ copyButton.textContent = originalText;
+ copyButton.style.backgroundColor = "";
+ }, 2000);
+ }
+}
+
//function that handles messages from the injected script into the site
function notify(message) {
// message for displaying markdown
diff --git a/src/shared/context-menus.js b/src/shared/context-menus.js
index fd049aef..50ede958 100644
--- a/src/shared/context-menus.js
+++ b/src/shared/context-menus.js
@@ -2,49 +2,49 @@
async function createMenus() {
const options = await getOptions();
- browser.contextMenus.removeAll();
+ chrome.contextMenus.removeAll();
if (options.contextMenus) {
// tab menu (chrome does not support this)
try {
- browser.contextMenus.create({
+ chrome.contextMenus.create({
id: "download-markdown-tab",
title: "Download Tab as Markdown",
contexts: ["tab"]
}, () => { });
- browser.contextMenus.create({
+ chrome.contextMenus.create({
id: "tab-download-markdown-alltabs",
title: "Download All Tabs as Markdown",
contexts: ["tab"]
}, () => { });
- browser.contextMenus.create({
+ chrome.contextMenus.create({
id: "copy-tab-as-markdown-link-tab",
title: "Copy Tab URL as Markdown Link",
contexts: ["tab"]
}, () => { });
- browser.contextMenus.create({
+ chrome.contextMenus.create({
id: "copy-tab-as-markdown-link-all-tab",
title: "Copy All Tab URLs as Markdown Link List",
contexts: ["tab"]
}, () => { });
- browser.contextMenus.create({
+ chrome.contextMenus.create({
id: "copy-tab-as-markdown-link-selected-tab",
title: "Copy Selected Tab URLs as Markdown Link List",
contexts: ["tab"]
}, () => { });
- browser.contextMenus.create({
+ chrome.contextMenus.create({
id: "tab-separator-1",
type: "separator",
contexts: ["tab"]
}, () => { });
- browser.contextMenus.create({
+ chrome.contextMenus.create({
id: "tabtoggle-includeTemplate",
type: "checkbox",
title: "Include front/back template",
@@ -52,7 +52,7 @@ async function createMenus() {
checked: options.includeTemplate
}, () => { });
- browser.contextMenus.create({
+ chrome.contextMenus.create({
id: "tabtoggle-downloadImages",
type: "checkbox",
title: "Download Images",
@@ -63,73 +63,73 @@ async function createMenus() {
}
// add the download all tabs option to the page context menu as well
- browser.contextMenus.create({
+ chrome.contextMenus.create({
id: "download-markdown-alltabs",
title: "Download All Tabs as Markdown",
contexts: ["all"]
}, () => { });
- browser.contextMenus.create({
+ chrome.contextMenus.create({
id: "separator-0",
type: "separator",
contexts: ["all"]
}, () => { });
// download actions
- browser.contextMenus.create({
+ chrome.contextMenus.create({
id: "download-markdown-selection",
title: "Download Selection As Markdown",
contexts: ["selection"]
}, () => { });
- browser.contextMenus.create({
+ chrome.contextMenus.create({
id: "download-markdown-all",
title: "Download Tab As Markdown",
contexts: ["all"]
}, () => { });
- browser.contextMenus.create({
+ chrome.contextMenus.create({
id: "separator-1",
type: "separator",
contexts: ["all"]
}, () => { });
// copy to clipboard actions
- browser.contextMenus.create({
+ chrome.contextMenus.create({
id: "copy-markdown-selection",
title: "Copy Selection As Markdown",
contexts: ["selection"]
}, () => { });
- browser.contextMenus.create({
+ chrome.contextMenus.create({
id: "copy-markdown-link",
title: "Copy Link As Markdown",
contexts: ["link"]
}, () => { });
- browser.contextMenus.create({
+ chrome.contextMenus.create({
id: "copy-markdown-image",
title: "Copy Image As Markdown",
contexts: ["image"]
}, () => { });
- browser.contextMenus.create({
+ chrome.contextMenus.create({
id: "copy-markdown-all",
title: "Copy Tab As Markdown",
contexts: ["all"]
}, () => { });
- browser.contextMenus.create({
+ chrome.contextMenus.create({
id: "copy-tab-as-markdown-link",
title: "Copy Tab URL as Markdown Link",
contexts: ["all"]
}, () => { });
- browser.contextMenus.create({
+ chrome.contextMenus.create({
id: "copy-tab-as-markdown-link-all",
title: "Copy All Tab URLs as Markdown Link List",
contexts: ["all"]
}, () => { });
- browser.contextMenus.create({
+ chrome.contextMenus.create({
id: "copy-tab-as-markdown-link-selected",
title: "Copy Selected Tab URLs as Markdown Link List",
contexts: ["all"]
}, () => { });
- browser.contextMenus.create({
+ chrome.contextMenus.create({
id: "separator-2",
type: "separator",
contexts: ["all"]
@@ -137,25 +137,25 @@ async function createMenus() {
if(options.obsidianIntegration){
// copy to clipboard actions
- browser.contextMenus.create({
+ chrome.contextMenus.create({
id: "copy-markdown-obsidian",
title: "Send Text selection to Obsidian",
contexts: ["selection"]
}, () => { });
- browser.contextMenus.create({
+ chrome.contextMenus.create({
id: "copy-markdown-obsall",
title: "Send Tab to Obsidian",
contexts: ["all"]
}, () => { });
}
- browser.contextMenus.create({
+ chrome.contextMenus.create({
id: "separator-3",
type: "separator",
contexts: ["all"]
}, () => { });
// options
- browser.contextMenus.create({
+ chrome.contextMenus.create({
id: "toggle-includeTemplate",
type: "checkbox",
title: "Include front/back template",
@@ -163,7 +163,7 @@ async function createMenus() {
checked: options.includeTemplate
}, () => { });
- browser.contextMenus.create({
+ chrome.contextMenus.create({
id: "toggle-downloadImages",
type: "checkbox",
title: "Download Images",
diff --git a/src/shared/default-options.js b/src/shared/default-options.js
index 6a309b3f..b532ccbb 100644
--- a/src/shared/default-options.js
+++ b/src/shared/default-options.js
@@ -32,10 +32,10 @@ const defaultOptions = {
async function getOptions() {
let options = defaultOptions;
try {
- options = await browser.storage.sync.get(defaultOptions);
+ options = await chrome.storage.sync.get(defaultOptions);
} catch (err) {
console.error(err);
}
- if (!browser.downloads) options.downloadMode = 'contentLink';
+ if (!chrome.downloads) options.downloadMode = 'contentLink';
return options;
}
\ No newline at end of file
From 02eb5c61dd8f6c5079e55f6c7f9aef40c248e73c Mon Sep 17 00:00:00 2001
From: TianleLin <940428567qq@gmail.com>
Date: Wed, 10 Sep 2025 23:23:28 +0800
Subject: [PATCH 26/29] delete v2 code, delete tab, fix service work
---
src/background/background.js | 1026 ------------------------------
src/background/service-worker.js | 65 +-
src/options/options.html | 1 -
src/options/options.js | 18 +-
src/popup/popup.js | 30 +-
src/shared/context-menus.js | 174 -----
6 files changed, 65 insertions(+), 1249 deletions(-)
delete mode 100644 src/background/background.js
delete mode 100644 src/shared/context-menus.js
diff --git a/src/background/background.js b/src/background/background.js
deleted file mode 100644
index 32b71387..00000000
--- a/src/background/background.js
+++ /dev/null
@@ -1,1026 +0,0 @@
-// log some info
-browser.runtime.getPlatformInfo().then(async platformInfo => {
- const browserInfo = browser.runtime.getBrowserInfo ? await browser.runtime.getBrowserInfo() : "Can't get browser info"
- console.info(platformInfo, browserInfo);
-});
-
-// add notification listener for foreground page messages
-browser.runtime.onMessage.addListener(notify);
-// create context menus
-createMenus()
-
-TurndownService.prototype.defaultEscape = TurndownService.prototype.escape;
-
-// function to convert the article content to markdown using Turndown
-function turndown(content, options, article) {
-
- if (options.turndownEscape) TurndownService.prototype.escape = TurndownService.prototype.defaultEscape;
- else TurndownService.prototype.escape = s => s;
-
- var turndownService = new TurndownService(options);
-
- turndownService.use(turndownPluginGfm.gfm)
-
- turndownService.keep(['iframe', 'sub', 'sup', 'u', 'ins', 'del', 'small', 'big']);
-
- let imageList = {};
- // add an image rule
- turndownService.addRule('images', {
- filter: function (node, tdopts) {
- // if we're looking at an img node with a src
- if (node.nodeName == 'IMG' && node.getAttribute('src')) {
-
- // get the original src
- let src = node.getAttribute('src')
- // set the new src
- node.setAttribute('src', validateUri(src, article.baseURI));
-
- // if we're downloading images, there's more to do.
- if (options.downloadImages) {
- // generate a file name for the image
- let imageFilename = getImageFilename(src, options, false);
- if (!imageList[src] || imageList[src] != imageFilename) {
- // if the imageList already contains this file, add a number to differentiate
- let i = 1;
- while (Object.values(imageList).includes(imageFilename)) {
- const parts = imageFilename.split('.');
- if (i == 1) parts.splice(parts.length - 1, 0, i++);
- else parts.splice(parts.length - 2, 1, i++);
- imageFilename = parts.join('.');
- }
- // add it to the list of images to download later
- imageList[src] = imageFilename;
- }
- // check if we're doing an obsidian style link
- const obsidianLink = options.imageStyle.startsWith("obsidian");
- // figure out the (local) src of the image
- const localSrc = options.imageStyle === 'obsidian-nofolder'
- // if using "nofolder" then we just need the filename, no folder
- ? imageFilename.substring(imageFilename.lastIndexOf('/') + 1)
- // otherwise we may need to modify the filename to uri encode parts for a pure markdown link
- : imageFilename.split('/').map(s => obsidianLink ? s : encodeURI(s)).join('/')
-
- // set the new src attribute to be the local filename
- if(options.imageStyle != 'originalSource' && options.imageStyle != 'base64') node.setAttribute('src', localSrc);
- // pass the filter if we're making an obsidian link (or stripping links)
- return true;
- }
- else return true
- }
- // don't pass the filter, just output a normal markdown link
- return false;
- },
- replacement: function (content, node, tdopts) {
- // if we're stripping images, output nothing
- if (options.imageStyle == 'noImage') return '';
- // if this is an obsidian link, so output that
- else if (options.imageStyle.startsWith('obsidian')) return `![[${node.getAttribute('src')}]]`;
- // otherwise, output the normal markdown link
- else {
- var alt = cleanAttribute(node.getAttribute('alt'));
- var src = node.getAttribute('src') || '';
- var title = cleanAttribute(node.getAttribute('title'));
- var titlePart = title ? ' "' + title + '"' : '';
- if (options.imageRefStyle == 'referenced') {
- var id = this.references.length + 1;
- this.references.push('[fig' + id + ']: ' + src + titlePart);
- return '![' + alt + '][fig' + id + ']';
- }
- else return src ? '![' + alt + ']' + '(' + src + titlePart + ')' : ''
- }
- },
- references: [],
- append: function (options) {
- var references = '';
- if (this.references.length) {
- references = '\n\n' + this.references.join('\n') + '\n\n';
- this.references = []; // Reset references
- }
- return references
- }
-
- });
-
- // add a rule for links
- turndownService.addRule('links', {
- filter: (node, tdopts) => {
- // check that this is indeed a link
- if (node.nodeName == 'A' && node.getAttribute('href')) {
- // get the href
- const href = node.getAttribute('href');
- // set the new href
- node.setAttribute('href', validateUri(href, article.baseURI));
- // if we are to strip links, the filter needs to pass
- return options.linkStyle == 'stripLinks';
- }
- // we're not passing the filter, just do the normal thing.
- return false;
- },
- // if the filter passes, we're stripping links, so just return the content
- replacement: (content, node, tdopts) => content
- });
-
- // handle multiple lines math
- turndownService.addRule('mathjax', {
- filter(node, options) {
- return article.math.hasOwnProperty(node.id);
- },
- replacement(content, node, options) {
- const math = article.math[node.id];
- let tex = math.tex.trim().replaceAll('\xa0', '');
-
- if (math.inline) {
- tex = tex.replaceAll('\n', ' ');
- return `$${tex}$`;
- }
- else
- return `$$\n${tex}\n$$`;
- }
- });
-
- function repeat(character, count) {
- return Array(count + 1).join(character);
- }
-
- function convertToFencedCodeBlock(node, options) {
- node.innerHTML = node.innerHTML.replaceAll(' ', '
');
- const langMatch = node.id?.match(/code-lang-(.+)/);
- const language = langMatch?.length > 0 ? langMatch[1] : '';
-
- const code = node.innerText;
-
- const fenceChar = options.fence.charAt(0);
- let fenceSize = 3;
- const fenceInCodeRegex = new RegExp('^' + fenceChar + '{3,}', 'gm');
-
- let match;
- while ((match = fenceInCodeRegex.exec(code))) {
- if (match[0].length >= fenceSize) {
- fenceSize = match[0].length + 1;
- }
- }
-
- const fence = repeat(fenceChar, fenceSize);
-
- return (
- '\n\n' + fence + language + '\n' +
- code.replace(/\n$/, '') +
- '\n' + fence + '\n\n'
- )
- }
-
- turndownService.addRule('fencedCodeBlock', {
- filter: function (node, options) {
- return (
- options.codeBlockStyle === 'fenced' &&
- node.nodeName === 'PRE' &&
- node.firstChild &&
- node.firstChild.nodeName === 'CODE'
- );
- },
- replacement: function (content, node, options) {
- return convertToFencedCodeBlock(node.firstChild, options);
- }
- });
-
- // handle as code blocks
- turndownService.addRule('pre', {
- filter: (node, tdopts) => {
- return node.nodeName == 'PRE'
- && (!node.firstChild || node.firstChild.nodeName != 'CODE')
- && !node.querySelector('img');
- },
- replacement: (content, node, tdopts) => {
- return convertToFencedCodeBlock(node, tdopts);
- }
- });
-
- let markdown = options.frontmatter + turndownService.turndown(content)
- + options.backmatter;
-
- // strip out non-printing special characters which CodeMirror displays as a red dot
- // see: https://codemirror.net/doc/manual.html#option_specialChars
- markdown = markdown.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f\u00ad\u061c\u200b-\u200f\u2028\u2029\ufeff\ufff9-\ufffc]/g, '');
-
- return { markdown: markdown, imageList: imageList };
-}
-
-function cleanAttribute(attribute) {
- return attribute ? attribute.replace(/(\n+\s*)+/g, '\n') : ''
-}
-
-function validateUri(href, baseURI) {
- // check if the href is a valid url
- try {
- new URL(href);
- }
- catch {
- // if it's not a valid url, that likely means we have to prepend the base uri
- const baseUri = new URL(baseURI);
-
- // if the href starts with '/', we need to go from the origin
- if (href.startsWith('/')) {
- href = baseUri.origin + href
- }
- // otherwise we need to go from the local folder
- else {
- href = baseUri.href + (baseUri.href.endsWith('/') ? '/' : '') + href
- }
- }
- return href;
-}
-
-function getImageFilename(src, options, prependFilePath = true) {
- const slashPos = src.lastIndexOf('/');
- const queryPos = src.indexOf('?');
- let filename = src.substring(slashPos + 1, queryPos > 0 ? queryPos : src.length);
-
- let imagePrefix = (options.imagePrefix || '');
-
- if (prependFilePath && options.title.includes('/')) {
- imagePrefix = options.title.substring(0, options.title.lastIndexOf('/') + 1) + imagePrefix;
- }
- else if (prependFilePath) {
- imagePrefix = options.title + (imagePrefix.startsWith('/') ? '' : '/') + imagePrefix
- }
-
- if (filename.includes(';base64,')) {
- // this is a base64 encoded image, so what are we going to do for a filename here?
- filename = 'image.' + filename.substring(0, filename.indexOf(';'));
- }
-
- let extension = filename.substring(filename.lastIndexOf('.'));
- if (extension == filename) {
- // there is no extension, so we need to figure one out
- // for now, give it an 'idunno' extension and we'll process it later
- filename = filename + '.idunno';
- }
-
- filename = generateValidFileName(filename, options.disallowedChars);
-
- return imagePrefix + filename;
-}
-
-// function to replace placeholder strings with article info
-function textReplace(string, article, disallowedChars = null) {
- for (const key in article) {
- if (article.hasOwnProperty(key) && key != "content") {
- let s = (article[key] || '') + '';
- if (s && disallowedChars) s = this.generateValidFileName(s, disallowedChars);
-
- string = string.replace(new RegExp('{' + key + '}', 'g'), s)
- .replace(new RegExp('{' + key + ':lower}', 'g'), s.toLowerCase())
- .replace(new RegExp('{' + key + ':upper}', 'g'), s.toUpperCase())
- .replace(new RegExp('{' + key + ':kebab}', 'g'), s.replace(/ /g, '-').toLowerCase())
- .replace(new RegExp('{' + key + ':mixed-kebab}', 'g'), s.replace(/ /g, '-'))
- .replace(new RegExp('{' + key + ':snake}', 'g'), s.replace(/ /g, '_').toLowerCase())
- .replace(new RegExp('{' + key + ':mixed_snake}', 'g'), s.replace(/ /g, '_'))
- // For Obsidian Custom Attachment Location plugin, we need to replace spaces with hyphens, but also remove any double hyphens.
- .replace(new RegExp('{' + key + ':obsidian-cal}', 'g'), s.replace(/ /g, '-').replace(/-{2,}/g, "-"))
- .replace(new RegExp('{' + key + ':camel}', 'g'), s.replace(/ ./g, (str) => str.trim().toUpperCase()).replace(/^./, (str) => str.toLowerCase()))
- .replace(new RegExp('{' + key + ':pascal}', 'g'), s.replace(/ ./g, (str) => str.trim().toUpperCase()).replace(/^./, (str) => str.toUpperCase()))
- }
- }
-
- // replace date formats
- const now = new Date();
- const dateRegex = /{date:(.+?)}/g
- const matches = string.match(dateRegex);
- if (matches && matches.forEach) {
- matches.forEach(match => {
- const format = match.substring(6, match.length - 1);
- const dateString = moment(now).format(format);
- string = string.replaceAll(match, dateString);
- });
- }
-
- // replace keywords
- const keywordRegex = /{keywords:?(.*)?}/g
- const keywordMatches = string.match(keywordRegex);
- if (keywordMatches && keywordMatches.forEach) {
- keywordMatches.forEach(match => {
- let seperator = match.substring(10, match.length - 1)
- try {
- seperator = JSON.parse(JSON.stringify(seperator).replace(/\\\\/g, '\\'));
- }
- catch { }
- const keywordsString = (article.keywords || []).join(seperator);
- string = string.replace(new RegExp(match.replace(/\\/g, '\\\\'), 'g'), keywordsString);
- })
- }
-
- // replace anything left in curly braces
- const defaultRegex = /{(.*?)}/g
- string = string.replace(defaultRegex, '')
-
- return string;
-}
-
-// function to convert an article info object into markdown
-async function convertArticleToMarkdown(article, downloadImages = null) {
- const options = await getOptions();
- if (downloadImages != null) {
- options.downloadImages = downloadImages;
- }
-
- // substitute front and backmatter templates if necessary
- if (options.includeTemplate) {
- options.frontmatter = textReplace(options.frontmatter, article) + '\n';
- options.backmatter = '\n' + textReplace(options.backmatter, article);
- }
- else {
- options.frontmatter = options.backmatter = '';
- }
-
- options.imagePrefix = textReplace(options.imagePrefix, article, options.disallowedChars)
- .split('/').map(s=>generateValidFileName(s, options.disallowedChars)).join('/');
-
- let result = turndown(article.content, options, article);
- if (options.downloadImages && options.downloadMode == 'downloadsApi') {
- // pre-download the images
- result = await preDownloadImages(result.imageList, result.markdown);
- }
- return result;
-}
-
-// function to turn the title into a valid file name
-function generateValidFileName(title, disallowedChars = null) {
- if (!title) return title;
- else title = title + '';
- // remove < > : " / \ | ? *
- var illegalRe = /[\/\?<>\\:\*\|":]/g;
- // and non-breaking spaces (thanks @Licat)
- var name = title.replace(illegalRe, "").replace(new RegExp('\u00A0', 'g'), ' ')
- // collapse extra whitespace
- .replace(new RegExp(/\s+/, 'g'), ' ')
- // remove leading/trailing whitespace that can cause issues when using {pageTitle} in a download path
- .trim();
-
- if (disallowedChars) {
- for (let c of disallowedChars) {
- if (`[\\^$.|?*+()`.includes(c)) c = `\\${c}`;
- name = name.replace(new RegExp(c, 'g'), '');
- }
- }
-
- return name;
-}
-
-async function preDownloadImages(imageList, markdown) {
- const options = await getOptions();
- let newImageList = {};
- // originally, I was downloading the markdown file first, then all the images
- // however, in some cases we need to download images *first* so we can get the
- // proper file extension to put into the markdown.
- // so... here we are waiting for all the downloads and replacements to complete
- await Promise.all(Object.entries(imageList).map(([src, filename]) => new Promise((resolve, reject) => {
- // we're doing an xhr so we can get it as a blob and determine filetype
- // before the final save
- const xhr = new XMLHttpRequest();
- xhr.open('GET', src);
- xhr.responseType = "blob";
- xhr.onload = async function () {
- // here's the returned blob
- const blob = xhr.response;
-
- if (options.imageStyle == 'base64') {
- var reader = new FileReader();
- reader.onloadend = function () {
- markdown = markdown.replaceAll(src, reader.result)
- resolve()
- }
- reader.readAsDataURL(blob);
- }
- else {
-
- let newFilename = filename;
- if (newFilename.endsWith('.idunno')) {
- // replace any unknown extension with a lookup based on mime type
- newFilename = filename.replace('.idunno', '.' + mimedb[blob.type]);
-
- // and replace any instances of this in the markdown
- // remember to url encode for replacement if it's not an obsidian link
- if (!options.imageStyle.startsWith("obsidian")) {
- markdown = markdown.replaceAll(filename.split('/').map(s => encodeURI(s)).join('/'), newFilename.split('/').map(s => encodeURI(s)).join('/'))
- }
- else {
- markdown = markdown.replaceAll(filename, newFilename)
- }
- }
-
- // create an object url for the blob (no point fetching it twice)
- const blobUrl = URL.createObjectURL(blob);
-
- // add this blob into the new image list
- newImageList[blobUrl] = newFilename;
-
- // resolve this promise now
- // (the file might not be saved yet, but the blob is and replacements are complete)
- resolve();
- }
- };
- xhr.onerror = function () {
- reject('A network error occurred attempting to download ' + src);
- };
- xhr.send();
- })));
-
- return { imageList: newImageList, markdown: markdown };
-}
-
-// function to actually download the markdown file
-async function downloadMarkdown(markdown, title, tabId, imageList = {}, mdClipsFolder = '') {
- // get the options
- const options = await getOptions();
-
- // download via the downloads API
- if (options.downloadMode == 'downloadsApi' && browser.downloads) {
-
- // create the object url with markdown data as a blob
- const url = URL.createObjectURL(new Blob([markdown], {
- type: "text/markdown;charset=utf-8"
- }));
-
- try {
-
- if(mdClipsFolder && !mdClipsFolder.endsWith('/')) mdClipsFolder += '/';
- // start the download
- const id = await browser.downloads.download({
- url: url,
- filename: mdClipsFolder + title + ".md",
- saveAs: options.saveAs
- });
-
- // add a listener for the download completion
- browser.downloads.onChanged.addListener(downloadListener(id, url));
-
- // download images (if enabled)
- if (options.downloadImages) {
- // get the relative path of the markdown file (if any) for image path
- let destPath = mdClipsFolder + title.substring(0, title.lastIndexOf('/'));
- if(destPath && !destPath.endsWith('/')) destPath += '/';
- Object.entries(imageList).forEach(async ([src, filename]) => {
- // start the download of the image
- const imgId = await browser.downloads.download({
- url: src,
- // set a destination path (relative to md file)
- filename: destPath ? destPath + filename : filename,
- saveAs: false
- })
- // add a listener (so we can release the blob url)
- browser.downloads.onChanged.addListener(downloadListener(imgId, src));
- });
- }
- }
- catch (err) {
- console.error("Download failed", err);
- }
- }
- // // download via obsidian://new uri
- // else if (options.downloadMode == 'obsidianUri') {
- // try {
- // await ensureScripts(tabId);
- // let uri = 'obsidian://new?';
- // uri += `${options.obsidianPathType}=${encodeURIComponent(title)}`;
- // if (options.obsidianVault) uri += `&vault=${encodeURIComponent(options.obsidianVault)}`;
- // uri += `&content=${encodeURIComponent(markdown)}`;
- // let code = `window.location='${uri}'`;
- // await browser.tabs.executeScript(tabId, {code: code});
- // }
- // catch (error) {
- // // This could happen if the extension is not allowed to run code in
- // // the page, for example if the tab is a privileged page.
- // console.error("Failed to execute script: " + error);
- // };
-
- // }
- // download via content link
- else {
- try {
- await ensureScripts(tabId);
- const filename = mdClipsFolder + generateValidFileName(title, options.disallowedChars) + ".md";
- const code = `downloadMarkdown("${filename}","${base64EncodeUnicode(markdown)}");`
- await browser.tabs.executeScript(tabId, {code: code});
- }
- catch (error) {
- // This could happen if the extension is not allowed to run code in
- // the page, for example if the tab is a privileged page.
- console.error("Failed to execute script: " + error);
- };
- }
-}
-
-function downloadListener(id, url) {
- const self = (delta) => {
- if (delta.id === id && delta.state && delta.state.current == "complete") {
- // detatch this listener
- browser.downloads.onChanged.removeListener(self);
- //release the url for the blob
- URL.revokeObjectURL(url);
- }
- }
- return self;
-}
-
-function base64EncodeUnicode(str) {
- // Firstly, escape the string using encodeURIComponent to get the UTF-8 encoding of the characters,
- // Secondly, we convert the percent encodings into raw bytes, and add it to btoa() function.
- const utf8Bytes = encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) {
- return String.fromCharCode('0x' + p1);
- });
-
- return btoa(utf8Bytes);
-}
-
-//function that handles messages from the injected script into the site
-async function notify(message) {
- const options = await this.getOptions();
- // message for initial clipping of the dom
- if (message.type == "clip") {
- // get the article info from the passed in dom
- const article = await getArticleFromDom(message.dom);
-
- // if selection info was passed in (and we're to clip the selection)
- // replace the article content
- if (message.selection && message.clipSelection) {
- article.content = message.selection;
- }
-
- // convert the article to markdown
- const { markdown, imageList } = await convertArticleToMarkdown(article);
-
- // format the title
- article.title = await formatTitle(article);
-
- // format the mdClipsFolder
- const mdClipsFolder = await formatMdClipsFolder(article);
-
- // display the data in the popup
- await browser.runtime.sendMessage({ type: "display.md", markdown: markdown, article: article, imageList: imageList, mdClipsFolder: mdClipsFolder});
- }
- // message for triggering download
- else if (message.type == "download") {
- downloadMarkdown(message.markdown, message.title, message.tab.id, message.imageList, message.mdClipsFolder);
- }
-}
-
-browser.commands.onCommand.addListener(function (command) {
- const tab = browser.tabs.getCurrent()
- if (command == "download_tab_as_markdown") {
- const info = { menuItemId: "download-markdown-all" };
- downloadMarkdownFromContext(info, tab);
- }
- else if (command == "copy_tab_as_markdown") {
- const info = { menuItemId: "copy-markdown-all" };
- copyMarkdownFromContext(info, tab);
- }
- else if (command == "copy_selection_as_markdown") {
- const info = { menuItemId: "copy-markdown-selection" };
- copyMarkdownFromContext(info, tab);
- }
- else if (command == "copy_tab_as_markdown_link") {
- copyTabAsMarkdownLink(tab);
- }
- else if (command == "copy_selected_tab_as_markdown_link") {
- copySelectedTabAsMarkdownLink(tab);
- }
- else if (command == "copy_selection_to_obsidian") {
- const info = { menuItemId: "copy-markdown-obsidian" };
- copyMarkdownFromContext(info, tab);
- }
- else if (command == "copy_tab_to_obsidian") {
- const info = { menuItemId: "copy-markdown-obsall" };
- copyMarkdownFromContext(info, tab);
- }
-});
-
-// click handler for the context menus
-browser.contextMenus.onClicked.addListener(function (info, tab) {
- // one of the copy to clipboard commands
- if (info.menuItemId.startsWith("copy-markdown")) {
- copyMarkdownFromContext(info, tab);
- }
- else if (info.menuItemId == "download-markdown-alltabs" || info.menuItemId == "tab-download-markdown-alltabs") {
- downloadMarkdownForAllTabs(info);
- }
- // one of the download commands
- else if (info.menuItemId.startsWith("download-markdown")) {
- downloadMarkdownFromContext(info, tab);
- }
- // copy tab as markdown link
- else if (info.menuItemId.startsWith("copy-tab-as-markdown-link-all")) {
- copyTabAsMarkdownLinkAll(tab);
- }
- // copy only selected tab as markdown link
- else if (info.menuItemId.startsWith("copy-tab-as-markdown-link-selected")) {
- copySelectedTabAsMarkdownLink(tab);
- }
- else if (info.menuItemId.startsWith("copy-tab-as-markdown-link")) {
- copyTabAsMarkdownLink(tab);
- }
- // a settings toggle command
- else if (info.menuItemId.startsWith("toggle-") || info.menuItemId.startsWith("tabtoggle-")) {
- toggleSetting(info.menuItemId.split('-')[1]);
- }
-});
-
-// this function toggles the specified option
-async function toggleSetting(setting, options = null) {
- // if there's no options object passed in, we need to go get one
- if (options == null) {
- // get the options from storage and toggle the setting
- await toggleSetting(setting, await getOptions());
- }
- else {
- // toggle the option and save back to storage
- options[setting] = !options[setting];
- await browser.storage.sync.set(options);
- if (setting == "includeTemplate") {
- browser.contextMenus.update("toggle-includeTemplate", {
- checked: options.includeTemplate
- });
- try {
- browser.contextMenus.update("tabtoggle-includeTemplate", {
- checked: options.includeTemplate
- });
- } catch { }
- }
-
- if (setting == "downloadImages") {
- browser.contextMenus.update("toggle-downloadImages", {
- checked: options.downloadImages
- });
- try {
- browser.contextMenus.update("tabtoggle-downloadImages", {
- checked: options.downloadImages
- });
- } catch { }
- }
- }
-}
-
-// this function ensures the content script is loaded (and loads it if it isn't)
-async function ensureScripts(tabId) {
- const results = await browser.tabs.executeScript(tabId, { code: "typeof getSelectionAndDom === 'function';" })
- // The content script's last expression will be true if the function
- // has been defined. If this is not the case, then we need to run
- // pageScraper.js to define function getSelectionAndDom.
- if (!results || results[0] !== true) {
- await browser.tabs.executeScript(tabId, {file: "/contentScript/contentScript.js"});
- }
-}
-
-// get Readability article info from the dom passed in
-async function getArticleFromDom(domString) {
- // parse the dom
- const parser = new DOMParser();
- const dom = parser.parseFromString(domString, "text/html");
-
- if (dom.documentElement.nodeName == "parsererror") {
- console.error("error while parsing");
- }
-
- const math = {};
-
- const storeMathInfo = (el, mathInfo) => {
- let randomId = URL.createObjectURL(new Blob([]));
- randomId = randomId.substring(randomId.length - 36);
- el.id = randomId;
- math[randomId] = mathInfo;
- };
-
- dom.body.querySelectorAll('script[id^=MathJax-Element-]')?.forEach(mathSource => {
- const type = mathSource.attributes.type.value
- storeMathInfo(mathSource, {
- tex: mathSource.innerText,
- inline: type ? !type.includes('mode=display') : false
- });
- });
-
- dom.body.querySelectorAll('[markdownload-latex]')?.forEach(mathJax3Node => {
- const tex = mathJax3Node.getAttribute('markdownload-latex')
- const display = mathJax3Node.getAttribute('display')
- const inline = !(display && display === 'true')
-
- const mathNode = document.createElement(inline ? "i" : "p")
- mathNode.textContent = tex;
- mathJax3Node.parentNode.insertBefore(mathNode, mathJax3Node.nextSibling)
- mathJax3Node.parentNode.removeChild(mathJax3Node)
-
- storeMathInfo(mathNode, {
- tex: tex,
- inline: inline
- });
- });
-
- dom.body.querySelectorAll('.katex-mathml')?.forEach(kaTeXNode => {
- storeMathInfo(kaTeXNode, {
- tex: kaTeXNode.querySelector('annotation').textContent,
- inline: true
- });
- });
-
- dom.body.querySelectorAll('[class*=highlight-text],[class*=highlight-source]')?.forEach(codeSource => {
- const language = codeSource.className.match(/highlight-(?:text|source)-([a-z0-9]+)/)?.[1]
- if (codeSource.firstChild.nodeName == "PRE") {
- codeSource.firstChild.id = `code-lang-${language}`
- }
- });
-
- dom.body.querySelectorAll('[class*=language-]')?.forEach(codeSource => {
- const language = codeSource.className.match(/language-([a-z0-9]+)/)?.[1]
- codeSource.id = `code-lang-${language}`;
- });
-
- dom.body.querySelectorAll('pre br')?.forEach(br => {
- // we need to keep
tags because they are removed by Readability.js
- br.outerHTML = ' ';
- });
-
- dom.body.querySelectorAll('.codehilite > pre')?.forEach(codeSource => {
- if (codeSource.firstChild.nodeName !== 'CODE' && !codeSource.className.includes('language')) {
- codeSource.id = `code-lang-text`;
- }
- });
-
- dom.body.querySelectorAll('h1, h2, h3, h4, h5, h6')?.forEach(header => {
- // Readability.js will strip out headings from the dom if certain words appear in their className
- // See: https://github.com/mozilla/readability/issues/807
- header.className = '';
- header.outerHTML = header.outerHTML;
- });
-
- // Prevent Readability from removing the element if has a 'class' attribute
- // which matches removal criteria.
- // Note: The document element is guaranteed to be the HTML tag because the 'text/html'
- // mime type was used when the DOM was created.
- dom.documentElement.removeAttribute('class')
-
- // simplify the dom into an article
- const article = new Readability(dom).parse();
-
- // get the base uri from the dom and attach it as important article info
- article.baseURI = dom.baseURI;
- // also grab the page title
- article.pageTitle = dom.title;
- // and some URL info
- const url = new URL(dom.baseURI);
- article.hash = url.hash;
- article.host = url.host;
- article.origin = url.origin;
- article.hostname = url.hostname;
- article.pathname = url.pathname;
- article.port = url.port;
- article.protocol = url.protocol;
- article.search = url.search;
-
-
- // make sure the dom has a head
- if (dom.head) {
- // and the keywords, should they exist, as an array
- article.keywords = dom.head.querySelector('meta[name="keywords"]')?.content?.split(',')?.map(s => s.trim());
-
- // add all meta tags, so users can do whatever they want
- dom.head.querySelectorAll('meta[name][content], meta[property][content]')?.forEach(meta => {
- const key = (meta.getAttribute('name') || meta.getAttribute('property'))
- const val = meta.getAttribute('content')
- if (key && val && !article[key]) {
- article[key] = val;
- }
- })
- }
-
- article.math = math
-
- // return the article
- return article;
-}
-
-// get Readability article info from the content of the tab id passed in
-// `selection` is a bool indicating whether we should just get the selected text
-async function getArticleFromContent(tabId, selection = false) {
- // run the content script function to get the details
- const results = await browser.tabs.executeScript(tabId, { code: "getSelectionAndDom()" });
-
- // make sure we actually got a valid result
- if (results && results[0] && results[0].dom) {
- const article = await getArticleFromDom(results[0].dom, selection);
-
- // if we're to grab the selection, and we've selected something,
- // replace the article content with the selection
- if (selection && results[0].selection) {
- article.content = results[0].selection;
- }
-
- //return the article
- return article;
- }
- else return null;
-}
-
-// function to apply the title template
-async function formatTitle(article) {
- let options = await getOptions();
-
- let title = textReplace(options.title, article, options.disallowedChars + '/');
- title = title.split('/').map(s=>generateValidFileName(s, options.disallowedChars)).join('/');
- return title;
-}
-
-async function formatMdClipsFolder(article) {
- let options = await getOptions();
-
- let mdClipsFolder = '';
- if (options.mdClipsFolder && options.downloadMode == 'downloadsApi') {
- mdClipsFolder = textReplace(options.mdClipsFolder, article, options.disallowedChars);
- mdClipsFolder = mdClipsFolder.split('/').map(s => generateValidFileName(s, options.disallowedChars)).join('/');
- if (!mdClipsFolder.endsWith('/')) mdClipsFolder += '/';
- }
-
- return mdClipsFolder;
-}
-
-async function formatObsidianFolder(article) {
- let options = await getOptions();
-
- let obsidianFolder = '';
- if (options.obsidianFolder) {
- obsidianFolder = textReplace(options.obsidianFolder, article, options.disallowedChars);
- obsidianFolder = obsidianFolder.split('/').map(s => generateValidFileName(s, options.disallowedChars)).join('/');
- if (!obsidianFolder.endsWith('/')) obsidianFolder += '/';
- }
-
- return obsidianFolder;
-}
-
-// function to download markdown, triggered by context menu
-async function downloadMarkdownFromContext(info, tab) {
- await ensureScripts(tab.id);
- const article = await getArticleFromContent(tab.id, info.menuItemId == "download-markdown-selection");
- const title = await formatTitle(article);
- const { markdown, imageList } = await convertArticleToMarkdown(article);
- // format the mdClipsFolder
- const mdClipsFolder = await formatMdClipsFolder(article);
- await downloadMarkdown(markdown, title, tab.id, imageList, mdClipsFolder);
-
-}
-
-// function to copy a tab url as a markdown link
-async function copyTabAsMarkdownLink(tab) {
- try {
- await ensureScripts(tab.id);
- const article = await getArticleFromContent(tab.id);
- const title = await formatTitle(article);
- await browser.tabs.executeScript(tab.id, { code: `copyToClipboard("[${title}](${article.baseURI})")` });
- // await navigator.clipboard.writeText(`[${title}](${article.baseURI})`);
- }
- catch (error) {
- // This could happen if the extension is not allowed to run code in
- // the page, for example if the tab is a privileged page.
- console.error("Failed to copy as markdown link: " + error);
- };
-}
-
-// function to copy all tabs as markdown links
-async function copyTabAsMarkdownLinkAll(tab) {
- try {
- const options = await getOptions();
- options.frontmatter = options.backmatter = '';
- const tabs = await browser.tabs.query({
- currentWindow: true
- });
-
- const links = [];
- for(const tab of tabs) {
- await ensureScripts(tab.id);
- const article = await getArticleFromContent(tab.id);
- const title = await formatTitle(article);
- const link = `${options.bulletListMarker} [${title}](${article.baseURI})`
- links.push(link)
- };
-
- const markdown = links.join(`\n`)
- await browser.tabs.executeScript(tab.id, { code: `copyToClipboard(${JSON.stringify(markdown)})` });
-
- }
- catch (error) {
- // This could happen if the extension is not allowed to run code in
- // the page, for example if the tab is a privileged page.
- console.error("Failed to copy as markdown link: " + error);
- };
-}
-
-// function to copy only selected tabs as markdown links
-async function copySelectedTabAsMarkdownLink(tab) {
- try {
- const options = await getOptions();
- options.frontmatter = options.backmatter = '';
- const tabs = await browser.tabs.query({
- currentWindow: true,
- highlighted: true
- });
-
- const links = [];
- for (const tab of tabs) {
- await ensureScripts(tab.id);
- const article = await getArticleFromContent(tab.id);
- const title = await formatTitle(article);
- const link = `${options.bulletListMarker} [${title}](${article.baseURI})`
- links.push(link)
- };
-
- const markdown = links.join(`\n`)
- await browser.tabs.executeScript(tab.id, { code: `copyToClipboard(${JSON.stringify(markdown)})` });
-
- }
- catch (error) {
- // This could happen if the extension is not allowed to run code in
- // the page, for example if the tab is a privileged page.
- console.error("Failed to copy as markdown link: " + error);
- };
-}
-
-// function to copy markdown to the clipboard, triggered by context menu
-async function copyMarkdownFromContext(info, tab) {
- try{
- await ensureScripts(tab.id);
-
- const platformOS = navigator.platform;
- var folderSeparator = "";
- if(platformOS.indexOf("Win") === 0){
- folderSeparator = "\\";
- }else{
- folderSeparator = "/";
- }
-
- if (info.menuItemId == "copy-markdown-link") {
- const options = await getOptions();
- options.frontmatter = options.backmatter = '';
- const article = await getArticleFromContent(tab.id, false);
- const { markdown } = turndown(`${info.linkText || info.selectionText}`, { ...options, downloadImages: false }, article);
- await browser.tabs.executeScript(tab.id, {code: `copyToClipboard(${JSON.stringify(markdown)})`});
- }
- else if (info.menuItemId == "copy-markdown-image") {
- await browser.tabs.executeScript(tab.id, {code: `copyToClipboard("")`});
- }
- else if(info.menuItemId == "copy-markdown-obsidian") {
- const article = await getArticleFromContent(tab.id, info.menuItemId == "copy-markdown-obsidian");
- const title = await formatTitle(article);
- const options = await getOptions();
- const obsidianVault = options.obsidianVault;
- const obsidianFolder = await formatObsidianFolder(article);
- const { markdown } = await convertArticleToMarkdown(article, downloadImages = false);
- await browser.tabs.executeScript(tab.id, { code: `copyToClipboard(${JSON.stringify(markdown)})` });
- await chrome.tabs.update({url: "obsidian://advanced-uri?vault=" + obsidianVault + "&clipboard=true&mode=new&filepath=" + obsidianFolder + generateValidFileName(title)});
- }
- else if(info.menuItemId == "copy-markdown-obsall") {
- const article = await getArticleFromContent(tab.id, info.menuItemId == "copy-markdown-obsall");
- const title = await formatTitle(article);
- const options = await getOptions();
- const obsidianVault = options.obsidianVault;
- const obsidianFolder = await formatObsidianFolder(article);
- const { markdown } = await convertArticleToMarkdown(article, downloadImages = false);
- await browser.tabs.executeScript(tab.id, { code: `copyToClipboard(${JSON.stringify(markdown)})` });
- await browser.tabs.update({url: "obsidian://advanced-uri?vault=" + obsidianVault + "&clipboard=true&mode=new&filepath=" + obsidianFolder + generateValidFileName(title)});
- }
- else {
- const article = await getArticleFromContent(tab.id, info.menuItemId == "copy-markdown-selection");
- const { markdown } = await convertArticleToMarkdown(article, downloadImages = false);
- await browser.tabs.executeScript(tab.id, { code: `copyToClipboard(${JSON.stringify(markdown)})` });
- }
- }
- catch (error) {
- // This could happen if the extension is not allowed to run code in
- // the page, for example if the tab is a privileged page.
- console.error("Failed to copy text: " + error);
- };
-}
-
-async function downloadMarkdownForAllTabs(info) {
- const tabs = await browser.tabs.query({
- currentWindow: true
- });
- tabs.forEach(tab => {
- downloadMarkdownFromContext(info, tab);
- });
-}
-
-/**
- * String.prototype.replaceAll() polyfill
- * https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/
- * @author Chris Ferdinandi
- * @license MIT
- */
-if (!String.prototype.replaceAll) {
- String.prototype.replaceAll = function(str, newStr){
-
- // If a regex pattern
- if (Object.prototype.toString.call(str).toLowerCase() === '[object regexp]') {
- return this.replace(str, newStr);
- }
-
- // If a string
- return this.replace(new RegExp(str, 'g'), newStr);
-
- };
-}
diff --git a/src/background/service-worker.js b/src/background/service-worker.js
index ee6afd36..07e425e4 100644
--- a/src/background/service-worker.js
+++ b/src/background/service-worker.js
@@ -220,7 +220,8 @@ async function createMenus() {
}
if (options.contextMenus) {
- const menuItems = [
+ // Page context menus
+ const pageMenuItems = [
{
id: "download-markdown-alltabs",
title: "Download All Tabs as Markdown",
@@ -272,28 +273,20 @@ async function createMenus() {
contexts: ["all"]
},
{
- id: "separator-2",
- type: "separator",
+ id: "copy-tab-as-markdown-link-all",
+ title: "Copy All Tab URLs as Markdown Link List",
contexts: ["all"]
},
{
- id: "toggle-includeTemplate",
- type: "checkbox",
- title: "Include front/back template",
- contexts: ["all"],
- checked: options.includeTemplate
- },
- {
- id: "toggle-downloadImages",
- type: "checkbox",
- title: "Download Images",
- contexts: ["all"],
- checked: options.downloadImages
+ id: "copy-tab-as-markdown-link-selected",
+ title: "Copy Selected Tab URLs as Markdown Link List",
+ contexts: ["all"]
}
];
+ // Add Obsidian integration menus if enabled
if (options.obsidianIntegration) {
- menuItems.splice(-2, 0,
+ pageMenuItems.push(
{
id: "copy-markdown-obsidian",
title: "Send Text selection to Obsidian",
@@ -303,23 +296,43 @@ async function createMenus() {
id: "copy-markdown-obsall",
title: "Send Tab to Obsidian",
contexts: ["all"]
- },
- {
- id: "separator-3",
- type: "separator",
- contexts: ["all"]
}
);
}
- for (const item of menuItems) {
+ // Add separator and toggle options
+ pageMenuItems.push(
+ {
+ id: "separator-2",
+ type: "separator",
+ contexts: ["all"]
+ },
+ {
+ id: "toggle-includeTemplate",
+ type: "checkbox",
+ title: "Include front/back template",
+ contexts: ["all"],
+ checked: options.includeTemplate
+ },
+ {
+ id: "toggle-downloadImages",
+ type: "checkbox",
+ title: "Download Images",
+ contexts: ["all"],
+ checked: options.downloadImages
+ }
+ );
+
+ // Create page context menus
+ for (const item of pageMenuItems) {
try {
await chrome.contextMenus.create(item);
- console.log('MarkDownload: Created menu item:', item.id, '- Title:', item.title);
+ console.log('MarkDownload: Created page menu item:', item.id, '- Title:', item.title);
} catch (e) {
- console.warn('MarkDownload: Failed to create menu item:', item.id, e);
+ console.warn('MarkDownload: Failed to create page menu item:', item.id, e);
}
}
+
console.log('MarkDownload: Context menus created successfully');
// Test: List all created menus for debugging
@@ -394,6 +407,10 @@ async function handleMessage(message, sender, sendResponse) {
const tabId = message.tabId || sender.tab?.id;
await downloadMarkdown(message.markdown, message.title, tabId, message.imageList, message.mdClipsFolder);
sendResponse({ success: true });
+ } else if (message.type === "recreateMenus") {
+ console.log('MarkDownload: Recreating context menus');
+ await createMenus();
+ sendResponse({ success: true });
} else {
sendResponse({ error: 'Unknown message type: ' + message.type });
}
diff --git a/src/options/options.html b/src/options/options.html
index c7dae766..c767b863 100644
--- a/src/options/options.html
+++ b/src/options/options.html
@@ -377,7 +377,6 @@ Import / Export
-
diff --git a/src/options/options.js b/src/options/options.js
index eb839293..ab342d9b 100644
--- a/src/options/options.js
+++ b/src/options/options.js
@@ -47,20 +47,10 @@ const save = () => {
chrome.contextMenus.update("toggle-includeTemplate", {
checked: options.includeTemplate
});
- try {
- chrome.contextMenus.update("tabtoggle-includeTemplate", {
- checked: options.includeTemplate
- });
- } catch { }
chrome.contextMenus.update("toggle-downloadImages", {
checked: options.downloadImages
});
- try {
- chrome.contextMenus.update("tabtoggle-downloadImages", {
- checked: options.downloadImages
- });
- } catch { }
})
.then(() => {
document.querySelectorAll(".status").forEach(statusEl => {
@@ -202,7 +192,8 @@ const inputChange = e => {
options = JSON.parse(lines);
setCurrentChoice(options);
chrome.contextMenus.removeAll()
- createMenus()
+ // Notify service worker to recreate menus
+ chrome.runtime.sendMessage({ type: "recreateMenus" }).catch(() => {});
save();
refereshElements();
};
@@ -213,7 +204,10 @@ const inputChange = e => {
options[key] = value;
if (key == "contextMenus") {
- if (value) { createMenus() }
+ if (value) {
+ // Notify service worker to recreate menus
+ chrome.runtime.sendMessage({ type: "recreateMenus" }).catch(() => {});
+ }
else { chrome.contextMenus.removeAll() }
}
diff --git a/src/popup/popup.js b/src/popup/popup.js
index c19d0ad0..aac5396b 100644
--- a/src/popup/popup.js
+++ b/src/popup/popup.js
@@ -49,7 +49,16 @@ const toggleClipSelection = options => {
options.clipSelection = !options.clipSelection;
document.querySelector("#selected").classList.toggle("checked");
document.querySelector("#document").classList.toggle("checked");
- chrome.storage.sync.set(options).then(() => clipSite()).catch((error) => {
+ chrome.storage.sync.set(options).then(() => {
+ // Get current tab ID and call clipSite
+ chrome.tabs.query({ currentWindow: true, active: true }).then(tabs => {
+ if (tabs[0]) {
+ clipSite(tabs[0].id);
+ }
+ }).catch(error => {
+ console.error('Failed to get tab ID:', error);
+ });
+ }).catch((error) => {
console.error(error);
});
}
@@ -61,12 +70,14 @@ const toggleIncludeTemplate = options => {
chrome.contextMenus.update("toggle-includeTemplate", {
checked: options.includeTemplate
});
- try {
- chrome.contextMenus.update("tabtoggle-includeTemplate", {
- checked: options.includeTemplate
- });
- } catch { }
- return clipSite()
+ // Get current tab ID and call clipSite
+ chrome.tabs.query({ currentWindow: true, active: true }).then(tabs => {
+ if (tabs[0]) {
+ clipSite(tabs[0].id);
+ }
+ }).catch(error => {
+ console.error('Failed to get tab ID:', error);
+ });
}).catch((error) => {
console.error(error);
});
@@ -79,11 +90,6 @@ const toggleDownloadImages = options => {
chrome.contextMenus.update("toggle-downloadImages", {
checked: options.downloadImages
});
- try {
- chrome.contextMenus.update("tabtoggle-downloadImages", {
- checked: options.downloadImages
- });
- } catch { }
}).catch((error) => {
console.error(error);
});
diff --git a/src/shared/context-menus.js b/src/shared/context-menus.js
deleted file mode 100644
index 50ede958..00000000
--- a/src/shared/context-menus.js
+++ /dev/null
@@ -1,174 +0,0 @@
-// create the context menus
-async function createMenus() {
- const options = await getOptions();
-
- chrome.contextMenus.removeAll();
-
- if (options.contextMenus) {
-
- // tab menu (chrome does not support this)
- try {
- chrome.contextMenus.create({
- id: "download-markdown-tab",
- title: "Download Tab as Markdown",
- contexts: ["tab"]
- }, () => { });
-
- chrome.contextMenus.create({
- id: "tab-download-markdown-alltabs",
- title: "Download All Tabs as Markdown",
- contexts: ["tab"]
- }, () => { });
-
- chrome.contextMenus.create({
- id: "copy-tab-as-markdown-link-tab",
- title: "Copy Tab URL as Markdown Link",
- contexts: ["tab"]
- }, () => { });
-
- chrome.contextMenus.create({
- id: "copy-tab-as-markdown-link-all-tab",
- title: "Copy All Tab URLs as Markdown Link List",
- contexts: ["tab"]
- }, () => { });
-
- chrome.contextMenus.create({
- id: "copy-tab-as-markdown-link-selected-tab",
- title: "Copy Selected Tab URLs as Markdown Link List",
- contexts: ["tab"]
- }, () => { });
-
- chrome.contextMenus.create({
- id: "tab-separator-1",
- type: "separator",
- contexts: ["tab"]
- }, () => { });
-
- chrome.contextMenus.create({
- id: "tabtoggle-includeTemplate",
- type: "checkbox",
- title: "Include front/back template",
- contexts: ["tab"],
- checked: options.includeTemplate
- }, () => { });
-
- chrome.contextMenus.create({
- id: "tabtoggle-downloadImages",
- type: "checkbox",
- title: "Download Images",
- contexts: ["tab"],
- checked: options.downloadImages
- }, () => { });
- } catch {
-
- }
- // add the download all tabs option to the page context menu as well
- chrome.contextMenus.create({
- id: "download-markdown-alltabs",
- title: "Download All Tabs as Markdown",
- contexts: ["all"]
- }, () => { });
- chrome.contextMenus.create({
- id: "separator-0",
- type: "separator",
- contexts: ["all"]
- }, () => { });
-
- // download actions
- chrome.contextMenus.create({
- id: "download-markdown-selection",
- title: "Download Selection As Markdown",
- contexts: ["selection"]
- }, () => { });
- chrome.contextMenus.create({
- id: "download-markdown-all",
- title: "Download Tab As Markdown",
- contexts: ["all"]
- }, () => { });
-
- chrome.contextMenus.create({
- id: "separator-1",
- type: "separator",
- contexts: ["all"]
- }, () => { });
-
- // copy to clipboard actions
- chrome.contextMenus.create({
- id: "copy-markdown-selection",
- title: "Copy Selection As Markdown",
- contexts: ["selection"]
- }, () => { });
- chrome.contextMenus.create({
- id: "copy-markdown-link",
- title: "Copy Link As Markdown",
- contexts: ["link"]
- }, () => { });
- chrome.contextMenus.create({
- id: "copy-markdown-image",
- title: "Copy Image As Markdown",
- contexts: ["image"]
- }, () => { });
- chrome.contextMenus.create({
- id: "copy-markdown-all",
- title: "Copy Tab As Markdown",
- contexts: ["all"]
- }, () => { });
- chrome.contextMenus.create({
- id: "copy-tab-as-markdown-link",
- title: "Copy Tab URL as Markdown Link",
- contexts: ["all"]
- }, () => { });
- chrome.contextMenus.create({
- id: "copy-tab-as-markdown-link-all",
- title: "Copy All Tab URLs as Markdown Link List",
- contexts: ["all"]
- }, () => { });
- chrome.contextMenus.create({
- id: "copy-tab-as-markdown-link-selected",
- title: "Copy Selected Tab URLs as Markdown Link List",
- contexts: ["all"]
- }, () => { });
-
- chrome.contextMenus.create({
- id: "separator-2",
- type: "separator",
- contexts: ["all"]
- }, () => { });
-
- if(options.obsidianIntegration){
- // copy to clipboard actions
- chrome.contextMenus.create({
- id: "copy-markdown-obsidian",
- title: "Send Text selection to Obsidian",
- contexts: ["selection"]
- }, () => { });
- chrome.contextMenus.create({
- id: "copy-markdown-obsall",
- title: "Send Tab to Obsidian",
- contexts: ["all"]
- }, () => { });
- }
- chrome.contextMenus.create({
- id: "separator-3",
- type: "separator",
- contexts: ["all"]
- }, () => { });
-
- // options
- chrome.contextMenus.create({
- id: "toggle-includeTemplate",
- type: "checkbox",
- title: "Include front/back template",
- contexts: ["all"],
- checked: options.includeTemplate
- }, () => { });
-
- chrome.contextMenus.create({
- id: "toggle-downloadImages",
- type: "checkbox",
- title: "Download Images",
- contexts: ["all"],
- checked: options.downloadImages
- }, () => { });
- }
-}
\ No newline at end of file
From 400ebb11ff514ea0d0ff8dfaa1fae3ef99d0455e Mon Sep 17 00:00:00 2001
From: TianleLin <940428567qq@gmail.com>
Date: Thu, 11 Sep 2025 18:39:09 +0800
Subject: [PATCH 27/29] optimize popup
---
src/background/service-worker.js | 43 ++
src/options/options.html | 2 +-
src/popup/popup.css | 1009 ++++++++++++++++++++++++++----
src/popup/popup.html | 143 ++++-
src/popup/popup.js | 697 ++++++++++++++++++---
5 files changed, 1658 insertions(+), 236 deletions(-)
diff --git a/src/background/service-worker.js b/src/background/service-worker.js
index 07e425e4..4b32bc04 100644
--- a/src/background/service-worker.js
+++ b/src/background/service-worker.js
@@ -411,6 +411,49 @@ async function handleMessage(message, sender, sendResponse) {
console.log('MarkDownload: Recreating context menus');
await createMenus();
sendResponse({ success: true });
+ } else if (message.type === "sendToObsidian") {
+ try {
+ console.log('MarkDownload: Received sendToObsidian request from popup');
+ const tabId = message.tabId || sender.tab?.id;
+ if (!tabId) {
+ sendResponse({ error: 'No active tab' });
+ return;
+ }
+ // Ensure content script and copy markdown to clipboard
+ await ensureContentScript(tabId);
+ await executeScript(tabId, (markdown) => copyToClipboard(markdown), [message.markdown]);
+ // Build Obsidian URI using options supplied or stored
+ const options = message.options || await getOptions();
+ if (!options.obsidianIntegration) {
+ sendResponse({ error: 'Obsidian integration not enabled' });
+ return;
+ }
+ const folder = options.obsidianFolder || '';
+ const vault = options.obsidianVault || '';
+ let obsidianFolder = '';
+ if (folder) {
+ const article = { title: message.title || 'Untitled', baseURI: '' };
+ obsidianFolder = textReplace(folder, article, options.disallowedChars);
+ obsidianFolder = obsidianFolder.split('/').map(s => generateValidFileName(s, options.disallowedChars)).join('/');
+ if (!obsidianFolder.endsWith('/')) obsidianFolder += '/';
+ }
+ const filename = generateValidFileName(message.title || 'Untitled', options.disallowedChars);
+ const filepath = obsidianFolder + filename;
+ const obsidianUri = 'obsidian://advanced-uri?vault=' + encodeURIComponent(vault) + '&clipboard=true&mode=new&filepath=' + encodeURIComponent(filepath);
+ try {
+ await chrome.tabs.update(tabId, { url: obsidianUri });
+ } catch (err) {
+ try {
+ await chrome.tabs.create({ url: obsidianUri });
+ } catch (err2) {
+ console.error('MarkDownload: Failed to open Obsidian URI:', err2);
+ }
+ }
+ sendResponse({ success: true });
+ } catch (err) {
+ console.error('MarkDownload: sendToObsidian handler error:', err);
+ sendResponse({ error: err?.message || String(err) });
+ }
} else {
sendResponse({ error: 'Unknown message type: ' + message.type });
}
diff --git a/src/options/options.html b/src/options/options.html
index c767b863..f3bec89e 100644
--- a/src/options/options.html
+++ b/src/options/options.html
@@ -163,7 +163,7 @@ Obsidian Path Type
-
+
diff --git a/src/popup/popup.css b/src/popup/popup.css
index 2d7ef0c7..ee45f02f 100644
--- a/src/popup/popup.css
+++ b/src/popup/popup.css
@@ -1,207 +1,998 @@
:root {
- --focus: orange;
- --bg: #fff;/*#0A001F;*/
- --fg: #000;
- --border: #eee;
+ /* Color Palette */
--primary: #4A90E2;
- --fg-primary: #eee;
- --bg-primary: #ECF5FF;
- --border-primary: #4A90E2;
- --font-size: 14px;
+ --primary-hover: #357ABD;
+ --primary-light: #ECF5FF;
+ --secondary: #6C757D;
+ --success: #28A745;
+ --warning: #FFC107;
+ --danger: #DC3545;
+
+ /* Light Theme */
+ --bg: #FFFFFF;
+ --bg-secondary: #F8F9FA;
+ --bg-tertiary: #E9ECEF;
+ --fg: #212529;
+ --fg-secondary: #6C757D;
+ --fg-muted: #ADB5BD;
+ --border: #DEE2E6;
+ --border-light: #E9ECEF;
+ --shadow: rgba(0, 0, 0, 0.1);
+ --shadow-hover: rgba(0, 0, 0, 0.15);
+
+ /* Typography */
+ --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
+ --font-size-xs: 11px;
+ --font-size-sm: 12px;
+ --font-size-base: 14px;
+ --font-size-lg: 16px;
+ --font-size-xl: 18px;
+ --font-weight-normal: 400;
+ --font-weight-medium: 500;
+ --font-weight-bold: 600;
+
+ /* Spacing */
+ --spacing-xs: 4px;
+ --spacing-sm: 8px;
+ --spacing-md: 12px;
+ --spacing-lg: 16px;
+ --spacing-xl: 20px;
+ --spacing-xxl: 24px;
+
+ /* Border Radius */
+ --radius-sm: 4px;
+ --radius-md: 8px;
+ --radius-lg: 12px;
+ --radius-xl: 16px;
+
+ /* Transitions */
+ --transition-fast: 0.15s ease;
+ --transition-normal: 0.3s ease;
+ --transition-slow: 0.5s ease;
}
-@media(prefers-color-scheme: dark) {
+/* Dark Theme */
+@media (prefers-color-scheme: dark) {
:root {
- --focus: orange;
- --bg: #2A2A2E;
- --fg: #f8f8f8;
- --border: #555;
- --primary: #4A90E2;
- --fg-primary: #eee;
- --bg-primary: hsl(212, 80%, 30%);
- --border-primary: #4A90E2;
- --pre-bg: #474749;
+ --bg: #1A1D23;
+ --bg-secondary: #2D3139;
+ --bg-tertiary: #3A3F4A;
+ --fg: #FFFFFF;
+ --fg-secondary: #B8BCC5;
+ --fg-muted: #8B949E;
+ --border: #404956;
+ --border-light: #404956;
+ --shadow: rgba(0, 0, 0, 0.3);
+ --shadow-hover: rgba(0, 0, 0, 0.4);
+ --primary-light: rgba(74, 144, 226, 0.1);
}
+
.cm-s-xq-dark.CodeMirror {
- background-color: var(--bg);
+ background-color: var(--bg-secondary);
+ border-color: var(--border);
}
}
-/* Works on Firefox */
+/* Base Styles */
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+*:focus {
+ outline: 2px solid var(--primary);
+ outline-offset: 2px;
+}
+
+/* Scrollbar Styles */
* {
scrollbar-width: thin;
- scrollbar-color: var(--pre-bg) var(--bg);
+ scrollbar-color: var(--border) transparent;
}
-/* Works on Chrome, Edge, and Safari */
*::-webkit-scrollbar {
- width: 12px;
+ width: 6px;
+ height: 6px;
}
*::-webkit-scrollbar-track {
- background: var(--bg);
+ background: transparent;
}
*::-webkit-scrollbar-thumb {
background-color: var(--border);
border-radius: 20px;
+ border: none;
}
-* {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
+*::-webkit-scrollbar-thumb:hover {
+ background-color: var(--fg-muted);
}
+/* Body */
body {
- width: 350px;
- height: 500px;
- border-radius: 0.5em;
+ width: 500px;
+ height: 650px;
+ font-family: var(--font-family);
+ font-size: var(--font-size-base);
+ background: var(--bg);
+ color: var(--fg);
overflow: hidden;
- font-family: sans-serif;
+ position: relative;
+ margin: 0;
+ padding: 0;
+}
+
+/* Loading Overlay */
+.loading-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
background: var(--bg);
- font-size: var(--font-size);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+ backdrop-filter: blur(4px);
}
-@keyframes spinner {
- to {
- transform: rotate(360deg);
- }
+.spinner-container {
+ text-align: center;
}
-* {
- box-sizing: border-box;
+
+.spinner {
+ width: 32px;
+ height: 32px;
+ border: 3px solid var(--border-light);
+ border-top: 3px solid var(--primary);
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+ margin: 0 auto var(--spacing-md);
}
-*:focus {
- border: 3px dotted var(--focus-color) !important;
+.spinner-container p {
+ color: var(--fg-secondary);
+ font-size: var(--font-size-sm);
+ margin: 0;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+/* Main Container */
+.main-container {
+ display: none;
+ flex-direction: column;
+ height: 100vh;
+ max-height: 650px;
+ overflow: hidden;
+ position: relative;
+}
+
+/* Header */
+.header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: var(--spacing-md) var(--spacing-lg) var(--spacing-sm);
+ background: linear-gradient(135deg, var(--primary) 0%, var(--primary-hover) 100%);
+ color: white;
+ position: relative;
+ overflow: hidden;
+ flex: 0 0 auto; /* fixed height section */
}
-#spinner:before {
+.header::before {
content: '';
position: absolute;
- top: 50%;
- left: 50%;
- width: 20px;
- height: 20px;
- margin-top: -10px;
- margin-left: -10px;
- border-radius: 50%;
- border-top: 2px solid var(--primary);
- border-right: 2px solid transparent;
- animation: spinner .6s linear infinite;
+ top: -50%;
+ left: -50%;
+ width: 200%;
+ height: 200%;
+ background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
+ animation: headerGlow 8s ease-in-out infinite alternate;
}
-.CodeMirror {
- border: none;
- padding: 0.5em;
- flex: 1;
+.header::after {
+ content: '';
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: 1px;
+ background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.3) 50%, transparent 100%);
+ z-index: 1;
}
-input#title {
- padding: 0.5em;
- color: var(--fg);
- background-color: var(--bg);
- border: 1px solid var(--border);
- font-size: 1.01em;
+@keyframes headerGlow {
+ 0% {
+ transform: translate(-10%, -10%) rotate(0deg);
+ opacity: 0.5;
+ }
+ 100% {
+ transform: translate(10%, 10%) rotate(5deg);
+ opacity: 0.8;
+ }
}
+.logo {
+ position: relative;
+ z-index: 2;
+}
+
+.header-actions {
+ position: relative;
+ z-index: 2;
+}
+
+.logo h1 {
+ font-size: var(--font-size-xl);
+ font-weight: var(--font-weight-bold);
+ margin: 0;
+ letter-spacing: -0.5px;
+}
+
+.logo .subtitle {
+ font-size: var(--font-size-xs);
+ opacity: 0.8;
+ font-weight: var(--font-weight-normal);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.header-actions {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-sm);
+}
+
+.icon-button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 32px;
+ height: 32px;
+ background: rgba(255, 255, 255, 0.1);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ border-radius: var(--radius-md);
+ color: white;
+ text-decoration: none;
+ transition: var(--transition-fast);
+}
+
+.icon-button:hover {
+ background: rgba(255, 255, 255, 0.2);
+ transform: translateY(-1px);
+}
+
+/* --- Legacy structure compatibility (anchor-based UI) --- */
+/* Container + spinner for old markup */
#container {
width: 100%;
height: 100%;
display: none;
flex-direction: column;
}
+
+/* Removed duplicate spinner - use .loading-overlay .spinner instead */
+
.row {
+ display: flex;
flex-direction: row;
- display:flex;
}
+/* Anchor buttons */
a.button {
display: block;
- padding: 0.5em 0;
+ padding: 0.5em 0.75em;
background-color: var(--primary);
- color: var(--fg-primary);
+ color: #fff;
text-align: center;
text-decoration: none;
- line-height: 1em;
- border: 1px solid var(--bg);
- border-collapse: collapse;
+ line-height: 1.2em;
+ border: 1px solid var(--primary);
+ border-radius: var(--radius-md);
+ margin: 6px 8px 0;
+ transition: var(--transition-fast);
+}
+
+a.button:hover {
+ background-color: var(--primary-hover);
+ border-color: var(--primary-hover);
}
+/* Toggle anchors */
+.button.toggle {
+ position: relative;
+ background: var(--bg);
+ color: var(--fg);
+ border: 1px solid var(--border);
+ box-shadow: 0 2px 4px var(--shadow);
+}
+
+.button.toggle:hover {
+ box-shadow: 0 4px 8px var(--shadow-hover);
+}
+
+.button.toggle.checked {
+ background: var(--primary-light);
+ border-color: var(--primary);
+}
+
+.button.toggle.active {
+ background: var(--primary-light);
+ border-color: var(--primary);
+}
+
+/* Leading check dot inside toggle anchors */
+.check {
+ background: var(--border-light);
+ border-radius: 50%;
+ content: '';
+ height: 18px;
+ left: 10px;
+ position: absolute;
+ top: calc(50% - 9px);
+ width: 18px;
+}
+
+.checked .check {
+ background-color: var(--primary);
+ background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHdpZHRoPSIyNiIgaGVpZ2h0PSIyMCIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIyLjAyOTY4IC00MC4wOTAzIDI2IDIwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik0yNy45NzQzLC0zNi4xMjcyYzAsMC40NDY0MjggLTAuMTU2MjUsMC44MjU4OTMgLTAuNDY4NzUsMS4xMzgzOWwtMTIuMTIwNSwxMi4xMjA1bC0yLjI3Njc5LDIuMjc2NzljLTAuMzEyNSwwLjMxMjUgLTAuNjkxOTY0LDAuNDY4NzUgLTEuMTM4MzksMC40Njg3NWMtMC40NDY0MjgsMCAtMC44MjU4OTMsLTAuMTU2MjUgLTEuMTM4MzksLTAuNDY4NzVsLTIuMjc2NzksLTIuMjc2NzlMLTYuMDYwMjcsLTYuMDYwMjdjLTAuMzEyNSwtMC4zMTI1IC0wLjQ2ODc1LC0wLjY5MTk2NSAtMC40Njg3NSwtMS4xMzgzOWMwLC0wLjQ0NjQyOSAwLjE1NjI1LC0wLjgyNTg5MyAwLjQ2ODc1LC0xLjEzODM5bDIuMjc2NzksLTIuMjc2NzlDMTAuMzEyNSwtNDIuMzEyNSAxMC42ODc5LC00Mi40Njg3NSAxMS4xMzgzOSwtNDIuNDY4NzVjMC40NDY0MjksMCAwLjgyNTg5MywwLjE1NjI1IDEuMTM4MzksMC40Njg3NWw0LjkyMTg4LDQuOTM4NjJsMTAuOTgyMSwtMTAuOTk4OWMwLjMxMjUsLTAuMzEyNSAwLjY5MTk2NCwtMC40Njg3NSAxLjEzODM5LC0wLjQ2ODc1YzAuNDQ2NDI4LDAgMC44MjU4OTMsMC4xNTYyNSAxLjEzODM5LDAuNDY4NzVsMi4yNzY3OCwyLjI3Njc5YzAuMzEyNSwwLjMxMjUgMC40Njg3NSwwLjY5MTk2NCAwLjQ2ODc1LDEuMTM4MzlaIiBmaWxsPSIjZmZmIi8+PC9zdmc+');
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: 12px;
+}
+
+.active .check {
+ background-color: var(--primary);
+ background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHdpZHRoPSIyNiIgaGVpZ2h0PSIyMCIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIyLjAyOTY4IC00MC4wOTAzIDI2IDIwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik0yNy45NzQzLC0zNi4xMjcyYzAsMC40NDY0MjggLTAuMTU2MjUsMC44MjU4OTMgLTAuNDY4NzUsMS4xMzgzOWwtMTIuMTIwNSwxMi4xMjA1bC0yLjI3Njc5LDIuMjc2NzljLTAuMzEyNSwwLjMxMjUgLTAuNjkxOTY0LDAuNDY4NzUgLTEuMTM4MzksMC40Njg3NWMtMC40NDY0MjgsMCAtMC44MjU4OTMsLTAuMTU2MjUgLTEuMTM4MzksLTAuNDY4NzVsLTIuMjc2NzksLTIuMjc2NzlMLTYuMDYwMjcsLTYuMDYwMjdjLTAuMzEyNSwtMC4zMTI1IC0wLjQ2ODc1LC0wLjY5MTk2NSAtMC40Njg3NSwtMS4xMzgzOWMwLC0wLjQ0NjQyOSAwLjE1NjI1LC0wLjgyNTg5MyAwLjQ2ODc1LC0xLjEzODM5bDIuMjc2NzksLTIuMjc2NzlDMTAuMzEyNSwtNDIuMzEyNSAxMC42ODc5LC00Mi40Njg3NSAxMS4xMzgzOSwtNDIuNDY4NzVjMC40NDY0MjksMCAwLjgyNTg5MywwLjE1NjI1IDEuMTM4MzksMC40Njg3NWw0LjkyMTg4LDQuOTM4NjJsMTAuOTgyMSwtMTAuOTk4OWMwLjMxMjUsLTAuMzEyNSAwLjY5MTk2NCwtMC40Njg3NSAxLjEzODM5LC0wLjQ2ODc1YzAuNDQ2NDI4LDAgMC44MjU4OTMsMC4xNTYyNSAxLjEzODM5LDAuNDY4NzVsMi4yNzY3OCwyLjI3Njc5YzAuMzEyNSwwLjMxMjUgMC40Njg3NSwwLjY5MTk2NCAwLjQ2ODc1LDEuMTM4MzlaIiBmaWxsPSIjZmZmIi8+PC9zdmc+');
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: 12px;
+}
+
+/* Layout details for legacy */
+#selected, #document { flex: 1; }
+#clipOption { display: none; }
+#downloadSelection { display: none; background-color: var(--primary-light); color: var(--fg); border-color: var(--primary); }
+
+/* Gear options button in legacy UI */
#options {
display: block;
position: absolute;
- top:2px;
- right:2px;
+ top: 6px;
+ right: 6px;
font-size: 0;
text-decoration: none;
background-color: var(--primary);
- color: var(--fg-primary);
- border-radius: 2rem;
+ color: #fff;
+ border-radius: 1rem;
width: 2rem;
height: 2rem;
text-align: center;
line-height: 2rem;
- font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", sans-serif;
}
-#options::before {
- content: '⚙';
- font-size: 1rem;
+#options::before { content: '⚙'; font-size: 1rem; }
+
+/* Title input in legacy UI */
+input#title {
+ margin: 8px;
+ padding: 0.5em;
+ color: var(--fg);
+ background-color: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ font-size: 1.01em;
+}
+
+/* Quick Settings */
+.quick-settings {
+ background: var(--bg-secondary);
+ border-bottom: 1px solid var(--border);
+ padding: var(--spacing-sm) var(--spacing-md);
+ flex: 0 0 auto; /* fixed height section */
+}
+
+.quick-settings h3 {
+ font-size: var(--font-size-sm);
+ font-weight: var(--font-weight-medium);
+ margin-bottom: var(--spacing-sm);
+ color: var(--fg);
+}
+
+.settings-grid {
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-sm);
+}
+
+.setting-item {
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-sm);
+}
+
+.setting-item label:first-child {
+ font-size: var(--font-size-sm);
+ font-weight: var(--font-weight-medium);
+ color: var(--fg);
+}
+
+/* Toggle Switch */
+.toggle-switch {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-md);
+ cursor: pointer;
+ user-select: none;
}
-#downloadSelection {
- background-color: var(--bg-primary);
+.toggle-switch input[type="checkbox"] {
display: none;
}
-#selected, #document {
- flex:1;
+.slider {
+ position: relative;
+ width: 44px;
+ height: 24px;
+ background: var(--border);
+ border-radius: 12px;
+ transition: var(--transition-normal);
+ flex-shrink: 0;
}
-#clipOption {
- display:none;
+.slider::before {
+ content: '';
+ position: absolute;
+ top: 2px;
+ left: 2px;
+ width: 20px;
+ height: 20px;
+ background: white;
+ border-radius: 50%;
+ transition: var(--transition-normal);
+ box-shadow: 0 2px 4px var(--shadow);
}
+.toggle-switch input:checked + .slider {
+ background: var(--primary);
+}
+.toggle-switch input:checked + .slider::before {
+ transform: translateX(20px);
+}
-.button.toggle {
+.toggle-switch .label {
+ font-size: var(--font-size-sm);
+ font-weight: var(--font-weight-normal);
+ color: var(--fg-secondary);
+}
+
+/* Button Group */
+.button-group {
+ display: flex;
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ overflow: hidden;
+}
+
+.toggle-button {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--spacing-sm);
+ padding: var(--spacing-sm) var(--spacing-md);
+ background: var(--bg);
+ border: none;
+ border-right: 1px solid var(--border);
+ font-size: var(--font-size-sm);
+ font-weight: var(--font-weight-medium);
+ color: var(--fg-secondary);
+ cursor: pointer;
+ transition: var(--transition-fast);
+}
+
+.toggle-button:last-child {
+ border-right: none;
+}
+
+.toggle-button:hover {
+ background: var(--bg-tertiary);
+}
+
+.toggle-button.active {
+ background: var(--primary);
+ color: white;
+}
+
+.toggle-button svg {
+ flex-shrink: 0;
+}
+
+/* Format Preview Styles */
+.format-preview {
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+ font-size: 10px;
+ line-height: 1.2;
+ text-align: center;
+ white-space: nowrap;
+ overflow: hidden;
+}
+
+/* Three-button groups */
+.button-group .toggle-button:first-child:nth-last-child(3),
+.button-group .toggle-button:first-child:nth-last-child(3) ~ .toggle-button {
+ flex: 1;
+ font-size: var(--font-size-xs);
+ padding: var(--spacing-xs) var(--spacing-sm);
+}
+
+/* Action Section - now at top */
+.action-section {
+ padding: var(--spacing-sm) var(--spacing-md);
+ background: var(--bg-secondary);
+ border-top: 1px solid var(--border);
+ border-bottom: 1px solid var(--border);
+ flex: 0 0 auto; /* fixed height section */
+}
+
+/* Content Section - contains title and preview */
+.content-section {
+ flex: 1 1 0;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden; /* prevent page scroll */
+ min-height: 200px; /* ensure content section is always visible */
+ max-height: none; /* let it take remaining space */
+}
+
+/* Title Section - now inside content section */
+.title-section {
+ flex: 0 0 auto;
+ padding: var(--spacing-md) var(--spacing-md) var(--spacing-sm);
+ background: var(--bg);
+}
+
+.content-header {
+ margin-bottom: var(--spacing-sm);
+}
+
+.title-label {
+ display: block;
+ font-size: var(--font-size-sm);
+ font-weight: var(--font-weight-medium);
+ color: var(--fg);
+ margin-bottom: var(--spacing-sm);
+}
+
+.title-input {
+ width: 100%;
+ padding: var(--spacing-md);
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ font-size: var(--font-size-base);
+ color: var(--fg);
+ transition: var(--transition-fast);
+}
+
+.title-input:focus {
+ border-color: var(--primary);
+ box-shadow: 0 0 0 3px var(--primary-light);
+}
+
+.title-input::placeholder {
+ color: var(--fg-muted);
+}
+
+/* Editor Container */
+.editor-container {
+ flex: 1 1 0;
+ display: flex;
+ flex-direction: column;
background: var(--bg);
border: 1px solid var(--border);
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+ border-radius: var(--radius-md);
+ overflow: hidden;
+ min-height: 180px; /* ensure preview always visible */
+ max-height: none; /* let it expand */
position: relative;
- color:var(--fg);
+ margin: 0 var(--spacing-md) var(--spacing-md);
+}
+
+/* Make textarea/editor area scroll independently */
+.editor-container > textarea,
+.editor-container > .CodeMirror {
+ min-height: 0;
+ flex: 1 1 auto;
+}
+
+.editor-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: var(--spacing-md) var(--spacing-lg);
+ background: var(--bg-secondary);
+ border-bottom: 1px solid var(--border);
+ flex: 0 0 auto;
+}
+
+.editor-label {
+ font-size: var(--font-size-sm);
+ font-weight: var(--font-weight-medium);
+ color: var(--fg);
+}
+
+.editor-actions {
+ display: flex;
+ gap: var(--spacing-sm);
+}
+
+.action-button {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-xs);
+ padding: var(--spacing-xs) var(--spacing-sm);
+ background: transparent;
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ font-size: var(--font-size-xs);
+ color: var(--fg-secondary);
cursor: pointer;
- padding: 10px 10px 10px 30px;
+ transition: var(--transition-fast);
+}
+
+.action-button:hover {
+ background: var(--bg-tertiary);
+ border-color: var(--primary);
+ color: var(--primary);
+}
+
+.action-button.secondary {
+ background: var(--bg);
+}
+
+/* Copy button text animation */
+#copy-text {
+ transition: all 0.2s ease-in-out;
display: inline-block;
- font-weight: 600;
- transition: .3s ease all;
}
-.button.toggle:hover {
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
+#copy-text.changing {
+ opacity: 0;
+ transform: translateY(-5px);
}
-.button.toggle.checked {
- background: var(--bg-primary);
- border-color: var(--border-primary);
+/* Copy button hover effect */
+#copy:hover #copy-text {
+ transform: translateX(2px);
}
-.check {
- background: var(--bg-primary);
- border-radius: 50%;
- content:'';
- height: 20px;
- left: 10px;
- position: absolute;
- top: calc(50% - 10px);
- transition: .3s ease background-color;
- width: 20px;
+.markdown-editor {
+ flex: 1 1 0;
+ min-height: 0; /* let flex container control height */
+ height: 100%; /* fill container */
+ padding: var(--spacing-md);
+ background: var(--bg);
+ border: none;
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+ font-size: var(--font-size-sm);
+ line-height: 1.5;
+ color: var(--fg);
+ resize: none;
+ outline: none;
+ overflow-y: auto; /* enable vertical scrolling */
+ overflow-x: hidden;
+ box-sizing: border-box;
}
-.checked .check {
- background-color: var(--primary);
- background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHdpZHRoPSIyNiIgaGVpZ2h0PSIyMCIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIyLjAyOTY4IC00MC4wOTAzIDI2IDIwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIj48IS0tR2VuZXJhdGVkIGJ5IElKU1ZHIChodHRwczovL2dpdGh1Yi5jb20vaWNvbmphci9JSlNWRyktLT48cGF0aCBkPSJNMjcuOTc0MywtMzYuMTI3MmMwLDAuNDQ2NDI4IC0wLjE1NjI1LDAuODI1ODkzIC0wLjQ2ODc1LDEuMTM4MzlsLTEyLjEyMDUsMTIuMTIwNWwtMi4yNzY3OSwyLjI3Njc5Yy0wLjMxMjUsMC4zMTI1IC0wLjY5MTk2NCwwLjQ2ODc1IC0xLjEzODM5LDAuNDY4NzVjLTAuNDQ2NDI4LDAgLTAuODI1ODkzLC0wLjE1NjI1IC0xLjEzODM5LC0wLjQ2ODc1bC0yLjI3Njc5LC0yLjI3Njc5bC02LjA2MDI3LC02LjA2MDI3Yy0wLjMxMjUsLTAuMzEyNSAtMC40Njg3NSwtMC42OTE5NjUgLTAuNDY4NzUsLTEuMTM4MzljMCwtMC40NDY0MjkgMC4xNTYyNSwtMC44MjU4OTMgMC40Njg3NSwtMS4xMzgzOWwyLjI3Njc5LC0yLjI3Njc5YzAuMzEyNSwtMC4zMTI1IDAuNjkxOTY1LC0wLjQ2ODc1IDEuMTM4MzksLTAuNDY4NzVjMC40NDY0MjksMCAwLjgyNTg5MywwLjE1NjI1IDEuMTM4MzksMC40Njg3NWw0LjkyMTg4LDQuOTM4NjJsMTAuOTgyMSwtMTAuOTk4OWMwLjMxMjUsLTAuMzEyNSAwLjY5MTk2NCwtMC40Njg3NSAxLjEzODM5LC0wLjQ2ODc1YzAuNDQ2NDI4LDAgMC44MjU4OTMsMC4xNTYyNSAxLjEzODM5LDAuNDY4NzVsMi4yNzY3OCwyLjI3Njc5YzAuMzEyNSwwLjMxMjUgMC40Njg3NSwwLjY5MTk2NCAwLjQ2ODc1LDEuMTM4MzlaIiB0cmFuc2Zvcm09InNjYWxlKDEuMDAxOTgpIiBmaWxsPSIjZmZmIj48L3BhdGg+PC9zdmc+');
- background-repeat: no-repeat;
- background-position: center;
- background-size: 12px;
+.markdown-editor::placeholder {
+ color: var(--fg-muted);
+ font-style: italic;
+}
+
+/* CodeMirror Integration */
+.CodeMirror {
+ flex: 1 1 0;
+ height: 100% !important; /* fill container completely */
+ border: none;
+ background: var(--bg);
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+ font-size: var(--font-size-sm);
+ line-height: 1.5;
+ overflow: hidden; /* let CodeMirror handle scrolling internally */
+}
+
+.CodeMirror-focused .CodeMirror-cursor {
+ border-left: 2px solid var(--primary);
+}
+
+.CodeMirror-scroll {
+ height: 100% !important;
+ overflow-y: auto !important;
+ overflow-x: hidden !important;
}
+
+.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
+ background-color: var(--bg);
+}
+
+/* Hide duplicate scrollbars */
+.CodeMirror-vscrollbar {
+ display: block !important;
+}
+
+.CodeMirror-hscrollbar {
+ display: none !important;
+}
+
+/* Ensure smooth scrolling */
+.CodeMirror-scroll {
+ scroll-behavior: smooth;
+}
+
+.CodeMirror-selected {
+ background: var(--primary-light);
+}
+
+
+.button-row {
+ display: flex;
+ gap: var(--spacing-md);
+ align-items: center;
+}
+
+
+.primary-button {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--spacing-sm);
+ padding: var(--spacing-lg) var(--spacing-xl);
+ background: var(--primary);
+ border: none;
+ border-radius: var(--radius-md);
+ font-size: var(--font-size-base);
+ font-weight: var(--font-weight-medium);
+ color: white;
+ cursor: pointer;
+ transition: var(--transition-fast);
+ box-shadow: 0 2px 4px var(--shadow);
+}
+
+.primary-button:hover {
+ background: var(--primary-hover);
+ transform: translateY(-1px);
+ box-shadow: 0 4px 8px var(--shadow-hover);
+}
+
+.primary-button:active {
+ transform: translateY(0);
+ box-shadow: 0 2px 4px var(--shadow);
+}
+
+.secondary-button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--spacing-sm);
+ padding: var(--spacing-lg) var(--spacing-xl);
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ font-size: var(--font-size-base);
+ font-weight: var(--font-weight-medium);
+ color: var(--fg);
+ cursor: pointer;
+ transition: var(--transition-fast);
+}
+
+.secondary-button:hover {
+ background: var(--bg-tertiary);
+ border-color: var(--primary);
+ color: var(--primary);
+ transform: translateY(-1px);
+}
+
+/* Status Bar - stays at bottom */
+.status-bar {
+ padding: var(--spacing-sm) var(--spacing-lg);
+ background: var(--bg-tertiary);
+ border-top: 1px solid var(--border);
+ text-align: center;
+ transition: var(--transition-fast);
+ flex: 0 0 auto; /* keep at bottom */
+}
+
+.status-bar.pulse {
+ animation: statusPulse 0.5s ease-out;
+}
+
+#status-text {
+ font-size: var(--font-size-xs);
+ color: var(--fg-muted);
+ transition: var(--transition-fast);
+}
+
+@keyframes statusPulse {
+ 0% {
+ transform: scale(1);
+ background: var(--bg-tertiary);
+ }
+ 50% {
+ transform: scale(1.02);
+ background: var(--primary-light);
+ }
+ 100% {
+ transform: scale(1);
+ background: var(--bg-tertiary);
+ }
+}
+
+/* Button State Classes */
+.action-button.success,
+.primary-button.success,
+.secondary-button.success {
+ background: var(--success);
+ border-color: var(--success);
+ color: white;
+}
+
+.action-button.error,
+.primary-button.error,
+.secondary-button.error {
+ background: var(--danger);
+ border-color: var(--danger);
+ color: white;
+}
+
+.action-button.warning,
+.primary-button.warning,
+.secondary-button.warning {
+ background: var(--warning);
+ border-color: var(--warning);
+ color: white;
+}
+
+/* Utility Classes */
+.hidden {
+ display: none !important;
+}
+
+.success {
+ color: var(--success);
+}
+
+.error {
+ color: var(--danger);
+}
+
+.warning {
+ color: var(--warning);
+}
+
+/* Loading state for buttons */
+.primary-button:disabled,
+.secondary-button:disabled,
+.action-button:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ transform: none;
+}
+
+/* Animation Classes */
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@keyframes slideIn {
+ from {
+ opacity: 0;
+ transform: translateX(-10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+}
+
+.fade-in {
+ animation: fadeIn var(--transition-normal) ease-out;
+}
+
+.slide-in {
+ animation: slideIn var(--transition-normal) ease-out;
+}
+
+/* Responsive adjustments */
+@media (max-width: 480px) {
+ body {
+ width: 450px;
+ height: 600px;
+ }
+
+ .header {
+ padding: var(--spacing-sm) var(--spacing-md);
+ }
+
+ .quick-settings {
+ padding: var(--spacing-xs) var(--spacing-sm);
+ }
+
+ .content-section {
+ padding: var(--spacing-sm);
+ min-height: 150px;
+ }
+
+ .action-section {
+ padding: var(--spacing-xs) var(--spacing-sm);
+ }
+
+ .button-row {
+ gap: var(--spacing-sm);
+ }
+}
+
+/* Focus states for accessibility */
+.toggle-switch:focus-within .slider {
+ box-shadow: 0 0 0 3px var(--primary-light);
+}
+
+.toggle-button:focus {
+ box-shadow: 0 0 0 3px var(--primary-light);
+}
+
+/* High contrast mode support */
+@media (prefers-contrast: high) {
+ :root {
+ --border: #000000;
+ --shadow: rgba(0, 0, 0, 0.5);
+ }
+
+ @media (prefers-color-scheme: dark) {
+ :root {
+ --border: #FFFFFF;
+ }
+ }
+}
+
+/* Reduced motion support */
+@media (prefers-reduced-motion: reduce) {
+ * {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ }
+
+ .header::before {
+ animation: none !important;
+ opacity: 0.3;
+ }
+
+ .status-bar.pulse {
+ animation: none !important;
+ }
+
+ .primary-button:hover,
+ .secondary-button:hover,
+ .action-button:hover,
+ .toggle-button:hover,
+ .icon-button:hover {
+ transform: none !important;
+ }
+}
\ No newline at end of file
diff --git a/src/popup/popup.html b/src/popup/popup.html
index 07a21960..a7ceded2 100644
--- a/src/popup/popup.html
+++ b/src/popup/popup.html
@@ -1,36 +1,135 @@
-
+
+ MarkDownload
-
-
-
- Download Images
- open options
-
- Selected Text
- Entire Document
+
+
+
+
+
+
+
+
+
+ MarkDownload
+ Web to Markdown
+
+
+
+
+
+
+
+
+
+
+ Quick Settings
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Markdown Preview
+
+
+
+
+
+
+
+
+
+
- Include front/back template
-
-
- Download
- Download selected
- Copy
-
-
-
-
-
-
-
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/popup/popup.js b/src/popup/popup.js
index aac5396b..27460e36 100644
--- a/src/popup/popup.js
+++ b/src/popup/popup.js
@@ -3,52 +3,216 @@
var selectedText = null;
var imageList = null;
var mdClipsFolder = '';
+var cm = null;
const darkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
-// set up event handlers
-const cm = CodeMirror.fromTextArea(document.getElementById("md"), {
- theme: darkMode ? "xq-dark" : "xq-light",
- mode: "markdown",
- lineWrapping: true
-});
-cm.on("cursorActivity", (cm) => {
- const somethingSelected = cm.somethingSelected();
- var a = document.getElementById("downloadSelection");
- if (somethingSelected) {
- if(a.style.display != "block") a.style.display = "block";
+// Initialize CodeMirror when DOM is ready
+const initializeCodeMirror = () => {
+ const textarea = document.getElementById("md");
+ if (textarea && !cm) {
+ cm = CodeMirror.fromTextArea(textarea, {
+ theme: darkMode ? "xq-dark" : "xq-light",
+ mode: "markdown",
+ lineWrapping: true,
+ lineNumbers: false,
+ readOnly: false,
+ viewportMargin: Infinity,
+ scrollbarStyle: "native"
+ });
+
+ cm.on("cursorActivity", (cm) => {
+ const somethingSelected = cm.somethingSelected();
+ const copyText = document.getElementById("copy-text");
+
+ if (somethingSelected) {
+ if (copyText && copyText.textContent !== "Copy Selection") {
+ animateTextChange(copyText, "Copy Selection");
+ }
+ } else {
+ if (copyText && copyText.textContent !== "Copy") {
+ animateTextChange(copyText, "Copy");
+ }
+ }
+ });
+
+ // Refresh CodeMirror after initialization
+ setTimeout(() => {
+ cm.refresh();
+ }, 100);
}
- else {
- if(a.style.display != "none") a.style.display = "none";
+};
+
+// Animate text change with smooth transition
+const animateTextChange = (element, newText) => {
+ if (!element) return;
+
+ element.classList.add('changing');
+
+ setTimeout(() => {
+ element.textContent = newText;
+ element.classList.remove('changing');
+ }, 100);
+};
+
+// Update status bar
+const updateStatus = (message, type = 'info') => {
+ const statusText = document.getElementById('status-text');
+ const statusBar = document.getElementById('status-bar');
+
+ if (statusText) {
+ statusText.textContent = message;
+ statusText.className = type; // success, error, warning
+
+ // Add pulse animation for visual feedback
+ if (statusBar && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
+ statusBar.classList.add('pulse');
+ setTimeout(() => {
+ statusBar.classList.remove('pulse');
+ }, 500);
+ }
+
+ // Auto-clear status after 3 seconds for success messages
+ if (type === 'success') {
+ setTimeout(() => {
+ statusText.textContent = 'Ready';
+ statusText.className = '';
+ }, 3000);
+ }
}
+};
+
+// Add basic event listeners (will be supplemented by setupEventListeners)
+document.addEventListener('DOMContentLoaded', () => {
+ document.getElementById("download")?.addEventListener("click", download);
+ document.getElementById("copy")?.addEventListener("click", copyToClipboard);
+ document.getElementById("sendToObsidian")?.addEventListener("click", sendToObsidian);
});
-document.getElementById("download").addEventListener("click", download);
-document.getElementById("downloadSelection").addEventListener("click", downloadSelection);
-document.getElementById("copy").addEventListener("click", copyToClipboard);
-const defaultOptions = {
+// Provide a local defaultOptions fallback in case shared script isn't loaded
+const localDefaultOptions = {
+ headingStyle: "atx",
+ hr: "___",
+ bulletListMarker: "-",
+ codeBlockStyle: "fenced",
+ fence: "```",
+ emDelimiter: "_",
+ strongDelimiter: "**",
+ linkStyle: "inlined",
+ linkReferenceStyle: "full",
+ imageStyle: "markdown",
+ imageRefStyle: "inlined",
+ frontmatter: "",
+ backmatter: "",
+ title: "{pageTitle}",
includeTemplate: false,
- clipSelection: true,
- downloadImages: false
+ saveAs: false,
+ downloadImages: false,
+ imagePrefix: '{pageTitle}/',
+ mdClipsFolder: null,
+ disallowedChars: '[]#^',
+ downloadMode: 'downloadsApi',
+ turndownEscape: true,
+ contextMenus: true,
+ obsidianIntegration: false,
+ obsidianVault: "",
+ obsidianFolder: ""
+};
+
+// Use shared defaultOptions if present, otherwise fallback
+const baseDefaults = (typeof defaultOptions !== 'undefined') ? defaultOptions : localDefaultOptions;
+
+// Add clipSelection to the default options
+const popupDefaultOptions = {
+ ...baseDefaults,
+ clipSelection: true
}
const checkInitialSettings = options => {
- if (options.includeTemplate)
- document.querySelector("#includeTemplate").classList.add("checked");
+ // Update includeTemplate state (supports input checkbox or legacy anchor)
+ const includeTemplateEl = document.querySelector("#includeTemplate");
+ if (includeTemplateEl) {
+ if (includeTemplateEl.tagName === 'INPUT') {
+ includeTemplateEl.checked = !!options.includeTemplate;
+ } else {
+ includeTemplateEl.classList.toggle('checked', !!options.includeTemplate);
+ }
+ }
- if (options.downloadImages)
- document.querySelector("#downloadImages").classList.add("checked");
+ // Update downloadImages state (supports input checkbox or legacy anchor)
+ const downloadImagesEl = document.querySelector("#downloadImages");
+ if (downloadImagesEl) {
+ if (downloadImagesEl.tagName === 'INPUT') {
+ downloadImagesEl.checked = !!options.downloadImages;
+ } else {
+ downloadImagesEl.classList.toggle('checked', !!options.downloadImages);
+ }
+ }
+
+ // Update toggle buttons for clip selection
+ const selectedBtn = document.querySelector("#selected");
+ const documentBtn = document.querySelector("#document");
+
+ if (selectedBtn && documentBtn) {
+ if (options.clipSelection) {
+ selectedBtn.classList.add("active");
+ documentBtn.classList.remove("active");
+ } else {
+ selectedBtn.classList.remove("active");
+ documentBtn.classList.add("active");
+ }
+ }
- if (options.clipSelection)
- document.querySelector("#selected").classList.add("checked");
- else
- document.querySelector("#document").classList.add("checked");
+ // Update heading style buttons
+ const atxBtn = document.querySelector("#heading-atx");
+ const setextBtn = document.querySelector("#heading-setext");
+
+ if (atxBtn && setextBtn) {
+ if (options.headingStyle === 'atx') {
+ atxBtn.classList.add("active");
+ setextBtn.classList.remove("active");
+ } else {
+ atxBtn.classList.remove("active");
+ setextBtn.classList.add("active");
+ }
+ }
+
+ // Update link style buttons
+ const inlinedBtn = document.querySelector("#link-inlined");
+ const referencedBtn = document.querySelector("#link-referenced");
+ const stripBtn = document.querySelector("#link-strip");
+
+ [inlinedBtn, referencedBtn, stripBtn].forEach(btn => {
+ if (btn) btn.classList.remove("active");
+ });
+
+ if (options.linkStyle === 'inlined' && inlinedBtn) {
+ inlinedBtn.classList.add("active");
+ } else if (options.linkStyle === 'referenced' && referencedBtn) {
+ referencedBtn.classList.add("active");
+ } else if (options.linkStyle === 'stripLinks' && stripBtn) {
+ stripBtn.classList.add("active");
+ }
}
-const toggleClipSelection = options => {
- options.clipSelection = !options.clipSelection;
- document.querySelector("#selected").classList.toggle("checked");
- document.querySelector("#document").classList.toggle("checked");
+const toggleClipSelection = (options, value) => {
+ options.clipSelection = (value === 'selection');
+
+ const selectedBtn = document.querySelector("#selected");
+ const documentBtn = document.querySelector("#document");
+
+ if (selectedBtn && documentBtn) {
+ if (options.clipSelection) {
+ selectedBtn.classList.add("active");
+ documentBtn.classList.remove("active");
+ } else {
+ selectedBtn.classList.remove("active");
+ documentBtn.classList.add("active");
+ }
+ }
+
+ updateStatus("Updating content selection...", "info");
+
chrome.storage.sync.set(options).then(() => {
// Get current tab ID and call clipSite
chrome.tabs.query({ currentWindow: true, active: true }).then(tabs => {
@@ -57,15 +221,26 @@ const toggleClipSelection = options => {
}
}).catch(error => {
console.error('Failed to get tab ID:', error);
+ updateStatus("Error updating selection", "error");
});
}).catch((error) => {
console.error(error);
+ updateStatus("Failed to save settings", "error");
});
}
const toggleIncludeTemplate = options => {
- options.includeTemplate = !options.includeTemplate;
- document.querySelector("#includeTemplate").classList.toggle("checked");
+ const el = document.querySelector("#includeTemplate");
+ if (!el) return;
+
+ if (el.tagName === 'INPUT') {
+ options.includeTemplate = !!el.checked;
+ } else {
+ options.includeTemplate = !options.includeTemplate;
+ el.classList.toggle('checked', !!options.includeTemplate);
+ }
+ updateStatus("Updating template settings...", "info");
+
chrome.storage.sync.set(options).then(() => {
chrome.contextMenus.update("toggle-includeTemplate", {
checked: options.includeTemplate
@@ -77,21 +252,105 @@ const toggleIncludeTemplate = options => {
}
}).catch(error => {
console.error('Failed to get tab ID:', error);
+ updateStatus("Error updating content", "error");
});
}).catch((error) => {
console.error(error);
+ updateStatus("Failed to save settings", "error");
});
}
const toggleDownloadImages = options => {
- options.downloadImages = !options.downloadImages;
- document.querySelector("#downloadImages").classList.toggle("checked");
+ const el = document.querySelector("#downloadImages");
+ if (!el) return;
+
+ if (el.tagName === 'INPUT') {
+ options.downloadImages = !!el.checked;
+ } else {
+ options.downloadImages = !options.downloadImages;
+ el.classList.toggle('checked', !!options.downloadImages);
+ }
+ updateStatus("Updating image settings...", "info");
+
chrome.storage.sync.set(options).then(() => {
chrome.contextMenus.update("toggle-downloadImages", {
checked: options.downloadImages
});
+ updateStatus("Settings saved", "success");
+ }).catch((error) => {
+ console.error(error);
+ updateStatus("Failed to save settings", "error");
+ });
+}
+
+const toggleHeadingStyle = (options, value) => {
+ options.headingStyle = value;
+
+ const atxBtn = document.querySelector("#heading-atx");
+ const setextBtn = document.querySelector("#heading-setext");
+
+ if (atxBtn && setextBtn) {
+ if (value === 'atx') {
+ atxBtn.classList.add("active");
+ setextBtn.classList.remove("active");
+ } else {
+ atxBtn.classList.remove("active");
+ setextBtn.classList.add("active");
+ }
+ }
+
+ updateStatus("Updating heading style...", "info");
+
+ chrome.storage.sync.set(options).then(() => {
+ // Get current tab ID and re-clip to show changes
+ chrome.tabs.query({ currentWindow: true, active: true }).then(tabs => {
+ if (tabs[0]) {
+ clipSite(tabs[0].id);
+ }
+ }).catch(error => {
+ console.error('Failed to get tab ID:', error);
+ });
+ updateStatus("Heading style updated", "success");
+ }).catch((error) => {
+ console.error(error);
+ updateStatus("Failed to save settings", "error");
+ });
+}
+
+const toggleLinkStyle = (options, value) => {
+ options.linkStyle = value;
+
+ const inlinedBtn = document.querySelector("#link-inlined");
+ const referencedBtn = document.querySelector("#link-referenced");
+ const stripBtn = document.querySelector("#link-strip");
+
+ [inlinedBtn, referencedBtn, stripBtn].forEach(btn => {
+ if (btn) btn.classList.remove("active");
+ });
+
+ if (value === 'inlined' && inlinedBtn) {
+ inlinedBtn.classList.add("active");
+ } else if (value === 'referenced' && referencedBtn) {
+ referencedBtn.classList.add("active");
+ } else if (value === 'stripLinks' && stripBtn) {
+ stripBtn.classList.add("active");
+ }
+
+ updateStatus("Updating link style...", "info");
+
+ chrome.storage.sync.set(options).then(() => {
+ // Get current tab ID and re-clip to show changes
+ chrome.tabs.query({ currentWindow: true, active: true }).then(tabs => {
+ if (tabs[0]) {
+ clipSite(tabs[0].id);
+ }
+ }).catch(error => {
+ console.error('Failed to get tab ID:', error);
+ });
+ updateStatus("Link style updated", "success");
}).catch((error) => {
console.error(error);
+ updateStatus("Failed to save settings", "error");
});
}
const showOrHideClipOption = selection => {
@@ -118,7 +377,7 @@ const clipSite = id => {
selection: result[0].result.selection,
tabId: id
}
- return chrome.storage.sync.get(defaultOptions).then(options => {
+ return chrome.storage.sync.get(popupDefaultOptions).then(options => {
chrome.runtime.sendMessage({
...message,
...options
@@ -128,7 +387,7 @@ const clipSite = id => {
showError(err)
return chrome.runtime.sendMessage({
...message,
- ...defaultOptions
+ ...popupDefaultOptions
});
}).catch(err => {
console.error(err);
@@ -142,40 +401,122 @@ const clipSite = id => {
}
// Initialize popup
-chrome.storage.sync.get(defaultOptions).then(options => {
- checkInitialSettings(options);
+const initializePopup = () => {
+ updateStatus("Loading settings...", "info");
- document.getElementById("selected").addEventListener("click", (e) => {
- e.preventDefault();
- toggleClipSelection(options);
- });
- document.getElementById("document").addEventListener("click", (e) => {
- e.preventDefault();
- toggleClipSelection(options);
- });
- document.getElementById("includeTemplate").addEventListener("click", (e) => {
- e.preventDefault();
- toggleIncludeTemplate(options);
- });
- document.getElementById("downloadImages").addEventListener("click", (e) => {
- e.preventDefault();
- toggleDownloadImages(options);
+ chrome.storage.sync.get(popupDefaultOptions).then(options => {
+ checkInitialSettings(options);
+ setupEventListeners(options);
+
+ return chrome.tabs.query({
+ currentWindow: true,
+ active: true
+ });
+ }).then((tabs) => {
+ if (tabs && tabs[0]) {
+ const id = tabs[0].id;
+ const url = tabs[0].url;
+
+ updateStatus("Processing page...", "info");
+ console.info("MarkDownload popup: Content script should be available");
+ return clipSite(id);
+ } else {
+ throw new Error("No active tab found");
+ }
+ }).catch((error) => {
+ console.error(error);
+ showError(error);
});
+};
+
+// Setup event listeners
+const setupEventListeners = (options) => {
+ // Toggle buttons for clip selection
+ const selectedBtn = document.getElementById("selected");
+ const documentBtn = document.getElementById("document");
- return chrome.tabs.query({
- currentWindow: true,
- active: true
- });
-}).then((tabs) => {
- var id = tabs[0].id;
- var url = tabs[0].url;
-
- // Content script is already injected via manifest, just call clipSite
- console.info("MarkDownload popup: Content script should be available");
- return clipSite(id);
-}).catch( (error) => {
- console.error(error);
- showError(error);
+ if (selectedBtn) {
+ selectedBtn.addEventListener("click", (e) => {
+ e.preventDefault();
+ toggleClipSelection(options, 'selection');
+ });
+ }
+
+ if (documentBtn) {
+ documentBtn.addEventListener("click", (e) => {
+ e.preventDefault();
+ toggleClipSelection(options, 'document');
+ });
+ }
+
+ // Heading style buttons
+ const headingAtxBtn = document.getElementById("heading-atx");
+ const headingSetextBtn = document.getElementById("heading-setext");
+
+ if (headingAtxBtn) {
+ headingAtxBtn.addEventListener("click", (e) => {
+ e.preventDefault();
+ toggleHeadingStyle(options, 'atx');
+ });
+ }
+
+ if (headingSetextBtn) {
+ headingSetextBtn.addEventListener("click", (e) => {
+ e.preventDefault();
+ toggleHeadingStyle(options, 'setext');
+ });
+ }
+
+ // Link style buttons
+ const linkInlinedBtn = document.getElementById("link-inlined");
+ const linkReferencedBtn = document.getElementById("link-referenced");
+ const linkStripBtn = document.getElementById("link-strip");
+
+ if (linkInlinedBtn) {
+ linkInlinedBtn.addEventListener("click", (e) => {
+ e.preventDefault();
+ toggleLinkStyle(options, 'inlined');
+ });
+ }
+
+ if (linkReferencedBtn) {
+ linkReferencedBtn.addEventListener("click", (e) => {
+ e.preventDefault();
+ toggleLinkStyle(options, 'referenced');
+ });
+ }
+
+ if (linkStripBtn) {
+ linkStripBtn.addEventListener("click", (e) => {
+ e.preventDefault();
+ toggleLinkStyle(options, 'stripLinks');
+ });
+ }
+
+ // Checkbox event listeners
+ const includeTemplateEl = document.getElementById("includeTemplate");
+ if (includeTemplateEl) {
+ const evt = (includeTemplateEl.tagName === 'INPUT') ? 'change' : 'click';
+ includeTemplateEl.addEventListener(evt, (e) => {
+ e.preventDefault();
+ toggleIncludeTemplate(options);
+ });
+ }
+
+ const downloadImagesEl = document.getElementById("downloadImages");
+ if (downloadImagesEl) {
+ const evt2 = (downloadImagesEl.tagName === 'INPUT') ? 'change' : 'click';
+ downloadImagesEl.addEventListener(evt2, (e) => {
+ e.preventDefault();
+ toggleDownloadImages(options);
+ });
+ }
+};
+
+// Call initialization when DOM is ready
+document.addEventListener('DOMContentLoaded', () => {
+ initializeCodeMirror();
+ initializePopup();
});
// listen for notifications from the background page
@@ -209,45 +550,66 @@ async function download(e) {
window.close();
}
-// event handler for download selected button
-async function downloadSelection(e) {
- e.preventDefault();
- if (cm.somethingSelected()) {
- await sendDownloadMessage(cm.getSelection());
- }
-}
// event handler for copy button
async function copyToClipboard(e) {
e.preventDefault();
+ if (!cm) {
+ updateStatus("Editor not initialized", "error");
+ return;
+ }
+
try {
- const textToCopy = cm.getValue();
+ // Check if there's a selection, if so copy selection, otherwise copy all
+ const hasSelection = cm.somethingSelected();
+ const textToCopy = hasSelection ? cm.getSelection() : cm.getValue();
+
+ if (!textToCopy.trim()) {
+ updateStatus("No content to copy", "warning");
+ return;
+ }
+
await navigator.clipboard.writeText(textToCopy);
// Show feedback to user
const copyButton = document.getElementById("copy");
- const originalText = copyButton.textContent;
- copyButton.textContent = "Copied!";
- copyButton.style.backgroundColor = "#4CAF50";
+ const originalHTML = copyButton.innerHTML;
+ const feedbackText = hasSelection ? "Selection Copied!" : "Copied!";
+
+ copyButton.innerHTML = `
+
+ ${feedbackText}
+ `;
+ copyButton.classList.add("success");
+ updateStatus("Content copied to clipboard", "success");
// Reset button after 2 seconds
setTimeout(() => {
- copyButton.textContent = originalText;
- copyButton.style.backgroundColor = "";
+ copyButton.innerHTML = originalHTML;
+ copyButton.classList.remove("success");
}, 2000);
} catch (error) {
console.error('Failed to copy text: ', error);
- // Fallback: show error feedback
+ // Show error feedback
const copyButton = document.getElementById("copy");
- const originalText = copyButton.textContent;
- copyButton.textContent = "Copy Failed";
- copyButton.style.backgroundColor = "#f44336";
+ const originalHTML = copyButton.innerHTML;
+
+ copyButton.innerHTML = `
+
+ Failed
+ `;
+ copyButton.classList.add("error");
+ updateStatus("Failed to copy content", "error");
setTimeout(() => {
- copyButton.textContent = originalText;
- copyButton.style.backgroundColor = "";
+ copyButton.innerHTML = originalHTML;
+ copyButton.classList.remove("error");
}, 2000);
}
}
@@ -256,27 +618,154 @@ async function copyToClipboard(e) {
function notify(message) {
// message for displaying markdown
if (message.type == "display.md") {
-
- // set the values from the message
- //document.getElementById("md").value = message.markdown;
- cm.setValue(message.markdown);
- document.getElementById("title").value = message.article.title;
- imageList = message.imageList;
- mdClipsFolder = message.mdClipsFolder;
+ if (!cm) {
+ // Initialize CodeMirror if not already done
+ initializeCodeMirror();
+ }
- // show the hidden elements
- document.getElementById("container").style.display = 'flex';
- document.getElementById("spinner").style.display = 'none';
- // focus the download button
- document.getElementById("download").focus();
- cm.refresh();
+ if (cm && message.markdown) {
+ // set the values from the message
+ cm.setValue(message.markdown);
+ setTimeout(() => cm.refresh(), 50);
+
+ // Set the title
+ const titleInput = document.getElementById("title");
+ if (titleInput && message.article && message.article.title) {
+ titleInput.value = message.article.title;
+ }
+
+ imageList = message.imageList;
+ mdClipsFolder = message.mdClipsFolder;
+
+ // show the main container and hide spinner
+ const container = document.getElementById("container");
+ const spinner = document.getElementById("spinner");
+
+ if (container) {
+ container.style.display = 'flex';
+ container.classList.add('fade-in');
+ }
+ if (spinner) {
+ spinner.style.display = 'none';
+ }
+
+ // focus the download button and refresh CodeMirror
+ const downloadBtn = document.getElementById("download");
+ if (downloadBtn) {
+ downloadBtn.focus();
+ }
+
+ if (cm) {
+ setTimeout(() => cm.refresh(), 100);
+ }
+
+ updateStatus("Page processed successfully", "success");
+ }
}
}
function showError(err) {
- // show the hidden elements
- document.getElementById("container").style.display = 'flex';
- document.getElementById("spinner").style.display = 'none';
- cm.setValue(`Error clipping the page\n\n${err}`)
+ console.error("MarkDownload Error:", err);
+
+ // show the main container and hide spinner
+ const container = document.getElementById("container");
+ const spinner = document.getElementById("spinner");
+
+ if (container) {
+ container.style.display = 'flex';
+ container.classList.add('fade-in');
+ }
+ if (spinner) {
+ spinner.style.display = 'none';
+ }
+
+ // Initialize CodeMirror if not already done
+ if (!cm) {
+ initializeCodeMirror();
+ }
+
+ // Show error message in editor
+ if (cm) {
+ const errorMessage = `# Error Processing Page\n\n**Error Details:**\n\`\`\`\n${err}\n\`\`\`\n\n**Troubleshooting:**\n- Try refreshing the page and clipping again\n- Check if the page has finished loading\n- Some pages may not be supported\n- Check browser console for more details`;
+ cm.setValue(errorMessage);
+ setTimeout(() => cm.refresh(), 100);
+ }
+
+ updateStatus(`Error: ${err}`, "error");
+}
+
+// event handler for send to Obsidian button
+async function sendToObsidian(e) {
+ e.preventDefault();
+ if (!cm) {
+ updateStatus("Editor not initialized", "error");
+ return;
+ }
+ try {
+ const markdownContent = cm.getValue();
+ if (!markdownContent.trim()) {
+ updateStatus("No content to send", "warning");
+ return;
+ }
+
+ updateStatus("Sending to Obsidian...", "info");
+
+ const tabs = await chrome.tabs.query({ currentWindow: true, active: true });
+ const options = await chrome.storage.sync.get(popupDefaultOptions);
+ if (!options.obsidianIntegration) {
+ updateStatus("Obsidian integration not enabled. Enable it in Options.", "error");
+ return;
+ }
+ if (!tabs[0]) {
+ updateStatus("No active tab found", "error");
+ return;
+ }
+
+ const titleInput = document.getElementById("title");
+ const title = titleInput ? titleInput.value.trim() : "Untitled";
+
+ const response = await chrome.runtime.sendMessage({
+ type: "sendToObsidian",
+ markdown: markdownContent,
+ title: title,
+ tabId: tabs[0].id,
+ options: options
+ });
+
+ if (response && response.success) {
+ updateStatus("Successfully sent to Obsidian!", "success");
+ const obsidianButton = document.getElementById("sendToObsidian");
+ const originalHTML = obsidianButton.innerHTML;
+ obsidianButton.innerHTML = `
+
+ Sent!
+ `;
+ obsidianButton.classList.add("success");
+ setTimeout(() => {
+ obsidianButton.innerHTML = originalHTML;
+ obsidianButton.classList.remove("success");
+ }, 3000);
+ } else {
+ throw new Error(response?.error || "Failed to send to Obsidian");
+ }
+ } catch (error) {
+ console.error('Failed to send to Obsidian: ', error);
+ updateStatus(`Failed to send to Obsidian: ${error.message}`, "error");
+ const obsidianButton = document.getElementById("sendToObsidian");
+ const originalHTML = obsidianButton.innerHTML;
+ obsidianButton.innerHTML = `
+
+ Failed
+ `;
+ obsidianButton.classList.add("error");
+ setTimeout(() => {
+ obsidianButton.innerHTML = originalHTML;
+ obsidianButton.classList.remove("error");
+ }, 3000);
+ }
}
From 7fc9e1136d6264bb8931563f8e7dafa97475887f Mon Sep 17 00:00:00 2001
From: TianleLin <940428567qq@gmail.com>
Date: Fri, 12 Sep 2025 00:53:08 +0800
Subject: [PATCH 28/29] fix download image
---
src/background/jszip.min.js | 13 ++
src/background/service-worker.js | 36 +++-
src/package-lock.json | 73 +++-----
src/package.json | 3 +
src/popup/popup.css | 226 ++++++++++++++++++++++++
src/popup/popup.html | 284 +++++++++++++++++++------------
src/popup/popup.js | 85 +++++++++
7 files changed, 557 insertions(+), 163 deletions(-)
create mode 100644 src/background/jszip.min.js
diff --git a/src/background/jszip.min.js b/src/background/jszip.min.js
new file mode 100644
index 00000000..ff4cfd5e
--- /dev/null
+++ b/src/background/jszip.min.js
@@ -0,0 +1,13 @@
+/*!
+
+JSZip v3.10.1 - A JavaScript class for generating and reading zip files
+
+
+(c) 2009-2016 Stuart Knightley
+Dual licenced under the MIT license or GPLv3. See https://raw.github.com/Stuk/jszip/main/LICENSE.markdown.
+
+JSZip uses the library pako released under the MIT license :
+https://github.com/nodeca/pako/blob/main/LICENSE
+*/
+
+!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).JSZip=e()}}(function(){return function s(a,o,h){function u(r,e){if(!o[r]){if(!a[r]){var t="function"==typeof require&&require;if(!e&&t)return t(r,!0);if(l)return l(r,!0);var n=new Error("Cannot find module '"+r+"'");throw n.code="MODULE_NOT_FOUND",n}var i=o[r]={exports:{}};a[r][0].call(i.exports,function(e){var t=a[r][1][e];return u(t||e)},i,i.exports,s,a,o,h)}return o[r].exports}for(var l="function"==typeof require&&require,e=0;e>2,s=(3&t)<<4|r>>4,a=1>6:64,o=2>4,r=(15&i)<<4|(s=p.indexOf(e.charAt(o++)))>>2,n=(3&s)<<6|(a=p.indexOf(e.charAt(o++))),l[h++]=t,64!==s&&(l[h++]=r),64!==a&&(l[h++]=n);return l}},{"./support":30,"./utils":32}],2:[function(e,t,r){"use strict";var n=e("./external"),i=e("./stream/DataWorker"),s=e("./stream/Crc32Probe"),a=e("./stream/DataLengthProbe");function o(e,t,r,n,i){this.compressedSize=e,this.uncompressedSize=t,this.crc32=r,this.compression=n,this.compressedContent=i}o.prototype={getContentWorker:function(){var e=new i(n.Promise.resolve(this.compressedContent)).pipe(this.compression.uncompressWorker()).pipe(new a("data_length")),t=this;return e.on("end",function(){if(this.streamInfo.data_length!==t.uncompressedSize)throw new Error("Bug : uncompressed data size mismatch")}),e},getCompressedWorker:function(){return new i(n.Promise.resolve(this.compressedContent)).withStreamInfo("compressedSize",this.compressedSize).withStreamInfo("uncompressedSize",this.uncompressedSize).withStreamInfo("crc32",this.crc32).withStreamInfo("compression",this.compression)}},o.createWorkerFrom=function(e,t,r){return e.pipe(new s).pipe(new a("uncompressedSize")).pipe(t.compressWorker(r)).pipe(new a("compressedSize")).withStreamInfo("compression",t)},t.exports=o},{"./external":6,"./stream/Crc32Probe":25,"./stream/DataLengthProbe":26,"./stream/DataWorker":27}],3:[function(e,t,r){"use strict";var n=e("./stream/GenericWorker");r.STORE={magic:"\0\0",compressWorker:function(){return new n("STORE compression")},uncompressWorker:function(){return new n("STORE decompression")}},r.DEFLATE=e("./flate")},{"./flate":7,"./stream/GenericWorker":28}],4:[function(e,t,r){"use strict";var n=e("./utils");var o=function(){for(var e,t=[],r=0;r<256;r++){e=r;for(var n=0;n<8;n++)e=1&e?3988292384^e>>>1:e>>>1;t[r]=e}return t}();t.exports=function(e,t){return void 0!==e&&e.length?"string"!==n.getTypeOf(e)?function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a>>8^i[255&(e^t[a])];return-1^e}(0|t,e,e.length,0):function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a>>8^i[255&(e^t.charCodeAt(a))];return-1^e}(0|t,e,e.length,0):0}},{"./utils":32}],5:[function(e,t,r){"use strict";r.base64=!1,r.binary=!1,r.dir=!1,r.createFolders=!0,r.date=null,r.compression=null,r.compressionOptions=null,r.comment=null,r.unixPermissions=null,r.dosPermissions=null},{}],6:[function(e,t,r){"use strict";var n=null;n="undefined"!=typeof Promise?Promise:e("lie"),t.exports={Promise:n}},{lie:37}],7:[function(e,t,r){"use strict";var n="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Uint32Array,i=e("pako"),s=e("./utils"),a=e("./stream/GenericWorker"),o=n?"uint8array":"array";function h(e,t){a.call(this,"FlateWorker/"+e),this._pako=null,this._pakoAction=e,this._pakoOptions=t,this.meta={}}r.magic="\b\0",s.inherits(h,a),h.prototype.processChunk=function(e){this.meta=e.meta,null===this._pako&&this._createPako(),this._pako.push(s.transformTo(o,e.data),!1)},h.prototype.flush=function(){a.prototype.flush.call(this),null===this._pako&&this._createPako(),this._pako.push([],!0)},h.prototype.cleanUp=function(){a.prototype.cleanUp.call(this),this._pako=null},h.prototype._createPako=function(){this._pako=new i[this._pakoAction]({raw:!0,level:this._pakoOptions.level||-1});var t=this;this._pako.onData=function(e){t.push({data:e,meta:t.meta})}},r.compressWorker=function(e){return new h("Deflate",e)},r.uncompressWorker=function(){return new h("Inflate",{})}},{"./stream/GenericWorker":28,"./utils":32,pako:38}],8:[function(e,t,r){"use strict";function A(e,t){var r,n="";for(r=0;r>>=8;return n}function n(e,t,r,n,i,s){var a,o,h=e.file,u=e.compression,l=s!==O.utf8encode,f=I.transformTo("string",s(h.name)),c=I.transformTo("string",O.utf8encode(h.name)),d=h.comment,p=I.transformTo("string",s(d)),m=I.transformTo("string",O.utf8encode(d)),_=c.length!==h.name.length,g=m.length!==d.length,b="",v="",y="",w=h.dir,k=h.date,x={crc32:0,compressedSize:0,uncompressedSize:0};t&&!r||(x.crc32=e.crc32,x.compressedSize=e.compressedSize,x.uncompressedSize=e.uncompressedSize);var S=0;t&&(S|=8),l||!_&&!g||(S|=2048);var z=0,C=0;w&&(z|=16),"UNIX"===i?(C=798,z|=function(e,t){var r=e;return e||(r=t?16893:33204),(65535&r)<<16}(h.unixPermissions,w)):(C=20,z|=function(e){return 63&(e||0)}(h.dosPermissions)),a=k.getUTCHours(),a<<=6,a|=k.getUTCMinutes(),a<<=5,a|=k.getUTCSeconds()/2,o=k.getUTCFullYear()-1980,o<<=4,o|=k.getUTCMonth()+1,o<<=5,o|=k.getUTCDate(),_&&(v=A(1,1)+A(B(f),4)+c,b+="up"+A(v.length,2)+v),g&&(y=A(1,1)+A(B(p),4)+m,b+="uc"+A(y.length,2)+y);var E="";return E+="\n\0",E+=A(S,2),E+=u.magic,E+=A(a,2),E+=A(o,2),E+=A(x.crc32,4),E+=A(x.compressedSize,4),E+=A(x.uncompressedSize,4),E+=A(f.length,2),E+=A(b.length,2),{fileRecord:R.LOCAL_FILE_HEADER+E+f+b,dirRecord:R.CENTRAL_FILE_HEADER+A(C,2)+E+A(p.length,2)+"\0\0\0\0"+A(z,4)+A(n,4)+f+b+p}}var I=e("../utils"),i=e("../stream/GenericWorker"),O=e("../utf8"),B=e("../crc32"),R=e("../signature");function s(e,t,r,n){i.call(this,"ZipFileWorker"),this.bytesWritten=0,this.zipComment=t,this.zipPlatform=r,this.encodeFileName=n,this.streamFiles=e,this.accumulate=!1,this.contentBuffer=[],this.dirRecords=[],this.currentSourceOffset=0,this.entriesCount=0,this.currentFile=null,this._sources=[]}I.inherits(s,i),s.prototype.push=function(e){var t=e.meta.percent||0,r=this.entriesCount,n=this._sources.length;this.accumulate?this.contentBuffer.push(e):(this.bytesWritten+=e.data.length,i.prototype.push.call(this,{data:e.data,meta:{currentFile:this.currentFile,percent:r?(t+100*(r-n-1))/r:100}}))},s.prototype.openedSource=function(e){this.currentSourceOffset=this.bytesWritten,this.currentFile=e.file.name;var t=this.streamFiles&&!e.file.dir;if(t){var r=n(e,t,!1,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);this.push({data:r.fileRecord,meta:{percent:0}})}else this.accumulate=!0},s.prototype.closedSource=function(e){this.accumulate=!1;var t=this.streamFiles&&!e.file.dir,r=n(e,t,!0,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);if(this.dirRecords.push(r.dirRecord),t)this.push({data:function(e){return R.DATA_DESCRIPTOR+A(e.crc32,4)+A(e.compressedSize,4)+A(e.uncompressedSize,4)}(e),meta:{percent:100}});else for(this.push({data:r.fileRecord,meta:{percent:0}});this.contentBuffer.length;)this.push(this.contentBuffer.shift());this.currentFile=null},s.prototype.flush=function(){for(var e=this.bytesWritten,t=0;t=this.index;t--)r=(r<<8)+this.byteAt(t);return this.index+=e,r},readString:function(e){return n.transformTo("string",this.readData(e))},readData:function(){},lastIndexOfSignature:function(){},readAndCheckSignature:function(){},readDate:function(){var e=this.readInt(4);return new Date(Date.UTC(1980+(e>>25&127),(e>>21&15)-1,e>>16&31,e>>11&31,e>>5&63,(31&e)<<1))}},t.exports=i},{"../utils":32}],19:[function(e,t,r){"use strict";var n=e("./Uint8ArrayReader");function i(e){n.call(this,e)}e("../utils").inherits(i,n),i.prototype.readData=function(e){this.checkOffset(e);var t=this.data.slice(this.zero+this.index,this.zero+this.index+e);return this.index+=e,t},t.exports=i},{"../utils":32,"./Uint8ArrayReader":21}],20:[function(e,t,r){"use strict";var n=e("./DataReader");function i(e){n.call(this,e)}e("../utils").inherits(i,n),i.prototype.byteAt=function(e){return this.data.charCodeAt(this.zero+e)},i.prototype.lastIndexOfSignature=function(e){return this.data.lastIndexOf(e)-this.zero},i.prototype.readAndCheckSignature=function(e){return e===this.readData(4)},i.prototype.readData=function(e){this.checkOffset(e);var t=this.data.slice(this.zero+this.index,this.zero+this.index+e);return this.index+=e,t},t.exports=i},{"../utils":32,"./DataReader":18}],21:[function(e,t,r){"use strict";var n=e("./ArrayReader");function i(e){n.call(this,e)}e("../utils").inherits(i,n),i.prototype.readData=function(e){if(this.checkOffset(e),0===e)return new Uint8Array(0);var t=this.data.subarray(this.zero+this.index,this.zero+this.index+e);return this.index+=e,t},t.exports=i},{"../utils":32,"./ArrayReader":17}],22:[function(e,t,r){"use strict";var n=e("../utils"),i=e("../support"),s=e("./ArrayReader"),a=e("./StringReader"),o=e("./NodeBufferReader"),h=e("./Uint8ArrayReader");t.exports=function(e){var t=n.getTypeOf(e);return n.checkSupport(t),"string"!==t||i.uint8array?"nodebuffer"===t?new o(e):i.uint8array?new h(n.transformTo("uint8array",e)):new s(n.transformTo("array",e)):new a(e)}},{"../support":30,"../utils":32,"./ArrayReader":17,"./NodeBufferReader":19,"./StringReader":20,"./Uint8ArrayReader":21}],23:[function(e,t,r){"use strict";r.LOCAL_FILE_HEADER="PK",r.CENTRAL_FILE_HEADER="PK",r.CENTRAL_DIRECTORY_END="PK",r.ZIP64_CENTRAL_DIRECTORY_LOCATOR="PK",r.ZIP64_CENTRAL_DIRECTORY_END="PK",r.DATA_DESCRIPTOR="PK\b"},{}],24:[function(e,t,r){"use strict";var n=e("./GenericWorker"),i=e("../utils");function s(e){n.call(this,"ConvertWorker to "+e),this.destType=e}i.inherits(s,n),s.prototype.processChunk=function(e){this.push({data:i.transformTo(this.destType,e.data),meta:e.meta})},t.exports=s},{"../utils":32,"./GenericWorker":28}],25:[function(e,t,r){"use strict";var n=e("./GenericWorker"),i=e("../crc32");function s(){n.call(this,"Crc32Probe"),this.withStreamInfo("crc32",0)}e("../utils").inherits(s,n),s.prototype.processChunk=function(e){this.streamInfo.crc32=i(e.data,this.streamInfo.crc32||0),this.push(e)},t.exports=s},{"../crc32":4,"../utils":32,"./GenericWorker":28}],26:[function(e,t,r){"use strict";var n=e("../utils"),i=e("./GenericWorker");function s(e){i.call(this,"DataLengthProbe for "+e),this.propName=e,this.withStreamInfo(e,0)}n.inherits(s,i),s.prototype.processChunk=function(e){if(e){var t=this.streamInfo[this.propName]||0;this.streamInfo[this.propName]=t+e.data.length}i.prototype.processChunk.call(this,e)},t.exports=s},{"../utils":32,"./GenericWorker":28}],27:[function(e,t,r){"use strict";var n=e("../utils"),i=e("./GenericWorker");function s(e){i.call(this,"DataWorker");var t=this;this.dataIsReady=!1,this.index=0,this.max=0,this.data=null,this.type="",this._tickScheduled=!1,e.then(function(e){t.dataIsReady=!0,t.data=e,t.max=e&&e.length||0,t.type=n.getTypeOf(e),t.isPaused||t._tickAndRepeat()},function(e){t.error(e)})}n.inherits(s,i),s.prototype.cleanUp=function(){i.prototype.cleanUp.call(this),this.data=null},s.prototype.resume=function(){return!!i.prototype.resume.call(this)&&(!this._tickScheduled&&this.dataIsReady&&(this._tickScheduled=!0,n.delay(this._tickAndRepeat,[],this)),!0)},s.prototype._tickAndRepeat=function(){this._tickScheduled=!1,this.isPaused||this.isFinished||(this._tick(),this.isFinished||(n.delay(this._tickAndRepeat,[],this),this._tickScheduled=!0))},s.prototype._tick=function(){if(this.isPaused||this.isFinished)return!1;var e=null,t=Math.min(this.max,this.index+16384);if(this.index>=this.max)return this.end();switch(this.type){case"string":e=this.data.substring(this.index,t);break;case"uint8array":e=this.data.subarray(this.index,t);break;case"array":case"nodebuffer":e=this.data.slice(this.index,t)}return this.index=t,this.push({data:e,meta:{percent:this.max?this.index/this.max*100:0}})},t.exports=s},{"../utils":32,"./GenericWorker":28}],28:[function(e,t,r){"use strict";function n(e){this.name=e||"default",this.streamInfo={},this.generatedError=null,this.extraStreamInfo={},this.isPaused=!0,this.isFinished=!1,this.isLocked=!1,this._listeners={data:[],end:[],error:[]},this.previous=null}n.prototype={push:function(e){this.emit("data",e)},end:function(){if(this.isFinished)return!1;this.flush();try{this.emit("end"),this.cleanUp(),this.isFinished=!0}catch(e){this.emit("error",e)}return!0},error:function(e){return!this.isFinished&&(this.isPaused?this.generatedError=e:(this.isFinished=!0,this.emit("error",e),this.previous&&this.previous.error(e),this.cleanUp()),!0)},on:function(e,t){return this._listeners[e].push(t),this},cleanUp:function(){this.streamInfo=this.generatedError=this.extraStreamInfo=null,this._listeners=[]},emit:function(e,t){if(this._listeners[e])for(var r=0;r "+e:e}},t.exports=n},{}],29:[function(e,t,r){"use strict";var h=e("../utils"),i=e("./ConvertWorker"),s=e("./GenericWorker"),u=e("../base64"),n=e("../support"),a=e("../external"),o=null;if(n.nodestream)try{o=e("../nodejs/NodejsStreamOutputAdapter")}catch(e){}function l(e,o){return new a.Promise(function(t,r){var n=[],i=e._internalType,s=e._outputType,a=e._mimeType;e.on("data",function(e,t){n.push(e),o&&o(t)}).on("error",function(e){n=[],r(e)}).on("end",function(){try{var e=function(e,t,r){switch(e){case"blob":return h.newBlob(h.transformTo("arraybuffer",t),r);case"base64":return u.encode(t);default:return h.transformTo(e,t)}}(s,function(e,t){var r,n=0,i=null,s=0;for(r=0;r>>6:(r<65536?t[s++]=224|r>>>12:(t[s++]=240|r>>>18,t[s++]=128|r>>>12&63),t[s++]=128|r>>>6&63),t[s++]=128|63&r);return t}(e)},s.utf8decode=function(e){return h.nodebuffer?o.transformTo("nodebuffer",e).toString("utf-8"):function(e){var t,r,n,i,s=e.length,a=new Array(2*s);for(t=r=0;t>10&1023,a[r++]=56320|1023&n)}return a.length!==r&&(a.subarray?a=a.subarray(0,r):a.length=r),o.applyFromCharCode(a)}(e=o.transformTo(h.uint8array?"uint8array":"array",e))},o.inherits(a,n),a.prototype.processChunk=function(e){var t=o.transformTo(h.uint8array?"uint8array":"array",e.data);if(this.leftOver&&this.leftOver.length){if(h.uint8array){var r=t;(t=new Uint8Array(r.length+this.leftOver.length)).set(this.leftOver,0),t.set(r,this.leftOver.length)}else t=this.leftOver.concat(t);this.leftOver=null}var n=function(e,t){var r;for((t=t||e.length)>e.length&&(t=e.length),r=t-1;0<=r&&128==(192&e[r]);)r--;return r<0?t:0===r?t:r+u[e[r]]>t?r:t}(t),i=t;n!==t.length&&(h.uint8array?(i=t.subarray(0,n),this.leftOver=t.subarray(n,t.length)):(i=t.slice(0,n),this.leftOver=t.slice(n,t.length))),this.push({data:s.utf8decode(i),meta:e.meta})},a.prototype.flush=function(){this.leftOver&&this.leftOver.length&&(this.push({data:s.utf8decode(this.leftOver),meta:{}}),this.leftOver=null)},s.Utf8DecodeWorker=a,o.inherits(l,n),l.prototype.processChunk=function(e){this.push({data:s.utf8encode(e.data),meta:e.meta})},s.Utf8EncodeWorker=l},{"./nodejsUtils":14,"./stream/GenericWorker":28,"./support":30,"./utils":32}],32:[function(e,t,a){"use strict";var o=e("./support"),h=e("./base64"),r=e("./nodejsUtils"),u=e("./external");function n(e){return e}function l(e,t){for(var r=0;r>8;this.dir=!!(16&this.externalFileAttributes),0==e&&(this.dosPermissions=63&this.externalFileAttributes),3==e&&(this.unixPermissions=this.externalFileAttributes>>16&65535),this.dir||"/"!==this.fileNameStr.slice(-1)||(this.dir=!0)},parseZIP64ExtraField:function(){if(this.extraFields[1]){var e=n(this.extraFields[1].value);this.uncompressedSize===s.MAX_VALUE_32BITS&&(this.uncompressedSize=e.readInt(8)),this.compressedSize===s.MAX_VALUE_32BITS&&(this.compressedSize=e.readInt(8)),this.localHeaderOffset===s.MAX_VALUE_32BITS&&(this.localHeaderOffset=e.readInt(8)),this.diskNumberStart===s.MAX_VALUE_32BITS&&(this.diskNumberStart=e.readInt(4))}},readExtraFields:function(e){var t,r,n,i=e.index+this.extraFieldsLength;for(this.extraFields||(this.extraFields={});e.index+4>>6:(r<65536?t[s++]=224|r>>>12:(t[s++]=240|r>>>18,t[s++]=128|r>>>12&63),t[s++]=128|r>>>6&63),t[s++]=128|63&r);return t},r.buf2binstring=function(e){return l(e,e.length)},r.binstring2buf=function(e){for(var t=new h.Buf8(e.length),r=0,n=t.length;r>10&1023,o[n++]=56320|1023&i)}return l(o,n)},r.utf8border=function(e,t){var r;for((t=t||e.length)>e.length&&(t=e.length),r=t-1;0<=r&&128==(192&e[r]);)r--;return r<0?t:0===r?t:r+u[e[r]]>t?r:t}},{"./common":41}],43:[function(e,t,r){"use strict";t.exports=function(e,t,r,n){for(var i=65535&e|0,s=e>>>16&65535|0,a=0;0!==r;){for(r-=a=2e3>>1:e>>>1;t[r]=e}return t}();t.exports=function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a>>8^i[255&(e^t[a])];return-1^e}},{}],46:[function(e,t,r){"use strict";var h,c=e("../utils/common"),u=e("./trees"),d=e("./adler32"),p=e("./crc32"),n=e("./messages"),l=0,f=4,m=0,_=-2,g=-1,b=4,i=2,v=8,y=9,s=286,a=30,o=19,w=2*s+1,k=15,x=3,S=258,z=S+x+1,C=42,E=113,A=1,I=2,O=3,B=4;function R(e,t){return e.msg=n[t],t}function T(e){return(e<<1)-(4e.avail_out&&(r=e.avail_out),0!==r&&(c.arraySet(e.output,t.pending_buf,t.pending_out,r,e.next_out),e.next_out+=r,t.pending_out+=r,e.total_out+=r,e.avail_out-=r,t.pending-=r,0===t.pending&&(t.pending_out=0))}function N(e,t){u._tr_flush_block(e,0<=e.block_start?e.block_start:-1,e.strstart-e.block_start,t),e.block_start=e.strstart,F(e.strm)}function U(e,t){e.pending_buf[e.pending++]=t}function P(e,t){e.pending_buf[e.pending++]=t>>>8&255,e.pending_buf[e.pending++]=255&t}function L(e,t){var r,n,i=e.max_chain_length,s=e.strstart,a=e.prev_length,o=e.nice_match,h=e.strstart>e.w_size-z?e.strstart-(e.w_size-z):0,u=e.window,l=e.w_mask,f=e.prev,c=e.strstart+S,d=u[s+a-1],p=u[s+a];e.prev_length>=e.good_match&&(i>>=2),o>e.lookahead&&(o=e.lookahead);do{if(u[(r=t)+a]===p&&u[r+a-1]===d&&u[r]===u[s]&&u[++r]===u[s+1]){s+=2,r++;do{}while(u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&sh&&0!=--i);return a<=e.lookahead?a:e.lookahead}function j(e){var t,r,n,i,s,a,o,h,u,l,f=e.w_size;do{if(i=e.window_size-e.lookahead-e.strstart,e.strstart>=f+(f-z)){for(c.arraySet(e.window,e.window,f,f,0),e.match_start-=f,e.strstart-=f,e.block_start-=f,t=r=e.hash_size;n=e.head[--t],e.head[t]=f<=n?n-f:0,--r;);for(t=r=f;n=e.prev[--t],e.prev[t]=f<=n?n-f:0,--r;);i+=f}if(0===e.strm.avail_in)break;if(a=e.strm,o=e.window,h=e.strstart+e.lookahead,u=i,l=void 0,l=a.avail_in,u=x)for(s=e.strstart-e.insert,e.ins_h=e.window[s],e.ins_h=(e.ins_h<=x&&(e.ins_h=(e.ins_h<=x)if(n=u._tr_tally(e,e.strstart-e.match_start,e.match_length-x),e.lookahead-=e.match_length,e.match_length<=e.max_lazy_match&&e.lookahead>=x){for(e.match_length--;e.strstart++,e.ins_h=(e.ins_h<=x&&(e.ins_h=(e.ins_h<=x&&e.match_length<=e.prev_length){for(i=e.strstart+e.lookahead-x,n=u._tr_tally(e,e.strstart-1-e.prev_match,e.prev_length-x),e.lookahead-=e.prev_length-1,e.prev_length-=2;++e.strstart<=i&&(e.ins_h=(e.ins_h<e.pending_buf_size-5&&(r=e.pending_buf_size-5);;){if(e.lookahead<=1){if(j(e),0===e.lookahead&&t===l)return A;if(0===e.lookahead)break}e.strstart+=e.lookahead,e.lookahead=0;var n=e.block_start+r;if((0===e.strstart||e.strstart>=n)&&(e.lookahead=e.strstart-n,e.strstart=n,N(e,!1),0===e.strm.avail_out))return A;if(e.strstart-e.block_start>=e.w_size-z&&(N(e,!1),0===e.strm.avail_out))return A}return e.insert=0,t===f?(N(e,!0),0===e.strm.avail_out?O:B):(e.strstart>e.block_start&&(N(e,!1),e.strm.avail_out),A)}),new M(4,4,8,4,Z),new M(4,5,16,8,Z),new M(4,6,32,32,Z),new M(4,4,16,16,W),new M(8,16,32,32,W),new M(8,16,128,128,W),new M(8,32,128,256,W),new M(32,128,258,1024,W),new M(32,258,258,4096,W)],r.deflateInit=function(e,t){return Y(e,t,v,15,8,0)},r.deflateInit2=Y,r.deflateReset=K,r.deflateResetKeep=G,r.deflateSetHeader=function(e,t){return e&&e.state?2!==e.state.wrap?_:(e.state.gzhead=t,m):_},r.deflate=function(e,t){var r,n,i,s;if(!e||!e.state||5>8&255),U(n,n.gzhead.time>>16&255),U(n,n.gzhead.time>>24&255),U(n,9===n.level?2:2<=n.strategy||n.level<2?4:0),U(n,255&n.gzhead.os),n.gzhead.extra&&n.gzhead.extra.length&&(U(n,255&n.gzhead.extra.length),U(n,n.gzhead.extra.length>>8&255)),n.gzhead.hcrc&&(e.adler=p(e.adler,n.pending_buf,n.pending,0)),n.gzindex=0,n.status=69):(U(n,0),U(n,0),U(n,0),U(n,0),U(n,0),U(n,9===n.level?2:2<=n.strategy||n.level<2?4:0),U(n,3),n.status=E);else{var a=v+(n.w_bits-8<<4)<<8;a|=(2<=n.strategy||n.level<2?0:n.level<6?1:6===n.level?2:3)<<6,0!==n.strstart&&(a|=32),a+=31-a%31,n.status=E,P(n,a),0!==n.strstart&&(P(n,e.adler>>>16),P(n,65535&e.adler)),e.adler=1}if(69===n.status)if(n.gzhead.extra){for(i=n.pending;n.gzindex<(65535&n.gzhead.extra.length)&&(n.pending!==n.pending_buf_size||(n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),F(e),i=n.pending,n.pending!==n.pending_buf_size));)U(n,255&n.gzhead.extra[n.gzindex]),n.gzindex++;n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),n.gzindex===n.gzhead.extra.length&&(n.gzindex=0,n.status=73)}else n.status=73;if(73===n.status)if(n.gzhead.name){i=n.pending;do{if(n.pending===n.pending_buf_size&&(n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),F(e),i=n.pending,n.pending===n.pending_buf_size)){s=1;break}s=n.gzindexi&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),0===s&&(n.gzindex=0,n.status=91)}else n.status=91;if(91===n.status)if(n.gzhead.comment){i=n.pending;do{if(n.pending===n.pending_buf_size&&(n.gzhead.hcrc&&n.pending>i&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),F(e),i=n.pending,n.pending===n.pending_buf_size)){s=1;break}s=n.gzindexi&&(e.adler=p(e.adler,n.pending_buf,n.pending-i,i)),0===s&&(n.status=103)}else n.status=103;if(103===n.status&&(n.gzhead.hcrc?(n.pending+2>n.pending_buf_size&&F(e),n.pending+2<=n.pending_buf_size&&(U(n,255&e.adler),U(n,e.adler>>8&255),e.adler=0,n.status=E)):n.status=E),0!==n.pending){if(F(e),0===e.avail_out)return n.last_flush=-1,m}else if(0===e.avail_in&&T(t)<=T(r)&&t!==f)return R(e,-5);if(666===n.status&&0!==e.avail_in)return R(e,-5);if(0!==e.avail_in||0!==n.lookahead||t!==l&&666!==n.status){var o=2===n.strategy?function(e,t){for(var r;;){if(0===e.lookahead&&(j(e),0===e.lookahead)){if(t===l)return A;break}if(e.match_length=0,r=u._tr_tally(e,0,e.window[e.strstart]),e.lookahead--,e.strstart++,r&&(N(e,!1),0===e.strm.avail_out))return A}return e.insert=0,t===f?(N(e,!0),0===e.strm.avail_out?O:B):e.last_lit&&(N(e,!1),0===e.strm.avail_out)?A:I}(n,t):3===n.strategy?function(e,t){for(var r,n,i,s,a=e.window;;){if(e.lookahead<=S){if(j(e),e.lookahead<=S&&t===l)return A;if(0===e.lookahead)break}if(e.match_length=0,e.lookahead>=x&&0e.lookahead&&(e.match_length=e.lookahead)}if(e.match_length>=x?(r=u._tr_tally(e,1,e.match_length-x),e.lookahead-=e.match_length,e.strstart+=e.match_length,e.match_length=0):(r=u._tr_tally(e,0,e.window[e.strstart]),e.lookahead--,e.strstart++),r&&(N(e,!1),0===e.strm.avail_out))return A}return e.insert=0,t===f?(N(e,!0),0===e.strm.avail_out?O:B):e.last_lit&&(N(e,!1),0===e.strm.avail_out)?A:I}(n,t):h[n.level].func(n,t);if(o!==O&&o!==B||(n.status=666),o===A||o===O)return 0===e.avail_out&&(n.last_flush=-1),m;if(o===I&&(1===t?u._tr_align(n):5!==t&&(u._tr_stored_block(n,0,0,!1),3===t&&(D(n.head),0===n.lookahead&&(n.strstart=0,n.block_start=0,n.insert=0))),F(e),0===e.avail_out))return n.last_flush=-1,m}return t!==f?m:n.wrap<=0?1:(2===n.wrap?(U(n,255&e.adler),U(n,e.adler>>8&255),U(n,e.adler>>16&255),U(n,e.adler>>24&255),U(n,255&e.total_in),U(n,e.total_in>>8&255),U(n,e.total_in>>16&255),U(n,e.total_in>>24&255)):(P(n,e.adler>>>16),P(n,65535&e.adler)),F(e),0=r.w_size&&(0===s&&(D(r.head),r.strstart=0,r.block_start=0,r.insert=0),u=new c.Buf8(r.w_size),c.arraySet(u,t,l-r.w_size,r.w_size,0),t=u,l=r.w_size),a=e.avail_in,o=e.next_in,h=e.input,e.avail_in=l,e.next_in=0,e.input=t,j(r);r.lookahead>=x;){for(n=r.strstart,i=r.lookahead-(x-1);r.ins_h=(r.ins_h<>>=y=v>>>24,p-=y,0===(y=v>>>16&255))C[s++]=65535&v;else{if(!(16&y)){if(0==(64&y)){v=m[(65535&v)+(d&(1<>>=y,p-=y),p<15&&(d+=z[n++]<>>=y=v>>>24,p-=y,!(16&(y=v>>>16&255))){if(0==(64&y)){v=_[(65535&v)+(d&(1<>>=y,p-=y,(y=s-a)>3,d&=(1<<(p-=w<<3))-1,e.next_in=n,e.next_out=s,e.avail_in=n>>24&255)+(e>>>8&65280)+((65280&e)<<8)+((255&e)<<24)}function s(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new I.Buf16(320),this.work=new I.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function a(e){var t;return e&&e.state?(t=e.state,e.total_in=e.total_out=t.total=0,e.msg="",t.wrap&&(e.adler=1&t.wrap),t.mode=P,t.last=0,t.havedict=0,t.dmax=32768,t.head=null,t.hold=0,t.bits=0,t.lencode=t.lendyn=new I.Buf32(n),t.distcode=t.distdyn=new I.Buf32(i),t.sane=1,t.back=-1,N):U}function o(e){var t;return e&&e.state?((t=e.state).wsize=0,t.whave=0,t.wnext=0,a(e)):U}function h(e,t){var r,n;return e&&e.state?(n=e.state,t<0?(r=0,t=-t):(r=1+(t>>4),t<48&&(t&=15)),t&&(t<8||15=s.wsize?(I.arraySet(s.window,t,r-s.wsize,s.wsize,0),s.wnext=0,s.whave=s.wsize):(n<(i=s.wsize-s.wnext)&&(i=n),I.arraySet(s.window,t,r-n,i,s.wnext),(n-=i)?(I.arraySet(s.window,t,r-n,n,0),s.wnext=n,s.whave=s.wsize):(s.wnext+=i,s.wnext===s.wsize&&(s.wnext=0),s.whave>>8&255,r.check=B(r.check,E,2,0),l=u=0,r.mode=2;break}if(r.flags=0,r.head&&(r.head.done=!1),!(1&r.wrap)||(((255&u)<<8)+(u>>8))%31){e.msg="incorrect header check",r.mode=30;break}if(8!=(15&u)){e.msg="unknown compression method",r.mode=30;break}if(l-=4,k=8+(15&(u>>>=4)),0===r.wbits)r.wbits=k;else if(k>r.wbits){e.msg="invalid window size",r.mode=30;break}r.dmax=1<>8&1),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0)),l=u=0,r.mode=3;case 3:for(;l<32;){if(0===o)break e;o--,u+=n[s++]<>>8&255,E[2]=u>>>16&255,E[3]=u>>>24&255,r.check=B(r.check,E,4,0)),l=u=0,r.mode=4;case 4:for(;l<16;){if(0===o)break e;o--,u+=n[s++]<>8),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0)),l=u=0,r.mode=5;case 5:if(1024&r.flags){for(;l<16;){if(0===o)break e;o--,u+=n[s++]<>>8&255,r.check=B(r.check,E,2,0)),l=u=0}else r.head&&(r.head.extra=null);r.mode=6;case 6:if(1024&r.flags&&(o<(d=r.length)&&(d=o),d&&(r.head&&(k=r.head.extra_len-r.length,r.head.extra||(r.head.extra=new Array(r.head.extra_len)),I.arraySet(r.head.extra,n,s,d,k)),512&r.flags&&(r.check=B(r.check,n,d,s)),o-=d,s+=d,r.length-=d),r.length))break e;r.length=0,r.mode=7;case 7:if(2048&r.flags){if(0===o)break e;for(d=0;k=n[s+d++],r.head&&k&&r.length<65536&&(r.head.name+=String.fromCharCode(k)),k&&d>9&1,r.head.done=!0),e.adler=r.check=0,r.mode=12;break;case 10:for(;l<32;){if(0===o)break e;o--,u+=n[s++]<>>=7&l,l-=7&l,r.mode=27;break}for(;l<3;){if(0===o)break e;o--,u+=n[s++]<>>=1)){case 0:r.mode=14;break;case 1:if(j(r),r.mode=20,6!==t)break;u>>>=2,l-=2;break e;case 2:r.mode=17;break;case 3:e.msg="invalid block type",r.mode=30}u>>>=2,l-=2;break;case 14:for(u>>>=7&l,l-=7&l;l<32;){if(0===o)break e;o--,u+=n[s++]<>>16^65535)){e.msg="invalid stored block lengths",r.mode=30;break}if(r.length=65535&u,l=u=0,r.mode=15,6===t)break e;case 15:r.mode=16;case 16:if(d=r.length){if(o>>=5,l-=5,r.ndist=1+(31&u),u>>>=5,l-=5,r.ncode=4+(15&u),u>>>=4,l-=4,286>>=3,l-=3}for(;r.have<19;)r.lens[A[r.have++]]=0;if(r.lencode=r.lendyn,r.lenbits=7,S={bits:r.lenbits},x=T(0,r.lens,0,19,r.lencode,0,r.work,S),r.lenbits=S.bits,x){e.msg="invalid code lengths set",r.mode=30;break}r.have=0,r.mode=19;case 19:for(;r.have>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<>>=_,l-=_,r.lens[r.have++]=b;else{if(16===b){for(z=_+2;l>>=_,l-=_,0===r.have){e.msg="invalid bit length repeat",r.mode=30;break}k=r.lens[r.have-1],d=3+(3&u),u>>>=2,l-=2}else if(17===b){for(z=_+3;l>>=_)),u>>>=3,l-=3}else{for(z=_+7;l>>=_)),u>>>=7,l-=7}if(r.have+d>r.nlen+r.ndist){e.msg="invalid bit length repeat",r.mode=30;break}for(;d--;)r.lens[r.have++]=k}}if(30===r.mode)break;if(0===r.lens[256]){e.msg="invalid code -- missing end-of-block",r.mode=30;break}if(r.lenbits=9,S={bits:r.lenbits},x=T(D,r.lens,0,r.nlen,r.lencode,0,r.work,S),r.lenbits=S.bits,x){e.msg="invalid literal/lengths set",r.mode=30;break}if(r.distbits=6,r.distcode=r.distdyn,S={bits:r.distbits},x=T(F,r.lens,r.nlen,r.ndist,r.distcode,0,r.work,S),r.distbits=S.bits,x){e.msg="invalid distances set",r.mode=30;break}if(r.mode=20,6===t)break e;case 20:r.mode=21;case 21:if(6<=o&&258<=h){e.next_out=a,e.avail_out=h,e.next_in=s,e.avail_in=o,r.hold=u,r.bits=l,R(e,c),a=e.next_out,i=e.output,h=e.avail_out,s=e.next_in,n=e.input,o=e.avail_in,u=r.hold,l=r.bits,12===r.mode&&(r.back=-1);break}for(r.back=0;g=(C=r.lencode[u&(1<>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<>v)])>>>16&255,b=65535&C,!(v+(_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<>>=v,l-=v,r.back+=v}if(u>>>=_,l-=_,r.back+=_,r.length=b,0===g){r.mode=26;break}if(32&g){r.back=-1,r.mode=12;break}if(64&g){e.msg="invalid literal/length code",r.mode=30;break}r.extra=15&g,r.mode=22;case 22:if(r.extra){for(z=r.extra;l>>=r.extra,l-=r.extra,r.back+=r.extra}r.was=r.length,r.mode=23;case 23:for(;g=(C=r.distcode[u&(1<>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<>v)])>>>16&255,b=65535&C,!(v+(_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<>>=v,l-=v,r.back+=v}if(u>>>=_,l-=_,r.back+=_,64&g){e.msg="invalid distance code",r.mode=30;break}r.offset=b,r.extra=15&g,r.mode=24;case 24:if(r.extra){for(z=r.extra;l>>=r.extra,l-=r.extra,r.back+=r.extra}if(r.offset>r.dmax){e.msg="invalid distance too far back",r.mode=30;break}r.mode=25;case 25:if(0===h)break e;if(d=c-h,r.offset>d){if((d=r.offset-d)>r.whave&&r.sane){e.msg="invalid distance too far back",r.mode=30;break}p=d>r.wnext?(d-=r.wnext,r.wsize-d):r.wnext-d,d>r.length&&(d=r.length),m=r.window}else m=i,p=a-r.offset,d=r.length;for(hd?(m=R[T+a[v]],A[I+a[v]]):(m=96,0),h=1<>S)+(u-=h)]=p<<24|m<<16|_|0,0!==u;);for(h=1<>=1;if(0!==h?(E&=h-1,E+=h):E=0,v++,0==--O[b]){if(b===w)break;b=t[r+a[v]]}if(k>>7)]}function U(e,t){e.pending_buf[e.pending++]=255&t,e.pending_buf[e.pending++]=t>>>8&255}function P(e,t,r){e.bi_valid>d-r?(e.bi_buf|=t<>d-e.bi_valid,e.bi_valid+=r-d):(e.bi_buf|=t<>>=1,r<<=1,0<--t;);return r>>>1}function Z(e,t,r){var n,i,s=new Array(g+1),a=0;for(n=1;n<=g;n++)s[n]=a=a+r[n-1]<<1;for(i=0;i<=t;i++){var o=e[2*i+1];0!==o&&(e[2*i]=j(s[o]++,o))}}function W(e){var t;for(t=0;t>1;1<=r;r--)G(e,s,r);for(i=h;r=e.heap[1],e.heap[1]=e.heap[e.heap_len--],G(e,s,1),n=e.heap[1],e.heap[--e.heap_max]=r,e.heap[--e.heap_max]=n,s[2*i]=s[2*r]+s[2*n],e.depth[i]=(e.depth[r]>=e.depth[n]?e.depth[r]:e.depth[n])+1,s[2*r+1]=s[2*n+1]=i,e.heap[1]=i++,G(e,s,1),2<=e.heap_len;);e.heap[--e.heap_max]=e.heap[1],function(e,t){var r,n,i,s,a,o,h=t.dyn_tree,u=t.max_code,l=t.stat_desc.static_tree,f=t.stat_desc.has_stree,c=t.stat_desc.extra_bits,d=t.stat_desc.extra_base,p=t.stat_desc.max_length,m=0;for(s=0;s<=g;s++)e.bl_count[s]=0;for(h[2*e.heap[e.heap_max]+1]=0,r=e.heap_max+1;r<_;r++)p<(s=h[2*h[2*(n=e.heap[r])+1]+1]+1)&&(s=p,m++),h[2*n+1]=s,u>=7;n>>=1)if(1&r&&0!==e.dyn_ltree[2*t])return o;if(0!==e.dyn_ltree[18]||0!==e.dyn_ltree[20]||0!==e.dyn_ltree[26])return h;for(t=32;t>>3,(s=e.static_len+3+7>>>3)<=i&&(i=s)):i=s=r+5,r+4<=i&&-1!==t?J(e,t,r,n):4===e.strategy||s===i?(P(e,2+(n?1:0),3),K(e,z,C)):(P(e,4+(n?1:0),3),function(e,t,r,n){var i;for(P(e,t-257,5),P(e,r-1,5),P(e,n-4,4),i=0;i>>8&255,e.pending_buf[e.d_buf+2*e.last_lit+1]=255&t,e.pending_buf[e.l_buf+e.last_lit]=255&r,e.last_lit++,0===t?e.dyn_ltree[2*r]++:(e.matches++,t--,e.dyn_ltree[2*(A[r]+u+1)]++,e.dyn_dtree[2*N(t)]++),e.last_lit===e.lit_bufsize-1},r._tr_align=function(e){P(e,2,3),L(e,m,z),function(e){16===e.bi_valid?(U(e,e.bi_buf),e.bi_buf=0,e.bi_valid=0):8<=e.bi_valid&&(e.pending_buf[e.pending++]=255&e.bi_buf,e.bi_buf>>=8,e.bi_valid-=8)}(e)}},{"../utils/common":41}],53:[function(e,t,r){"use strict";t.exports=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}},{}],54:[function(e,t,r){(function(e){!function(r,n){"use strict";if(!r.setImmediate){var i,s,t,a,o=1,h={},u=!1,l=r.document,e=Object.getPrototypeOf&&Object.getPrototypeOf(r);e=e&&e.setTimeout?e:r,i="[object process]"==={}.toString.call(r.process)?function(e){process.nextTick(function(){c(e)})}:function(){if(r.postMessage&&!r.importScripts){var e=!0,t=r.onmessage;return r.onmessage=function(){e=!1},r.postMessage("","*"),r.onmessage=t,e}}()?(a="setImmediate$"+Math.random()+"$",r.addEventListener?r.addEventListener("message",d,!1):r.attachEvent("onmessage",d),function(e){r.postMessage(a+e,"*")}):r.MessageChannel?((t=new MessageChannel).port1.onmessage=function(e){c(e.data)},function(e){t.port2.postMessage(e)}):l&&"onreadystatechange"in l.createElement("script")?(s=l.documentElement,function(e){var t=l.createElement("script");t.onreadystatechange=function(){c(e),t.onreadystatechange=null,s.removeChild(t),t=null},s.appendChild(t)}):function(e){setTimeout(c,0,e)},e.setImmediate=function(e){"function"!=typeof e&&(e=new Function(""+e));for(var t=new Array(arguments.length-1),r=0;r 0) {
+ const zip = new JSZip();
+ let safeTitle = title.replace(/[\\/|\\:*?"<>]/g, ' ');
+ const imageFolder = zip.folder("images");
+
+ // loop through the image list and replace the images in the markdown
+ for (const [src, filename] of Object.entries(imageList)) {
+ const localImagePath = "images/" + filename;
+ markdown = markdown.replaceAll(src, localImagePath);
+ imageFolder.file(filename, fetch(src).then(res => res.blob()));
+ }
+
+ // add the markdown file to the zip
+ zip.file(safeTitle + '.md', markdown);
+
+ // generate the zip and download it
+ const content = await zip.generateAsync({ type: 'base64' });
+ const dataUrl = 'data:application/zip;base64,' + content;
+
+ const id = await chrome.downloads.download({
+ url: dataUrl,
+ filename: mdClipsFolder + safeTitle + ".zip",
+ saveAs: options.saveAs
+ });
+
+ return;
+ }
+
if (options.downloadMode == 'downloadsApi' && chrome.downloads) {
console.log('MarkDownload: Using downloads API');
// Use data URL directly; createObjectURL is not available in MV3 service worker
- const dataUrl = 'data:text/markdown;charset=utf-8,' + encodeURIComponent(markdown);
+const dataUrl = 'data:text/markdown;charset=utf-8,' + encodeURIComponent(markdown);
console.log('MarkDownload: Data URL length:', dataUrl.length);
try {
diff --git a/src/package-lock.json b/src/package-lock.json
index d7ce449b..0fc36389 100644
--- a/src/package-lock.json
+++ b/src/package-lock.json
@@ -4,7 +4,9 @@
"requires": true,
"packages": {
"": {
- "name": "src",
+ "dependencies": {
+ "jszip": "^3.10.1"
+ },
"devDependencies": {
"web-ext": "^7.4.0"
}
@@ -1420,8 +1422,7 @@
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
- "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
- "dev": true
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
"node_modules/cross-spawn": {
"version": "7.0.3",
@@ -2771,8 +2772,7 @@
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
- "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=",
- "dev": true
+ "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps="
},
"node_modules/import-fresh": {
"version": "3.3.0",
@@ -2821,8 +2821,7 @@
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
- "dev": true
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/ini": {
"version": "2.0.0",
@@ -3029,8 +3028,7 @@
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
- "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
- "dev": true
+ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"node_modules/isexe": {
"version": "2.0.0",
@@ -3190,7 +3188,7 @@
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
- "dev": true,
+ "license": "(MIT OR GPL-3.0-or-later)",
"dependencies": {
"lie": "~3.3.0",
"pako": "~1.0.2",
@@ -3272,7 +3270,6 @@
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
- "dev": true,
"dependencies": {
"immediate": "~3.0.5"
}
@@ -3904,8 +3901,7 @@
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
- "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
- "dev": true
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
},
"node_modules/parent-module": {
"version": "1.0.1",
@@ -4101,8 +4097,7 @@
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
- "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
- "dev": true
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"node_modules/process-warning": {
"version": "3.0.0",
@@ -4258,7 +4253,6 @@
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
- "dev": true,
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
@@ -4272,8 +4266,7 @@
"node_modules/readable-stream/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
- "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
- "dev": true
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"node_modules/real-require": {
"version": "0.2.0",
@@ -4626,8 +4619,7 @@
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
- "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
- "dev": true
+ "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
},
"node_modules/sha.js": {
"version": "2.4.11",
@@ -4812,7 +4804,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
- "dev": true,
"dependencies": {
"safe-buffer": "~5.1.0"
}
@@ -4820,8 +4811,7 @@
"node_modules/string_decoder/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
- "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
- "dev": true
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"node_modules/string-width": {
"version": "5.1.2",
@@ -5219,8 +5209,7 @@
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
- "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
- "dev": true
+ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"node_modules/uuid": {
"version": "8.3.2",
@@ -6692,8 +6681,7 @@
"core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
- "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
- "dev": true
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
"cross-spawn": {
"version": "7.0.3",
@@ -7673,8 +7661,7 @@
"immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
- "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=",
- "dev": true
+ "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps="
},
"import-fresh": {
"version": "3.3.0",
@@ -7711,8 +7698,7 @@
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
- "dev": true
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"ini": {
"version": "2.0.0",
@@ -7853,8 +7839,7 @@
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
- "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
- "dev": true
+ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"isexe": {
"version": "2.0.0",
@@ -7994,7 +7979,6 @@
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
- "dev": true,
"requires": {
"lie": "~3.3.0",
"pako": "~1.0.2",
@@ -8064,7 +8048,6 @@
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
- "dev": true,
"requires": {
"immediate": "~3.0.5"
}
@@ -8529,8 +8512,7 @@
"pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
- "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
- "dev": true
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
},
"parent-module": {
"version": "1.0.1",
@@ -8686,8 +8668,7 @@
"process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
- "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
- "dev": true
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"process-warning": {
"version": "3.0.0",
@@ -8804,7 +8785,6 @@
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
- "dev": true,
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
@@ -8818,8 +8798,7 @@
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
- "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
- "dev": true
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
}
}
},
@@ -9069,8 +9048,7 @@
"setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
- "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
- "dev": true
+ "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
},
"sha.js": {
"version": "2.4.11",
@@ -9224,7 +9202,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
- "dev": true,
"requires": {
"safe-buffer": "~5.1.0"
},
@@ -9232,8 +9209,7 @@
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
- "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
- "dev": true
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
}
}
},
@@ -9534,8 +9510,7 @@
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
- "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
- "dev": true
+ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"uuid": {
"version": "8.3.2",
diff --git a/src/package.json b/src/package.json
index 2231dc45..4b0a8349 100644
--- a/src/package.json
+++ b/src/package.json
@@ -9,5 +9,8 @@
},
"devDependencies": {
"web-ext": "^7.4.0"
+ },
+ "dependencies": {
+ "jszip": "^3.10.1"
}
}
diff --git a/src/popup/popup.css b/src/popup/popup.css
index ee45f02f..9f2d2bb1 100644
--- a/src/popup/popup.css
+++ b/src/popup/popup.css
@@ -749,6 +749,232 @@ input#title {
align-items: center;
}
+/* Button with Overlay Help */
+.button-with-overlay-help {
+ position: relative;
+}
+
+.button-with-overlay-help:hover .overlay-help-icon {
+ opacity: 1;
+ transform: scale(1.05);
+}
+
+.overlay-help-icon {
+ position: absolute;
+ top: 4px;
+ right: 4px;
+ width: 20px;
+ height: 20px;
+ background: rgba(255, 255, 255, 0.9);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ opacity: 0.7;
+ color: var(--primary);
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ backdrop-filter: blur(4px);
+}
+
+.overlay-help-icon:hover {
+ opacity: 1;
+ transform: scale(1.1);
+ background: rgba(255, 255, 255, 0.95);
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
+}
+
+/* Dark mode support for overlay help */
+@media (prefers-color-scheme: dark) {
+ .overlay-help-icon {
+ background: rgba(0, 0, 0, 0.8);
+ color: var(--primary-light);
+ }
+
+ .overlay-help-icon:hover {
+ background: rgba(0, 0, 0, 0.9);
+ }
+}
+
+/* Modal Styles */
+.modal-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.5);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+ backdrop-filter: blur(4px);
+ animation: fadeIn 0.2s ease-out;
+}
+
+.modal-content {
+ background: var(--bg);
+ border-radius: var(--radius-lg);
+ width: 90%;
+ max-width: 480px;
+ max-height: 80vh;
+ overflow-y: auto;
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
+ animation: slideUp 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.modal-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: var(--spacing-lg);
+ border-bottom: 1px solid var(--border);
+ background: var(--bg-secondary);
+ border-radius: var(--radius-lg) var(--radius-lg) 0 0;
+}
+
+.modal-header h3 {
+ margin: 0;
+ font-size: var(--font-size-lg);
+ font-weight: var(--font-weight-medium);
+ color: var(--fg);
+}
+
+.close-button {
+ background: none;
+ border: none;
+ font-size: 24px;
+ cursor: pointer;
+ color: var(--fg-muted);
+ padding: var(--spacing-xs);
+ border-radius: var(--radius-sm);
+ transition: var(--transition-fast);
+ line-height: 1;
+}
+
+.close-button:hover {
+ background: var(--bg-tertiary);
+ color: var(--fg);
+}
+
+.modal-body {
+ padding: var(--spacing-lg);
+}
+
+@keyframes fadeIn {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
+@keyframes slideUp {
+ from {
+ opacity: 0;
+ transform: translateY(20px) scale(0.95);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+}
+
+/* Setup Steps */
+.setup-step {
+ display: flex;
+ gap: var(--spacing-md);
+ margin-bottom: var(--spacing-lg);
+ align-items: flex-start;
+}
+
+.step-number {
+ background: var(--primary);
+ color: white;
+ width: 28px;
+ height: 28px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: var(--font-weight-medium);
+ font-size: var(--font-size-sm);
+ flex-shrink: 0;
+}
+
+.step-content {
+ flex: 1;
+}
+
+.step-content h4 {
+ margin: 0 0 var(--spacing-sm) 0;
+ font-size: var(--font-size-base);
+ font-weight: var(--font-weight-medium);
+ color: var(--fg);
+}
+
+.step-content p {
+ margin: 0;
+ color: var(--fg-muted);
+ line-height: 1.5;
+}
+
+.quick-setup {
+ margin-top: var(--spacing-md);
+ padding: var(--spacing-md);
+ background: var(--bg-secondary);
+ border-radius: var(--radius-md);
+ border: 1px solid var(--border);
+}
+
+.quick-setup label {
+ display: block;
+ font-size: var(--font-size-sm);
+ font-weight: var(--font-weight-medium);
+ color: var(--fg);
+ margin-bottom: var(--spacing-xs);
+ margin-top: var(--spacing-sm);
+}
+
+.quick-setup label:first-child {
+ margin-top: 0;
+}
+
+.setup-input {
+ width: 100%;
+ padding: var(--spacing-sm);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ background: var(--bg);
+ color: var(--fg);
+ font-size: var(--font-size-sm);
+ margin-bottom: var(--spacing-sm);
+ transition: var(--transition-fast);
+ box-sizing: border-box;
+}
+
+.setup-input:focus {
+ outline: none;
+ border-color: var(--primary);
+ box-shadow: 0 0 0 2px var(--primary-light);
+}
+
+.setup-button {
+ background: var(--primary);
+ color: white;
+ border: none;
+ padding: var(--spacing-sm) var(--spacing-lg);
+ border-radius: var(--radius-md);
+ font-size: var(--font-size-sm);
+ font-weight: var(--font-weight-medium);
+ cursor: pointer;
+ transition: var(--transition-fast);
+ margin-top: var(--spacing-sm);
+}
+
+.setup-button:hover {
+ background: var(--primary-hover);
+ transform: translateY(-1px);
+}
+
.primary-button {
flex: 1;
diff --git a/src/popup/popup.html b/src/popup/popup.html
index a7ceded2..7d381fdc 100644
--- a/src/popup/popup.html
+++ b/src/popup/popup.html
@@ -1,135 +1,197 @@
-
-
-
-
- MarkDownload
-
-
-
-
-
-
-
-