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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

All notable changes to Markdown Printer.

## v1.2.2 (2026-10-25)

- Fixed the popup showing "Saved!" when nothing was saved — a failed save now says what went wrong (for example, on browser internal pages and extension stores, which browsers don't let extensions read)
- Fixed pages being left without saving on sites whose menus are links: while expanding collapsed sections, the capture no longer clicks links to other pages or buttons that submit a form
- A save started from the right-click menu that fails now shows a "!" on the toolbar icon; open the popup to see the reason

## v1.2.1 (2026-10-25)

- Fixed save statistics, rating preferences, and "Copy diagnostics" not being remembered — the extension now declares the `storage` permission it needs (data stays local in your browser)
Expand Down
12 changes: 12 additions & 0 deletions _locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@
"message": "Error - Try again",
"description": "Save button label after a save failure"
},
"errorProtectedPage": {
"message": "This page can't be saved. Browsers don't let extensions read internal browser pages or extension stores.",
"description": "Shown in the popup when the current page cannot be scripted by extensions"
},
"errorPageNavigated": {
"message": "The page changed while it was being saved, so nothing was saved. Reload the page and try again.",
"description": "Shown in the popup when the page navigated away during a save"
},
"errorGeneric": {
"message": "Nothing was saved because of an unexpected error.",
"description": "Shown in the popup when a save failed for an unrecognized reason"
},
"ratingPromptTitle": {
"message": "Enjoying Markdown Printer?",
"description": "Initial rating prompt title"
Expand Down
12 changes: 12 additions & 0 deletions _locales/fr/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@
"message": "Erreur - Réessayer",
"description": "Save button label after a save failure"
},
"errorProtectedPage": {
"message": "Impossible d'enregistrer cette page. Les navigateurs interdisent aux extensions de lire les pages internes du navigateur et les boutiques d'extensions.",
"description": "Shown in the popup when the current page cannot be scripted by extensions"
},
"errorPageNavigated": {
"message": "La page a changé pendant l'enregistrement, rien n'a donc été enregistré. Rechargez la page et réessayez.",
"description": "Shown in the popup when the page navigated away during a save"
},
"errorGeneric": {
"message": "Rien n'a été enregistré à cause d'une erreur inattendue.",
"description": "Shown in the popup when a save failed for an unrecognized reason"
},
"ratingPromptTitle": {
"message": "Vous aimez Markdown Printer ?",
"description": "Initial rating prompt title"
Expand Down
12 changes: 12 additions & 0 deletions _locales/he/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@
"message": "שגיאה - נסה שוב",
"description": "Save button label after a save failure"
},
"errorProtectedPage": {
"message": "לא ניתן לשמור את הדף הזה. דפדפנים לא מאפשרים לתוספים לקרוא דפים פנימיים של הדפדפן או חנויות תוספים.",
"description": "Shown in the popup when the current page cannot be scripted by extensions"
},
"errorPageNavigated": {
"message": "הדף השתנה בזמן השמירה, ולכן שום דבר לא נשמר. טען את הדף מחדש ונסה שוב.",
"description": "Shown in the popup when the page navigated away during a save"
},
"errorGeneric": {
"message": "שום דבר לא נשמר בגלל שגיאה לא צפויה.",
"description": "Shown in the popup when a save failed for an unrecognized reason"
},
"ratingPromptTitle": {
"message": "נהנים מ-Markdown Printer?",
"description": "Initial rating prompt title"
Expand Down
12 changes: 12 additions & 0 deletions _locales/hi/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@
"message": "त्रुटि - पुनः प्रयास करें",
"description": "Save button label after a save failure"
},
"errorProtectedPage": {
"message": "यह पेज सेव नहीं किया जा सकता। ब्राउज़र एक्सटेंशन को ब्राउज़र के आंतरिक पेज या एक्सटेंशन स्टोर पढ़ने नहीं देते।",
"description": "Shown in the popup when the current page cannot be scripted by extensions"
},
"errorPageNavigated": {
"message": "सेव करते समय पेज बदल गया, इसलिए कुछ भी सेव नहीं हुआ। पेज रीलोड करके फिर से कोशिश करें।",
"description": "Shown in the popup when the page navigated away during a save"
},
"errorGeneric": {
"message": "एक अनपेक्षित त्रुटि के कारण कुछ भी सेव नहीं हुआ।",
"description": "Shown in the popup when a save failed for an unrecognized reason"
},
"ratingPromptTitle": {
"message": "क्या आपको Markdown Printer पसंद आ रहा है?",
"description": "Initial rating prompt title"
Expand Down
1 change: 1 addition & 0 deletions build.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const sharedSourceFiles = [
'log-buffer.js',
'logger.js',
'rating-policy.js',
'expand-policy.js',
'popup.html',
'popup.js',
'turndown.js',
Expand Down
5 changes: 4 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ module.exports = [
mdpGetDiagnostics: 'readonly',
mdpLogBuffer: 'readonly',
mdpRatingPolicy: 'readonly',
mdpExpandPolicy: 'readonly',
},
},
rules: {
Expand Down Expand Up @@ -77,7 +78,8 @@ module.exports = [
},
},
{
// The UMD-lite wrappers in src/log-buffer.js and src/rating-policy.js
// The UMD-lite wrappers in src/log-buffer.js, src/rating-policy.js and
// src/expand-policy.js
// reference `module` so they can also be required from Node tests.
files: [
'build.js',
Expand All @@ -87,6 +89,7 @@ module.exports = [
'jest.config.js',
'src/log-buffer.js',
'src/rating-policy.js',
'src/expand-policy.js',
],
languageOptions: {
globals: {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "markdown-printer",
"version": "1.2.1",
"version": "1.2.2",
"description": "Save web pages as Markdown files with preserved formatting",
"scripts": {
"build": "node build.js",
Expand Down
85 changes: 68 additions & 17 deletions src/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,37 +54,71 @@ async function bumpSaveCount() {
}
}

function saveError(code, message) {
const error = new Error(message);
error.code = code;
return error;
}

// The context menu has no UI of its own, so a failed save from it marks the
// toolbar icon and leaves the error for the popup to show when opened.
async function reportContextMenuError(error) {
try {
await browserAPI.storage.local.set({
mdpLastSaveError: { code: error.code || null, message: error.message, at: Date.now() },
});
await browserAPI.action.setBadgeBackgroundColor({ color: '#d32f2f' });
await browserAPI.action.setBadgeText({ text: '!' });
} catch (e) {
log.error('reportContextMenuError failed', e);
}
}

async function clearSaveError() {
try {
await browserAPI.storage.local.remove('mdpLastSaveError');
await browserAPI.action.setBadgeText({ text: '' });
} catch (e) {
log.error('clearSaveError failed', e);
}
}

// Handle context menu click
browserAPI.contextMenus.onClicked.addListener((info, tab) => {
if (info.menuItemId === 'saveAsMarkdown') {
savePageAsMarkdown(tab.id);
savePageAsMarkdown(tab.id).catch(reportContextMenuError);
}
});

// Handle messages from popup
browserAPI.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'saveAsMarkdown') {
browserAPI.tabs.query({ active: true, currentWindow: true }, async tabs => {
if (tabs[0]) {
try {
await savePageAsMarkdown(tabs[0].id);
sendResponse({ success: true });
} catch (error) {
sendResponse({ success: false, error: error.message });
try {
if (!tabs || !tabs[0]) {
throw saveError('no-tab', 'Could not find the current tab');
}
const outcome = await savePageAsMarkdown(tabs[0].id);
sendResponse({ success: true, cancelled: outcome.cancelled });
} catch (error) {
sendResponse({ success: false, error: error.message, code: error.code || null });
}
});
return true; // Keep the message channel open for async response
}
});

// Resolves to { cancelled } once the download has started; rejects (with
// error.code set where the cause is known) when nothing was saved.
async function savePageAsMarkdown(tabId) {
try {
// Inject Turndown library and conversion script
await clearSaveError();

// Inject Turndown library and the expand-click policy used by the conversion script
await browserAPI.scripting
.executeScript({
target: { tabId: tabId },
files: ['turndown.js'],
files: ['turndown.js', 'expand-policy.js'],
})
.catch(error => {
// Better error message for protected pages
Expand All @@ -93,7 +127,8 @@ async function savePageAsMarkdown(tabId) {
error.message.includes('Cannot access') ||
error.message.includes('extensions gallery')
) {
throw new Error(
throw saveError(
'protected-page',
'Cannot save this page - extensions are blocked on browser internal pages and extension stores'
);
}
Expand All @@ -112,9 +147,18 @@ async function savePageAsMarkdown(tabId) {

const result = results[0].result;

// If operation was cancelled, exit without showing save dialog
if (!result || result === null) {
return;
// The user pressed Abort on the progress overlay: no download, not an error
if (result && result.cancelled) {
return { cancelled: true };
}

// No result at all means the capture script died before finishing,
// which happens when the tab navigates to another page mid-capture
if (!result) {
throw saveError(
'page-navigated',
'The page changed while it was being saved (it may have navigated to another page)'
);
}

const { markdown, title, url } = result;
Expand Down Expand Up @@ -171,6 +215,7 @@ async function savePageAsMarkdown(tabId) {
});
}
await bumpSaveCount();
return { cancelled: false };
} catch (error) {
log.error('Error saving markdown:', error);
throw error;
Expand Down Expand Up @@ -407,6 +452,11 @@ async function extractAndConvertToMarkdown() {
const capturedContent = [];
const capturedHashes = new Set();

// Never click links or submit buttons: navigating away ends the capture
const isSafeToExpand = el =>
typeof mdpExpandPolicy !== 'undefined' &&
mdpExpandPolicy.isSafeToExpand(el, window.location.href);

// First, try to expand any collapsed/hidden sections
const expandCollapsedSections = () => {
// Find and click on common expandable elements
Expand All @@ -427,10 +477,11 @@ async function extractAndConvertToMarkdown() {
try {
if (el.tagName === 'DETAILS') {
el.open = true;
} else if (el.click) {
expandedCount++;
} else if (el.click && isSafeToExpand(el)) {
el.click();
expandedCount++;
}
expandedCount++;
} catch (_e) {
// Ignore errors
}
Expand Down Expand Up @@ -730,9 +781,9 @@ async function extractAndConvertToMarkdown() {
// Scroll through the page first
const scrollResult = await scrollToBottom();

// If scrolling was cancelled, return null to indicate no download should occur
// If scrolling was cancelled, tell the background no download should occur
if (scrollResult.cancelled) {
return null;
return { cancelled: true };
}

// Combine all captured content sections
Expand Down
60 changes: 60 additions & 0 deletions src/expand-policy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Decides whether the page capture may click an element to expand it.
// Injected into the page before extractAndConvertToMarkdown (which can't
// import anything, since it is serialized by scripting.executeScript).
// Clicking a link or a submit button navigates the tab away mid-capture,
// which kills the capture script and loses the save. No DOM globals used
// here, so it stays testable in Node.
(function (root, factory) {
const mod = factory();
if (typeof module !== 'undefined' && module.exports) {
module.exports = mod;
} else {
root.mdpExpandPolicy = mod;
}
})(typeof self !== 'undefined' ? self : globalThis, function () {
function withoutHash(url) {
return url.split('#')[0];
}

function isSafeLink(link, pageHref) {
if (link.hasAttribute('download')) {
return false;
}
const target = (link.getAttribute('target') || '').toLowerCase();
if (target && target !== '_self') {
return false;
}
let url;
try {
url = new URL(link.getAttribute('href') || '', pageHref);
} catch (_e) {
return false;
}
if (url.protocol === 'javascript:') {
return true;
}
return withoutHash(url.href) === withoutHash(new URL(pageHref).href);
}

function submitsForm(el) {
const tag = el.tagName;
const type = (el.type || '').toLowerCase();
const isSubmit =
(tag === 'BUTTON' && type === 'submit') ||
(tag === 'INPUT' && (type === 'submit' || type === 'image'));
return isSubmit && !!(el.form || el.closest('form'));
}

function isSafeToExpand(el, pageHref) {
if (!el) {
return false;
}
const link = el.closest('a[href]');
if (link && !isSafeLink(link, pageHref)) {
return false;
}
return !submitsForm(el);
}

return { isSafeToExpand };
});
14 changes: 14 additions & 0 deletions src/popup.html
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,19 @@
background: #ccc;
cursor: not-allowed;
}
.save-error {
margin-top: 10px;
padding: 8px 10px;
background: #fdecea;
border: 1px solid #f5c2bd;
border-radius: 4px;
color: #b71c1c;
font-size: 12px;
line-height: 1.4;
}
.save-error[hidden] {
display: none;
}
.info {
margin-top: 15px;
font-size: 12px;
Expand Down Expand Up @@ -151,6 +164,7 @@ <h1>
<span id="version" class="version"></span>
</h1>
<button id="saveBtn"></button>
<div id="saveError" class="save-error" role="alert" hidden></div>

<div id="ratingPrompt" class="rating-prompt" hidden>
<button id="rpDismiss" class="rp-dismiss" aria-label="Dismiss">×</button>
Expand Down
Loading
Loading