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 +1 -1
- package/src/api/file.js +45 -0
- package/src/static/app.js +94 -2
- package/src/static/styles.css +47 -1
- package/src/utils/fileTypes.js +2 -2
package/package.json
CHANGED
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, '<')
|
|
150
|
+
.replace(/>/g, '>')
|
|
151
|
+
.replace(/"/g, '"')
|
|
152
|
+
.replace(/'/g, ''');
|
|
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 === '
|
|
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.
|
|
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') {
|
package/src/static/styles.css
CHANGED
|
@@ -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
|
-
|
|
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; }
|
package/src/utils/fileTypes.js
CHANGED
|
@@ -29,8 +29,8 @@ const FILE_TYPES = {
|
|
|
29
29
|
jsx: code('react', 'jsx'),
|
|
30
30
|
|
|
31
31
|
// Code - Web
|
|
32
|
-
html:
|
|
33
|
-
htm:
|
|
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'),
|