@docmd/live 0.4.0
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/LICENSE +21 -0
- package/README.md +24 -0
- package/bin/docmd-live.js +3 -0
- package/dist/assets/css/docmd-highlight-dark.css +86 -0
- package/dist/assets/css/docmd-highlight-light.css +86 -0
- package/dist/assets/css/docmd-live-preview.css +33 -0
- package/dist/assets/css/docmd-main.css +1720 -0
- package/dist/assets/css/docmd-theme-retro.css +867 -0
- package/dist/assets/css/docmd-theme-ruby.css +629 -0
- package/dist/assets/css/docmd-theme-sky.css +617 -0
- package/dist/assets/js/docmd-main.js +276 -0
- package/dist/docmd-live.css +275 -0
- package/dist/docmd-live.js +26725 -0
- package/dist/index.html +189 -0
- package/index.js +42 -0
- package/package.json +26 -0
- package/src/build.js +141 -0
package/dist/index.html
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
<!-- Source file from the docmd project — https://github.com/docmd-io/docmd -->
|
|
2
|
+
|
|
3
|
+
<!DOCTYPE html>
|
|
4
|
+
<html lang="en">
|
|
5
|
+
<head>
|
|
6
|
+
<meta charset="UTF-8">
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
8
|
+
<title>Docmd Live Editor</title>
|
|
9
|
+
<meta name="description" content="Real-time Markdown preview and editor powered by docmd.">
|
|
10
|
+
|
|
11
|
+
<link rel="stylesheet" href="docmd-live.css">
|
|
12
|
+
<script src="docmd-live.js"></script>
|
|
13
|
+
|
|
14
|
+
<script async src="https://www.googletagmanager.com/gtag/js?id=G-VCMQ0MCSHN"></script>
|
|
15
|
+
<script>
|
|
16
|
+
window.dataLayer = window.dataLayer || [];
|
|
17
|
+
function gtag() { dataLayer.push(arguments); }
|
|
18
|
+
gtag('js', new Date());
|
|
19
|
+
gtag('config', 'G-VCMQ0MCSHN');
|
|
20
|
+
</script>
|
|
21
|
+
</head>
|
|
22
|
+
|
|
23
|
+
<body class="mode-split">
|
|
24
|
+
|
|
25
|
+
<!-- Top Bar -->
|
|
26
|
+
<div class="top-bar">
|
|
27
|
+
<div class="logo">
|
|
28
|
+
<!--<a href="/" class="back-link" id="back-btn" title="Previous Page" aria-label="Back to previous page">
|
|
29
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none"
|
|
30
|
+
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
31
|
+
<path d="m12 19-7-7 7-7" />
|
|
32
|
+
<path d="M19 12H5" />
|
|
33
|
+
</svg>
|
|
34
|
+
</a>-->
|
|
35
|
+
<a href="https://docmd.io" class="docmd-logo" title="Back to home" aria-label="Back to homepage"><svg width="24" height="24" id="icon-feather" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.24 12.24a6 6 0 0 0-8.49-8.49L5 10.5V19h8.5z"></path><line x1="16" y1="8" x2="2" y2="22"></line><line x1="17.5" y1="15" x2="9" y2="15"></line></svg></a> docmd <span>Live</span>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<!-- Unified 3-Way Switcher -->
|
|
39
|
+
<div class="view-switcher desktop-only">
|
|
40
|
+
<button class="view-btn active" onclick="setMode('split')" data-mode="split">Split</button>
|
|
41
|
+
<button class="view-btn" onclick="setMode('editor')" data-mode="editor">Editor</button>
|
|
42
|
+
<button class="view-btn" onclick="setMode('preview')" data-mode="preview">Preview</button>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<!-- Workspace -->
|
|
47
|
+
<div class="workspace" id="workspace">
|
|
48
|
+
<!-- Editor -->
|
|
49
|
+
<div class="pane editor-pane" id="editorPane">
|
|
50
|
+
<div class="pane-header">Markdown</div>
|
|
51
|
+
<textarea id="input" spellcheck="false">
|
|
52
|
+
---
|
|
53
|
+
title: My Documentation
|
|
54
|
+
description: Start editing to see changes instantly.
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
# Hello World
|
|
58
|
+
|
|
59
|
+
This is a **live** preview.
|
|
60
|
+
|
|
61
|
+
::: callout tip
|
|
62
|
+
Try resizing the window or switching to mobile view!
|
|
63
|
+
:::
|
|
64
|
+
|
|
65
|
+
## Features
|
|
66
|
+
1. Responsive Design
|
|
67
|
+
2. Split or Tabbed view
|
|
68
|
+
3. Instant Rendering
|
|
69
|
+
</textarea>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<!-- Resizer Handle -->
|
|
73
|
+
<div class="resizer" id="resizer"></div>
|
|
74
|
+
|
|
75
|
+
<!-- Preview -->
|
|
76
|
+
<div class="pane preview-pane" id="previewPane">
|
|
77
|
+
<div class="pane-header">Preview</div>
|
|
78
|
+
<iframe id="preview"></iframe>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<!-- Mobile Bottom Tabs (Keep for small screens) -->
|
|
83
|
+
<div class="mobile-tabs">
|
|
84
|
+
<button class="mobile-tab-btn active" onclick="setMode('editor')" id="mob-edit">Editor</button>
|
|
85
|
+
<button class="mobile-tab-btn" onclick="setMode('preview')" id="mob-prev">Preview</button>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<script>
|
|
89
|
+
// --- Core Logic ---
|
|
90
|
+
const input = document.getElementById('input');
|
|
91
|
+
const preview = document.getElementById('preview');
|
|
92
|
+
const backBtn = document.getElementById('back-btn');
|
|
93
|
+
|
|
94
|
+
function render() {
|
|
95
|
+
try {
|
|
96
|
+
let html = docmd.compile(input.value, {
|
|
97
|
+
siteTitle: 'My Project',
|
|
98
|
+
search: false,
|
|
99
|
+
theme: { name: 'sky', defaultMode: 'light' },
|
|
100
|
+
sidebar: { collapsible: false }
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Injections
|
|
104
|
+
html = html.replace('<head>', '<head><base target="_blank">');
|
|
105
|
+
const customStyle = `
|
|
106
|
+
<style>
|
|
107
|
+
.sidebar-header { display: none !important; }
|
|
108
|
+
.sidebar-nav { margin-top: 1rem; }
|
|
109
|
+
</style>
|
|
110
|
+
`;
|
|
111
|
+
html = html.replace('</body>', `${customStyle}</body>`);
|
|
112
|
+
|
|
113
|
+
const doc = preview.contentWindow.document;
|
|
114
|
+
doc.open();
|
|
115
|
+
doc.write(html);
|
|
116
|
+
doc.close();
|
|
117
|
+
} catch (e) { console.error(e); }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let timer;
|
|
121
|
+
input.addEventListener('input', () => {
|
|
122
|
+
clearTimeout(timer);
|
|
123
|
+
timer = setTimeout(render, 300);
|
|
124
|
+
});
|
|
125
|
+
render();
|
|
126
|
+
|
|
127
|
+
// --- Resizer Logic ---
|
|
128
|
+
const resizer = document.getElementById('resizer');
|
|
129
|
+
const editorPane = document.getElementById('editorPane');
|
|
130
|
+
const workspace = document.getElementById('workspace');
|
|
131
|
+
let isResizing = false;
|
|
132
|
+
|
|
133
|
+
resizer.addEventListener('mousedown', (e) => {
|
|
134
|
+
isResizing = true;
|
|
135
|
+
resizer.classList.add('resizing');
|
|
136
|
+
preview.style.pointerEvents = 'none';
|
|
137
|
+
document.body.style.cursor = 'col-resize';
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
document.addEventListener('mousemove', (e) => {
|
|
141
|
+
if (!isResizing) return;
|
|
142
|
+
const containerWidth = workspace.offsetWidth;
|
|
143
|
+
const newEditorWidth = (e.clientX / containerWidth) * 100;
|
|
144
|
+
if (newEditorWidth > 15 && newEditorWidth < 85) {
|
|
145
|
+
editorPane.style.width = newEditorWidth + '%';
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
document.addEventListener('mouseup', () => {
|
|
150
|
+
if (isResizing) {
|
|
151
|
+
isResizing = false;
|
|
152
|
+
resizer.classList.remove('resizing');
|
|
153
|
+
preview.style.pointerEvents = 'auto';
|
|
154
|
+
document.body.style.cursor = 'default';
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// --- Unified View Logic ---
|
|
159
|
+
function setMode(mode) {
|
|
160
|
+
// 1. Update UI Buttons (Desktop)
|
|
161
|
+
document.querySelectorAll('.view-switcher .view-btn').forEach(btn => {
|
|
162
|
+
btn.classList.toggle('active', btn.dataset.mode === mode);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// 2. Update Mobile Buttons
|
|
166
|
+
const mobMode = mode === 'split' ? 'editor' : mode; // Map split to editor on mobile
|
|
167
|
+
document.getElementById('mob-edit').classList.toggle('active', mobMode === 'editor');
|
|
168
|
+
document.getElementById('mob-prev').classList.toggle('active', mobMode === 'preview');
|
|
169
|
+
|
|
170
|
+
// 3. Update Layout Classes
|
|
171
|
+
// Reset
|
|
172
|
+
document.body.classList.remove('mode-split', 'mode-single', 'show-editor', 'show-preview');
|
|
173
|
+
|
|
174
|
+
if (mode === 'split') {
|
|
175
|
+
document.body.classList.add('mode-split');
|
|
176
|
+
} else {
|
|
177
|
+
document.body.classList.add('mode-single');
|
|
178
|
+
document.body.classList.add('show-' + mode);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// --- Back Button ---
|
|
183
|
+
/*
|
|
184
|
+
if (window.history.length <= 1) backBtn.style.display = 'none';
|
|
185
|
+
else backBtn.addEventListener('click', (e) => { e.preventDefault(); window.history.back(); });
|
|
186
|
+
*/
|
|
187
|
+
</script>
|
|
188
|
+
</body>
|
|
189
|
+
</html>
|
package/index.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { spawn } = require('child_process');
|
|
3
|
+
const { build } = require('./src/build');
|
|
4
|
+
|
|
5
|
+
async function start() {
|
|
6
|
+
// 1. Build the editor
|
|
7
|
+
await build();
|
|
8
|
+
|
|
9
|
+
const distDir = path.resolve(process.cwd(), 'dist');
|
|
10
|
+
console.log(`\n🌍 Launching Live Editor at ${distDir}...`);
|
|
11
|
+
console.log(' (Press Ctrl+C to stop)');
|
|
12
|
+
|
|
13
|
+
// 2. Resolve the 'serve' executable path safely
|
|
14
|
+
// We do this to avoid using 'npx' (which triggers npm warnings in pnpm repos)
|
|
15
|
+
// and to avoid shell injection issues.
|
|
16
|
+
let serveBinPath;
|
|
17
|
+
try {
|
|
18
|
+
const servePkgJsonPath = require.resolve('serve/package.json');
|
|
19
|
+
const serveDir = path.dirname(servePkgJsonPath);
|
|
20
|
+
const servePkg = require(servePkgJsonPath);
|
|
21
|
+
// 'bin' can be a string or an object in package.json
|
|
22
|
+
const binPath = typeof servePkg.bin === 'string' ? servePkg.bin : servePkg.bin.serve;
|
|
23
|
+
serveBinPath = path.join(serveDir, binPath);
|
|
24
|
+
} catch (e) {
|
|
25
|
+
console.error('❌ Could not locate "serve" package. Ensure it is installed.');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 3. Spawn Node directly
|
|
30
|
+
// shell: false is the default (fixing DEP0190)
|
|
31
|
+
// We pass the script path directly to node
|
|
32
|
+
const p = spawn(process.execPath, [serveBinPath, distDir], {
|
|
33
|
+
stdio: 'inherit'
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
process.on('SIGINT', () => {
|
|
37
|
+
p.kill();
|
|
38
|
+
process.exit();
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { start, build };
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@docmd/live",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Browser-based editor engine for docmd",
|
|
5
|
+
"bin": {
|
|
6
|
+
"docmd-live": "./bin/docmd-live.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin",
|
|
10
|
+
"dist",
|
|
11
|
+
"index.js",
|
|
12
|
+
"src/build.js"
|
|
13
|
+
],
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"esbuild": "^0.20.0",
|
|
16
|
+
"buffer": "^6.0.3",
|
|
17
|
+
"serve": "^14.2.1",
|
|
18
|
+
"@docmd/ui": "0.4.0",
|
|
19
|
+
"@docmd/themes": "0.4.0",
|
|
20
|
+
"@docmd/parser": "0.4.0"
|
|
21
|
+
},
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "node -e \"require('./src/build.js').build()\""
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/build.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs/promises');
|
|
3
|
+
const esbuild = require('esbuild');
|
|
4
|
+
const ui = require('@docmd/ui');
|
|
5
|
+
const themes = require('@docmd/themes'); // New import
|
|
6
|
+
|
|
7
|
+
// Path Constants
|
|
8
|
+
const PKG_ROOT = path.resolve(__dirname, '..');
|
|
9
|
+
const SRC_DIR = path.join(PKG_ROOT, 'src');
|
|
10
|
+
const DIST_DIR = path.resolve(process.cwd(), 'dist');
|
|
11
|
+
|
|
12
|
+
async function build() {
|
|
13
|
+
console.log('📦 Building Live Editor...');
|
|
14
|
+
|
|
15
|
+
// 1. Prepare Dist
|
|
16
|
+
await fs.rm(DIST_DIR, { recursive: true, force: true });
|
|
17
|
+
await fs.mkdir(DIST_DIR, { recursive: true });
|
|
18
|
+
|
|
19
|
+
// 2. Generate Shims
|
|
20
|
+
const shimPath = path.join(SRC_DIR, 'shims.js');
|
|
21
|
+
await fs.writeFile(shimPath, `import { Buffer } from 'buffer'; globalThis.Buffer = Buffer;`);
|
|
22
|
+
|
|
23
|
+
// 3. Template Plugin (Same as before, keep your existing logic here)
|
|
24
|
+
const templatePlugin = {
|
|
25
|
+
name: 'docmd-templates',
|
|
26
|
+
setup(build) {
|
|
27
|
+
build.onResolve({ filter: /^virtual:docmd-templates$/ }, args => ({
|
|
28
|
+
path: args.path, namespace: 'docmd-templates-ns',
|
|
29
|
+
}));
|
|
30
|
+
build.onLoad({ filter: /.*/, namespace: 'docmd-templates-ns' }, async () => {
|
|
31
|
+
const templatesDir = ui.getTemplatesDir();
|
|
32
|
+
const templates = {};
|
|
33
|
+
|
|
34
|
+
const tryRead = async (f) => {
|
|
35
|
+
const p = path.join(templatesDir, f);
|
|
36
|
+
try { return await fs.readFile(p, 'utf8'); } catch(e) { return null; }
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const files = await fs.readdir(templatesDir);
|
|
40
|
+
for (const file of files) {
|
|
41
|
+
if (file.endsWith('.ejs')) templates[file] = await tryRead(file);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const themeInit = await tryRead('partials/theme-init.js');
|
|
45
|
+
if (themeInit) templates['partials/theme-init.js'] = themeInit;
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
contents: `module.exports = ${JSON.stringify(templates)};`,
|
|
49
|
+
loader: 'js',
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// 4. Node Shim Plugin (Same as before)
|
|
56
|
+
const nodeShimPlugin = {
|
|
57
|
+
name: 'node-deps-shim',
|
|
58
|
+
setup(build) {
|
|
59
|
+
build.onResolve({ filter: /^(node:)?path$/ }, args => ({ path: args.path, namespace: 'path-shim' }));
|
|
60
|
+
build.onLoad({ filter: /.*/, namespace: 'path-shim' }, () => ({
|
|
61
|
+
contents: `module.exports = {
|
|
62
|
+
join: (...a) => a.filter(Boolean).join('/'),
|
|
63
|
+
resolve: (...a) => '/' + a.filter(Boolean).join('/'),
|
|
64
|
+
basename: (p) => p ? p.split(/[\\\\/]/).pop() : '',
|
|
65
|
+
dirname: (p) => p ? p.split(/[\\\\/]/).slice(0, -1).join('/') || '.' : '.',
|
|
66
|
+
extname: (p) => p ? '.' + p.split('.').pop() : '',
|
|
67
|
+
sep: '/'
|
|
68
|
+
};`, loader: 'js'
|
|
69
|
+
}));
|
|
70
|
+
build.onResolve({ filter: /^(node:)?fs(\/promises)?|fs-extra$/ }, args => ({ path: args.path, namespace: 'fs-shim' }));
|
|
71
|
+
build.onLoad({ filter: /.*/, namespace: 'fs-shim' }, () => ({
|
|
72
|
+
contents: `module.exports = { promises: {}, existsSync: ()=>false };`, loader: 'js'
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
// 5. Bundle JS
|
|
79
|
+
await esbuild.build({
|
|
80
|
+
entryPoints: [path.join(SRC_DIR, 'browser-entry.js')],
|
|
81
|
+
bundle: true,
|
|
82
|
+
outfile: path.join(DIST_DIR, 'docmd-live.js'),
|
|
83
|
+
platform: 'browser',
|
|
84
|
+
format: 'iife',
|
|
85
|
+
globalName: 'docmd',
|
|
86
|
+
minify: true,
|
|
87
|
+
define: { 'process.env.NODE_ENV': '"production"' },
|
|
88
|
+
inject: [shimPath],
|
|
89
|
+
plugins: [templatePlugin, nodeShimPlugin]
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// 6. Copy Static Assets
|
|
93
|
+
await fs.copyFile(path.join(SRC_DIR, 'index.html'), path.join(DIST_DIR, 'index.html'));
|
|
94
|
+
await fs.copyFile(path.join(SRC_DIR, 'docmd-live.css'), path.join(DIST_DIR, 'docmd-live.css'));
|
|
95
|
+
|
|
96
|
+
const cssDest = path.join(DIST_DIR, 'assets/css');
|
|
97
|
+
const jsDest = path.join(DIST_DIR, 'assets/js');
|
|
98
|
+
await fs.mkdir(cssDest, { recursive: true });
|
|
99
|
+
await fs.mkdir(jsDest, { recursive: true });
|
|
100
|
+
await fs.copyFile(path.join(SRC_DIR, 'docmd-live-preview.css'), path.join(cssDest, 'docmd-live-preview.css'));
|
|
101
|
+
|
|
102
|
+
// Helper copy function
|
|
103
|
+
const copy = async (src, destName) => {
|
|
104
|
+
try {
|
|
105
|
+
await fs.copyFile(src, path.join(path.extname(destName) === '.js' ? jsDest : cssDest, destName));
|
|
106
|
+
} catch(e) { console.warn(`⚠️ Missing asset: ${path.basename(src)}`); }
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// UI Assets (Source: main.css -> Dest: docmd-main.css)
|
|
110
|
+
await copy(path.join(ui.getAssetsDir(), 'css/docmd-main.css'), 'docmd-main.css');
|
|
111
|
+
await copy(path.join(ui.getAssetsDir(), 'css/docmd-highlight-light.css'), 'docmd-highlight-light.css');
|
|
112
|
+
await copy(path.join(ui.getAssetsDir(), 'css/docmd-highlight-dark.css'), 'docmd-highlight-dark.css');
|
|
113
|
+
await copy(path.join(ui.getAssetsDir(), 'js/docmd-main.js'), 'docmd-main.js');
|
|
114
|
+
|
|
115
|
+
// Theme Assets (Source: sky.css -> Dest: docmd-theme-sky.css)
|
|
116
|
+
const themesDir = themes.getThemesDir();
|
|
117
|
+
const themeFiles = await fs.readdir(themesDir);
|
|
118
|
+
for (const t of themeFiles) {
|
|
119
|
+
if (t.endsWith('.css')) {
|
|
120
|
+
// Remove prefix if source has it, then add it back standardly
|
|
121
|
+
const cleanName = t.replace('docmd-theme-', '');
|
|
122
|
+
await copy(path.join(themesDir, t), `docmd-theme-${cleanName}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Copy User Assets (if in playground context)
|
|
127
|
+
const userAssets = path.resolve(process.cwd(), 'assets');
|
|
128
|
+
const distAssets = path.join(DIST_DIR, 'assets');
|
|
129
|
+
// Simple check to avoid copying into itself if CWD is somehow dist parent
|
|
130
|
+
try {
|
|
131
|
+
await fs.cp(userAssets, distAssets, { recursive: true, force: false });
|
|
132
|
+
} catch(e) {} // Ignore if user assets don't exist
|
|
133
|
+
|
|
134
|
+
console.log('✅ Live Editor built in ./dist');
|
|
135
|
+
} catch (e) {
|
|
136
|
+
console.error('❌ Live build failed:', e);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = { build };
|