@mgks/docmd 0.2.0 → 0.2.1

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,79 @@ 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
 
46
+ const md = new MarkdownIt(mdOptions);
45
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);
46
54
 
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);
55
+ // Register renderers for all containers
56
+ Object.keys(containers).forEach(containerName => {
57
+ const container = containers[containerName];
58
+ md.renderer.rules[`container_${containerName}_open`] = container.render;
59
+ md.renderer.rules[`container_${containerName}_close`] = container.render;
60
+ });
53
61
 
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
- };
62
+ // Register the enhanced rules
63
+ md.block.ruler.before('fence', 'steps_container', stepsContainerRule, {
64
+ alt: ['paragraph', 'reference', 'blockquote', 'list']
65
+ });
66
+ md.block.ruler.before('fence', 'enhanced_tabs', enhancedTabsRule, {
67
+ alt: ['paragraph', 'reference', 'blockquote', 'list']
68
+ });
69
+ md.block.ruler.before('paragraph', 'advanced_container', advancedContainerRule, {
70
+ alt: ['paragraph', 'reference', 'blockquote', 'list']
71
+ });
77
72
 
73
+ // Register all custom renderers
74
+ md.renderer.rules.ordered_list_open = customOrderedListOpenRenderer;
75
+ md.renderer.rules.list_item_open = customListItemOpenRenderer;
76
+ md.renderer.rules.image = customImageRenderer;
77
+
78
+ // Register tabs renderers
79
+ md.renderer.rules.tabs_open = tabsOpenRenderer;
80
+ md.renderer.rules.tabs_nav_open = tabsNavOpenRenderer;
81
+ md.renderer.rules.tabs_nav_close = tabsNavCloseRenderer;
82
+ md.renderer.rules.tabs_nav_item = tabsNavItemRenderer;
83
+ md.renderer.rules.tabs_content_open = tabsContentOpenRenderer;
84
+ md.renderer.rules.tabs_content_close = tabsContentCloseRenderer;
85
+ md.renderer.rules.tab_pane_open = tabPaneOpenRenderer;
86
+ md.renderer.rules.tab_pane_close = tabPaneCloseRenderer;
87
+ md.renderer.rules.tabs_close = tabsCloseRenderer;
88
+
89
+ // Register heading ID plugin
90
+ md.use(headingIdPlugin);
91
+
92
+ // Register standalone closing rule
93
+ md.block.ruler.before('paragraph', 'standalone_closing', standaloneClosingRule, {
94
+ alt: ['paragraph', 'reference', 'blockquote', 'list']
95
+ });
78
96
 
97
+ return md;
98
+ }
79
99
 
80
100
  // ===================================================================
81
101
  // --- ADVANCED NESTED CONTAINER SYSTEM ---
@@ -452,7 +472,7 @@ function enhancedTabsRule(state, startLine, endLine, silent) {
452
472
  '</code></pre>';
453
473
  } catch (e) { console.error(`Error highlighting language ${lang}:`, e); }
454
474
  }
455
- return '<pre class="hljs"><code>' + str + '</code></pre>';
475
+ return '<pre class="hljs"><code>' + MarkdownIt.utils.escapeHtml(str) + '</code></pre>';
456
476
  }
457
477
  });
458
478
 
@@ -481,6 +501,30 @@ function enhancedTabsRule(state, startLine, endLine, silent) {
481
501
  alt: ['paragraph', 'reference', 'blockquote', 'list']
482
502
  });
483
503
 
504
+ // Register custom renderers for the tab markdown instance
505
+ tabMd.renderer.rules.ordered_list_open = customOrderedListOpenRenderer;
506
+ tabMd.renderer.rules.list_item_open = customListItemOpenRenderer;
507
+ tabMd.renderer.rules.image = customImageRenderer;
508
+
509
+ // Register tabs renderers for the tab markdown instance
510
+ tabMd.renderer.rules.tabs_open = tabsOpenRenderer;
511
+ tabMd.renderer.rules.tabs_nav_open = tabsNavOpenRenderer;
512
+ tabMd.renderer.rules.tabs_nav_close = tabsNavCloseRenderer;
513
+ tabMd.renderer.rules.tabs_nav_item = tabsNavItemRenderer;
514
+ tabMd.renderer.rules.tabs_content_open = tabsContentOpenRenderer;
515
+ tabMd.renderer.rules.tabs_content_close = tabsContentCloseRenderer;
516
+ tabMd.renderer.rules.tab_pane_open = tabPaneOpenRenderer;
517
+ tabMd.renderer.rules.tab_pane_close = tabPaneCloseRenderer;
518
+ tabMd.renderer.rules.tabs_close = tabsCloseRenderer;
519
+
520
+ // Register heading ID plugin for the tab markdown instance
521
+ tabMd.use(headingIdPlugin);
522
+
523
+ // Register standalone closing rule for the tab markdown instance
524
+ tabMd.block.ruler.before('paragraph', 'standalone_closing', standaloneClosingRule, {
525
+ alt: ['paragraph', 'reference', 'blockquote', 'list']
526
+ });
527
+
484
528
  // Render the tab content
