@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.
@@ -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 };