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 @@ + + + + + + Markdown Preview + + + +

+  
+
+
diff --git a/content/preview.js b/content/preview.js
new file mode 100644
index 0000000..f49a09f
--- /dev/null
+++ b/content/preview.js
@@ -0,0 +1,78 @@
+;(async () => {
+  const localData = await chrome.storage.local.get('temporaryMarkdown')
+  const markdown = localData.temporaryMarkdown || ''
+
+  const pre = document.querySelector('pre')
+  pre.textContent = markdown
+
+  const state = await chrome.storage.sync.get()
+  
+  window.args = {
+    theme: state.theme,
+    raw: state.raw,
+    themes: state.themes,
+    content: state.content,
+    compiler: state.compiler,
+    custom: state.custom,
+    icon: state.settings.icon,
+  }
+
+  const loadCSS = (href) => {
+    return new Promise((resolve) => {
+      const link = document.createElement('link')
+      link.rel = 'stylesheet'
+      link.type = 'text/css'
+      link.href = href
+      link.onload = resolve
+      document.head.appendChild(link)
+    })
+  }
+
+  const loadScript = (src) => {
+    return new Promise((resolve, reject) => {
+      const script = document.createElement('script')
+      script.src = src
+      script.onload = resolve
+      script.onerror = reject
+      document.body.appendChild(script)
+    })
+  }
+
+  await Promise.all([
+    loadCSS('/content/index.css'),
+    loadCSS('/content/themes.css')
+  ])
+
+  await loadScript('/vendor/mithril.min.js')
+  await loadScript('/vendor/mammoth.browser.min.js')
+  await loadScript('/vendor/jszip.min.js')
+  await loadScript('/vendor/docx-preview.min.js')
+
+  if (window.args.content.syntax) {
+    await loadScript('/vendor/prism.min.js')
+    await loadScript('/vendor/prism-autoloader.min.js')
+    await loadScript('/content/prism.js')
+  }
+
+  if (window.args.content.emoji) {
+    await loadScript('/content/emoji.js')
+  }
+
+  if (window.args.content.mermaid) {
+    await loadScript('/vendor/mermaid.min.js')
+    await loadScript('/vendor/panzoom.min.js')
+    await loadScript('/content/mermaid.js')
+  }
+
+  if (window.args.content.mathjax) {
+    await loadScript('/content/mathjax.js')
+    await loadScript('/vendor/mathjax/tex-mml-chtml.js')
+  }
+
+  await loadScript('/content/index.js')
+  await loadScript('/content/scroll.js')
+
+  if (window.args.content.autoreload) {
+    await loadScript('/content/autoreload.js')
+  }
+})()
diff --git a/content/prism.js b/content/prism.js
index e615315..6b9effe 100644
--- a/content/prism.js
+++ b/content/prism.js
@@ -1,7 +1,16 @@
 
 Prism.plugins.autoloader.addScript = (language, done) => {
-  chrome.runtime.sendMessage({
-    message: 'prism',
-    language
-  }, done)
+  if (location.protocol.endsWith('-extension:')) {
+    var script = document.createElement('script')
+    script.src = `/vendor/prism/prism-${language}.min.js`
+    script.onload = () => done()
+    script.onerror = () => done()
+    document.body.appendChild(script)
+  }
+  else {
+    chrome.runtime.sendMessage({
+      message: 'prism',
+      language
+    }, done)
+  }
 }
diff --git a/manifest.chrome.json b/manifest.chrome.json
index 7f52256..20d976e 100644
--- a/manifest.chrome.json
+++ b/manifest.chrome.json
@@ -45,14 +45,18 @@
         "/themes/*",
         "/vendor/mathjax/fonts/*",
         "/vendor/prism.min.css",
