@mgks/docmd 0.3.4 → 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.
@@ -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
- 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>`;
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
- // Favicon (built-in handling)
76
+ const safeRoot = relativePathToRoot || './';
77
+
78
+ // Favicon
32
79
  if (config.favicon) {
33
- const faviconPath = config.favicon.startsWith('/') ? config.favicon.substring(1) : config.favicon;
34
- faviconLinkHtml = `<link rel="shortcut icon" href="${relativePathToRoot}${faviconPath}" type="image/x-icon">\n`;
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="${relativePathToRoot}${themeCssPath}">\n`;
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, relativePathToRoot);
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
- const {
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
- // Process plugins to get their HTML contributions
75
- const pluginOutputs = await processPluginHooks(
76
- config,
77
- { frontmatter, outputPath },
78
- relativePathToRoot
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
- // Initialize mdInstance if not already done
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
- // Normalize URL (remove trailing slash)
109
- const baseUrl = config.editLink.baseUrl.replace(/\/$/, '');
110
-
111
- // Get the source file path relative to srcDir
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
- pageTitle,
127
- themeInitScript,
128
- description: frontmatter.description,
129
- siteTitle,
130
- navigationHtml,
131
- editUrl,
132
- editLinkText,
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(layoutTemplate, ejsData, {
159
- filename: layoutTemplatePath
162
+ return ejs.render(templateContent, ejsData, {
163
+ filename: filename,
164
+ ...options
160
165
  });
161
166
  } catch (e) {
162
- console.error(`❌ Error rendering EJS template for ${outputPath}: ${e.message}`);
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
- async function generateNavigationHtml(navItems, currentPagePath, relativePathToRoot, config) {
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
- return ejs.render(navTemplate, {
178
- navItems,
179
- currentPagePath,
180
- relativePathToRoot,
181
- config,
182
- ...ejsHelpers
183
- }, {
184
- filename: navTemplatePath
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 };
@@ -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>