Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 9 additions & 75 deletions background.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
// Storage configuration
const STORAGE_KEY = 'ezycopy_settings';
const DEFAULT_SETTINGS = {
copyToClipboard: true,
downloadMarkdown: true,
includeImages: true,
experimental: {
selectiveCopy: false,
downloadImagesLocally: false
}
};
// Shared settings/helpers
importScripts('settings.js');
importScripts('file-helpers.js');
importScripts('injection-files.js');

const { sanitizeImageFilename, EZYCOPY_FOLDER, IMAGES_SUBFOLDER } = self.EzyCopyFiles;
const { CONTENT_SCRIPTS } = self.EzyCopyInjection;

// Base folder for downloads (relative to Downloads folder)
const EZYCOPY_FOLDER = 'EzyCopy';
const IMAGES_SUBFOLDER = 'images';

// Create context menu when extension is installed
chrome.runtime.onInstalled.addListener(() => {
Expand All @@ -29,71 +22,12 @@ chrome.contextMenus.onClicked.addListener((info, tab) => {
// Inject libraries first, then the content script
chrome.scripting.executeScript({
target: { tabId: tab.id },
files: [
"lib/readability.js",
"lib/turndown.js",
"lib/turndown-plugin-gfm.js",
"lib/ezycopy.js",
"lib/platform.js",
"content-script.js",
],
files: CONTENT_SCRIPTS,
});
}
});

// Load settings from storage with migration support
async function loadSettings() {
const result = await chrome.storage.local.get(STORAGE_KEY);
const stored = result[STORAGE_KEY] || {};

return {
copyToClipboard: stored.copyToClipboard ?? DEFAULT_SETTINGS.copyToClipboard,
downloadMarkdown: stored.downloadMarkdown ?? DEFAULT_SETTINGS.downloadMarkdown,
includeImages: stored.includeImages ?? DEFAULT_SETTINGS.includeImages,
experimental: {
selectiveCopy: stored.experimental?.selectiveCopy ?? false,
downloadImagesLocally: stored.experimental?.downloadImagesLocally ?? false
}
};
}

/**
* Generate safe filename from URL
*/
function sanitizeImageFilename(url, index) {
try {
const urlObj = new URL(url);
let filename = urlObj.pathname.split('/').pop() || `image-${index}`;

// Remove query string if present
filename = filename.split('?')[0];

// Decode URI components
try {
filename = decodeURIComponent(filename);
} catch (e) {
// Keep as-is if decode fails
}

// Replace unsafe characters
filename = filename.replace(/[^a-zA-Z0-9._-]/g, '_');

// Ensure it has an extension
if (!filename.match(/\.(jpg|jpeg|png|gif|webp|svg|bmp|ico)$/i)) {
filename += '.png';
}

// Truncate if too long
if (filename.length > 200) {
const ext = filename.match(/\.[^.]+$/)?.[0] || '.png';
filename = filename.substring(0, 200 - ext.length) + ext;
}

return filename;
} catch (e) {
return `image-${index}.png`;
}
}
const { loadSettings } = self.EzyCopySettings;

/**
* Download a single image and return its local path
Expand Down
77 changes: 77 additions & 0 deletions file-helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Shared file/path helpers usable in window, content scripts, and service worker
(function (root) {
if (root.EzyCopyFiles) return;

const EZYCOPY_FOLDER = 'EzyCopy';
const IMAGES_SUBFOLDER = 'images';

function getBasePath() {
return EZYCOPY_FOLDER;
}

function getImagesPath(pageSubfolder) {
return `${EZYCOPY_FOLDER}/${IMAGES_SUBFOLDER}/${pageSubfolder}`;
}

function sanitizeImageFilename(url, index) {
try {
const urlObj = new URL(url);
let filename = urlObj.pathname.split('/').pop() || `image-${index}`;

filename = filename.split('?')[0];

try {
filename = decodeURIComponent(filename);
} catch (e) {
// keep as-is if decode fails
}

filename = filename.replace(/[^a-zA-Z0-9._-]/g, '_');

if (!filename.match(/\.(jpg|jpeg|png|gif|webp|svg|bmp|ico)$/i)) {
filename += '.png';
}

if (filename.length > 200) {
const ext = filename.match(/\.[^.]+$/)?.[0] || '.png';
filename = filename.substring(0, 200 - ext.length) + ext;
}

return filename;
} catch (e) {
return `image-${index}.png`;
}
}

function generatePageSubfolder(pageTitle) {
const safeTitle = pageTitle
.substring(0, 50)
.replace(/[^a-zA-Z0-9]/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
const timestamp = new Date().toISOString().slice(0, 10);
return `${safeTitle}-${timestamp}`;
}

function rewriteImagePaths(markdown, urlToPathMap) {
let result = markdown;

for (const [originalUrl, localPath] of Object.entries(urlToPathMap)) {
const escapedUrl = originalUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`(!\\[[^\\]]*\\]\\()${escapedUrl}((?:\\s+"[^"]*")?\\))`, 'g');
result = result.replace(regex, `$1${localPath}$2`);
}

return result;
}

root.EzyCopyFiles = {
EZYCOPY_FOLDER,
IMAGES_SUBFOLDER,
getBasePath,
getImagesPath,
sanitizeImageFilename,
generatePageSubfolder,
rewriteImagePaths,
};
})(typeof self !== 'undefined' ? self : this);
18 changes: 18 additions & 0 deletions injection-files.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Shared list of scripts to inject into the active tab
(function (root) {
if (root.EzyCopyInjection) return;

const CONTENT_SCRIPTS = [
"lib/readability.js",
"lib/turndown.js",
"lib/turndown-plugin-gfm.js",
"file-helpers.js",
"lib/ezycopy.js",
"lib/platform.js",
"content-script.js",
];

root.EzyCopyInjection = {
CONTENT_SCRIPTS,
};
})(typeof self !== 'undefined' ? self : this);
115 changes: 14 additions & 101 deletions lib/platform.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,108 +2,21 @@
// Prevent redeclaration when scripts are injected multiple times
if (window.EzyCopyPlatform) return;

/**
* EzyCopy - Platform utilities for file operations
*/

// Base folder name (relative to Downloads folder)
const EZYCOPY_FOLDER = 'EzyCopy';
const IMAGES_SUBFOLDER = 'images';

/**
* Get the relative path prefix for EzyCopy downloads
* Chrome downloads API uses paths relative to Downloads folder
*/
function getEzyCopyBasePath() {
return EZYCOPY_FOLDER;
}

/**
* Get the images folder path for a specific page
* @param {string} pageSubfolder - Sanitized page title/date subfolder name
* @returns {string} Path like "EzyCopy/images/page-name-2025-01-15"
*/
function getImagesPath(pageSubfolder) {
return `${EZYCOPY_FOLDER}/${IMAGES_SUBFOLDER}/${pageSubfolder}`;
}

/**
* Generate a safe filename from an image URL
* Handles query strings, special characters, and long names
* @param {string} url - Original image URL
* @param {number} index - Index for fallback naming
* @returns {string} Safe filename like "image-name.png"
*/
function sanitizeImageFilename(url, index) {
try {
const urlObj = new URL(url);
let filename = urlObj.pathname.split('/').pop() || `image-${index}`;

// Remove query string if it got included
filename = filename.split('?')[0];

// Decode URI components
try {
filename = decodeURIComponent(filename);
} catch (e) {
// Keep as-is if decode fails
}

// Replace unsafe characters
filename = filename.replace(/[^a-zA-Z0-9._-]/g, '_');

// Ensure it has an extension, default to .png
if (!filename.match(/\.(jpg|jpeg|png|gif|webp|svg|bmp|ico)$/i)) {
filename += '.png';
}

// Truncate if too long (keep extension)
if (filename.length > 200) {
const ext = filename.match(/\.[^.]+$/)?.[0] || '.png';
filename = filename.substring(0, 200 - ext.length) + ext;
}

return filename;
} catch (e) {
return `image-${index}.png`;
}
// Expect shared helpers to be loaded first
if (!window.EzyCopyFiles) {
console.error('EzyCopyFiles not loaded; platform helpers unavailable');
return;
}

/**
* Generate the page subfolder name from page title and date
* @param {string} pageTitle - Page title
* @returns {string} Sanitized subfolder name
*/
function generatePageSubfolder(pageTitle) {
const safeTitle = pageTitle
.substring(0, 50)
.replace(/[^a-zA-Z0-9]/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
const timestamp = new Date().toISOString().slice(0, 10);
return `${safeTitle}-${timestamp}`;
}

/**
* Rewrite image URLs in markdown with local file paths
* @param {string} markdown - Original markdown content
* @param {Object} urlToPathMap - Map of original URL to local absolute path
* @returns {string} Markdown with rewritten image paths
*/
function rewriteImagePaths(markdown, urlToPathMap) {
let result = markdown;

for (const [originalUrl, localPath] of Object.entries(urlToPathMap)) {
// Escape special regex characters in the URL
const escapedUrl = originalUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

// Match markdown image syntax: ![alt](url) or ![alt](url "title")
const regex = new RegExp(`(!\\[[^\\]]*\\]\\()${escapedUrl}((?:\\s+"[^"]*")?\\))`, 'g');
result = result.replace(regex, `$1${localPath}$2`);
}

return result;
}
const {
EZYCOPY_FOLDER,
IMAGES_SUBFOLDER,
getBasePath: getEzyCopyBasePath,
getImagesPath,
sanitizeImageFilename,
generatePageSubfolder,
rewriteImagePaths,
} = window.EzyCopyFiles;

// Expose API once to avoid polluting global scope with redeclarable consts
window.EzyCopyPlatform = {
Expand All @@ -113,7 +26,7 @@
getImagesPath,
sanitizeImageFilename,
generatePageSubfolder,
rewriteImagePaths
rewriteImagePaths,
};

// Backwards compatibility for existing global calls
Expand Down
3 changes: 3 additions & 0 deletions popup.html
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@
</div>
</section>
</div>
<script src="settings.js"></script>
<script src="file-helpers.js"></script>
<script src="injection-files.js"></script>
<script src="popup.js"></script>
</body>
</html>
39 changes: 2 additions & 37 deletions popup.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,4 @@
// Storage configuration
const STORAGE_KEY = 'ezycopy_settings';
const DEFAULT_SETTINGS = {
copyToClipboard: true,
downloadMarkdown: true,
includeImages: true,
experimental: {
selectiveCopy: false,
downloadImagesLocally: false
}
};
const { loadSettings, saveSettings } = window.EzyCopySettings;

// Ensure at least one output method is active
function enforceAtLeastOneActive(settings, changedToggle) {
Expand All @@ -21,26 +11,6 @@ function enforceAtLeastOneActive(settings, changedToggle) {
return settings;
}

// Load settings from chrome.storage.local with migration support
async function loadSettings() {
const result = await chrome.storage.local.get(STORAGE_KEY);
const stored = result[STORAGE_KEY] || {};

return {
copyToClipboard: stored.copyToClipboard ?? DEFAULT_SETTINGS.copyToClipboard,
downloadMarkdown: stored.downloadMarkdown ?? DEFAULT_SETTINGS.downloadMarkdown,
includeImages: stored.includeImages ?? DEFAULT_SETTINGS.includeImages,
experimental: {
selectiveCopy: stored.experimental?.selectiveCopy ?? false,
downloadImagesLocally: stored.experimental?.downloadImagesLocally ?? false
}
};
}

// Save settings to chrome.storage.local
async function saveSettings(settings) {
await chrome.storage.local.set({ [STORAGE_KEY]: settings });
}

document.addEventListener("DOMContentLoaded", async function () {
// DOM elements - main settings
Expand Down Expand Up @@ -128,12 +98,7 @@ document.addEventListener("DOMContentLoaded", async function () {
await chrome.scripting.executeScript({
target: { tabId: tab.id },
files: [
"lib/readability.js",
"lib/turndown.js",
"lib/turndown-plugin-gfm.js",
"lib/ezycopy.js",
"lib/platform.js",
"content-script.js",
...window.EzyCopyInjection.CONTENT_SCRIPTS,
],
});

Expand Down
Loading