-        "/vendor/prism-okaidia.min.css"
+        "/vendor/prism-okaidia.min.css",
+        "/vendor/mammoth.browser.min.js",
+        "/vendor/jszip.min.js",
+        "/vendor/docx-preview.min.js"
       ]
     }
   ],
 
   "permissions": [
     "storage",
-    "scripting"
+    "scripting",
+    "activeTab"
   ],
 
   "host_permissions": [
diff --git a/manifest.firefox.json b/manifest.firefox.json
index 8a0a721..4b02440 100644
--- a/manifest.firefox.json
+++ b/manifest.firefox.json
@@ -67,14 +67,18 @@
         "/themes/*",
         "/vendor/mathjax/fonts/*",
         "/vendor/prism.min.css",
-        "/vendor/prism-okaidia.min.css"
+        "/vendor/prism-okaidia.min.css",
+        "/vendor/mammoth.browser.min.js",
+        "/vendor/jszip.min.js",
+        "/vendor/docx-preview.min.js"
       ]
     }
   ],
 
   "permissions": [
     "storage",
-    "scripting"
+    "scripting",
+    "activeTab"
   ],
 
   "host_permissions": [
diff --git a/popup/index.css b/popup/index.css
index 15542f3..fbf26f3 100644
--- a/popup/index.css
+++ b/popup/index.css
@@ -67,9 +67,9 @@ body.dark .m-button {
 /*button*/
 
 /*defaults button*/
-#popup button:nth-child(2) { float: right; }
+#popup > button:nth-child(2) { float: right; }
 /*advanced options button*/
-#popup button:nth-child(2n+3) { float: right; margin-top: 20px; }
+#popup > button:nth-child(2n+3) { float: right; margin-top: 20px; }
 
 .m-button {
   background-color: #ececec !important;
@@ -145,14 +145,16 @@ body.dark .m-button {
   line-height: 36px;
   border-bottom: 1px solid #e0e0e0;
   display: inline-block;
-  width: 33.33%;
-  max-width: 33.33%;
-  min-width: 33.33%;
+  width: 20%;
+  max-width: 20%;
+  min-width: 20%;
   height: 36px;
   max-height: 36px;
   min-height: 36px;
   padding: 0;
   align-content: stretch;
+  font-size: 11px !important;
+  text-align: center;
 }
 .m-tabs .mdc-tab-bar__indicator { background: #607d8b; }
 
@@ -160,6 +162,157 @@ body.dark .m-button {
 .m-panel { display: none; }
 .m-panel.is-active { display: block; }
 
+/* preview tab styling */
+.preview-textarea {
+  width: 100%;
+  height: 180px;
+  box-sizing: border-box;
+  margin-top: 15px;
+  padding: 10px;
+  border: 1px solid #ccc;
+  border-radius: 4px;
+  font-family: monospace;
+  font-size: 13px;
+  resize: vertical;
+  background-color: #f9f9f9;
+  color: #333;
+  transition: border-color 0.2s ease, box-shadow 0.2s ease;
+}
+.preview-textarea:focus {
+  outline: none;
+  border-color: #607d8b;
+  box-shadow: 0 0 0 2px rgba(96, 125, 139, 0.25);
+}
+.preview-textarea.drag-over {
+  border: 2px dashed #607d8b !important;
+  background-color: rgba(96, 125, 139, 0.08) !important;
+  color: #607d8b;
+}
+.preview-textarea:disabled {
+  background-color: #f1f1f1;
+  color: #777;
+  cursor: not-allowed;
+}
+
+/* preview controls layout */
+.preview-controls {
+  display: flex;
+  gap: 10px;
+  margin-top: 15px;
+}
+.clear-btn {
+  flex: 1;
+  background-color: #f5f5f5 !important;
+  color: #333 !important;
+  margin-top: 0 !important;
+}
+.preview-btn-half {
+  flex: 1;
+  margin-top: 0 !important;
+}
+
+/* history panel styling */
+.history-scroll {
+  max-height: 250px;
+  margin-top: 15px;
+}
+.history-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 8px 10px;
+  border-bottom: 1px solid #eee;
+  font-family: sans-serif !important;
+  transition: background-color 0.2s ease;
+}
+.history-item:hover {
+  background-color: #f7f7f7;
+}
+.history-info {
+  flex: 1;
+  min-width: 0;
+}
+.history-title {
+  font-size: 13px;
+  font-weight: bold;
+  color: #333;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+.history-time {
+  font-size: 11px;
+  color: #888;
+  margin-top: 2px;
+}
+.history-actions {
+  display: flex;
+  gap: 3px;
+  margin-left: 10px;
+}
+.action-btn {
+  border: none;
+  background: none;
+  font-size: 16px;
+  cursor: pointer;
+  padding: 4px 8px;
+  border-radius: 4px;
+  transition: background-color 0.2s ease, color 0.2s ease;
+  line-height: 1;
+}
+.reopen-btn {
+  color: #607d8b;
+}
+.reopen-btn:hover {
+  background-color: rgba(96, 125, 139, 0.15);
+}
+.rename-btn {
+  color: #ff9800;
+}
+.rename-btn:hover {
+  background-color: rgba(255, 152, 0, 0.15);
+}
+.delete-btn {
+  color: #d32f2f;
+}
+.delete-btn:hover {
+  background-color: rgba(211, 47, 47, 0.15);
+}
+.save-btn {
+  color: #2e7d32;
+}
+.save-btn:hover {
+  background-color: rgba(46, 125, 50, 0.15);
+}
+.discard-btn {
+  color: #c62828;
+}
+.discard-btn:hover {
+  background-color: rgba(198, 40, 40, 0.15);
+}
+.history-empty {
+  text-align: center;
+  padding: 30px;
+  color: #999;
+  font-size: 14px;
+  font-family: sans-serif !important;
+}
+
+/* inline rename input styling */
+.edit-title-input {
+  font-size: 13px;
+  padding: 2px 5px;
+  border: 1px solid #607d8b;
+  border-radius: 4px;
+  width: 100%;
+  box-sizing: border-box;
+}
+.edit-title-input:focus {
+  outline: none;
+  box-shadow: 0 0 0 2px rgba(96, 125, 139, 0.25);
+}
+
+
 /*---------------------------------------------------------------------------*/
 /*options scroll*/
 
diff --git a/popup/index.html b/popup/index.html
index 6404ef5..23944ec 100644
--- a/popup/index.html
+++ b/popup/index.html
@@ -10,5 +10,6 @@
   
 
 
+
 
 
diff --git a/popup/index.js b/popup/index.js
index fe01bae..e614e8c 100644
--- a/popup/index.js
+++ b/popup/index.js
@@ -53,7 +53,7 @@ var Popup = () => {
     ],
     raw: false,
     tab: '',
-    tabs: ['theme', 'compiler', 'content'],
+    tabs: ['theme', 'compiler', 'content', 'preview', 'history'],
     compilers: [],
     description: {
       themes: {},
@@ -67,7 +67,32 @@ var Popup = () => {
         syntax: 'Syntax highlighting for fenced code blocks',
       }
     },
-    settings: {}
+    settings: {},
+    tempMarkdown: '',
+    history: [],
+    editingId: null,
+    editingTitle: '',
+    dragging: false,
+    loading: false,
+    loadingMessage: ''
+  }
+
+  var openPreview = (previewUrl) => {
+    chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
+      var activeTab = tabs[0]
+      var isNewTab = activeTab && activeTab.url && (
+        activeTab.url === 'chrome://newtab/' ||
+        activeTab.url === 'about:newtab' ||
+        activeTab.url === 'about:home' ||
+        activeTab.url === 'about:blank' ||
+        activeTab.url.startsWith('chrome-search://')
+      )
+      if (isNewTab) {
+        chrome.tabs.update(activeTab.id, { url: previewUrl })
+      } else {
+        chrome.tabs.create({ url: previewUrl })
+      }
+    })
   }
 
   var events = {
@@ -77,6 +102,73 @@ var Popup = () => {
       return false
     },
 
+    dragover: (e) => {
+      e.preventDefault()
+      state.dragging = true
+    },
+
+    dragenter: (e) => {
+      e.preventDefault()
+      state.dragging = true
+    },
+
+    dragleave: (e) => {
+      state.dragging = false
+    },
+
+    drop: (e) => {
+      e.preventDefault()
+      state.dragging = false
+
+      var files = e.dataTransfer.files
+      if (files.length === 0) return
+
+      var file = files[0]
+      var ext = file.name.split('.').pop().toLowerCase()
+
+      if (ext === 'docx') {
+        state.loading = true
+        state.loadingMessage = 'Loading DOCX file...'
+        m.redraw()
+
+        var reader = new FileReader()
+        reader.onload = (event) => {
+          var base64String = event.target.result.split(',')[1]
+          state.tempMarkdown = `[DOCX Document] ${file.name}`
+          chrome.storage.local.set({ 
+            temporaryDocx: base64String,
+            temporaryMarkdown: state.tempMarkdown
+          }, () => {
+            state.loading = false
+            state.loadingMessage = ''
+            m.redraw()
+          })
+        }
+        reader.onerror = (err) => {
+          console.error(err)
+          state.loading = false
+          state.loadingMessage = ''
+          alert('Error reading DOCX file')
+          m.redraw()
+        }
+        reader.readAsDataURL(file)
+      } else {
+        state.loading = true
+        state.loadingMessage = 'Reading file...'
+        m.redraw()
+
+        var reader = new FileReader()
+        reader.onload = (event) => {
+          state.tempMarkdown = event.target.result
+          chrome.storage.local.set({ temporaryMarkdown: state.tempMarkdown })
+          state.loading = false
+          state.loadingMessage = ''
+          m.redraw()
+        }
+        reader.readAsText(file)
+      }
+    },
+
     compiler: {
       name: (e) => {
         state.compiler = state.compilers[e.target.selectedIndex]
@@ -141,6 +233,89 @@ var Popup = () => {
 
     advanced: () => {
       chrome.runtime.sendMessage({message: 'popup.advanced'})
+    },
+
+    preview: () => {
+      chrome.storage.local.set({ temporaryMarkdown: state.tempMarkdown || '' }, () => {
+        chrome.storage.local.get('previewHistory', (localRes) => {
+          var history = localRes.previewHistory || []
+          
+          var title = ''
+          var lines = (state.tempMarkdown || '').trim().split('\n')
+          if (lines.length && lines[0].startsWith('#')) {
+            title = lines[0].replace(/^#+\s*/, '').trim()
+          }
+          if (!title) {
+            var snippet = (state.tempMarkdown || '').trim().substring(0, 30)
+            title = snippet ? (snippet + (state.tempMarkdown.length > 30 ? '...' : '')) : 'Empty Markdown'
+          }
+          
+          var historyItem = {
+            id: Date.now().toString(),
+            title: title,
+            timestamp: new Date().toLocaleString(),
+            markdown: state.tempMarkdown || ''
+          }
+          
+          history.unshift(historyItem)
+          if (history.length > 50) {
+            history = history.slice(0, 50)
+          }
+          state.history = history
+          
+          chrome.storage.local.set({ previewHistory: history }, () => {
+            openPreview(chrome.runtime.getURL('/content/preview.html'))
+          })
+        })
+      })
+    },
+
+    clear: () => {
+      state.tempMarkdown = ''
+      chrome.storage.local.set({ temporaryMarkdown: '', temporaryDocx: '' }, () => {
+        m.redraw()
+      })
+    },
+
+    reopenHistory: (item) => {
+      chrome.storage.local.set({ temporaryMarkdown: item.markdown, temporaryDocx: '' }, () => {
+        openPreview(chrome.runtime.getURL('/content/preview.html'))
+      })
+    },
+
+    startRename: (item) => {
+      state.editingId = item.id
+      state.editingTitle = item.title
+    },
+
+    saveRename: (itemId) => {
+      if (state.editingTitle.trim() !== '') {
+        var history = state.history.map((item) => {
+          if (item.id === itemId) {
+            item.title = state.editingTitle.trim()
+          }
+          return item
+        })
+        state.history = history
+        chrome.storage.local.set({ previewHistory: history }, () => {
+          state.editingId = null
+          state.editingTitle = ''
+          m.redraw()
+        })
+      }
+    },
+
+    cancelRename: () => {
+      state.editingId = null
+      state.editingTitle = ''
+    },
+
+    deleteHistory: (itemId) => {
+      var history = state.history.filter((item) => item.id !== itemId)
+      state.history = history
+      chrome.storage.local.set({ previewHistory: history }, () => {
+        m.redraw()
+      })
     }
   }
 
@@ -152,7 +327,7 @@ var Popup = () => {
     state.themes = res.themes
 
     state.raw = res.raw
-    state.tab = localStorage.getItem('tab') || 'theme'
+    state.tab = localStorage.getItem('tab') || 'preview'
     state.compilers = res.compilers
     state.description.compiler = res.description
 
@@ -162,7 +337,13 @@ var Popup = () => {
     m.redraw()
   }
 
-  chrome.runtime.sendMessage({message: 'popup'}, init)
+  chrome.runtime.sendMessage({message: 'popup'}, (res) => {
+    chrome.storage.local.get(['temporaryMarkdown', 'previewHistory'], (localRes) => {
+      state.tempMarkdown = localRes.temporaryMarkdown || ''
+      state.history = localRes.previewHistory || []
+      init(res)
+    })
+  })
 
   var oncreate = {
     ripple: (vnode) => {
@@ -293,6 +474,92 @@ var Popup = () => {
               m('span.mdc-switch-label', key)
             ))
           )
+        ),
+        // preview
+        m('.m-panel', {
+          class: state.tab === 'preview' ? 'is-active' : ''
+          },
+          m('textarea.preview-textarea', {
+            class: state.dragging ? 'drag-over' : '',
+            disabled: state.loading,
+            placeholder: state.loading ? state.loadingMessage : 'Paste your Markdown, or drag & drop a .md / .docx file here...',
+            value: state.loading ? state.loadingMessage : state.tempMarkdown,
+            oninput: (e) => {
+              state.tempMarkdown = e.target.value
+            },
+            ondragover: events.dragover,
+            ondragenter: events.dragenter,
+            ondragleave: events.dragleave,
+            ondrop: events.drop
+          }),
+          m('.preview-controls',
+            m('button.mdc-button mdc-button--raised m-button clear-btn', {
+              oncreate: oncreate.ripple,
+              disabled: state.loading,
+              onclick: events.clear
+            }, 'Clear'),
+            m('button.mdc-button mdc-button--raised m-button preview-btn-half', {
+              oncreate: oncreate.ripple,
+              disabled: state.loading,
+              onclick: events.preview
+            }, 'Preview')
+          )
+        ),
+        // history
+        m('.m-panel', {
+          class: state.tab === 'history' ? 'is-active' : ''
+          },
+          m('.scroll.history-scroll',
+            state.history && state.history.length ?
+              state.history.map((item) =>
+                m('.history-item',
+                  m('.history-info',
+                    state.editingId === item.id ?
+                      m('input.edit-title-input', {
+                        value: state.editingTitle,
+                        oninput: (e) => {
+                          state.editingTitle = e.target.value
+                        },
+                        onkeydown: (e) => {
+                          if (e.key === 'Enter') events.saveRename(item.id)
+                          if (e.key === 'Escape') events.cancelRename()
+                        }
+                      })
+                    : [
+                        m('.history-title', item.title),
+                        m('.history-time', item.timestamp)
+                      ]
+                  ),
+                  state.editingId === item.id ?
+                    m('.history-actions',
+                      m('button.action-btn.save-btn', {
+                        title: 'Save',
+                        onclick: () => events.saveRename(item.id)
+                      }, m.trust('')),
+                      m('button.action-btn.discard-btn', {
+                        title: 'Discard',
+                        onclick: () => events.cancelRename()
+                      }, m.trust(''))
+                    )
+                  :
+                    m('.history-actions',
+                      m('button.action-btn.reopen-btn', {
+                        title: 'Open preview',
+                        onclick: () => events.reopenHistory(item)
+                      }, m.trust('')),
+                      m('button.action-btn.rename-btn', {
+                        title: 'Rename',
+                        onclick: () => events.startRename(item)
+                      }, m.trust('')),
+                      m('button.action-btn.delete-btn', {
+                        title: 'Delete',
+                        onclick: () => events.deleteHistory(item.id)
+                      }, m.trust(''))
+                    )
+                )
+              )
+            : m('.history-empty', 'No preview history found.')
+          )
         )
       ),