485
529
  const renderedContent = tabMd.render(tabContent);
486
530
  const htmlToken = state.push('html_block', '', 0);
@@ -495,19 +539,8 @@ function enhancedTabsRule(state, startLine, endLine, silent) {
495
539
  return true;
496
540
  }
497
541
 
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
542
  // Add a rule to handle standalone closing tags
510
- md.block.ruler.before('paragraph', 'standalone_closing', (state, startLine, endLine, silent) => {
543
+ const standaloneClosingRule = (state, startLine, endLine, silent) => {
511
544
  const start = state.bMarks[startLine] + state.tShift[startLine];
512
545
  const max = state.eMarks[startLine];
513
546
  const lineContent = state.src.slice(start, max).trim();
@@ -520,19 +553,10 @@ md.block.ruler.before('paragraph', 'standalone_closing', (state, startLine, endL
520
553
  }
521
554
 
522
555
  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
- });
556
+ };
533
557
 
534
558
  // Custom renderer for ordered lists in steps containers
535
- md.renderer.rules.ordered_list_open = function(tokens, idx, options, env, self) {
559
+ const customOrderedListOpenRenderer = function(tokens, idx, options, env, self) {
536
560
  const token = tokens[idx];
537
561
  // Check if we're inside a steps container by looking at the context
538
562
  let isInSteps = false;
@@ -561,7 +585,7 @@ md.renderer.rules.ordered_list_open = function(tokens, idx, options, env, self)
561
585
  };
562
586
 
563
587
  // Custom renderer for list items in steps containers
564
- md.renderer.rules.list_item_open = function(tokens, idx, options, env, self) {
588
+ const customListItemOpenRenderer = function(tokens, idx, options, env, self) {
565
589
  const token = tokens[idx];
566
590
  // Check if we're inside a steps container and this is a direct child
567
591
  let isInStepsList = false;
@@ -596,34 +620,34 @@ md.renderer.rules.list_item_open = function(tokens, idx, options, env, self) {
596
620
  };
597
621
 
598
622
  // Enhanced tabs renderers
599
- md.renderer.rules.tabs_open = (tokens, idx) => {
623
+ const tabsOpenRenderer = (tokens, idx) => {
600
624
  const token = tokens[idx];
601
625
  return `<div class="${token.attrs.map(attr => attr[1]).join(' ')}">`;
602
626
  };
603
627
 
604
- md.renderer.rules.tabs_nav_open = () => '<div class="docmd-tabs-nav">';
605
- md.renderer.rules.tabs_nav_close = () => '</div>';
628
+ const tabsNavOpenRenderer = () => '<div class="docmd-tabs-nav">';
629
+ const tabsNavCloseRenderer = () => '</div>';
606
630
 
607
- md.renderer.rules.tabs_nav_item = (tokens, idx) => {
631
+ const tabsNavItemRenderer = (tokens, idx) => {
608
632
  const token = tokens[idx];
609
633
  return `<div class="${token.attrs[0][1]}">${token.content}</div>`;
610
634
  };
611
635
 
612
- md.renderer.rules.tabs_content_open = () => '<div class="docmd-tabs-content">';
613
- md.renderer.rules.tabs_content_close = () => '</div>';
636
+ const tabsContentOpenRenderer = () => '<div class="docmd-tabs-content">';
637
+ const tabsContentCloseRenderer = () => '</div>';
614
638
 
615
- md.renderer.rules.tab_pane_open = (tokens, idx) => {
639
+ const tabPaneOpenRenderer = (tokens, idx) => {
616
640
  const token = tokens[idx];
617
641
  return `<div class="${token.attrs[0][1]}">`;
618
642
  };
619
643
 
620
- md.renderer.rules.tab_pane_close = () => '</div>';
644
+ const tabPaneCloseRenderer = () => '</div>';
621
645
 
622
- md.renderer.rules.tabs_close = () => '</div>';
646
+ const tabsCloseRenderer = () => '</div>';
623
647
 
624
648
  // 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) {
649
+ const customImageRenderer = function(tokens, idx, options, env, self) {
650
+ const defaultImageRenderer = function(tokens, idx, options, env, self) { return self.renderToken(tokens, idx, options); };
627
651
  const renderedImage = defaultImageRenderer(tokens, idx, options, env, self);
628
652
  const nextToken = tokens[idx + 1];
629
653
  if (nextToken && nextToken.type === 'attrs_block') {
@@ -635,11 +659,11 @@ md.renderer.rules.image = function(tokens, idx, options, env, self) {
635
659
  };
636
660
 
637
661
  // 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) {
662
+ const headingIdPlugin = (md) => {
663
+ const defaultRender = function(tokens, idx, options, env, self) {
640
664
  return self.renderToken(tokens, idx, options);
641
665
  };
642
- md.renderer.rules.heading_open = function(tokens, idx, options, env, self) {
666
+ const headingOpenRenderer = function(tokens, idx, options, env, self) {
643
667
  const token = tokens[idx];
644
668
  const contentToken = tokens[idx + 1];
645
669
  if (contentToken && contentToken.type === 'inline') {
@@ -648,8 +672,8 @@ md.use((md) => {
648
672
  if (id) { token.attrSet('id', id); }
649
673
  }
650
674
  return defaultRender(tokens, idx, options, env, self);
651
- };
652
- });
675
+ }
676
+ }
653
677
 
654
678
 
655
679
  // ===================================================================
@@ -685,13 +709,21 @@ function extractHeadingsFromHtml(htmlContent) {
685
709
  return headings;
686
710
  }
687
711
 
688
- async function processMarkdownFile(filePath, options = { isDev: false }, config) {
712
+ async function processMarkdownFile(filePath, md, config) {
689
713
  const rawContent = await fs.readFile(filePath, 'utf8');
690
- let { data: frontmatter, content: markdownContent } = matter(rawContent);
714
+ let frontmatter, markdownContent;
715
+
716
+ try {
717
+ ({ data: frontmatter, content: markdownContent } = matter(rawContent));
718
+ } catch (e) {
719
+ console.error(`❌ Error parsing frontmatter in ${formatPathForDisplay(filePath)}:`);
720
+ console.error(` ${e.message}`);
721
+ console.error(' This page will be skipped. Please fix the YAML syntax.');
722
+ return null;
723
+ }
691
724
 
692
- // Handle autoTitleFromH1
693
725
  if (!frontmatter.title) {
694
- if (config.autoTitleFromH1 !== false) { // Default to true
726
+ if (config.autoTitleFromH1 !== false) {
695
727
  const h1Match = markdownContent.match(/^#\s+(.*)/m);
696
728
  if (h1Match && h1Match[1]) {
697
729
  frontmatter.title = h1Match[1].trim();
@@ -702,11 +734,12 @@ async function processMarkdownFile(filePath, options = { isDev: false }, config)
702
734
  }
703
735
  }
704
736
 
705
- // For no-style pages, skip markdown processing and treat content as raw HTML
706
737
  let htmlContent, headings;
707
738
  if (frontmatter.noStyle === true) {
708
- htmlContent = markdownContent; // Use raw content as HTML
709
- headings = []; // No headings extraction for no-style pages
739
+ // For noStyle pages, NO markdown processing at all
740
+ // Pass the raw content directly as-is
741
+ htmlContent = markdownContent;
742
+ headings = [];
710
743
  } else {
711
744
  htmlContent = md.render(markdownContent);
712
745
  headings = extractHeadingsFromHtml(htmlContent);
@@ -735,7 +768,7 @@ async function findMarkdownFiles(dir) {
735
768
 
736
769
  module.exports = {
737
770
  processMarkdownFile,
738
- mdInstance: md,
771
+ createMarkdownItInstance,
739
772
  extractHeadingsFromHtml,
740
773
  findMarkdownFiles
741
774
  };
@@ -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 = '';