From 2acd843b2b9547c203073253e40bee34f97974ea Mon Sep 17 00:00:00 2001 From: Paul Spenke Date: Sat, 14 Mar 2026 21:40:01 +0100 Subject: [PATCH] Remove Memos Clipper and all associated resources to decommission the extension. --- .gitignore | 8 + background.js | 5 - content.js | 266 ------------------------------ icons/icon128.png | Bin 341 -> 0 bytes icons/icon16.png | Bin 99 -> 0 bytes icons/icon48.png | Bin 145 -> 0 bytes manifest.json | 38 ----- marked.min.js | 146 ----------------- popup.css | 404 ---------------------------------------------- popup.html | 134 --------------- popup.js | 384 ------------------------------------------- settings.css | 157 ------------------ settings.html | 77 --------- settings.js | 63 -------- 14 files changed, 8 insertions(+), 1674 deletions(-) create mode 100644 .gitignore delete mode 100644 background.js delete mode 100644 content.js delete mode 100644 icons/icon128.png delete mode 100644 icons/icon16.png delete mode 100644 icons/icon48.png delete mode 100644 manifest.json delete mode 100644 marked.min.js delete mode 100644 popup.css delete mode 100644 popup.html delete mode 100644 popup.js delete mode 100644 settings.css delete mode 100644 settings.html delete mode 100644 settings.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f5c07bf --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# Dependencies +node_modules/ + +# Build output +dist/ + +# IDE +.idea/ diff --git a/background.js b/background.js deleted file mode 100644 index 8e5e3f8..0000000 --- a/background.js +++ /dev/null @@ -1,5 +0,0 @@ -// background.js - service worker - -chrome.runtime.onInstalled.addListener(() => { - console.log("Memos Clipper installed."); -}); diff --git a/content.js b/content.js deleted file mode 100644 index 37b1805..0000000 --- a/content.js +++ /dev/null @@ -1,266 +0,0 @@ -// content.js - Extracts page content and converts to Markdown -// Guard against double-injection (MV3 scripting.executeScript can fire multiple times) -if (window.__memosClipperLoaded) { /* skip */ } else { -window.__memosClipperLoaded = true; - -(function () { - // ── Turndown-lite: a minimal but solid HTML→Markdown converter ────────────── - function htmlToMarkdown(element) { - const clone = element.cloneNode(true); - - // Remove unwanted elements — comprehensive list covering real-world sites - const removeSelectors = [ - // Semantic structural chrome - 'script', 'style', 'noscript', 'template', - 'nav', 'header', 'footer', 'aside', - // ARIA roles for chrome - '[role="navigation"]', '[role="banner"]', '[role="complementary"]', - '[role="contentinfo"]', '[role="search"]', '[role="toolbar"]', - '[role="menubar"]', '[role="menu"]', '[role="dialog"]', - // Common class/id patterns for site chrome - '[class*="navbar"]', '[class*="nav-bar"]', '[class*="site-nav"]', - '[class*="site-header"]', '[class*="site-footer"]', - '[class*="page-header"]', '[class*="page-footer"]', - '[id*="navbar"]', '[id*="site-nav"]', '[id*="site-header"]', '[id*="site-footer"]', - // Ads and tracking - '[class*="advertisement"]', '[class*="advert"]', '[class*=" ad-"]', - '[class*="google-ad"]', '[class*="sponsored"]', - '[id*="advertisement"]', '[id*="google_ad"]', - // Cookie banners, popups, overlays - '[class*="cookie"]', '[id*="cookie"]', - '[class*="consent"]', '[id*="consent"]', - '[class*="gdpr"]', '[id*="gdpr"]', - '[class*="popup"]', '[class*="modal"]', '[class*="overlay"]', - '[class*="banner"]', '[id*="banner"]', - // Social / share widgets - '[class*="share-bar"]', '[class*="social-bar"]', '[class*="share-buttons"]', - '[class*="sharing"]', - // Subscription / newsletter prompts - '[class*="newsletter"]', '[class*="subscribe"]', - // Comments sections - '[id="comments"]', '[class*="comments-section"]', '[id*="disqus"]', - // Related / recommended articles - '[class*="related-posts"]', '[class*="recommended"]', '[class*="more-articles"]', - // Sidebar - '[class*="sidebar"]', '[id*="sidebar"]', - // Print/hidden - '[hidden]', '[aria-hidden="true"]', - ].join(', '); - - clone.querySelectorAll(removeSelectors).forEach((el) => el.remove()); - - // Also remove elements that are visually hidden via inline style - clone.querySelectorAll('[style*="display:none"],[style*="display: none"],[style*="visibility:hidden"]') - .forEach((el) => el.remove()); - - return nodeToMd(clone).replace(/\n{3,}/g, "\n\n").trim(); - } - - function nodeToMd(node, ctx = { listDepth: 0, ordered: false, index: 0 }) { - if (node.nodeType === Node.TEXT_NODE) { - return node.textContent.replace(/\s+/g, " "); - } - if (node.nodeType !== Node.ELEMENT_NODE) return ""; - - const tag = node.tagName.toLowerCase(); - const children = () => - Array.from(node.childNodes) - .map((c) => nodeToMd(c, ctx)) - .join(""); - - switch (tag) { - case "h1": return `\n\n# ${children().trim()}\n\n`; - case "h2": return `\n\n## ${children().trim()}\n\n`; - case "h3": return `\n\n### ${children().trim()}\n\n`; - case "h4": return `\n\n#### ${children().trim()}\n\n`; - case "h5": return `\n\n##### ${children().trim()}\n\n`; - case "h6": return `\n\n###### ${children().trim()}\n\n`; - case "p": return `\n\n${children().trim()}\n\n`; - case "br": return " \n"; - case "hr": return "\n\n---\n\n"; - - case "strong": - case "b": return `**${children()}**`; - case "em": - case "i": return `_${children()}_`; - case "s": - case "del": return `~~${children()}~~`; - case "code": { - const text = node.textContent; - return text.includes("`") ? `\`\`${text}\`\`` : `\`${text}\``; - } - case "pre": { - const codeEl = node.querySelector("code"); - const lang = codeEl - ? (codeEl.className.match(/language-(\S+)/) || [])[1] || "" - : ""; - const text = (codeEl || node).textContent; - return `\n\n\`\`\`${lang}\n${text}\n\`\`\`\n\n`; - } - case "blockquote": return `\n\n${children() - .trim() - .split("\n") - .map((l) => `> ${l}`) - .join("\n")}\n\n`; - - case "a": { - const href = node.getAttribute("href") || ""; - const text = children().trim(); - if (!text) return href; - try { - const abs = new URL(href, location.href).href; - return `[${text}](${abs})`; - } catch { - return `[${text}](${href})`; - } - } - - case "img": { - const src = node.getAttribute("src") || ""; - const alt = node.getAttribute("alt") || ""; - try { - const abs = new URL(src, location.href).href; - return `![${alt}](${abs})`; - } catch { - return src ? `![${alt}](${src})` : ""; - } - } - - case "ul": { - const lines = Array.from(node.children) - .map((li) => `${" ".repeat(ctx.listDepth)}- ${nodeToMd(li, { ...ctx, listDepth: ctx.listDepth + 1 }).trim()}`) - .join("\n"); - return `\n\n${lines}\n\n`; - } - case "ol": { - const lines = Array.from(node.children) - .map((li, i) => `${" ".repeat(ctx.listDepth)}${i + 1}. ${nodeToMd(li, { ...ctx, listDepth: ctx.listDepth + 1 }).trim()}`) - .join("\n"); - return `\n\n${lines}\n\n`; - } - case "li": return children(); - - case "table": return convertTable(node); - - case "figure": { - const img = node.querySelector("img"); - const caption = node.querySelector("figcaption"); - let md = img ? nodeToMd(img, ctx) : children(); - if (caption) md += `\n*${caption.textContent.trim()}*`; - return `\n\n${md}\n\n`; - } - - // skip presentational / hidden - case "svg": - case "canvas": - case "video": - case "audio": - case "iframe": - case "button": - case "input": - case "select": - case "textarea": - case "form": - case "nav": - case "header": - case "footer": - case "aside": - return ""; - - default: - return children(); - } - } - - function convertTable(table) { - const rows = Array.from(table.querySelectorAll("tr")); - if (!rows.length) return ""; - const toRow = (tr) => - "| " + - Array.from(tr.querySelectorAll("th,td")) - .map((c) => c.textContent.trim().replace(/\|/g, "\\|")) - .join(" | ") + - " |"; - const header = toRow(rows[0]); - const sep = - "| " + - Array.from(rows[0].querySelectorAll("th,td")) - .map(() => "---") - .join(" | ") + - " |"; - const body = rows.slice(1).map(toRow).join("\n"); - return `\n\n${header}\n${sep}\n${body}\n\n`; - } - - // ── Extract images from HTML ──────────────────────────────────────────────── - function extractImages(element) { - const seen = new Set(); - const imgs = Array.from(element.querySelectorAll("img")); - return imgs - .filter((img) => { - const src = img.getAttribute("src") || ""; - if (!src) return false; - // skip tiny icons / tracking pixels by rendered size - const w = img.naturalWidth || img.width || 0; - const h = img.naturalHeight || img.height || 0; - if (w > 0 && w < 32 && h > 0 && h < 32) return false; - // skip 1x1 gif trackers - if (src.startsWith("data:image/gif")) return false; - return true; - }) - .map((img) => { - const src = img.getAttribute("src") || ""; - let abs = src; - try { abs = new URL(src, location.href).href; } catch {} - return { src: abs, alt: img.getAttribute("alt") || "" }; - }) - .filter((img) => { - if (seen.has(img.src)) return false; - seen.add(img.src); - return true; - }); - } - - // ── Message handler ───────────────────────────────────────────────────────── - chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { - if (msg.action === "getContent") { - try { - let markdown = ""; - let images = []; - let title = document.title || location.href; - - if (msg.mode === "selection") { - const sel = window.getSelection(); - if (sel && sel.rangeCount > 0) { - const frag = sel.getRangeAt(0).cloneContents(); - const div = document.createElement("div"); - div.appendChild(frag); - markdown = htmlToMarkdown(div); - images = extractImages(div); - } else { - markdown = ""; - } - } else { - // full page — prefer article/main, fall back to body - const root = - document.querySelector("article") || - document.querySelector("main") || - document.querySelector('[role="main"]') || - document.body; - markdown = htmlToMarkdown(root); - images = extractImages(root); - } - - // Prepend source line - const sourceNote = `> Source: [${title}](${location.href})\n\n`; - markdown = sourceNote + markdown; - - sendResponse({ ok: true, markdown, images, title, url: location.href }); - } catch (e) { - sendResponse({ ok: false, error: e.message }); - } - return true; // async - } - }); -})(); -} // end guard diff --git a/icons/icon128.png b/icons/icon128.png deleted file mode 100644 index e434d526895a42dedcbdc569968fc8bbd1a46950..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 341 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1SEZ8zRh7^V3hH6aSW-L^Y-RO-op+&ERLlL zJAAJ%*e82Smm|R8|9hEj3{1kGEY447vp%NZ!Oq}d&A@Prk>LU#Lqiz@!xmQ6 zm`*@8I-2vr`Te_re!U@`a2nNF4iXOhXpTqL36g4fSa(V(HbFpI6zD|;22WQ%mvv4F FO#q%{U2p&Z diff --git a/icons/icon16.png b/icons/icon16.png deleted file mode 100644 index ee769390b09a93b5e2af6688589b10e9467e8e59..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 99 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61SBU+%rFB|TAnVBAr*6yA3PUV;WV7Ea-rI5 xYr~FzQvV*aEtdL_p{|19+Ch6 diff --git a/icons/icon48.png b/icons/icon48.png deleted file mode 100644 index b551fdff740856e99e7e5c0a35b7f8ea3a39d392..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 145 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1SD@H9QR&>qbXqI{Xa|F*tDnm{r-UW|$|Wu^ diff --git a/manifest.json b/manifest.json deleted file mode 100644 index c5af39a..0000000 --- a/manifest.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "manifest_version": 3, - "name": "Memos Clipper", - "version": "1.0.0", - "description": "Clip web pages or selections as Markdown to your usememos instance", - "permissions": [ - "activeTab", - "scripting", - "storage" - ], - "host_permissions": [ - "" - ], - "action": { - "default_popup": "popup.html", - "default_icon": { - "16": "icons/icon16.png", - "48": "icons/icon48.png", - "128": "icons/icon128.png" - } - }, - "icons": { - "16": "icons/icon16.png", - "48": "icons/icon48.png", - "128": "icons/icon128.png" - }, - "background": { - "service_worker": "background.js" - }, - "options_page": "settings.html", - "content_scripts": [ - { - "matches": [""], - "js": ["content.js"], - "run_at": "document_idle" - } - ] -} diff --git a/marked.min.js b/marked.min.js deleted file mode 100644 index 0826999..0000000 --- a/marked.min.js +++ /dev/null @@ -1,146 +0,0 @@ -/*! - * marked-mini - minimal Markdown renderer for Memos Clipper - * Supports: headings, bold, italic, strike, code, pre, blockquote, - * links, images, ul, ol, hr, tables, line breaks - */ -(function(global){ -"use strict"; -function escape(s){return s.replace(/&/g,"&").replace(//g,">").replace(/"/g,""");} - -function parseInline(s){ - // code spans - s = s.replace(/`([^`]+)`/g,(_,c)=>`${escape(c)}`); - // bold+italic - s = s.replace(/\*\*\*(.+?)\*\*\*/g,"$1"); - // bold - s = s.replace(/\*\*(.+?)\*\*/g,"$1"); - s = s.replace(/__(.+?)__/g,"$1"); - // italic - s = s.replace(/\*(.+?)\*/g,"$1"); - s = s.replace(/_([^_]+)_/g,"$1"); - // strikethrough - s = s.replace(/~~(.+?)~~/g,"$1"); - // images - s = s.replace(/!\[([^\]]*)\]\(([^)]+)\)/g,(_,alt,src)=>`${escape(alt)}`); - // links - s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g,(_,txt,href)=>`${txt}`); - // auto-link - s = s.replace(/(?)]+)/g,url=>`${escape(url)}`); - // hard break - s = s.replace(/ \n/g,"
"); - return s; -} - -function parse(src){ - const lines = src.split("\n"); - const out = []; - let i = 0; - - while(i < lines.length){ - const line = lines[i]; - - // Fenced code block - const fenceM = line.match(/^```(\w*)/); - if(fenceM){ - const lang = fenceM[1]||""; - const code = []; - i++; - while(i${escape(code.join("\n"))}`); - continue; - } - - // Headings - const hM = line.match(/^(#{1,6})\s+(.*)/); - if(hM){ - const lvl = hM[1].length; - out.push(`${parseInline(hM[2])}`); - i++; continue; - } - - // HR - if(/^[-*_]{3,}\s*$/.test(line)){ - out.push("
"); - i++; continue; - } - - // Blockquote - if(line.startsWith("> ")){ - const bq = []; - while(i ")){ - bq.push(lines[i].slice(2)); - i++; - } - out.push(`
${parse(bq.join("\n"))}
`); - continue; - } - - // Unordered list - if(/^[-*+]\s/.test(line)){ - const items = []; - while(i${parseInline(lines[i].replace(/^[-*+]\s/,""))}`); - i++; - } - out.push(`
    ${items.join("")}
