From 8b7d524eca0a6ef0554e34b77cb4ca137a05b663 Mon Sep 17 00:00:00 2001 From: Harsh Vadhiya Date: Sat, 13 Jun 2026 12:46:19 +0530 Subject: [PATCH 1/2] feat: add code block copy button and popup markdown previewer with history --- background/messages.js | 36 +++++--- background/storage.js | 17 +++- content/index.css | 98 ++++++++++++++++++++ content/index.js | 53 +++++++++++ content/mathjax.js | 21 +++-- content/preview.html | 17 ++++ content/preview.js | 75 ++++++++++++++++ content/prism.js | 17 +++- manifest.chrome.json | 3 +- manifest.firefox.json | 3 +- popup/index.css | 153 ++++++++++++++++++++++++++++++-- popup/index.js | 197 ++++++++++++++++++++++++++++++++++++++++- 12 files changed, 652 insertions(+), 38 deletions(-) create mode 100644 content/preview.html create mode 100644 content/preview.js 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/content/index.css b/content/index.css index 63b8dd9..7ee75ff 100644 --- a/content/index.css +++ b/content/index.css @@ -359,3 +359,101 @@ 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; +} + diff --git a/content/index.js b/content/index.js index b7cf50e..21ac864 100644 --- a/content/index.js +++ b/content/index.js @@ -110,6 +110,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) => { 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..99ab5bc
--- /dev/null
+++ b/content/preview.js
@@ -0,0 +1,75 @@
+;(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')
+
+  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..9a93aef 100644
--- a/manifest.chrome.json
+++ b/manifest.chrome.json
@@ -52,7 +52,8 @@
 
   "permissions": [
     "storage",
-    "scripting"
+    "scripting",
+    "activeTab"
   ],
 
   "host_permissions": [
diff --git a/manifest.firefox.json b/manifest.firefox.json
index 8a0a721..cf325df 100644
--- a/manifest.firefox.json
+++ b/manifest.firefox.json
@@ -74,7 +74,8 @@
 
   "permissions": [
     "storage",
-    "scripting"
+    "scripting",
+    "activeTab"
   ],
 
   "host_permissions": [
diff --git a/popup/index.css b/popup/index.css
index 15542f3..0610617 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,147 @@ 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 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.js b/popup/index.js
index fe01bae..458e906 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,29 @@ var Popup = () => {
         syntax: 'Syntax highlighting for fenced code blocks',
       }
     },
-    settings: {}
+    settings: {},
+    tempMarkdown: '',
+    history: [],
+    editingId: null,
+    editingTitle: ''
+  }
+
+  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 = {
@@ -141,6 +163,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: '' }, () => {
+        m.redraw()
+      })
+    },
+
+    reopenHistory: (item) => {
+      chrome.storage.local.set({ temporaryMarkdown: item.markdown }, () => {
+        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 +257,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 +267,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 +404,84 @@ var Popup = () => {
               m('span.mdc-switch-label', key)
             ))
           )
+        ),
+        // preview
+        m('.m-panel', {
+          class: state.tab === 'preview' ? 'is-active' : ''
+          },
+          m('textarea.preview-textarea', {
+            placeholder: 'Paste your Markdown here...',
+            value: state.tempMarkdown,
+            oninput: (e) => {
+              state.tempMarkdown = e.target.value
+            }
+          }),
+          m('.preview-controls',
+            m('button.mdc-button mdc-button--raised m-button clear-btn', {
+              oncreate: oncreate.ripple,
+              onclick: events.clear
+            }, 'Clear'),
+            m('button.mdc-button mdc-button--raised m-button preview-btn-half', {
+              oncreate: oncreate.ripple,
+              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.')
+          )
         )
       ),
 

From 99f74996bb8da04fc8384881d335f875cc453f02 Mon Sep 17 00:00:00 2001
From: Harsh Vadhiya 
Date: Tue, 14 Jul 2026 10:52:50 +0530
Subject: [PATCH 2/2] feat: docx preview (drag and drop the docx file)

---
 .DS_Store                       | Bin 0 -> 8196 bytes
 build/docx-preview/build.sh     |   5 +
 build/docx-preview/package.json |   5 +
 build/jszip/build.sh            |   5 +
 build/jszip/package.json        |   5 +
 build/mammoth/build.sh          |  13 ++
 build/mammoth/package.json      |   5 +
 build/package.sh                |   3 +
 content/index.css               | 110 ++++++++++++++++
 content/index.js                | 225 ++++++++++++++++++++++++++------
 content/preview.js              |   3 +
 manifest.chrome.json            |   5 +-
 manifest.firefox.json           |   5 +-
 popup/index.css                 |  10 ++
 popup/index.html                |   1 +
 popup/index.js                  |  90 ++++++++++++-
 16 files changed, 440 insertions(+), 50 deletions(-)
 create mode 100644 .DS_Store
 create mode 100644 build/docx-preview/build.sh
 create mode 100644 build/docx-preview/package.json
 create mode 100644 build/jszip/build.sh
 create mode 100644 build/jszip/package.json
 create mode 100644 build/mammoth/build.sh
 create mode 100644 build/mammoth/package.json

diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000000000000000000000000000000000000..5ac6241ecbe03cb443f3f773146cc8ffad8ed0a3
GIT binary patch
literal 8196
zcmeHMJ#Q015S<0vShz`nL<)pXD0B#vhKk~_40H$-C{hI+Td{@BSxJoOazB9vgg}5O
zprA)cRCH-0rA!xk6iOQ4&F;pxb05$l1hdlayX?HVefMs4=eZD(X-|8LMDs+{L}#39
zVJK-F=U!+>{LBK-AfHBSi%YA?cv4{MggT%Or~~SNI-m~xEe_zF&8=DS-gi+Q)d6+j
zzjT0~4-q@js{9~~HM2>`5N+BO`|@c}UM7<)_|B7$aID$u1GUt$=Sj`_&*Vvngq
zmrlkPAI7h2e1&4{>NtO7>11MuI;sQeK+yrN-D`B7)@VqZqJBTs{t(8-NvG2tC0)!l
z&+R{b{o>2^Wf|}NGCmmu-v&;9LMk1nF5RV7x`)-*DWNIdr=HM2{YzE6P|u)s>*F)0
zr}UiTB-8dguF&b8CUxkX!kAn&sV*U0xQ!}%@TUd0T#m*551xdp@mj%%4n$L$M@Mi3i87{
zQ_hg<^YXabgeGufXkE!^Smqs_V}v?RUbua@R|>4mk7}0CLyuTb9coz7lqS-T!Altp
zb?jc8=?L;eKgt<${gl^lz>5L)#u#0?QG5R_9}hLO8Yd3-s5nQcqKYGQ)OVbY2X=?{
z;KsUeqW|W))e*!)9eYpSz4CjsL4uRrkt&YRv2oykK9Gs9Aez4aoew&y19=CIx`}zN
z|F2Yk|IerC-|B!m@DDj)YMrG{8yDnj>%ygRtsSA?LFdN2)S*qmV9Rl!Eysbge;DF8
Y0+n-OkEuh /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 7ee75ff..ac89626 100644
--- a/content/index.css
+++ b/content/index.css
@@ -457,3 +457,113 @@ body {
   --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 21ac864..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,
@@ -195,15 +198,112 @@ function mount () {
   var md = $('pre').innerText
   favicon()
 
-  m.mount($('body'), {
-    oninit: () => {
-      render(md)
-    },
-    view: () => {
-      var dom = []
+  var dragCounter = 0
 
-      if (state.html) {
-        state._themes.custom = state.custom.color
+  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 (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' ||
@@ -218,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))
+              }
+            }
           }))
