@mgks/docmd 0.2.0 → 0.2.2

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.
@@ -1,9 +1,9 @@
1
- // src/core/file-processor.js
1
+ // Source file from the docmd project — https://github.com/mgks/docmd
2
+
2
3
  const fs = require('fs-extra');
3
4
  const MarkdownIt = require('markdown-it');
4
5
  const matter = require('gray-matter');
5
6
  const hljs = require('highlight.js');
6
- const container = require('markdown-it-container');
7
7
  const attrs = require('markdown-it-attrs');
8
8
  const path = require('path');
9
9
  const markdown_it_footnote = require('markdown-it-footnote');
@@ -23,59 +23,80 @@ function formatPathForDisplay(absolutePath) {
23
23
  return relativePath;
24
24
  }
25
25
 
26
- // Initialize MarkdownIt with plugins and options.
27
- const md = new MarkdownIt({
28
- html: true,
29
- linkify: true,
30
- typographer: true,
31
- breaks: true,
32
- highlight: function (str, lang) {
33
- if (lang && hljs.getLanguage(lang)) {
34
- try {
35
- return '<pre class="hljs"><code>' +
36
- hljs.highlight(str, { language: lang, ignoreIllegals: true }).value +
37
- '</code></pre>';
38
- } catch (e) { console.error(`Error highlighting language ${lang}:`, e); }
39
- }
40
- // For non-language code blocks, preserve the original content without processing
41
- return '<pre class="hljs"><code>' + str + '</code></pre>';
26
+ function createMarkdownItInstance(config) {
27
+ const mdOptions = {
28
+ html: true,
29
+ linkify: true,
30
+ typographer: true,
31
+ breaks: true,
32
+ };
33
+
34
+ // Conditionally enable highlighting
35
+ if (config.theme?.codeHighlight !== false) {
36
+ mdOptions.highlight = function (str, lang) {
37
+ if (lang && hljs.getLanguage(lang)) {
38
+ try {
39
+ return `<pre class="hljs"><code>${hljs.highlight(str, { language: lang, ignoreIllegals: true }).value}</code></pre>`;
40
+ } catch (e) { console.error(`Highlighting error for lang ${lang}:`, e); }
41
+ }
42
+ return `<pre class="hljs"><code>${new MarkdownIt().utils.escapeHtml(str)}</code></pre>`;
43
+ };
42
44
  }
43
- });
44
-
45
45
 
46
+ const md = new MarkdownIt(mdOptions);
47
+
48
+ // --- Attach all plugins and rules to this instance ---
49
+ md.use(attrs, { leftDelimiter: '{', rightDelimiter: '}' });
50
+ md.use(markdown_it_footnote);
51
+ md.use(markdown_it_task_lists);
52
+ md.use(markdown_it_abbr);
53
+ md.use(markdown_it_deflist);
54
+ md.use(headingIdPlugin);
55
+
56
+ // Register renderers for all containers
57
+ Object.keys(containers).forEach(containerName => {
58
+ const container = containers[containerName];
59
+ md.renderer.rules[`container_${containerName}_open`] = container.render;
60
+ md.renderer.rules[`container_${containerName}_close`] = container.render;
61
+ });
46
62
 
47
- // Use standard markdown-it plugins for extended syntax support.
48
- md.use(attrs, { leftDelimiter: '{', rightDelimiter: '}' });
49
- md.use(markdown_it_footnote);
50
- md.use(markdown_it_task_lists);
51
- md.use(markdown_it_abbr);
52
- md.use(markdown_it_deflist);
53
-
54
- // Override the default fence renderer to preserve original content for all code blocks
55
- const defaultFenceRenderer = md.renderer.rules.fence;
56
- md.renderer.rules.fence = function(tokens, idx, options, env, self) {
57
- const token = tokens[idx];
58
-
59
- // Escape HTML entities to prevent rendering
60
- const escapedContent = token.content
61
- .replace(/&/g, '&amp;')
62
- .replace(/</g, '&lt;')
63
- .replace(/>/g, '&gt;')
64
- .replace(/"/g, '&quot;')
65
- .replace(/'/g, '&#39;');
66
-
67
- // If no language is specified, preserve the original content without processing
68
- if (!token.info || token.info.trim() === '') {
69
- return '<pre class="hljs"><code>' + escapedContent + '</code></pre>';
70
- }
71
-
72
- // For all language blocks, preserve the original content to avoid processing HTML or other content
73
- // This ensures code blocks are treated as literal text only
74
- const language = token.info.trim();
75
- return '<pre class="hljs"><code class="language-' + language + '">' + escapedContent + '</code></pre>';
76
- };
63
+ // Register the enhanced rules
64
+ md.block.ruler.before('fence', 'steps_container', stepsContainerRule, {
65
+ alt: ['paragraph', 'reference', 'blockquote', 'list']
66
+ });
67
+ md.block.ruler.before('fence', 'enhanced_tabs', enhancedTabsRule, {
68
+ alt: ['paragraph', 'reference', 'blockquote', 'list']
69
+ });
70
+ md.block.ruler.before('paragraph', 'advanced_container', advancedContainerRule, {
71
+ alt: ['paragraph', 'reference', 'blockquote', 'list']
72
+ });
77
73
 
74
+ // Register all custom renderers
75
+ md.renderer.rules.ordered_list_open = customOrderedListOpenRenderer;
76
+ md.renderer.rules.list_item_open = customListItemOpenRenderer;
77
+ md.renderer.rules.image = customImageRenderer;
78
+
79
+ // Register tabs renderers
80
+ md.renderer.rules.tabs_open = tabsOpenRenderer;
81
+ md.renderer.rules.tabs_nav_open = tabsNavOpenRenderer;
82
+ md.renderer.rules.tabs_nav_close = tabsNavCloseRenderer;
83
+ md.renderer.rules.tabs_nav_item = tabsNavItemRenderer;
84
+ md.renderer.rules.tabs_content_open = tabsContentOpenRenderer;
85
+ md.renderer.rules.tabs_content_close = tabsContentCloseRenderer;
86
+ md.renderer.rules.tab_pane_open = tabPaneOpenRenderer;
87
+ md.renderer.rules.tab_pane_close = tabPaneCloseRenderer;
88
+ md.renderer.rules.tabs_close = tabsCloseRenderer;
89
+
90
+ // Register heading ID plugin
91
+ md.use(headingIdPlugin);
92
+
93
+ // Register standalone closing rule
94
+ md.block.ruler.before('paragraph', 'standalone_closing', standaloneClosingRule, {
95
+ alt: ['paragraph', 'reference', 'blockquote', 'list']
96
+ });
78
97
 
98
+ return md;
99
+ }
79
100
 
80
101
  // ===================================================================
81
102
  // --- ADVANCED NESTED CONTAINER SYSTEM ---
@@ -164,8 +185,6 @@ const containers = {
164
185
  // }
165
186
  };
166
187
 
167
-
168
-
169
188
  // Advanced container rule with proper nesting support
170
189
  function advancedContainerRule(state, startLine, endLine, silent) {
171
190
  const start = state.bMarks[startLine] + state.tShift[startLine];
@@ -452,7 +471,7 @@ function enhancedTabsRule(state, startLine, endLine, silent) {
452
471
  '</code></pre>';
453
472
  } catch (e) { console.error(`Error highlighting language ${lang}:`, e); }
454
473
  }
455
- return '<pre class="hljs"><code>' + str + '</code></pre>';
474
+ return '<pre class="hljs"><code>' + MarkdownIt.utils.escapeHtml(str) + '</code></pre>';
456
475
  }
457
476
  });
