diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..5ac6241 Binary files /dev/null and b/.DS_Store differ diff --git a/background/messages.js b/background/messages.js index 14a1603..12bd672 100644 --- a/background/messages.js +++ b/background/messages.js @@ -26,22 +26,30 @@ md.messages = ({storage: {defaults, state, set}, compilers, mathjax, xhr, webreq }) } else if (req.message === 'prism') { - chrome.scripting.executeScript({ - target: {tabId: sender.tab.id}, - files: [ - `/vendor/prism/prism-${req.language}.min.js`, - ], - injectImmediately: true - }, sendResponse) + if (sender.tab) { + chrome.scripting.executeScript({ + target: {tabId: sender.tab.id}, + files: [ + `/vendor/prism/prism-${req.language}.min.js`, + ], + injectImmediately: true + }, sendResponse) + } else { + sendResponse() + } } else if (req.message === 'mathjax') { - chrome.scripting.executeScript({ - target: {tabId: sender.tab.id}, - files: [ - `/vendor/mathjax/extensions/${req.extension}.js`, - ], - injectImmediately: true - }, sendResponse) + if (sender.tab) { + chrome.scripting.executeScript({ + target: {tabId: sender.tab.id}, + files: [ + `/vendor/mathjax/extensions/${req.extension}.js`, + ], + injectImmediately: true + }, sendResponse) + } else { + sendResponse() + } } // popup diff --git a/background/storage.js b/background/storage.js index 8f5e403..1a6911b 100644 --- a/background/storage.js +++ b/background/storage.js @@ -82,10 +82,19 @@ md.storage.bug = (res) => { // reload extension bug chrome.permissions.getAll((permissions) => { var origins = Object.keys(res.origins || {}) - chrome.permissions.remove({ - origins: permissions.origins - .filter((origin) => origins.indexOf(origin.slice(0, -2)) === -1) - }) + var originsToRemove = (permissions.origins || []) + .filter((origin) => origins.indexOf(origin.slice(0, -2)) === -1) + if (originsToRemove.length) { + var promise = chrome.permissions.remove({ + origins: originsToRemove + }, () => { + // Ignore error when trying to remove required permissions + var error = chrome.runtime.lastError + }) + if (promise && promise.catch) { + promise.catch(() => {}) + } + } }) } diff --git a/build/docx-preview/build.sh b/build/docx-preview/build.sh new file mode 100644 index 0000000..6d66e7c --- /dev/null +++ b/build/docx-preview/build.sh @@ -0,0 +1,5 @@ +#!/bin/bash +cd "$(dirname "$0")" +npm ci 2> /dev/null || npm i +cp node_modules/docx-preview/dist/docx-preview.min.js ../../vendor/docx-preview.min.js +rm -rf node_modules/ diff --git a/build/docx-preview/package.json b/build/docx-preview/package.json new file mode 100644 index 0000000..42ed975 --- /dev/null +++ b/build/docx-preview/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "docx-preview": "0.1.20" + } +} diff --git a/build/jszip/build.sh b/build/jszip/build.sh new file mode 100644 index 0000000..e72021f --- /dev/null +++ b/build/jszip/build.sh @@ -0,0 +1,5 @@ +#!/bin/bash +cd "$(dirname "$0")" +npm ci 2> /dev/null || npm i +cp node_modules/jszip/dist/jszip.min.js ../../vendor/jszip.min.js +rm -rf node_modules/ diff --git a/build/jszip/package.json b/build/jszip/package.json new file mode 100644 index 0000000..892daa7 --- /dev/null +++ b/build/jszip/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "jszip": "3.10.1" + } +} diff --git a/build/mammoth/build.sh b/build/mammoth/build.sh new file mode 100644 index 0000000..1e17611 --- /dev/null +++ b/build/mammoth/build.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +# set current working directory to directory of the shell script +cd "$(dirname "$0")" + +# before +npm ci 2> /dev/null || npm i + +# copy +cp node_modules/mammoth/mammoth.browser.min.js ../../vendor/mammoth.browser.min.js + +# after +rm -rf node_modules/ diff --git a/build/mammoth/package.json b/build/mammoth/package.json new file mode 100644 index 0000000..933fcfd --- /dev/null +++ b/build/mammoth/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "mammoth": "1.11.0" + } +} diff --git a/build/package.sh b/build/package.sh index 5349c33..4a806e1 100755 --- a/build/package.sh +++ b/build/package.sh @@ -27,6 +27,9 @@ sh csso/build.sh sh markdown-it/build.sh sh marked/build.sh sh mathjax/build.sh +sh mammoth/build.sh +sh jszip/build.sh +sh docx-preview/build.sh sh mdc/build.sh sh mermaid/build.sh sh mithril/build.sh diff --git a/content/index.css b/content/index.css index 63b8dd9..ac89626 100644 --- a/content/index.css +++ b/content/index.css @@ -359,3 +359,211 @@ img.emojione { /* prevent img stretch */ width: auto; } + +/*---------------------------------------------------------------------------*/ +/*copy button*/ + +#_html pre { + position: relative; +} + +#_html pre .markdown-copy-button { + position: absolute; + top: 8px; + right: 8px; + opacity: 0; + transition: opacity 0.2s ease, background-color 0.2s ease, border-color 0.2s ease; + border: 1px solid var(--copy-btn-border); + background-color: var(--copy-btn-bg); + color: var(--copy-btn-color); + padding: 6px; + border-radius: 6px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + z-index: 10; + user-select: none; + -webkit-user-select: none; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); +} + +/* Show button on pre hover and focus */ +#_html pre:hover .markdown-copy-button, +#_html pre .markdown-copy-button:focus { + opacity: 1; +} + +#_html pre .markdown-copy-button:hover { + background-color: var(--copy-btn-bg-hover); + border-color: var(--copy-btn-border-hover); + color: var(--copy-btn-color-hover); +} + +#_html pre .markdown-copy-button.copied { + background-color: var(--copy-btn-bg-copied); + border-color: var(--copy-btn-border-copied); + color: var(--copy-btn-color-copied); +} + +/* Base Theme Variables */ +body { + --copy-btn-bg: rgba(255, 255, 255, 0.85); + --copy-btn-bg-hover: rgba(240, 240, 240, 0.95); + --copy-btn-border: #e1e4e8; + --copy-btn-border-hover: #b0b5bc; + --copy-btn-color: #57606a; + --copy-btn-color-hover: #24292f; + --copy-btn-bg-copied: #dcffe4; + --copy-btn-border-copied: #85e89d; + --copy-btn-color-copied: #22863a; +} + +@media (prefers-color-scheme: dark) { + body { + --copy-btn-bg: rgba(33, 38, 45, 0.85); + --copy-btn-bg-hover: rgba(48, 54, 61, 0.95); + --copy-btn-border: #30363d; + --copy-btn-border-hover: #8b949e; + --copy-btn-color: #8b949e; + --copy-btn-color-hover: #c9d1d9; + --copy-btn-bg-copied: rgba(56, 139, 253, 0.15); + --copy-btn-border-copied: rgba(56, 139, 253, 0.4); + --copy-btn-color-copied: #58a6ff; + } +} + +._color-light { + --copy-btn-bg: rgba(255, 255, 255, 0.85); + --copy-btn-bg-hover: rgba(240, 240, 240, 0.95); + --copy-btn-border: #e1e4e8; + --copy-btn-border-hover: #b0b5bc; + --copy-btn-color: #57606a; + --copy-btn-color-hover: #24292f; + --copy-btn-bg-copied: #dcffe4; + --copy-btn-border-copied: #85e89d; + --copy-btn-color-copied: #22863a; +} + +._color-dark { + --copy-btn-bg: rgba(33, 38, 45, 0.85); + --copy-btn-bg-hover: rgba(48, 54, 61, 0.95); + --copy-btn-border: #30363d; + --copy-btn-border-hover: #8b949e; + --copy-btn-color: #8b949e; + --copy-btn-color-hover: #c9d1d9; + --copy-btn-bg-copied: rgba(56, 139, 253, 0.15); + --copy-btn-border-copied: rgba(56, 139, 253, 0.4); + --copy-btn-color-copied: #58a6ff; +} + +/* Drag and Drop Premium Overlay */ +.dropzone-overlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(255, 255, 255, 0.75); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + z-index: 10000; + opacity: 0; + visibility: hidden; + transition: opacity 0.3s ease, visibility 0.3s ease; + pointer-events: none; +} + +body._color-dark .dropzone-overlay { + background: rgba(15, 17, 23, 0.8); +} + +.dropzone-overlay.active { + opacity: 1; + visibility: visible; +} + +.dropzone-box { + border: 3px dashed #607d8b; + border-radius: 20px; + padding: 40px 60px; + background: rgba(255, 255, 255, 0.95); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; + max-width: 80%; + text-align: center; + transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); + transform: scale(0.9); +} + +body._color-dark .dropzone-box { + background: rgba(30, 34, 45, 0.95); + border-color: #8b949e; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); +} + +.dropzone-overlay.active .dropzone-box { + transform: scale(1); +} + +.dropzone-icon { + width: 64px; + height: 64px; + color: #607d8b; +} + +body._color-dark .dropzone-icon { + color: #8b949e; +} + +.dropzone-text { + font-family: sans-serif !important; + font-size: 20px; + font-weight: 600; + color: #24292f; + margin: 0; +} + +body._color-dark .dropzone-text { + color: #c9d1d9; +} + +.dropzone-subtext { + font-family: sans-serif !important; + font-size: 14px; + color: #57606a; + margin: 0; +} + +body._color-dark .dropzone-subtext { + color: #8b949e; +} + +/* DOCX Mode full page overrides */ +body.docx-mode { + padding: 0 !important; + margin: 0 !important; + background: #f4f4f4 !important; /* light gray desktop background */ +} + +body.docx-mode #_html { + visibility: visible !important; + width: 100% !important; + max-width: 100% !important; + margin: 0 !important; + padding: 0 !important; + box-sizing: border-box !important; + display: block !important; +} + +body.docx-mode #_toc { + display: none !important; +} + diff --git a/content/index.js b/content/index.js index b7cf50e..68e786f 100644 --- a/content/index.js +++ b/content/index.js @@ -12,6 +12,9 @@ var state = { html: '', markdown: '', toc: '', + dragActive: false, + isDocxMode: false, + docxBuffer: null, reload: { interval: null, ms: 1000, @@ -110,6 +113,59 @@ var update = (update) => { if (state.content.mathjax) { setTimeout(() => mj.render(), 60) } + + setTimeout(() => addCopyButtons(), 80) +} + +var addCopyButtons = () => { + var preElements = document.querySelectorAll('#_html pre') + preElements.forEach((pre) => { + if (pre.querySelector('code.mermaid') || pre.querySelector('svg[id^=mermaid]')) { + return + } + if (pre.querySelector('.markdown-copy-button')) { + return + } + + var button = document.createElement('button') + button.className = 'markdown-copy-button' + button.type = 'button' + button.title = 'Copy code' + button.setAttribute('aria-label', 'Copy code') + + button.innerHTML = ` + + + ` + + button.addEventListener('click', () => { + var codeElement = pre.querySelector('code') + var text = codeElement ? codeElement.innerText : pre.innerText + + navigator.clipboard.writeText(text).then(() => { + var copyIcon = button.querySelector('.copy-icon') + var checkIcon = button.querySelector('.check-icon') + copyIcon.style.display = 'none' + checkIcon.style.display = 'inline' + button.classList.add('copied') + + setTimeout(() => { + copyIcon.style.display = 'inline' + checkIcon.style.display = 'none' + button.classList.remove('copied') + }, 2000) + }).catch((err) => { + console.error('Failed to copy text: ', err) + }) + }) + + pre.appendChild(button) + }) } var render = (md) => { @@ -142,15 +198,112 @@ function mount () { var md = $('pre').innerText favicon() - m.mount($('body'), { - oninit: () => { - render(md) - }, - view: () => { - var dom = [] + var dragCounter = 0 + + window.addEventListener('dragenter', (e) => { + e.preventDefault() + dragCounter++ + if (dragCounter === 1) { + state.dragActive = true + m.redraw() + } + }) + + window.addEventListener('dragover', (e) => { + e.preventDefault() + }) + + window.addEventListener('dragleave', (e) => { + e.preventDefault() + dragCounter-- + if (dragCounter === 0) { + state.dragActive = false + m.redraw() + } + }) + + window.addEventListener('drop', (e) => { + e.preventDefault() + dragCounter = 0 + state.dragActive = false + m.redraw() + + var files = e.dataTransfer.files + if (files.length === 0) return + + var file = files[0] + var ext = file.name.split('.').pop().toLowerCase() + + document.title = file.name - if (state.html) { - state._themes.custom = state.custom.color + if (ext === 'docx') { + var loadDocxScripts = () => { + return new Promise((resolve) => { + if (typeof docx !== 'undefined' && typeof JSZip !== 'undefined') { + resolve() + return + } + var loadJSZip = () => { + return new Promise((r) => { + var s = document.createElement('script') + s.src = chrome.runtime.getURL('/vendor/jszip.min.js') + s.onload = () => r() + document.body.appendChild(s) + }) + } + var loadDocx = () => { + return new Promise((r) => { + var s = document.createElement('script') + s.src = chrome.runtime.getURL('/vendor/docx-preview.min.js') + s.onload = () => r() + document.body.appendChild(s) + }) + } + loadJSZip().then(loadDocx).then(() => resolve()) + }) + } + + loadDocxScripts().then(() => { + var reader = new FileReader() + reader.onload = (event) => { + state.isDocxMode = true + state.docxBuffer = event.target.result + m.redraw() + } + reader.readAsArrayBuffer(file) + }) + } else { + state.isDocxMode = false + state.docxBuffer = null + var reader = new FileReader() + reader.onload = (event) => { + render(event.target.result) + } + reader.readAsText(file) + } + }) + + chrome.storage.local.get(['temporaryDocx'], (res) => { + var isPreviewPage = window.location.pathname.endsWith('preview.html') + if (isPreviewPage && res.temporaryDocx) { + var binaryString = window.atob(res.temporaryDocx) + var len = binaryString.length + var bytes = new Uint8Array(len) + for (var i = 0; i < len; i++) { + bytes[i] = binaryString.charCodeAt(i) + } + state.isDocxMode = true + state.docxBuffer = bytes.buffer + } + + m.mount($('body'), { + oninit: () => { + if (!state.isDocxMode) { + render(md) + } + }, + view: () => { + var dom = [] var color = state._themes[state.theme] === 'dark' || @@ -165,45 +318,86 @@ function mount () { })) $('body').classList.add(`_theme-${state.theme}`, `_color-${color}`) - if (state.content.syntax) { - dom.push(m('link#_prism', { - rel: 'stylesheet', type: 'text/css', - href: chrome.runtime.getURL(`/vendor/${color === 'dark' ? 'prism-okaidia' : 'prism'}.min.css`), + if (state.isDocxMode) { + $('body').classList.add('docx-mode') + $('body').classList.remove('_toc-left', '_toc-right') + + dom.push(m('#_html', { + oncreate: (vnode) => { + vnode.dom.style.visibility = 'visible' + if (state.docxBuffer) { + vnode.dom.innerHTML = '' + docx.renderAsync(state.docxBuffer, vnode.dom) + .then(() => console.log('DOCX rendered successfully')) + .catch(err => console.error('DOCX render error:', err)) + } + } })) - } + } else { + $('body').classList.remove('docx-mode') + if (state.html) { + state._themes.custom = state.custom.color - var theme = - (/github(-dark)?/.test(state.theme) ? 'markdown-body' : 'markdown-theme') + - (state.themes.width !== 'auto' ? ` _width-${state.themes.width}` : '') + if (state.content.syntax) { + dom.push(m('link#_prism', { + rel: 'stylesheet', type: 'text/css', + href: chrome.runtime.getURL(`/vendor/${color === 'dark' ? 'prism-okaidia' : 'prism'}.min.css`), + })) + } - if (state.raw) { - if (state.content.syntax) { - dom.push(m('#_markdown', {oncreate: oncreate.html, onupdate: onupdate.html, class: theme}, - m.trust(`
${_escape(state.markdown)}`)
- ))
- }
- else {
- dom.push(m('pre#_markdown', {oncreate: oncreate.html, onupdate: onupdate.html}, state.markdown))
+ var theme =
+ (/github(-dark)?/.test(state.theme) ? 'markdown-body' : 'markdown-theme') +
+ (state.themes.width !== 'auto' ? ` _width-${state.themes.width}` : '')
+
+ if (state.raw) {
+ if (state.content.syntax) {
+ dom.push(m('#_markdown', {oncreate: oncreate.html, onupdate: onupdate.html, class: theme},
+ m.trust(`${_escape(state.markdown)}`)
+ ))
+ }
+ else {
+ dom.push(m('pre#_markdown', {oncreate: oncreate.html, onupdate: onupdate.html}, state.markdown))
+ }
+ }
+ else {
+ dom.push(m('#_html', {oncreate: oncreate.html, onupdate: onupdate.html, class: theme},
+ m.trust(state.html)
+ ))
+ }
+
+ if (state.content.toc) {
+ dom.push(m('#_toc.tex2jax-ignore', m.trust(state.toc)))
+ state.raw ? $('body').classList.remove('_toc-left') : $('body').classList.add('_toc-left')
+ }
+
+ if (state.theme === 'custom') {
+ dom.push(m('style', {type: 'text/css'}, state.custom.theme))
+ }
}
}
- else {
- dom.push(m('#_html', {oncreate: oncreate.html, onupdate: onupdate.html, class: theme},
- m.trust(state.html)
- ))
- }
- if (state.content.toc) {
- dom.push(m('#_toc.tex2jax-ignore', m.trust(state.toc)))
- state.raw ? $('body').classList.remove('_toc-left') : $('body').classList.add('_toc-left')
- }
+ dom.push(m('.dropzone-overlay', { class: state.dragActive ? 'active' : '' },
+ m('.dropzone-box',
+ m('svg.dropzone-icon', {
+ viewBox: "0 0 24 24",
+ fill: "none",
+ stroke: "currentColor",
+ "stroke-width": "2",
+ "stroke-linecap": "round",
+ "stroke-linejoin": "round"
+ },
+ m('path', { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
+ m('polyline', { points: "17 8 12 3 7 8" }),
+ m('line', { x1: "12", y1: "3", x2: "12", y2: "15" })
+ ),
+ m('h2.dropzone-text', 'Drop .md or .docx file here'),
+ m('p.dropzone-subtext', 'We will render it instantly in the browser')
+ )
+ ))
- if (state.theme === 'custom') {
- dom.push(m('style', {type: 'text/css'}, state.custom.theme))
- }
+ return dom
}
-
- return dom
- }
+ })
})
}
diff --git a/content/mathjax.js b/content/mathjax.js
index d105b90..a360961 100644
--- a/content/mathjax.js
+++ b/content/mathjax.js
@@ -4,11 +4,22 @@ var MathJax = {
pathFilters: [
({name}) => name.startsWith('[tex]') ? false : true // keep the name
],
- require: (path) => path.startsWith('[tex]') ?
- chrome.runtime.sendMessage({
- message: 'mathjax',
- extension: path.replace('[tex]/', '')
- }) : null
+ require: (path) => {
+ if (path.startsWith('[tex]')) {
+ var extension = path.replace('[tex]/', '')
+ if (location.protocol.endsWith('-extension:')) {
+ var script = document.createElement('script')
+ script.src = `/vendor/mathjax/extensions/${extension}.js`
+ document.body.appendChild(script)
+ return null
+ }
+ return chrome.runtime.sendMessage({
+ message: 'mathjax',
+ extension
+ })
+ }
+ return null
+ }
},
tex: {
inlineMath: [
diff --git a/content/preview.html b/content/preview.html
new file mode 100644
index 0000000..06bffbd
--- /dev/null
+++ b/content/preview.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+