-        }
-
-        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 { + $('body').classList.remove('docx-mode') + if (state.html) { + state._themes.custom = state.custom.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`), + })) + } + + 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') - } - if (state.theme === 'custom') { - dom.push(m('style', {type: 'text/css'}, state.custom.theme)) - } + 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') + ) + )) + + return dom } - - return dom - } + }) }) } diff --git a/content/preview.js b/content/preview.js index 99ab5bc..f49a09f 100644 --- a/content/preview.js +++ b/content/preview.js @@ -44,6 +44,9 @@ ]) 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') diff --git a/manifest.chrome.json b/manifest.chrome.json index 9a93aef..20d976e 100644 --- a/manifest.chrome.json +++ b/manifest.chrome.json @@ -45,7 +45,10 @@ "/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" ] } ], diff --git a/manifest.firefox.json b/manifest.firefox.json index cf325df..4b02440 100644 --- a/manifest.firefox.json +++ b/manifest.firefox.json @@ -67,7 +67,10 @@ "/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" ] } ], diff --git a/popup/index.css b/popup/index.css index 0610617..fbf26f3 100644 --- a/popup/index.css +++ b/popup/index.css @@ -183,6 +183,16 @@ body.dark .m-button { 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 { 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 458e906..e614e8c 100644 --- a/popup/index.js +++ b/popup/index.js @@ -71,7 +71,10 @@ var Popup = () => { tempMarkdown: '', history: [], editingId: null, - editingTitle: '' + editingTitle: '', + dragging: false, + loading: false, + loadingMessage: '' } var openPreview = (previewUrl) => { @@ -99,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] @@ -202,13 +272,13 @@ var Popup = () => { clear: () => { state.tempMarkdown = '' - chrome.storage.local.set({ temporaryMarkdown: '' }, () => { + chrome.storage.local.set({ temporaryMarkdown: '', temporaryDocx: '' }, () => { m.redraw() }) }, reopenHistory: (item) => { - chrome.storage.local.set({ temporaryMarkdown: item.markdown }, () => { + chrome.storage.local.set({ temporaryMarkdown: item.markdown, temporaryDocx: '' }, () => { openPreview(chrome.runtime.getURL('/content/preview.html')) }) }, @@ -410,19 +480,27 @@ var Popup = () => { class: state.tab === 'preview' ? 'is-active' : '' }, m('textarea.preview-textarea', { - placeholder: 'Paste your Markdown here...', - value: state.tempMarkdown, + 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') )