@mgks/docmd 0.3.3 → 0.3.5
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/README.md +92 -100
- package/bin/docmd.js +55 -93
- package/docmd.config.js +175 -0
- package/package.json +16 -15
- package/scripts/build-live.js +157 -0
- package/scripts/test-live.js +54 -0
- package/src/assets/css/docmd-highlight-dark.css +1 -0
- package/src/assets/css/docmd-highlight-light.css +1 -0
- package/src/assets/css/docmd-main.css +1627 -0
- package/src/assets/css/docmd-theme-retro.css +868 -0
- package/src/assets/css/docmd-theme-ruby.css +629 -0
- package/src/assets/css/docmd-theme-sky.css +618 -0
- package/src/assets/favicon.ico +0 -0
- package/src/assets/images/docmd-logo-dark.png +0 -0
- package/src/assets/images/docmd-logo-light.png +0 -0
- package/src/assets/images/docmd-logo.png +0 -0
- package/src/assets/js/docmd-image-lightbox.js +74 -0
- package/src/assets/js/docmd-main.js +214 -0
- package/src/assets/js/docmd-mermaid.js +205 -0
- package/src/assets/js/docmd-search.js +218 -0
- package/src/commands/build.js +11 -6
- package/src/commands/dev.js +100 -189
- package/src/commands/init.js +8 -8
- package/src/core/config-loader.js +23 -3
- package/src/core/file-processor.js +8 -2
- package/src/core/html-generator.js +107 -102
- package/src/live/core.js +63 -0
- package/src/live/index.html +201 -0
- package/src/live/live.css +167 -0
- package/src/live/shims.js +1 -0
- package/src/live/templates.js +9 -0
- package/src/templates/layout.ejs +11 -11
- package/src/templates/navigation.ejs +69 -7
- package/config.js +0 -175
|
@@ -8,18 +8,63 @@ const { generateSeoMetaTags } = require('../plugins/seo');
|
|
|
8
8
|
const { generateAnalyticsScripts } = require('../plugins/analytics');
|
|
9
9
|
const { renderIcon } = require('./icon-renderer');
|
|
10
10
|
|
|
11
|
-
// Create a markdown instance for inline rendering
|
|
12
11
|
let mdInstance = null;
|
|
13
|
-
|
|
14
12
|
let themeInitScript = '';
|
|
13
|
+
|
|
15
14
|
(async () => {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
if (typeof __dirname !== 'undefined') {
|
|
16
|
+
const themeInitPath = path.join(__dirname, '..', 'templates', 'partials', 'theme-init.js');
|
|
17
|
+
if (await fs.pathExists(themeInitPath)) {
|
|
18
|
+
const scriptContent = await fs.readFile(themeInitPath, 'utf8');
|
|
19
|
+
themeInitScript = `<script>${scriptContent}</script>`;
|
|
20
|
+
}
|
|
20
21
|
}
|
|
21
22
|
})();
|
|
22
23
|
|
|
24
|
+
// Helper to handle link rewriting based on build mode
|
|
25
|
+
function fixHtmlLinks(htmlContent, relativePathToRoot, isOfflineMode) {
|
|
26
|
+
if (!htmlContent) return '';
|
|
27
|
+
const root = relativePathToRoot || './';
|
|
28
|
+
|
|
29
|
+
// Regex matches hrefs starting with /, ./, or ../
|
|
30
|
+
return htmlContent.replace(/href="((?:\/|\.\/|\.\.\/)[^"]*)"/g, (match, href) => {
|
|
31
|
+
let finalPath = href;
|
|
32
|
+
|
|
33
|
+
// 1. Convert Absolute to Relative
|
|
34
|
+
if (href.startsWith('/')) {
|
|
35
|
+
finalPath = root + href.substring(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 2. Logic based on Mode
|
|
39
|
+
if (isOfflineMode) {
|
|
40
|
+
// Offline Mode: Force index.html for directories
|
|
41
|
+
const cleanPath = finalPath.split('#')[0].split('?')[0];
|
|
42
|
+
// If it has no extension (like .html, .css, .png), treat as directory
|
|
43
|
+
if (!path.extname(cleanPath)) {
|
|
44
|
+
if (finalPath.includes('#')) {
|
|
45
|
+
// Handle anchors: ./foo/#bar -> ./foo/index.html#bar
|
|
46
|
+
const parts = finalPath.split('#');
|
|
47
|
+
const prefix = parts[0].endsWith('/') ? parts[0] : parts[0] + '/';
|
|
48
|
+
finalPath = prefix + 'index.html#' + parts[1];
|
|
49
|
+
} else {
|
|
50
|
+
if (finalPath.endsWith('/')) {
|
|
51
|
+
finalPath += 'index.html';
|
|
52
|
+
} else {
|
|
53
|
+
finalPath += '/index.html';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
// Online/Dev Mode: Strip index.html for clean URLs
|
|
59
|
+
if (finalPath.endsWith('/index.html')) {
|
|
60
|
+
finalPath = finalPath.substring(0, finalPath.length - 10);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return `href="${finalPath}"`;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
23
68
|
async function processPluginHooks(config, pageData, relativePathToRoot) {
|
|
24
69
|
let metaTagsHtml = '';
|
|
25
70
|
let faviconLinkHtml = '';
|
|
@@ -28,63 +73,52 @@ async function processPluginHooks(config, pageData, relativePathToRoot) {
|
|
|
28
73
|
let pluginHeadScriptsHtml = '';
|
|
29
74
|
let pluginBodyScriptsHtml = '';
|
|
30
75
|
|
|
31
|
-
|
|
76
|
+
const safeRoot = relativePathToRoot || './';
|
|
77
|
+
|
|
78
|
+
// Favicon
|
|
32
79
|
if (config.favicon) {
|
|
33
|
-
const
|
|
34
|
-
|
|
80
|
+
const cleanFaviconPath = config.favicon.startsWith('/') ? config.favicon.substring(1) : config.favicon;
|
|
81
|
+
const finalFaviconHref = `${safeRoot}${cleanFaviconPath}`;
|
|
82
|
+
|
|
83
|
+
faviconLinkHtml = ` <link rel="icon" href="${finalFaviconHref}" type="image/x-icon" sizes="any">\n`;
|
|
84
|
+
faviconLinkHtml += ` <link rel="shortcut icon" href="${finalFaviconHref}" type="image/x-icon">\n`;
|
|
35
85
|
}
|
|
36
86
|
|
|
37
|
-
// Theme CSS (built-in handling for theme.name)
|
|
38
87
|
if (config.theme && config.theme.name && config.theme.name !== 'default') {
|
|
39
88
|
const themeCssPath = `assets/css/docmd-theme-${config.theme.name}.css`;
|
|
40
|
-
themeCssLinkHtml = ` <link rel="stylesheet" href="${
|
|
89
|
+
themeCssLinkHtml = ` <link rel="stylesheet" href="${safeRoot}${themeCssPath}">\n`;
|
|
41
90
|
}
|
|
42
91
|
|
|
43
|
-
// SEO Plugin (if configured)
|
|
44
92
|
if (config.plugins?.seo) {
|
|
45
|
-
metaTagsHtml += generateSeoMetaTags(config, pageData,
|
|
93
|
+
metaTagsHtml += generateSeoMetaTags(config, pageData, safeRoot);
|
|
46
94
|
}
|
|
47
95
|
|
|
48
|
-
// Analytics Plugin (if configured)
|
|
49
96
|
if (config.plugins?.analytics) {
|
|
50
97
|
const analyticsScripts = generateAnalyticsScripts(config, pageData);
|
|
51
98
|
pluginHeadScriptsHtml += analyticsScripts.headScriptsHtml;
|
|
52
99
|
pluginBodyScriptsHtml += analyticsScripts.bodyScriptsHtml;
|
|
53
100
|
}
|
|
54
101
|
|
|
55
|
-
return {
|
|
56
|
-
metaTagsHtml,
|
|
57
|
-
faviconLinkHtml,
|
|
58
|
-
themeCssLinkHtml,
|
|
59
|
-
pluginStylesHtml,
|
|
60
|
-
pluginHeadScriptsHtml,
|
|
61
|
-
pluginBodyScriptsHtml,
|
|
62
|
-
};
|
|
102
|
+
return { metaTagsHtml, faviconLinkHtml, themeCssLinkHtml, pluginStylesHtml, pluginHeadScriptsHtml, pluginBodyScriptsHtml };
|
|
63
103
|
}
|
|
64
104
|
|
|
65
|
-
async function generateHtmlPage(templateData) {
|
|
66
|
-
|
|
67
|
-
content, siteTitle, navigationHtml,
|
|
68
|
-
relativePathToRoot, config, frontmatter, outputPath,
|
|
69
|
-
prevPage, nextPage, currentPagePath, headings
|
|
70
|
-
} = templateData;
|
|
71
|
-
|
|
105
|
+
async function generateHtmlPage(templateData, isOfflineMode = false) {
|
|
106
|
+
let { content, siteTitle, navigationHtml, relativePathToRoot, config, frontmatter, outputPath, prevPage, nextPage, currentPagePath, headings } = templateData;
|
|
72
107
|
const pageTitle = frontmatter.title;
|
|
73
108
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
);
|
|
109
|
+
if (!relativePathToRoot) relativePathToRoot = './';
|
|
110
|
+
|
|
111
|
+
// Fix Content Links based on mode
|
|
112
|
+
content = fixHtmlLinks(content, relativePathToRoot, isOfflineMode);
|
|
113
|
+
|
|
114
|
+
const pluginOutputs = await processPluginHooks(config, { frontmatter, outputPath }, relativePathToRoot);
|
|
80
115
|
|
|
81
116
|
let footerHtml = '';
|
|
82
117
|
if (config.footer) {
|
|
83
|
-
|
|
84
|
-
if (!mdInstance) {
|
|
85
|
-
mdInstance = createMarkdownItInstance(config);
|
|
86
|
-
}
|
|
118
|
+
if (!mdInstance) mdInstance = createMarkdownItInstance(config);
|
|
87
119
|
footerHtml = mdInstance.renderInline(config.footer);
|
|
120
|
+
// Fix Footer Links based on mode
|
|
121
|
+
footerHtml = fixHtmlLinks(footerHtml, relativePathToRoot, isOfflineMode);
|
|
88
122
|
}
|
|
89
123
|
|
|
90
124
|
let templateName = 'layout.ejs';
|
|
@@ -100,89 +134,60 @@ async function generateHtmlPage(templateData) {
|
|
|
100
134
|
|
|
101
135
|
const isActivePage = currentPagePath && content && content.trim().length > 0;
|
|
102
136
|
|
|
103
|
-
// Calculate Edit Link
|
|
104
137
|
let editUrl = null;
|
|
105
138
|
let editLinkText = 'Edit this page';
|
|
106
|
-
|
|
107
139
|
if (config.editLink && config.editLink.enabled && config.editLink.baseUrl) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
let relativeSourcePath = outputPath
|
|
113
|
-
.replace(/\/index\.html$/, '.md') // folder/index.html -> folder.md
|
|
114
|
-
.replace(/\\/g, '/'); // fix windows slashes
|
|
115
|
-
|
|
116
|
-
// Special case: The root index.html comes from index.md
|
|
117
|
-
if (relativeSourcePath === 'index.html') relativeSourcePath = 'index.md';
|
|
118
|
-
|
|
119
|
-
// Let's assume a standard 1:1 mapping for v0.2.x
|
|
120
|
-
editUrl = `${baseUrl}/${relativeSourcePath}`;
|
|
121
|
-
editLinkText = config.editLink.text || editLinkText;
|
|
140
|
+
editUrl = `${config.editLink.baseUrl.replace(/\/$/, '')}/${outputPath.replace(/\/index\.html$/, '.md').replace(/\\/g, '/')}`;
|
|
141
|
+
if (outputPath.endsWith('index.html') && outputPath !== 'index.html') editUrl = editUrl.replace('.md', '/index.md');
|
|
142
|
+
if (outputPath === 'index.html') editUrl = `${config.editLink.baseUrl.replace(/\/$/, '')}/index.md`;
|
|
143
|
+
editLinkText = config.editLink.text || editLinkText;
|
|
122
144
|
}
|
|
123
145
|
|
|
124
146
|
const ejsData = {
|
|
125
|
-
content,
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
defaultMode: config.theme?.defaultMode || 'light',
|
|
134
|
-
relativePathToRoot,
|
|
135
|
-
logo: config.logo,
|
|
136
|
-
sidebarConfig: {
|
|
137
|
-
collapsible: config.sidebar?.collapsible ?? false,
|
|
138
|
-
defaultCollapsed: config.sidebar?.defaultCollapsed ?? false,
|
|
139
|
-
},
|
|
140
|
-
theme: config.theme,
|
|
141
|
-
customCssFiles: config.theme?.customCss || [],
|
|
142
|
-
customJsFiles: config.customJs || [],
|
|
143
|
-
sponsor: config.sponsor,
|
|
144
|
-
footer: config.footer,
|
|
145
|
-
footerHtml,
|
|
146
|
-
renderIcon,
|
|
147
|
-
prevPage,
|
|
148
|
-
nextPage,
|
|
149
|
-
currentPagePath,
|
|
150
|
-
headings: frontmatter.toc !== false ? (headings || []) : [],
|
|
151
|
-
isActivePage,
|
|
152
|
-
frontmatter,
|
|
153
|
-
config: config,
|
|
154
|
-
...pluginOutputs,
|
|
147
|
+
content, pageTitle, themeInitScript, description: frontmatter.description, siteTitle, navigationHtml,
|
|
148
|
+
editUrl, editLinkText, defaultMode: config.theme?.defaultMode || 'light', relativePathToRoot,
|
|
149
|
+
logo: config.logo, sidebarConfig: config.sidebar || {}, theme: config.theme,
|
|
150
|
+
customCssFiles: config.theme?.customCss || [], customJsFiles: config.customJs || [],
|
|
151
|
+
sponsor: config.sponsor, footer: config.footer, footerHtml, renderIcon,
|
|
152
|
+
prevPage, nextPage, currentPagePath, headings: frontmatter.toc !== false ? (headings || []) : [],
|
|
153
|
+
isActivePage, frontmatter, config, ...pluginOutputs,
|
|
154
|
+
isOfflineMode
|
|
155
155
|
};
|
|
156
156
|
|
|
157
|
+
return renderHtmlPage(layoutTemplate, ejsData, layoutTemplatePath);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function renderHtmlPage(templateContent, ejsData, filename = 'template.ejs', options = {}) {
|
|
157
161
|
try {
|
|
158
|
-
return ejs.render(
|
|
159
|
-
filename:
|
|
162
|
+
return ejs.render(templateContent, ejsData, {
|
|
163
|
+
filename: filename,
|
|
164
|
+
...options
|
|
160
165
|
});
|
|
161
166
|
} catch (e) {
|
|
162
|
-
console.error(`❌ Error rendering EJS template
|
|
163
|
-
console.error("EJS Data:", JSON.stringify(ejsData, null, 2).substring(0, 1000) + "...");
|
|
167
|
+
console.error(`❌ Error rendering EJS template: ${e.message}`);
|
|
164
168
|
throw e;
|
|
165
169
|
}
|
|
166
170
|
}
|
|
167
171
|
|
|
168
|
-
|
|
172
|
+
// FIX: Added isOfflineMode parameter
|
|
173
|
+
async function generateNavigationHtml(navItems, currentPagePath, relativePathToRoot, config, isOfflineMode = false) {
|
|
169
174
|
const navTemplatePath = path.join(__dirname, '..', 'templates', 'navigation.ejs');
|
|
170
175
|
if (!await fs.pathExists(navTemplatePath)) {
|
|
171
176
|
throw new Error(`Navigation template not found: ${navTemplatePath}`);
|
|
172
177
|
}
|
|
173
178
|
const navTemplate = await fs.readFile(navTemplatePath, 'utf8');
|
|
174
|
-
|
|
175
179
|
const ejsHelpers = { renderIcon };
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
180
|
+
|
|
181
|
+
const safeRoot = relativePathToRoot || './';
|
|
182
|
+
|
|
183
|
+
return ejs.render(navTemplate, {
|
|
184
|
+
navItems,
|
|
185
|
+
currentPagePath,
|
|
186
|
+
relativePathToRoot: safeRoot,
|
|
187
|
+
config,
|
|
188
|
+
isOfflineMode, // <--- Passing the variable here
|
|
189
|
+
...ejsHelpers
|
|
190
|
+
}, { filename: navTemplatePath });
|
|
186
191
|
}
|
|
187
192
|
|
|
188
|
-
module.exports = { generateHtmlPage, generateNavigationHtml };
|
|
193
|
+
module.exports = { generateHtmlPage, generateNavigationHtml, renderHtmlPage };
|
package/src/live/core.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const { processMarkdownContent, createMarkdownItInstance } = require('../core/file-processor');
|
|
2
|
+
const { renderHtmlPage } = require('../core/html-generator');
|
|
3
|
+
const templates = require('./templates');
|
|
4
|
+
|
|
5
|
+
function compile(markdown, config = {}, options = {}) {
|
|
6
|
+
// Default config values for the browser
|
|
7
|
+
const defaults = {
|
|
8
|
+
siteTitle: 'Live Preview',
|
|
9
|
+
theme: { defaultMode: 'light', name: 'default' },
|
|
10
|
+
...config
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const md = createMarkdownItInstance(defaults);
|
|
14
|
+
const result = processMarkdownContent(markdown, md, defaults, 'memory');
|
|
15
|
+
|
|
16
|
+
if (!result) return '<p>Error parsing markdown</p>';
|
|
17
|
+
|
|
18
|
+
const { frontmatter, htmlContent, headings } = result;
|
|
19
|
+
|
|
20
|
+
const pageData = {
|
|
21
|
+
content: htmlContent,
|
|
22
|
+
frontmatter,
|
|
23
|
+
headings,
|
|
24
|
+
siteTitle: defaults.siteTitle,
|
|
25
|
+
pageTitle: frontmatter.title || 'Untitled',
|
|
26
|
+
description: frontmatter.description || '',
|
|
27
|
+
defaultMode: defaults.theme.defaultMode,
|
|
28
|
+
editUrl: null,
|
|
29
|
+
editLinkText: '',
|
|
30
|
+
navigationHtml: '', // Navigation is usually empty in a single-page preview
|
|
31
|
+
relativePathToRoot: options.relativePathToRoot || './', // Important for finding CSS in dist/assets
|
|
32
|
+
outputPath: 'index.html',
|
|
33
|
+
currentPagePath: '/index',
|
|
34
|
+
prevPage: null, nextPage: null,
|
|
35
|
+
config: defaults,
|
|
36
|
+
// Empty hooks
|
|
37
|
+
metaTagsHtml: '', faviconLinkHtml: '', themeCssLinkHtml: '',
|
|
38
|
+
pluginStylesHtml: '', pluginHeadScriptsHtml: '', pluginBodyScriptsHtml: '',
|
|
39
|
+
themeInitScript: '',
|
|
40
|
+
logo: defaults.logo, sidebarConfig: { collapsible: false }, theme: defaults.theme,
|
|
41
|
+
customCssFiles: [], customJsFiles: [],
|
|
42
|
+
sponsor: { enabled: false }, footer: '', footerHtml: '',
|
|
43
|
+
renderIcon: () => '', // Icons disabled in live preview to save weight
|
|
44
|
+
isActivePage: true
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
let templateName = frontmatter.noStyle === true ? 'no-style.ejs' : 'layout.ejs';
|
|
48
|
+
const templateContent = templates[templateName];
|
|
49
|
+
|
|
50
|
+
if (!templateContent) return `Template ${templateName} not found`;
|
|
51
|
+
|
|
52
|
+
const ejsOptions = {
|
|
53
|
+
includer: (originalPath) => {
|
|
54
|
+
let name = originalPath.endsWith('.ejs') ? originalPath : originalPath + '.ejs';
|
|
55
|
+
if (templates[name]) return { template: templates[name] };
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return renderHtmlPage(templateContent, pageData, templateName, ejsOptions);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { compile };
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Docmd Live</title>
|
|
7
|
+
<link rel="stylesheet" href="live.css">
|
|
8
|
+
<script src="docmd-live.js"></script>
|
|
9
|
+
</head>
|
|
10
|
+
<body class="mode-split mobile-tab-editor">
|
|
11
|
+
|
|
12
|
+
<!-- Top Bar (Desktop) -->
|
|
13
|
+
<div class="top-bar">
|
|
14
|
+
<div class="logo">
|
|
15
|
+
<a href="/" class="back-link" id="back-btn" title="Back to Home" aria-label="Back to Home">
|
|
16
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
17
|
+
<path d="m12 19-7-7 7-7"/>
|
|
18
|
+
<path d="M19 12H5"/>
|
|
19
|
+
</svg>
|
|
20
|
+
</a>
|
|
21
|
+
docmd <span>Live</span>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div class="view-controls desktop-only">
|
|
25
|
+
<button class="view-btn active" onclick="setViewMode('split')" id="btn-split">Split View</button>
|
|
26
|
+
<button class="view-btn" onclick="setViewMode('single')" id="btn-single">Single View</button>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<!-- Single View Tabs (Visible only when in Single Mode on Desktop) -->
|
|
30
|
+
<div class="view-controls" id="single-view-tabs" style="display:none;">
|
|
31
|
+
<button class="view-btn active" onclick="switchSingleTab('editor')" id="btn-tab-edit">Editor</button>
|
|
32
|
+
<button class="view-btn" onclick="switchSingleTab('preview')" id="btn-tab-prev">Preview</button>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<!-- Workspace -->
|
|
37
|
+
<div class="workspace" id="workspace">
|
|
38
|
+
<!-- Editor -->
|
|
39
|
+
<div class="pane editor-pane" id="editorPane">
|
|
40
|
+
<div class="pane-header">Markdown</div>
|
|
41
|
+
<textarea id="input" spellcheck="false">---
|
|
42
|
+
title: My Page
|
|
43
|
+
description: Start editing to see changes instantly.
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
# Hello World
|
|
47
|
+
|
|
48
|
+
This is a **live** preview.
|
|
49
|
+
|
|
50
|
+
::: callout tip
|
|
51
|
+
Try resizing the window or switching to mobile view!
|
|
52
|
+
:::
|
|
53
|
+
|
|
54
|
+
## Features
|
|
55
|
+
1. Responsive Design
|
|
56
|
+
2. Split or Tabbed view
|
|
57
|
+
3. Instant Rendering
|
|
58
|
+
</textarea>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<!-- Resizer Handle -->
|
|
62
|
+
<div class="resizer" id="resizer"></div>
|
|
63
|
+
|
|
64
|
+
<!-- Preview -->
|
|
65
|
+
<div class="pane preview-pane" id="previewPane">
|
|
66
|
+
<div class="pane-header">Preview</div>
|
|
67
|
+
<iframe id="preview"></iframe>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<!-- Mobile Bottom Tabs -->
|
|
72
|
+
<div class="mobile-tabs">
|
|
73
|
+
<button class="mobile-tab-btn active" onclick="setMobileTab('editor')" id="mob-edit">Editor</button>
|
|
74
|
+
<button class="mobile-tab-btn" onclick="setMobileTab('preview')" id="mob-prev">Preview</button>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<script>
|
|
78
|
+
// --- Core Logic ---
|
|
79
|
+
const input = document.getElementById('input');
|
|
80
|
+
const preview = document.getElementById('preview');
|
|
81
|
+
const backBtn = document.getElementById('back-btn');
|
|
82
|
+
|
|
83
|
+
function render() {
|
|
84
|
+
try {
|
|
85
|
+
let html = docmd.compile(input.value, {
|
|
86
|
+
siteTitle: 'My Project', // User can change this in real config, but here we set a default
|
|
87
|
+
search: false,
|
|
88
|
+
theme: { name: 'sky', defaultMode: 'light' },
|
|
89
|
+
sidebar: { collapsible: false }
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// --- INJECTIONS ---
|
|
93
|
+
|
|
94
|
+
// 1. Force links to open in new tab (Fixes infinite iframe recursion)
|
|
95
|
+
html = html.replace('<head>', '<head><base target="_blank">');
|
|
96
|
+
|
|
97
|
+
// 2. Hide Sidebar Header via CSS
|
|
98
|
+
const customStyle = `
|
|
99
|
+
<style>
|
|
100
|
+
.sidebar-header { display: none !important; }
|
|
101
|
+
.sidebar-nav { margin-top: 1rem; }
|
|
102
|
+
</style>
|
|
103
|
+
`;
|
|
104
|
+
html = html.replace('</body>', `${customStyle}</body>`);
|
|
105
|
+
// ------------------
|
|
106
|
+
|
|
107
|
+
const doc = preview.contentWindow.document;
|
|
108
|
+
doc.open();
|
|
109
|
+
doc.write(html);
|
|
110
|
+
doc.close();
|
|
111
|
+
} catch (e) { console.error(e); }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Debounce Render
|
|
115
|
+
let timer;
|
|
116
|
+
input.addEventListener('input', () => {
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
timer = setTimeout(render, 300);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Initial Render
|
|
122
|
+
render();
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
// --- Resizer Logic ---
|
|
126
|
+
const resizer = document.getElementById('resizer');
|
|
127
|
+
const editorPane = document.getElementById('editorPane');
|
|
128
|
+
const workspace = document.getElementById('workspace');
|
|
129
|
+
let isResizing = false;
|
|
130
|
+
|
|
131
|
+
resizer.addEventListener('mousedown', (e) => {
|
|
132
|
+
isResizing = true;
|
|
133
|
+
resizer.classList.add('resizing');
|
|
134
|
+
preview.style.pointerEvents = 'none';
|
|
135
|
+
document.body.style.cursor = 'col-resize';
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
document.addEventListener('mousemove', (e) => {
|
|
139
|
+
if (!isResizing) return;
|
|
140
|
+
const containerWidth = workspace.offsetWidth;
|
|
141
|
+
const newEditorWidth = (e.clientX / containerWidth) * 100;
|
|
142
|
+
if (newEditorWidth > 15 && newEditorWidth < 85) {
|
|
143
|
+
editorPane.style.width = newEditorWidth + '%';
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
document.addEventListener('mouseup', () => {
|
|
148
|
+
if (isResizing) {
|
|
149
|
+
isResizing = false;
|
|
150
|
+
resizer.classList.remove('resizing');
|
|
151
|
+
preview.style.pointerEvents = 'auto';
|
|
152
|
+
document.body.style.cursor = 'default';
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
// --- View Mode Logic (Desktop) ---
|
|
158
|
+
function setViewMode(mode) {
|
|
159
|
+
document.body.classList.remove('mode-split', 'mode-single');
|
|
160
|
+
document.body.classList.add('mode-' + mode);
|
|
161
|
+
|
|
162
|
+
document.getElementById('btn-split').classList.toggle('active', mode === 'split');
|
|
163
|
+
document.getElementById('btn-single').classList.toggle('active', mode === 'single');
|
|
164
|
+
|
|
165
|
+
document.getElementById('single-view-tabs').style.display = mode === 'single' ? 'flex' : 'none';
|
|
166
|
+
|
|
167
|
+
if (mode === 'single') {
|
|
168
|
+
switchSingleTab('editor');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function switchSingleTab(tab) {
|
|
173
|
+
document.body.classList.remove('show-editor', 'show-preview');
|
|
174
|
+
document.body.classList.add('show-' + tab);
|
|
175
|
+
|
|
176
|
+
document.getElementById('btn-tab-edit').classList.toggle('active', tab === 'editor');
|
|
177
|
+
document.getElementById('btn-tab-prev').classList.toggle('active', tab === 'preview');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
// --- Mobile Logic ---
|
|
182
|
+
function setMobileTab(tab) {
|
|
183
|
+
document.body.classList.remove('mobile-tab-editor', 'mobile-tab-preview');
|
|
184
|
+
document.body.classList.add('mobile-tab-' + tab);
|
|
185
|
+
|
|
186
|
+
document.getElementById('mob-edit').classList.toggle('active', tab === 'editor');
|
|
187
|
+
document.getElementById('mob-prev').classList.toggle('active', tab === 'preview');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// --- Back Button Logic ---
|
|
191
|
+
if (window.history.length <= 1) {
|
|
192
|
+
backBtn.style.display = 'none';
|
|
193
|
+
} else {
|
|
194
|
+
backBtn.addEventListener('click', (e) => {
|
|
195
|
+
e.preventDefault();
|
|
196
|
+
window.history.back();
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
</script>
|
|
200
|
+
</body>
|
|
201
|
+
</html>
|