`); - continue; - } - - // Ordered list - if(/^\d+\.\s/.test(line)){ - const items = []; - while(i${parseInline(lines[i].replace(/^\d+\.\s/,""))}`); - i++; - } - out.push(`
    ${items.join("")}
`); - continue; - } - - // Table (| col | col |) - if(line.startsWith("|") && i+1idx>0&&idx`${parseInline(h.trim())}`); - i+=2; // skip header + separator - const rows = []; - while(iidx>0&&idx`${parseInline(c.trim())}`); - rows.push(`${cells.join("")}`); - i++; - } - out.push(`${headers.join("")}${rows.join("")}
`); - continue; - } - - // Empty line - if(line.trim()===""){ - i++; continue; - } - - // Paragraph — collect until blank line - const para = []; - while(i|[-*+]\s|\d+\.\s|[-*_]{3,}\s*$)/.test(lines[i])) break; - para.push(lines[i]); - i++; - } - if(para.length) out.push(`

${parseInline(para.join(" "))}

`); - } - return out.join("\n"); -} - -global.marked = { - parse: function(src, opts) { - const breaks = opts && opts.breaks; - // With breaks:true, single newlines in paragraphs become
- if (breaks) { - // Pre-process: single \n inside paragraph text → two spaces + \n (markdown hard break) - src = src.replace(/([^\n])\n([^\n])/g, "$1 \n$2"); - } - return parse(src); - } -};})(window); diff --git a/popup.css b/popup.css deleted file mode 100644 index fcf92fe..0000000 --- a/popup.css +++ /dev/null @@ -1,404 +0,0 @@ -@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@400;500;600&display=swap'); - -*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } - -:root { - --bg: #0f0f11; - --surface: #1a1a1f; - --surface2: #22222a; - --border: #2a2a35; - --accent: #7c6af7; - --accent-dim: #7c6af720; - --accent-hover: #9585fa; - --text: #e8e8f0; - --text-dim: #777788; - --text-muted: #444455; - --success: #4ade80; - --error: #f87171; - --radius: 6px; - --font: 'IBM Plex Sans', sans-serif; - --mono: 'IBM Plex Mono', monospace; -} - -html, body { - width: 440px; - min-height: 200px; - background: var(--bg); - color: var(--text); - font-family: var(--font); - font-size: 13px; - line-height: 1.5; - overflow-x: hidden; -} - -/* ── Views ── */ -.view { display: flex; flex-direction: column; } -.view.hidden { display: none !important; } - -/* ── Loading ── */ -#view-loading { - align-items: center; - justify-content: center; - gap: 12px; - padding: 48px; - color: var(--text-dim); - font-size: 13px; -} -.spinner { - width: 22px; height: 22px; - border: 2px solid var(--border); - border-top-color: var(--accent); - border-radius: 50%; - animation: spin .7s linear infinite; -} -@keyframes spin { to { transform: rotate(360deg); } } - -/* ── Setup / error / success ── */ -.setup-box, .success-box { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 12px; - padding: 40px 32px; - text-align: center; - color: var(--text-dim); - font-size: 13px; -} -.setup-icon { - width: 44px; height: 44px; - border-radius: 50%; - display: flex; align-items: center; justify-content: center; - background: var(--surface); - border: 1px solid var(--border); -} -.setup-icon svg { stroke: var(--text-dim); } -.setup-icon.err { color: var(--error); font-size: 20px; font-weight: 700; } - -.success-icon { - width: 44px; height: 44px; - border-radius: 50%; - background: #4ade8015; - border: 1px solid #4ade8040; - color: var(--success); - font-size: 22px; - display: flex; align-items: center; justify-content: center; -} -.success-box p { font-size: 14px; font-weight: 600; color: var(--text); } -.success-box a { color: var(--accent); text-decoration: none; font-size: 12px; } -.success-box a:hover { text-decoration: underline; } - -/* ── Header ── */ -header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 12px 14px 10px; - border-bottom: 1px solid var(--border); - background: var(--surface); -} -.logo { - display: flex; - align-items: center; - gap: 8px; - font-weight: 600; - font-size: 13px; - color: var(--text); -} -.logo svg { stroke: var(--accent); flex-shrink: 0; } -.page-title { - max-width: 220px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-weight: 400; - font-size: 12px; - color: var(--text-dim); -} -.header-actions { display: flex; gap: 4px; } -.icon-btn { - background: transparent; - border: 1px solid transparent; - border-radius: var(--radius); - padding: 5px 6px; - cursor: pointer; - color: var(--text-dim); - display: flex; align-items: center; - transition: all .12s; -} -.icon-btn:hover { border-color: var(--border); color: var(--text); background: var(--surface2); } -.icon-btn svg { display: block; } - -/* ── Tabs ── */ -.tabs { - display: flex; - position: relative; - border-bottom: 1px solid var(--border); - background: var(--surface); - padding: 0 14px; - gap: 2px; -} -.tab { - background: none; - border: none; - color: var(--text-dim); - font-family: var(--font); - font-size: 12px; - font-weight: 500; - padding: 8px 12px; - cursor: pointer; - position: relative; - transition: color .12s; - border-bottom: 2px solid transparent; - margin-bottom: -1px; -} -.tab:hover { color: var(--text); } -.tab.active { color: var(--accent); border-bottom-color: var(--accent); } - -/* ── Tab panels ── */ -.tab-panel { flex: 1; } -.tab-panel.hidden { display: none; } - -#md-editor { - display: block; - width: 100%; - height: 260px; - background: var(--bg); - border: none; - border-bottom: 1px solid var(--border); - color: var(--text); - font-family: var(--mono); - font-size: 12px; - line-height: 1.7; - padding: 14px; - resize: none; - outline: none; -} -#md-editor::placeholder { color: var(--text-muted); } - -.preview-body { - height: 260px; - overflow-y: auto; - padding: 14px 16px; - border-bottom: 1px solid var(--border); - background: var(--bg); - font-size: 13px; - line-height: 1.7; -} - -/* preview markdown styles */ -.preview-body h1 { font-size: 18px; margin: 0 0 12px; border-bottom: 1px solid var(--border); padding-bottom: 6px; } -.preview-body h2 { font-size: 15px; margin: 16px 0 8px; } -.preview-body h3 { font-size: 13px; font-weight: 600; margin: 14px 0 6px; } -.preview-body p { margin: 0 0 10px; } -.preview-body blockquote { - border-left: 3px solid var(--accent); - margin: 10px 0; - padding: 4px 12px; - color: var(--text-dim); - background: var(--accent-dim); - border-radius: 0 4px 4px 0; -} -.preview-body code { - background: var(--surface2); - border: 1px solid var(--border); - border-radius: 3px; - padding: 1px 5px; - font-family: var(--mono); - font-size: 11px; -} -.preview-body pre { - background: var(--surface2); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: 10px 12px; - overflow-x: auto; - margin: 10px 0; -} -.preview-body pre code { background: none; border: none; padding: 0; } -.preview-body a { color: var(--accent); } -.preview-body img { max-width: 100%; border-radius: 4px; margin: 8px 0; } -.preview-body table { width: 100%; border-collapse: collapse; margin: 10px 0; font-size: 12px; } -.preview-body th, .preview-body td { border: 1px solid var(--border); padding: 6px 10px; text-align: left; } -.preview-body th { background: var(--surface2); } -.preview-body hr { border: none; border-top: 1px solid var(--border); margin: 14px 0; } -.preview-body ul, .preview-body ol { padding-left: 20px; margin: 0 0 10px; } - -/* ── Images section ── */ -#images-section { - border-bottom: 1px solid var(--border); - background: var(--surface); -} -.images-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 8px 14px; - font-size: 12px; - color: var(--text-dim); -} -.badge { - display: inline-block; - background: var(--accent-dim); - color: var(--accent); - border-radius: 10px; - padding: 1px 6px; - font-size: 10px; - font-weight: 600; - margin-left: 4px; -} -.toggle-label { - display: flex; - align-items: center; - gap: 6px; - cursor: pointer; - font-size: 11px; -} -.toggle-label input { accent-color: var(--accent); cursor: pointer; } - -#images-list { - display: flex; - flex-wrap: wrap; - gap: 6px; - padding: 0 14px 10px; - max-height: 90px; - overflow-y: auto; -} -.img-chip { - display: flex; - align-items: center; - gap: 5px; - background: var(--surface2); - border: 1px solid var(--border); - border-radius: 4px; - padding: 3px 8px 3px 5px; - font-size: 11px; - color: var(--text-dim); - max-width: 160px; -} -.img-chip img { - width: 20px; height: 20px; - object-fit: cover; - border-radius: 2px; - flex-shrink: 0; -} -.img-chip span { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - flex: 1; -} -.img-chip .remove-img { - background: none; - border: none; - color: var(--text-muted); - cursor: pointer; - font-size: 13px; - line-height: 1; - padding: 0; - flex-shrink: 0; -} -.img-chip .remove-img:hover { color: var(--error); } -.img-chip.skipped { opacity: .4; } - -/* ── Tags row ── */ -#tags-row { - padding: 6px 14px; - border-bottom: 1px solid var(--border); - background: var(--surface); -} -#tags-input { - width: 100%; - background: transparent; - border: none; - outline: none; - color: var(--accent); - font-family: var(--mono); - font-size: 12px; - padding: 2px 0; -} -#tags-input::placeholder { color: var(--text-muted); font-family: var(--font); } - -/* ── Char counter ── */ -.char-counter { - padding: 3px 14px; - font-size: 10px; - color: var(--text-muted); - font-family: var(--mono); - text-align: right; - background: var(--bg); - border-bottom: 1px solid var(--border); -} - -/* ── Footer ── */ -footer { - display: flex; - align-items: center; - justify-content: space-between; - padding: 10px 14px; - background: var(--surface); - border-top: 1px solid var(--border); - gap: 8px; -} -.footer-left, .footer-right { - display: flex; - align-items: center; - gap: 8px; -} - -select { - background: var(--surface2); - border: 1px solid var(--border); - border-radius: var(--radius); - color: var(--text-dim); - font-family: var(--font); - font-size: 12px; - padding: 5px 8px; - outline: none; - cursor: pointer; - transition: border-color .12s; -} -select:focus { border-color: var(--accent); } -select option { background: var(--surface); } - -.secondary-btn { - background: transparent; - border: 1px solid var(--border); - border-radius: var(--radius); - color: var(--text-dim); - font-family: var(--font); - font-size: 12px; - padding: 6px 10px; - cursor: pointer; - transition: all .12s; -} -.secondary-btn:hover { border-color: var(--accent); color: var(--accent); } - -.send-btn { - display: flex; - align-items: center; - gap: 6px; - background: var(--accent); - border: none; - border-radius: var(--radius); - color: #fff; - font-family: var(--font); - font-size: 12px; - font-weight: 600; - padding: 7px 14px; - cursor: pointer; - transition: background .12s, transform .08s; -} -.send-btn:hover { background: var(--accent-hover); } -.send-btn:active { transform: scale(.97); } -.send-btn:disabled { opacity: .5; cursor: default; } -.send-btn svg { flex-shrink: 0; } - -/* scrollbar */ -::-webkit-scrollbar { width: 4px; height: 4px; } -::-webkit-scrollbar-track { background: transparent; } -::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; } -::-webkit-scrollbar-thumb:hover { background: var(--text-muted); } - -/* mode badge */ -#mode-toggle.mode-selection { color: var(--accent); background: var(--accent-dim); border-color: var(--accent); } diff --git a/popup.html b/popup.html deleted file mode 100644 index dec80c5..0000000 --- a/popup.html +++ /dev/null @@ -1,134 +0,0 @@ - - - - - Memos Clipper - - - - -
-
- Extracting content… -
- - - - - - - - - - - - - - - - - diff --git a/popup.js b/popup.js deleted file mode 100644 index fc3acc3..0000000 --- a/popup.js +++ /dev/null @@ -1,384 +0,0 @@ -// popup.js -"use strict"; - -// ── State ─────────────────────────────────────────────────────────────────── -const state = { - markdown: "", - images: [], // {src, alt, keep: bool} - mode: "page", // "page" | "selection" - settings: null, -}; - -// ── DOM refs ───────────────────────────────────────────────────────────────── -const views = { - loading: document.getElementById("view-loading"), - setup: document.getElementById("view-setup"), - main: document.getElementById("view-main"), - success: document.getElementById("view-success"), - error: document.getElementById("view-error"), -}; - -const mdEditor = document.getElementById("md-editor"); -const mdPreview = document.getElementById("md-preview"); -const imagesList = document.getElementById("images-list"); -const imgCount = document.getElementById("img-count"); -const pageTitle = document.getElementById("page-title"); -const visibilityEl = document.getElementById("visibility-select"); -const attachCheck = document.getElementById("attach-images"); -const modeToggle = document.getElementById("mode-toggle"); -const sendBtn = document.getElementById("send-btn"); -const reloadBtn = document.getElementById("reload-btn"); -const errorMsg = document.getElementById("error-msg"); - -// ── Helpers ─────────────────────────────────────────────────────────────────── -function showView(name) { - Object.values(views).forEach((v) => v.classList.add("hidden")); - views[name].classList.remove("hidden"); -} - -async function getSettings() { - return chrome.storage.sync.get([ - "memosUrl", "apiToken", "visibility", "clipMode", "includeImages", "includeTags" - ]); -} - -async function getActiveTab() { - const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); - return tab; -} - -// ── Boot ────────────────────────────────────────────────────────────────────── -(async () => { - showView("loading"); - state.settings = await getSettings(); - - if (!state.settings.memosUrl || !state.settings.apiToken) { - showView("setup"); - return; - } - - // Apply saved defaults - state.mode = state.settings.clipMode || "page"; - if (state.settings.visibility) visibilityEl.value = state.settings.visibility; - if (state.settings.includeImages === false) attachCheck.checked = false; - - updateModeButton(); - await clip(); -})(); - -// ── Clip page ───────────────────────────────────────────────────────────────── -async function clip() { - showView("loading"); - try { - const tab = await getActiveTab(); - pageTitle.textContent = tab.title || tab.url; - - // Ensure content script is available (MV3 — inject on demand) - try { - await chrome.scripting.executeScript({ - target: { tabId: tab.id }, - files: ["content.js"], - }); - } catch (_) { /* already injected or restricted page */ } - - const response = await new Promise((resolve, reject) => { - chrome.tabs.sendMessage(tab.id, { action: "getContent", mode: state.mode }, (res) => { - if (chrome.runtime.lastError) reject(new Error(chrome.runtime.lastError.message)); - else resolve(res); - }); - }); - - if (!response || !response.ok) { - throw new Error(response?.error || "Content script did not respond. Try reloading the page."); - } - - state.markdown = response.markdown; - // Warn if selection mode returned nothing - if (state.mode === "selection" && !response.markdown.replace(/^>.*\n\n/, "").trim()) { - state.markdown = response.markdown + "\n\n> ⚠️ No text was selected. Select some text on the page first, or switch to full-page mode."; - } - state.images = response.images.map((img) => ({ ...img, keep: true })); - - mdEditor.value = state.markdown; - updateCharCounter(); - renderImages(); - showView("main"); - switchTab("edit"); - } catch (e) { - errorMsg.textContent = e.message; - showView("error"); - } -} - -// ── Tabs ────────────────────────────────────────────────────────────────────── -function switchTab(name) { - document.querySelectorAll(".tab").forEach((t) => t.classList.remove("active")); - document.querySelectorAll(".tab-panel").forEach((p) => p.classList.add("hidden")); - document.querySelector(`.tab[data-tab="${name}"]`).classList.add("active"); - document.getElementById(`tab-${name}`).classList.remove("hidden"); - if (name === "preview") renderPreview(); -} - -document.querySelectorAll(".tab").forEach((btn) => { - btn.addEventListener("click", () => switchTab(btn.dataset.tab)); -}); - -// ── Preview ─────────────────────────────────────────────────────────────────── -function renderPreview() { - state.markdown = mdEditor.value; - if (typeof marked !== "undefined") { - mdPreview.innerHTML = marked.parse(state.markdown, { breaks: true }); - } else { - // Fallback: basic escaping - mdPreview.innerHTML = `
${escHtml(state.markdown)}
`; - } -} - -function escHtml(s) { - return s.replace(/&/g, "&").replace(//g, ">"); -} - -// ── Images ───────────────────────────────────────────────────────────────────── -function renderImages() { - // Deduplicate by src, exclude oversized data: URIs - const seen = new Set(); - const visible = state.images.filter((img) => { - if (seen.has(img.src)) return false; - seen.add(img.src); - if (img.src.startsWith("data:") && img.src.length >= 500_000) return false; - return true; - }); - - // Sync keep flags back (dedup may have removed some) - state.images = visible; - - const uploadable = visible.filter((img) => !img.src.startsWith("data:")).length; - imgCount.textContent = uploadable; - imagesList.innerHTML = ""; - - if (!visible.length) { - document.getElementById("images-section").style.display = "none"; - return; - } - document.getElementById("images-section").style.display = ""; - - visible.forEach((img, i) => { - const isDataUri = img.src.startsWith("data:"); - const chip = document.createElement("div"); - chip.className = "img-chip" + (img.keep ? "" : " skipped"); - - const thumb = document.createElement("img"); - thumb.src = img.src; - thumb.onerror = () => { thumb.style.display = "none"; }; - - const label = document.createElement("span"); - const name = isDataUri - ? `inline-image-${i + 1}` - : (img.src.split("/").pop().split("?")[0].slice(0, 28) || `image-${i + 1}`); - label.textContent = name; - if (isDataUri) label.title = "Embedded data URI — cannot be uploaded"; - - const remove = document.createElement("button"); - remove.className = "remove-img"; - remove.textContent = "×"; - remove.title = img.keep ? "Exclude this image" : "Include this image"; - if (isDataUri) { - remove.style.display = "none"; // data URIs can't be uploaded anyway - } - remove.addEventListener("click", () => { - img.keep = !img.keep; - chip.classList.toggle("skipped", !img.keep); - remove.title = img.keep ? "Exclude this image" : "Include this image"; - const kept = state.images.filter((im) => im.keep && !im.src.startsWith("data:")).length; - imgCount.textContent = kept; - }); - - chip.append(thumb, label, remove); - imagesList.appendChild(chip); - }); -} - -// ── Char counter ───────────────────────────────────────────────────────────── -const charCounter = document.getElementById("char-counter"); -function updateCharCounter() { - const n = mdEditor.value.length; - charCounter.textContent = `${n.toLocaleString()} char${n !== 1 ? "s" : ""}`; -} -mdEditor.addEventListener("input", updateCharCounter); - -// ── Tags input ──────────────────────────────────────────────────────────────── -const tagsInput = document.getElementById("tags-input"); - -// Load saved tags default -getSettings().then((s) => { - if (s.includeTags) tagsInput.value = "#clipped"; -}); - -// ── Mode toggle ─────────────────────────────────────────────────────────────── -function updateModeButton() { - modeToggle.title = state.mode === "page" - ? "Currently: full page — click for selection mode" - : "Currently: selection — click for full page mode"; - modeToggle.classList.toggle("mode-selection", state.mode === "selection"); -} - -modeToggle.addEventListener("click", async () => { - state.mode = state.mode === "page" ? "selection" : "page"; - updateModeButton(); - await clip(); -}); - -// ── Reload ──────────────────────────────────────────────────────────────────── -reloadBtn.addEventListener("click", () => clip()); - -// ── Settings buttons ─────────────────────────────────────────────────────────── -document.getElementById("settings-btn").addEventListener("click", () => { - chrome.runtime.openOptionsPage(); -}); -document.getElementById("open-settings-btn").addEventListener("click", () => { - chrome.runtime.openOptionsPage(); -}); -document.getElementById("retry-btn").addEventListener("click", () => clip()); -document.getElementById("new-clip-btn").addEventListener("click", () => clip()); - -// ── Send to Memos ───────────────────────────────────────────────────────────── -sendBtn.addEventListener("click", async () => { - state.markdown = mdEditor.value; - // Append tags from tag bar if any - const tagStr = tagsInput.value.trim(); - const finalContent = tagStr ? `${state.markdown}\n\n${tagStr}` : state.markdown; - const settings = await getSettings(); - const baseUrl = settings.memosUrl.replace(/\/$/, ""); - const token = settings.apiToken; - const visibility = visibilityEl.value; - - sendBtn.disabled = true; - - try { - let resourceNames = []; - - // 1. Upload images if requested - if (attachCheck.checked) { - const toUpload = state.images.filter((img) => img.keep && !img.src.startsWith("data:")); - let uploaded = 0; - for (const img of toUpload) { - sendBtn.textContent = `Uploading image ${uploaded + 1}/${toUpload.length}…`; - try { - const resource = await uploadImage(baseUrl, token, img); - if (resource) { - // v0.22+ uses resource.name; older APIs return resource.id - resourceNames.push(resource.name || `resources/${resource.id}`); - } - uploaded++; - } catch (e) { - console.warn("Failed to upload image:", img.src, e.message); - } - } - } - - sendBtn.textContent = "Creating memo…"; - - // 2. Create memo - // Build content: append uploaded image markdown links so they appear inline - // even on instances where resource attachment display is not supported. - let contentWithImages = finalContent; - if (resourceNames.length) { - const imgLinks = resourceNames - .map((name, i) => { - // v0.22+ resource URL pattern: /file/{name}/filename - const urlName = name.startsWith("resources/") ? name : `resources/${name}`; - return `![attachment-${i + 1}](${baseUrl}/file/${urlName.replace("resources/", "")})`; - }) - .join("\n"); - contentWithImages = finalContent + "\n\n" + imgLinks; - } - - const body = { - content: contentWithImages, - visibility, - }; - // Also set resources array for native attachment display (v0.22+) - if (resourceNames.length) { - body.resources = resourceNames.map((name) => { - // Normalise: API wants just "resources/abc123" not a full path - const norm = name.startsWith("resources/") ? name : `resources/${name}`; - return { name: norm }; - }); - } - - const res = await fetch(`${baseUrl}/api/v1/memos`, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify(body), - }); - - if (!res.ok) { - const txt = await res.text(); - throw new Error(`API error ${res.status}: ${txt.slice(0, 200)}`); - } - - const memo = await res.json(); - // Handle both v0.22+ (memo.name = "memos/123") and older (memo.id = "123" or memo.uid) - let memoId = ""; - if (memo.name) { - memoId = memo.name.replace(/^memos\//, ""); - } else if (memo.uid) { - memoId = memo.uid; - } else if (memo.id) { - memoId = memo.id; - } - - document.getElementById("memo-link").href = memoId ? `${baseUrl}/memos/${memoId}` : baseUrl; - showView("success"); - } catch (e) { - errorMsg.textContent = e.message; - showView("error"); - } finally { - sendBtn.disabled = false; - sendBtn.innerHTML = ` Send to Memos`; - } -}); - -// ── Upload a single image resource ──────────────────────────────────────────── -async function uploadImage(baseUrl, token, img) { - const response = await fetch(img.src); - if (!response.ok) throw new Error(`Fetch failed: ${response.status}`); - const blob = await response.blob(); - - // Derive a filename with a valid extension - let filename = img.src.split("/").pop().split("?")[0].split("#")[0]; - // Strip non-filename characters - filename = filename.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 80); - // If no extension, infer from MIME type - if (!/\.\w{2,5}$/.test(filename)) { - const mimeToExt = { - "image/jpeg": "jpg", "image/jpg": "jpg", "image/png": "png", - "image/gif": "gif", "image/webp": "webp", "image/svg+xml": "svg", - "image/avif": "avif","image/tiff": "tiff", - }; - const ext = mimeToExt[blob.type] || "jpg"; - filename = (filename || "image") + "." + ext; - } - if (!filename || filename === ".") filename = "image.jpg"; - - const formData = new FormData(); - formData.append("file", blob, filename); - - const res = await fetch(`${baseUrl}/api/v1/resources`, { - method: "POST", - headers: { Authorization: `Bearer ${token}` }, - body: formData, - }); - if (!res.ok) { - const txt = await res.text(); - throw new Error(`Resource upload failed ${res.status}: ${txt.slice(0, 120)}`); - } - const resource = await res.json(); - // Normalise the returned name — could be "resources/abc", "abc", or an int id - if (resource.name) return resource; - if (resource.id) return { ...resource, name: `resources/${resource.id}` }; - throw new Error("Resource upload returned unexpected shape: " + JSON.stringify(resource).slice(0, 80)); -} diff --git a/settings.css b/settings.css deleted file mode 100644 index e7b6c9b..0000000 --- a/settings.css +++ /dev/null @@ -1,157 +0,0 @@ -@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@400;500;600&display=swap'); - -*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } - -:root { - --bg: #0f0f11; - --surface: #1a1a1f; - --border: #2a2a35; - --accent: #7c6af7; - --accent-dim: #7c6af730; - --text: #e8e8f0; - --text-dim: #888899; - --success: #4ade80; - --error: #f87171; - --radius: 8px; - --font: 'IBM Plex Sans', sans-serif; - --mono: 'IBM Plex Mono', monospace; -} - -body { - font-family: var(--font); - background: var(--bg); - color: var(--text); - min-height: 100vh; - font-size: 14px; - line-height: 1.6; -} - -.page { max-width: 560px; margin: 0 auto; padding: 32px 24px; } - -header { - margin-bottom: 32px; -} - -.logo { - display: flex; - align-items: center; - gap: 10px; - font-size: 16px; - font-weight: 600; - color: var(--text); -} -.logo svg { width: 22px; height: 22px; stroke: var(--accent); } - -.card { - background: var(--surface); - border: 1px solid var(--border); - border-radius: 12px; - padding: 24px; - margin-bottom: 20px; -} - -.card h2 { - font-size: 13px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: .08em; - color: var(--text-dim); - margin-bottom: 20px; -} - -label { - display: flex; - flex-direction: column; - gap: 6px; - margin-bottom: 16px; -} - -label > span { - font-size: 13px; - font-weight: 500; - color: var(--text); -} - -input[type="url"], -input[type="password"], -input[type="text"], -select { - background: var(--bg); - border: 1px solid var(--border); - border-radius: var(--radius); - color: var(--text); - font-family: var(--mono); - font-size: 13px; - padding: 9px 12px; - outline: none; - transition: border-color .15s; - width: 100%; -} - -input:focus, select:focus { - border-color: var(--accent); - box-shadow: 0 0 0 3px var(--accent-dim); -} - -select option { background: var(--surface); } - -small { - font-size: 11px; - color: var(--text-dim); - font-family: var(--font); -} - -.checkbox-label { - flex-direction: row; - align-items: center; - gap: 10px; - cursor: pointer; -} -.checkbox-label input[type="checkbox"] { - width: 16px; - height: 16px; - accent-color: var(--accent); - cursor: pointer; -} -.checkbox-label span { font-size: 13px; font-weight: 400; } - -.actions { - display: flex; - gap: 10px; - margin-top: 20px; -} - -button { - font-family: var(--font); - font-size: 13px; - font-weight: 500; - padding: 9px 18px; - border-radius: var(--radius); - border: none; - cursor: pointer; - transition: all .15s; -} - -button:not(.secondary) { - background: var(--accent); - color: #fff; -} -button:not(.secondary):hover { filter: brightness(1.15); } - -button.secondary { - background: transparent; - border: 1px solid var(--border); - color: var(--text-dim); -} -button.secondary:hover { border-color: var(--accent); color: var(--accent); } - -.status { - margin-top: 12px; - padding: 9px 12px; - border-radius: var(--radius); - font-size: 12px; - font-family: var(--mono); -} -.status.hidden { display: none; } -.status.ok { background: #4ade8015; border: 1px solid #4ade8040; color: var(--success); } -.status.err { background: #f8717115; border: 1px solid #f8717140; color: var(--error); } diff --git a/settings.html b/settings.html deleted file mode 100644 index 3ccce36..0000000 --- a/settings.html +++ /dev/null @@ -1,77 +0,0 @@ - - - - - Memos Clipper — Settings - - - -
-
- -
- -
-
-

Connection

- - -
- - -
- -
- -
-

Defaults

- - - - -
- -
- -
-
-
- - - diff --git a/settings.js b/settings.js deleted file mode 100644 index 900e10c..0000000 --- a/settings.js +++ /dev/null @@ -1,63 +0,0 @@ -// settings.js -const $ = (id) => document.getElementById(id); - -async function load() { - const s = await chrome.storage.sync.get([ - "memosUrl", "apiToken", "visibility", "clipMode", "includeImages", "includeTags" - ]); - if (s.memosUrl) $("memos-url").value = s.memosUrl; - if (s.apiToken) $("api-token").value = s.apiToken; - if (s.visibility) $("visibility").value = s.visibility; - if (s.clipMode) $("clip-mode").value = s.clipMode; - if (s.includeImages !== undefined) $("include-images").checked = s.includeImages; - if (s.includeTags !== undefined) $("include-tags").checked = s.includeTags; -} - -function showStatus(el, ok, msg) { - el.textContent = msg; - el.className = `status ${ok ? "ok" : "err"}`; -} - -$("save-btn").addEventListener("click", async () => { - const url = $("memos-url").value.trim().replace(/\/$/, ""); - const token = $("api-token").value.trim(); - if (!url || !token) { - showStatus($("status"), false, "URL and token are required."); - return; - } - await chrome.storage.sync.set({ memosUrl: url, apiToken: token }); - showStatus($("status"), true, "✓ Saved."); -}); - -$("save-defaults-btn").addEventListener("click", async () => { - await chrome.storage.sync.set({ - visibility: $("visibility").value, - clipMode: $("clip-mode").value, - includeImages: $("include-images").checked, - includeTags: $("include-tags").checked, - }); - showStatus($("defaults-status"), true, "✓ Defaults saved."); -}); - -$("test-btn").addEventListener("click", async () => { - const url = $("memos-url").value.trim().replace(/\/$/, ""); - const token = $("api-token").value.trim(); - $("test-btn").textContent = "Testing…"; - try { - const res = await fetch(`${url}/api/v1/memos?pageSize=1`, { - headers: { Authorization: `Bearer ${token}` } - }); - if (res.ok) { - showStatus($("status"), true, `✓ Connected! (HTTP ${res.status})`); - } else { - const txt = await res.text(); - showStatus($("status"), false, `✗ HTTP ${res.status}: ${txt.slice(0, 120)}`); - } - } catch (e) { - showStatus($("status"), false, `✗ ${e.message}`); - } finally { - $("test-btn").textContent = "Test Connection"; - } -}); - -load();