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 e434d52..0000000 Binary files a/icons/icon128.png and /dev/null differ diff --git a/icons/icon16.png b/icons/icon16.png deleted file mode 100644 index ee76939..0000000 Binary files a/icons/icon16.png and /dev/null differ diff --git a/icons/icon48.png b/icons/icon48.png deleted file mode 100644 index b551fdf..0000000 Binary files a/icons/icon48.png and /dev/null differ 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();