458
477
 
@@ -481,6 +500,30 @@ function enhancedTabsRule(state, startLine, endLine, silent) {
481
500
  alt: ['paragraph', 'reference', 'blockquote', 'list']
482
501
  });
483
502
 
503
+ // Register custom renderers for the tab markdown instance
504
+ tabMd.renderer.rules.ordered_list_open = customOrderedListOpenRenderer;
505
+ tabMd.renderer.rules.list_item_open = customListItemOpenRenderer;
506
+ tabMd.renderer.rules.image = customImageRenderer;
507
+
508
+ // Register tabs renderers for the tab markdown instance
509
+ tabMd.renderer.rules.tabs_open = tabsOpenRenderer;
510
+ tabMd.renderer.rules.tabs_nav_open = tabsNavOpenRenderer;
511
+ tabMd.renderer.rules.tabs_nav_close = tabsNavCloseRenderer;
512
+ tabMd.renderer.rules.tabs_nav_item = tabsNavItemRenderer;
513
+ tabMd.renderer.rules.tabs_content_open = tabsContentOpenRenderer;
514
+ tabMd.renderer.rules.tabs_content_close = tabsContentCloseRenderer;
515
+ tabMd.renderer.rules.tab_pane_open = tabPaneOpenRenderer;
516
+ tabMd.renderer.rules.tab_pane_close = tabPaneCloseRenderer;
517
+ tabMd.renderer.rules.tabs_close = tabsCloseRenderer;
518
+
519
+ // Register heading ID plugin for the tab markdown instance
520
+ tabMd.use(headingIdPlugin);
521
+
522
+ // Register standalone closing rule for the tab markdown instance
523
+ tabMd.block.ruler.before('paragraph', 'standalone_closing', standaloneClosingRule, {
524
+ alt: ['paragraph', 'reference', 'blockquote', 'list']
525
+ });
526
+
484
527
  // Render the tab content
