mdv-live 0.3.7 → 0.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mdv-live",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
4
4
  "description": "Markdown Viewer - File tree + Live preview + Marp support + Hot reload",
5
5
  "main": "src/server.js",
6
6
  "bin": {
package/src/api/file.js CHANGED
@@ -89,6 +89,32 @@ function buildBinaryFileResponse(name, fileType, downloadUrl) {
89
89
  export function setupFileRoutes(app) {
90
90
  const { rootDir } = app.locals;
91
91
 
92
+ // Serve raw files (for HTML preview with relative paths)
93
+ app.get('/raw/*', async (req, res) => {
94
+ const relativePath = req.params[0];
95
+ const { valid, fullPath } = resolveAndValidate(relativePath, rootDir);
96
+
97
+ if (!relativePath || !valid) {
98
+ return res.status(403).json({ error: 'Access denied' });
99
+ }
100
+
101
+ try {
102
+ const stat = await fs.stat(fullPath);
103
+ if (!stat.isFile()) {
104
+ return res.status(400).json({ error: 'Not a file' });
105
+ }
106
+
107
+ const mimeType = mime.lookup(fullPath) || 'application/octet-stream';
108
+ res.setHeader('Content-Type', mimeType);
109
+ res.sendFile(fullPath);
110
+ } catch (err) {
111
+ if (err.code === 'ENOENT') {
112
+ return res.status(404).json({ error: 'File not found' });
113
+ }
114
+ res.status(500).json({ error: err.message });
115
+ }
116
+ });
117
+
92
118
  // Get file content
93
119
  app.get('/api/file', async (req, res) => {
94
120
  const { path: relativePath } = req.query;
@@ -115,6 +141,25 @@ export function setupFileRoutes(app) {
115
141
  return res.json(buildBinaryFileResponse(name, fileType, downloadUrl));
116
142
  }
117
143
 
144
+ // HTML files: return htmlUrl for iframe preview + raw content for editing
145
+ if (fileType.type === 'html') {
146
+ const content = await fs.readFile(fullPath, 'utf-8');
147
+ const escaped = content
148
+ .replace(/&/g, '&')
149
+ .replace(/</g, '&lt;')
150
+ .replace(/>/g, '&gt;')
151
+ .replace(/"/g, '&quot;')
152
+ .replace(/'/g, '&#x27;');
153
+ return res.json({
154
+ name,
155
+ fileType: 'html',
156
+ icon: 'html',
157
+ htmlUrl: `/raw/${relativePath}`,
158
+ content: `<pre><code class="language-html">${escaped}</code></pre>`,
159
+ raw: content
160
+ });
161
+ }
162
+
118
163
  const rendered = await renderFile(fullPath);
119
164
  res.json({ name, ...rendered });
120
165
  } catch (err) {
package/src/static/app.js CHANGED
@@ -613,6 +613,11 @@
613
613
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" />
614
614
  </svg>
615
615
  </button>
616
+ <button class="marp-close-nav" title="Hide (N to show)">
617
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
618
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
619
+ </svg>
620
+ </button>
616
621
  `;
617
622
  marpit.appendChild(nav);
618
623
  }
@@ -676,17 +681,83 @@
676
681
 
677
682
  // Fullscreen toggle
678
683
  const fullscreenBtn = elements.content.querySelector('.marp-fullscreen-btn');
684
+ const expandIcon = '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" /></svg>';
685
+ const shrinkIcon = '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 9V4m0 5H4m5 0L4 4m11 5h5m-5 0V4m0 5l5-5M9 15v5m0-5H4m5 0l-5 5m11-5h5m-5 0v5m0-5l5 5" /></svg>';
679
686
  const toggleFullscreen = () => {
680
687
  document.body.classList.toggle('marp-fullscreen');
688
+ const isFullscreen = document.body.classList.contains('marp-fullscreen');
689
+ if (fullscreenBtn) {
690
+ fullscreenBtn.innerHTML = isFullscreen ? shrinkIcon : expandIcon;
691
+ fullscreenBtn.title = isFullscreen ? 'Exit Fullscreen (Esc)' : 'Fullscreen (F)';
692
+ }
693
+ // Reset nav position when exiting fullscreen
694
+ const nav = elements.content.querySelector('.marp-nav');
695
+ if (!isFullscreen && nav) {
696
+ nav.style.left = '';
697
+ nav.style.top = '';
698
+ nav.style.right = '';
699
+ nav.style.bottom = '';
700
+ nav.style.transform = '';
701
+ }
681
702
  };
682
703
  if (fullscreenBtn) fullscreenBtn.addEventListener('click', toggleFullscreen);
683
704
 
705
+ // Make nav draggable and closeable
706
+ const nav = elements.content.querySelector('.marp-nav');
707
+ if (nav) {
708
+ let isDragging = false;
709
+ let dragStartX, dragStartY, navStartX, navStartY;
710
+
711
+ nav.addEventListener('mousedown', (e) => {
712
+ // Don't drag when clicking buttons or not in fullscreen
713
+ if (e.target.closest('button')) return;
714
+ if (!document.body.classList.contains('marp-fullscreen')) return;
715
+ isDragging = true;
716
+ nav.classList.add('dragging');
717
+ dragStartX = e.clientX;
718
+ dragStartY = e.clientY;
719
+ const rect = nav.getBoundingClientRect();
720
+ navStartX = rect.left;
721
+ navStartY = rect.top;
722
+ e.preventDefault();
723
+ });
724
+
725
+ document.addEventListener('mousemove', (e) => {
726
+ if (!isDragging) return;
727
+ const dx = e.clientX - dragStartX;
728
+ const dy = e.clientY - dragStartY;
729
+ const newX = Math.max(0, Math.min(window.innerWidth - nav.offsetWidth, navStartX + dx));
730
+ const newY = Math.max(0, Math.min(window.innerHeight - nav.offsetHeight, navStartY + dy));
731
+ nav.style.left = newX + 'px';
732
+ nav.style.top = newY + 'px';
733
+ nav.style.right = 'auto';
734
+ nav.style.bottom = 'auto';
735
+ nav.style.transform = 'none';
736
+ });
737
+
738
+ document.addEventListener('mouseup', () => {
739
+ if (isDragging) {
740
+ isDragging = false;
741
+ nav.classList.remove('dragging');
742
+ }
743
+ });
744
+
745
+ // Close button to hide nav
746
+ const closeBtn = nav.querySelector('.marp-close-nav');
747
+ if (closeBtn) {
748
+ closeBtn.addEventListener('click', () => {
749
+ nav.classList.add('hidden');
750
+ });
751
+ }
752
+ }
753
+
684
754
  // Keyboard navigation
685
755
  marpKeyHandler = (e) => {
686
756
  // Don't handle if editing or in dialog
687
757
  if (state.isEditMode || !elements.dialogOverlay.classList.contains('hidden')) {
688
758
  return;
689
759
  }
760
+ const nav = elements.content.querySelector('.marp-nav');
690
761
  if (e.key === 'ArrowRight' || e.key === ' ') {
691
762
  e.preventDefault();
692
763
  nextSlide();
@@ -696,9 +767,16 @@
696
767
  } else if (e.key === 'f' || e.key === 'F') {
697
768
  e.preventDefault();
698
769
  toggleFullscreen();
699
- } else if (e.key === 'Escape' && document.body.classList.contains('marp-fullscreen')) {
770
+ } else if (e.key === 'n' || e.key === 'N') {
771
+ e.preventDefault();
772
+ if (nav) nav.classList.toggle('hidden');
773
+ } else if (e.key === 'Escape') {
700
774
  e.preventDefault();
701
- document.body.classList.remove('marp-fullscreen');
775
+ if (document.body.classList.contains('marp-fullscreen')) {
776
+ toggleFullscreen();
777
+ } else if (nav && nav.classList.contains('hidden')) {
778
+ nav.classList.remove('hidden');
779
+ }
702
780
  }
703
781
  };
704
782
  document.addEventListener('keydown', marpKeyHandler);
@@ -752,6 +830,17 @@
752
830
  `;
753
831
  },
754
832
 
833
+ renderHTML(htmlUrl, name) {
834
+ elements.content.style.padding = '0';
835
+ elements.content.innerHTML = `
836
+ <div class="html-preview">
837
+ <iframe src="${htmlUrl}" title="${name}"
838
+ sandbox="allow-scripts allow-same-origin allow-forms">
839
+ </iframe>
840
+ </div>
841
+ `;
842
+ },
843
+
755
844
  renderVideo(mediaUrl, name) {
756
845
  elements.content.innerHTML = `
757
846
  <div class="video-preview">
@@ -830,6 +919,7 @@
830
919
  css: data.css || null, // Marp CSS from marp-core
831
920
  imageUrl: data.imageUrl,
832
921
  pdfUrl: data.pdfUrl,
922
+ htmlUrl: data.htmlUrl,
833
923
  mediaUrl: data.mediaUrl,
834
924
  downloadUrl: data.downloadUrl,
835
925
  scrollTop: 0
@@ -940,6 +1030,8 @@
940
1030
  ContentRenderer.renderImage(tab.imageUrl, tab.name);
941
1031
  } else if (fileType === 'pdf') {
942
1032
  ContentRenderer.renderPDF(tab.pdfUrl, tab.name);
1033
+ } else if (fileType === 'html' && tab.htmlUrl && !state.isEditMode) {
1034
+ ContentRenderer.renderHTML(tab.htmlUrl, tab.name);
943
1035
  } else if (fileType === 'video') {
944
1036
  ContentRenderer.renderVideo(tab.mediaUrl, tab.name);
945
1037
  } else if (fileType === 'audio') {
@@ -520,6 +520,22 @@ body {
520
520
  text-align: center;
521
521
  }
522
522
 
523
+ /* ============================================================
524
+ HTML Preview
525
+ ============================================================ */
526
+
527
+ .html-preview {
528
+ width: 100%;
529
+ height: 100%;
530
+ }
531
+
532
+ .html-preview iframe {
533
+ width: 100%;
534
+ height: 100%;
535
+ border: none;
536
+ background: white;
537
+ }
538
+
523
539
  /* ============================================================
524
540
  Editor Mode
525
541
  ============================================================ */
@@ -875,6 +891,7 @@ body {
875
891
  border-radius: 8px;
876
892
  border: 1px solid var(--border);
877
893
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
894
+ user-select: none;
878
895
  }
879
896
 
880
897
  .marp-nav button {
@@ -911,6 +928,19 @@ body {
911
928
  text-align: center;
912
929
  }
913
930
 
931
+ body.marp-fullscreen .marp-nav .slide-counter {
932
+ cursor: grab;
933
+ }
934
+
935
+ body.marp-fullscreen .marp-nav.dragging {
936
+ cursor: grabbing;
937
+ opacity: 0.9;
938
+ }
939
+
940
+ body.marp-fullscreen .marp-nav.dragging .slide-counter {
941
+ cursor: grabbing;
942
+ }
943
+
914
944
  .marp-nav .keyboard-hint {
915
945
  font-size: 11px;
916
946
  color: var(--text-muted);
@@ -950,7 +980,10 @@ body.marp-fullscreen .content {
950
980
 
951
981
  body.marp-fullscreen .marp-nav {
952
982
  bottom: 32px;
953
- right: 32px;
983
+ left: 50%;
984
+ right: auto;
985
+ transform: translateX(-50%);
986
+ cursor: grab;
954
987
  }
955
988
 
956
989
  body.marp-fullscreen .marpit {
@@ -970,6 +1003,19 @@ body.marp-fullscreen .marpit > svg[data-marpit-svg] {
970
1003
  margin-left: 4px;
971
1004
  }
972
1005
 
1006
+ .marp-close-nav {
1007
+ margin-left: 4px;
1008
+ }
1009
+
1010
+ .marp-close-nav:hover:not(:disabled) {
1011
+ background: var(--danger) !important;
1012
+ border-color: var(--danger) !important;
1013
+ }
1014
+
1015
+ .marp-nav.hidden {
1016
+ display: none !important;
1017
+ }
1018
+
973
1019
  /* Print styles for Marp */
974
1020
  @media print {
975
1021
  .marp-nav { display: none !important; }
@@ -29,8 +29,8 @@ const FILE_TYPES = {
29
29
  jsx: code('react', 'jsx'),
30
30
 
31
31
  // Code - Web
32
- html: code('html', 'html'),
33
- htm: code('html', 'html'),
32
+ html: { type: 'html', icon: 'html', lang: 'html', binary: false },
33
+ htm: { type: 'html', icon: 'html', lang: 'html', binary: false },
34
34
  css: code('css', 'css'),
35
35
  scss: code('css', 'scss'),
36
36
  less: code('css', 'less'),