485
528
  const renderedContent = tabMd.render(tabContent);
486
529
  const htmlToken = state.push('html_block', '', 0);
@@ -495,19 +538,8 @@ function enhancedTabsRule(state, startLine, endLine, silent) {
495
538
  return true;
496
539
  }
497
540
 
498
- // Register the enhanced rules
499
- md.block.ruler.before('fence', 'steps_container', stepsContainerRule, {
500
- alt: ['paragraph', 'reference', 'blockquote', 'list']
501
- });
502
- md.block.ruler.before('fence', 'enhanced_tabs', enhancedTabsRule, {
503
- alt: ['paragraph', 'reference', 'blockquote', 'list']
504
- });
505
- md.block.ruler.before('paragraph', 'advanced_container', advancedContainerRule, {
506
- alt: ['paragraph', 'reference', 'blockquote', 'list']
507
- });
508
-
509
541
  // Add a rule to handle standalone closing tags
510
- md.block.ruler.before('paragraph', 'standalone_closing', (state, startLine, endLine, silent) => {
542
+ const standaloneClosingRule = (state, startLine, endLine, silent) => {
511
543
  const start = state.bMarks[startLine] + state.tShift[startLine];
512
544
  const max = state.eMarks[startLine];
513
545
  const lineContent = state.src.slice(start, max).trim();
@@ -520,19 +552,10 @@ md.block.ruler.before('paragraph', 'standalone_closing', (state, startLine, endL
520
552
  }
521
553
 
522
554
  return false;
523
- }, {
524
- alt: ['paragraph', 'reference', 'blockquote', 'list']
525
- });
526
-
527
- // Register renderers for all containers
528
- Object.keys(containers).forEach(containerName => {
529
- const container = containers[containerName];
530
- md.renderer.rules[`container_${containerName}_open`] = container.render;
531
- md.renderer.rules[`container_${containerName}_close`] = container.render;
532
- });
555
+ };
533
556
 
534
557
  // Custom renderer for ordered lists in steps containers
535
- md.renderer.rules.ordered_list_open = function(tokens, idx, options, env, self) {
558
+ const customOrderedListOpenRenderer = function(tokens, idx, options, env, self) {
536
559
  const token = tokens[idx];
537
560
  // Check if we're inside a steps container by looking at the context
538
561
  let isInSteps = false;
@@ -561,7 +584,7 @@ md.renderer.rules.ordered_list_open = function(tokens, idx, options, env, self)
561
584
  };
562
585
 
563
586
  // Custom renderer for list items in steps containers
564
- md.renderer.rules.list_item_open = function(tokens, idx, options, env, self) {
587
+ const customListItemOpenRenderer = function(tokens, idx, options, env, self) {
565
588
  const token = tokens[idx];
566
589
  // Check if we're inside a steps container and this is a direct child
567
590
  let isInStepsList = false;
@@ -596,34 +619,34 @@ md.renderer.rules.list_item_open = function(tokens, idx, options, env, self) {
596
619
  };
597
620
 
598
621
  // Enhanced tabs renderers
599
- md.renderer.rules.tabs_open = (tokens, idx) => {
622
+ const tabsOpenRenderer = (tokens, idx) => {
600
623
  const token = tokens[idx];
601
624
  return `<div class="${token.attrs.map(attr => attr[1]).join(' ')}">`;
602
625
  };
603
626
 
604
- md.renderer.rules.tabs_nav_open = () => '<div class="docmd-tabs-nav">';
605
- md.renderer.rules.tabs_nav_close = () => '</div>';
627
+ const tabsNavOpenRenderer = () => '<div class="docmd-tabs-nav">';
628
+ const tabsNavCloseRenderer = () => '</div>';
606
629
 
607
- md.renderer.rules.tabs_nav_item = (tokens, idx) => {
630
+ const tabsNavItemRenderer = (tokens, idx) => {
608
631
  const token = tokens[idx];
609
632
  return `<div class="${token.attrs[0][1]}">${token.content}</div>`;
610
633
  };
611
634
 
612
- md.renderer.rules.tabs_content_open = () => '<div class="docmd-tabs-content">';
613
- md.renderer.rules.tabs_content_close = () => '</div>';
635
+ const tabsContentOpenRenderer = () => '<div class="docmd-tabs-content">';
636
+ const tabsContentCloseRenderer = () => '</div>';
614
637
 
615
- md.renderer.rules.tab_pane_open = (tokens, idx) => {
638
+ const tabPaneOpenRenderer = (tokens, idx) => {
616
639
  const token = tokens[idx];
617
640
  return `<div class="${token.attrs[0][1]}">`;
618
641
  };
619
642
 
620
- md.renderer.rules.tab_pane_close = () => '</div>';
643
+ const tabPaneCloseRenderer = () => '</div>';
621
644
 
622
- md.renderer.rules.tabs_close = () => '</div>';
645
+ const tabsCloseRenderer = () => '</div>';
623
646
 
624
647
  // Override the default image renderer to properly handle attributes like {.class}.
625
- const defaultImageRenderer = md.renderer.rules.image;
626
- md.renderer.rules.image = function(tokens, idx, options, env, self) {
648
+ const customImageRenderer = function(tokens, idx, options, env, self) {
649
+ const defaultImageRenderer = function(tokens, idx, options, env, self) { return self.renderToken(tokens, idx, options); };
627
650
  const renderedImage = defaultImageRenderer(tokens, idx, options, env, self);
628
651
  const nextToken = tokens[idx + 1];
629
652
  if (nextToken && nextToken.type === 'attrs_block') {
@@ -635,22 +658,33 @@ md.renderer.rules.image = function(tokens, idx, options, env, self) {
635
658
  };
636
659
 
637
660
  // Add IDs to headings for anchor links, used by the Table of Contents.
638
- md.use((md) => {
639
- const defaultRender = md.renderer.rules.heading_open || function(tokens, idx, options, env, self) {
661
+ const headingIdPlugin = (md) => {
662
+ const originalHeadingOpen = md.renderer.rules.heading_open || function(tokens, idx, options, env, self) {
640
663
  return self.renderToken(tokens, idx, options);
641
664
  };
665
+
642
666
  md.renderer.rules.heading_open = function(tokens, idx, options, env, self) {
643
667
  const token = tokens[idx];
644
668
  const contentToken = tokens[idx + 1];
645
- if (contentToken && contentToken.type === 'inline') {
669
+
670
+ if (contentToken && contentToken.type === 'inline' && contentToken.content) {
646
671
  const headingText = contentToken.content;
647
- const id = headingText.toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]/g, '').replace(/--+/g, '-').replace(/^-+|-+$/g, '');
648
- if (id) { token.attrSet('id', id); }
672
+ const id = headingText
673
+ .toLowerCase()
674
+ .replace(/\s+/g, '-') // Replace spaces with -
675
+ .replace(/[^\w-]+/g, '') // Remove all non-word chars
676
+ .replace(/--+/g, '-') // Replace multiple - with single -
677
+ .replace(/^-+/, '') // Trim - from start of text
678
+ .replace(/-+$/, ''); // Trim - from end of text
679
+
680
+ if (id) {
681
+ token.attrSet('id', id);
682
+ }
649
683
  }
650
- return defaultRender(tokens, idx, options, env, self);
684
+
685
+ return originalHeadingOpen(tokens, idx, options, env, self);
651
686
  };
652
- });
653
-
687
+ };
654
688
 
655
689
  // ===================================================================
656
690
  // --- SAFE CONTAINER WRAPPER (FOR SIMPLE CONTAINERS) ---
@@ -685,13 +719,21 @@ function extractHeadingsFromHtml(htmlContent) {
685
719
  return headings;
686
720
  }
687
721
 
688
- async function processMarkdownFile(filePath, options = { isDev: false }, config) {
722
+ async function processMarkdownFile(filePath, md, config) {
689
723
  const rawContent = await fs.readFile(filePath, 'utf8');
690
- let { data: frontmatter, content: markdownContent } = matter(rawContent);
724
+ let frontmatter, markdownContent;
725
+
726
+ try {
727
+ ({ data: frontmatter, content: markdownContent } = matter(rawContent));
728
+ } catch (e) {
729
+ console.error(`❌ Error parsing frontmatter in ${formatPathForDisplay(filePath)}:`);
730
+ console.error(` ${e.message}`);
731
+ console.error(' This page will be skipped. Please fix the YAML syntax.');
732
+ return null;
733
+ }
691
734
 
692
- // Handle autoTitleFromH1
693
735
  if (!frontmatter.title) {
694
- if (config.autoTitleFromH1 !== false) { // Default to true
736
+ if (config.autoTitleFromH1 !== false) {
695
737
  const h1Match = markdownContent.match(/^#\s+(.*)/m);
696
738
  if (h1Match && h1Match[1]) {
697
739
  frontmatter.title = h1Match[1].trim();
@@ -702,11 +744,12 @@ async function processMarkdownFile(filePath, options = { isDev: false }, config)
702
744
  }
703
745
  }
704
746
 
705
- // For no-style pages, skip markdown processing and treat content as raw HTML
706
747
  let htmlContent, headings;
707
748
  if (frontmatter.noStyle === true) {
708
- htmlContent = markdownContent; // Use raw content as HTML
709
- headings = []; // No headings extraction for no-style pages
749
+ // For noStyle pages, NO markdown processing at all
750
+ // Pass the raw content directly as-is
751
+ htmlContent = markdownContent;
752
+ headings = [];
710
753
  } else {
711
754
  htmlContent = md.render(markdownContent);
712
755
  headings = extractHeadingsFromHtml(htmlContent);
@@ -735,7 +778,7 @@ async function findMarkdownFiles(dir) {
735
778
 
736
779
  module.exports = {
737
780
  processMarkdownFile,
738
- mdInstance: md,
781
+ createMarkdownItInstance,
739
782
  extractHeadingsFromHtml,
740
783
  findMarkdownFiles
741
784
  };
@@ -1,38 +1,51 @@
1
- // src/core/html-generator.js
1
+ //
2
+
2
3
  const ejs = require('ejs');
3
4
  const path = require('path');
4
5
  const fs = require('fs-extra');
5
- const { mdInstance } = require('./file-processor'); // Import mdInstance for footer
6
+ const { createMarkdownItInstance } = require('./file-processor');
6
7
  const { generateSeoMetaTags } = require('../plugins/seo');
7
8
  const { generateAnalyticsScripts } = require('../plugins/analytics');
8
- const { renderIcon } = require('./icon-renderer'); // Import icon renderer
9
+ const { renderIcon } = require('./icon-renderer');
10
+
11
+ // Create a markdown instance for inline rendering
12
+ let mdInstance = null;
13
+
14
+ let themeInitScript = '';
15
+ (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>`;
20
+ }
21
+ })();
9
22
 
10
23
  async function processPluginHooks(config, pageData, relativePathToRoot) {
11
24
  let metaTagsHtml = '';
12
25
  let faviconLinkHtml = '';
13
- let themeCssLinkHtml = ''; // For theme.name CSS file
14
- let pluginStylesHtml = ''; // For plugin-specific CSS
26
+ let themeCssLinkHtml = '';
27
+ let pluginStylesHtml = '';
15
28
  let pluginHeadScriptsHtml = '';
16
29
  let pluginBodyScriptsHtml = '';
17
30
 
18
- // 1. Favicon (built-in handling)
31
+ // Favicon (built-in handling)
19
32
  if (config.favicon) {
20
33
  const faviconPath = config.favicon.startsWith('/') ? config.favicon.substring(1) : config.favicon;
21
34
  faviconLinkHtml = `<link rel="shortcut icon" href="${relativePathToRoot}${faviconPath}" type="image/x-icon">\n`;
22
35
  }
23
36
 
24
- // 2. Theme CSS (built-in handling for theme.name)
37
+ // Theme CSS (built-in handling for theme.name)
25
38
  if (config.theme && config.theme.name && config.theme.name !== 'default') {
26
39
  const themeCssPath = `assets/css/docmd-theme-${config.theme.name}.css`;
27
40
  themeCssLinkHtml = ` <link rel="stylesheet" href="${relativePathToRoot}${themeCssPath}">\n`;
28
41
  }
29
42
 
30
- // 3. SEO Plugin (if configured)
43
+ // SEO Plugin (if configured)
31
44
  if (config.plugins?.seo) {
32
45
  metaTagsHtml += generateSeoMetaTags(config, pageData, relativePathToRoot);
33
46
  }
34
47
 
35
- // 4. Analytics Plugin (if configured)
48
+ // Analytics Plugin (if configured)
36
49
  if (config.plugins?.analytics) {
37
50
  const analyticsScripts = generateAnalyticsScripts(config, pageData);
38
51
  pluginHeadScriptsHtml += analyticsScripts.headScriptsHtml;
@@ -56,17 +69,21 @@ async function generateHtmlPage(templateData) {
56
69
  prevPage, nextPage, currentPagePath, headings
57
70
  } = templateData;
58
71
 
59
- const pageTitle = frontmatter.title; // Get title from frontmatter (already processed in file-processor)
72
+ const pageTitle = frontmatter.title;
60
73
 
61
74
  // Process plugins to get their HTML contributions
62
75
  const pluginOutputs = await processPluginHooks(
63
76
  config,
64
- { frontmatter, outputPath }, // pageData object
77
+ { frontmatter, outputPath },
65
78
  relativePathToRoot
66
79
  );
67
80
 
68
81
  let footerHtml = '';
69
82
  if (config.footer) {
83
+ // Initialize mdInstance if not already done
84
+ if (!mdInstance) {
85
+ mdInstance = createMarkdownItInstance(config);
86
+ }
70
87
  footerHtml = mdInstance.renderInline(config.footer);
71
88
  }
72
89
 
@@ -85,7 +102,8 @@ async function generateHtmlPage(templateData) {
85
102
 
86
103
  const ejsData = {
87
104
  content,
88
- pageTitle, // Pass the potentially undefined title
105
+ pageTitle,
106
+ themeInitScript,
89
107
  description: frontmatter.description,
90
108
  siteTitle,
91
109
  navigationHtml,
@@ -109,6 +127,7 @@ async function generateHtmlPage(templateData) {
109
127
  headings: headings || [],
110
128
  isActivePage,
111
129
  frontmatter,
130
+ config: config,
112
131
  ...pluginOutputs,
113
132
  };
114
133
 
@@ -1,5 +1,6 @@
1
- // src/core/icon-renderer.js
2
- const lucideStatic = require('lucide-static'); // Access the raw icon data
1
+ // Source file from the docmd project — https://github.com/mgks/docmd
2
+
3
+ const lucideStatic = require('lucide-static');
3
4
 
4
5
  // On first load, log debug information about a specific icon to understand its structure
5
6
  let debugRun = false;
@@ -1,4 +1,8 @@
1
- // src/plugins/analytics.js
1
+ // Source file from the docmd project — https://github.com/mgks/docmd
2
+
3
+ /*
4
+ * Generate analytics scripts for a page
5
+ */
2
6
 
3
7
  function generateAnalyticsScripts(config, pageData) {
4
8
  let headScriptsHtml = '';