Top Level Namespace

Defined Under Namespace

Modules: CMark Classes: Apex, String

Constant Summary collapse

CMARK_NOT_FOUND_MSG =

—- cmark-gfm system detection ——————————————

We link against a system cmark-gfm installation, using the vendored headers (if present) for internal types like CMARK_NODE_TABLE.

Set APEX_SHOW_CMARK_ERROR=1 to print the “cmark-gfm not found” message and exit without building (useful for previewing the user-facing error).

<<~MSG
  -------------------------------------------------------------------------------
  The apex gem could not find the cmark-gfm C library.
  -------------------------------------------------------------------------------

  This gem requires cmark-gfm to be installed before you can build the native
  extension. Install it for your system, then run `gem install apex-ruby` again.

  macOS (Homebrew):
    brew install cmark-gfm

  Other platforms:
    Install the cmark-gfm development package for your distribution, ensure
    pkg-config is available, then retry. See:
    https://github.com/github/cmark-gfm

  -------------------------------------------------------------------------------
MSG
MODE =

Default to multi-page docset

ARGV[0] || 'multi'
SCRIPT_DIR =
File.expand_path(__dir__)
DOCS_DIR =
File.join(SCRIPT_DIR, '..')
DOCSETS_DIR =
File.join(DOCS_DIR, 'documentation', 'docsets')
WIKI_DIR =
File.join(DOCS_DIR, 'documentation', 'apex.wiki')
MMD2CHEATSET =
File.expand_path('~/Desktop/Code/mmd2cheatset/mmd2cheatset.rb')
APEX_BIN =
find_apex_binary
HTML_DIR =
File.join(DOCS_DIR, 'documentation', 'html')
TRANSFORMED_DIR =
File.join(DOCS_DIR, 'documentation', 'app-transformed')
SETTINGS_TABLE_FILE =
File.join(DOCS_DIR, 'documentation', 'app-settings-table.md')
APP_PAGES =

App-focused pages

[
  'Syntax',
  'Inline-Attribute-Lists',
  'Modes',
  'Multi-File-Documents',
  'Citations',
  'Metadata-Transforms',
  'Header-IDs',
  'Plugins',
  'Credits'
].freeze
PROMPT_FILE =
File.join(SCRIPT_DIR, 'transform_for_app.md')

Instance Method Summary collapse

Instance Method Details

#basic_transform(markdown_content) ⇒ Object



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'ext/apex_ext/apex_src/documentation/generate_app_docs_ai.rb', line 108

def basic_transform(markdown_content)
  # Basic fallback: remove obvious command-line code blocks
  # This is a simple heuristic - AI transformation is preferred

  lines = markdown_content.lines
  result = []
  in_code_block = false
  code_block_type = nil
  code_block_lines = []

  lines.each do |line|
    # Detect code block start
    if line =~ /^```(\w*)$/
      code_block_type = $1
      code_block_lines = [line]
      in_code_block = true
      next
    end

    # Detect code block end
    if in_code_block && line =~ /^```$/
      code_block_lines << line
      code_block_content = code_block_lines.join

      # Check if this is a command-line example
      if code_block_content =~ /apex\s+--/ || code_block_content =~ /^```(bash|sh|shell|zsh|fish)/
        # Skip command-line code blocks
        in_code_block = false
        code_block_lines = []
        next
      else
        # Keep non-CLI code blocks
        result.concat(code_block_lines)
      end

      in_code_block = false
      code_block_lines = []
      next
    end

    # Collect code block lines
    if in_code_block
      code_block_lines << line
      next
    end

    # Regular line
    result << line
  end

  # Handle unclosed code block
  if in_code_block
    result.concat(code_block_lines)
  end

  result.join
end

#cleanup_wikiObject



593
594
595
596
597
598
599
# File 'ext/apex_ext/apex_src/documentation/generate_docset.rb', line 593

def cleanup_wiki
  if File.exist?(WIKI_DIR)
    puts "\nCleaning up wiki clone..."
    FileUtils.rm_rf(WIKI_DIR)
    puts "Wiki clone removed"
  end
end

#clone_wikiObject



575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
# File 'ext/apex_ext/apex_src/documentation/generate_docset.rb', line 575

def clone_wiki
  wiki_url = 'https://github.com/ApexMarkdown/apex.wiki.git'
  puts "Cloning wiki from GitHub..."

  if File.exist?(WIKI_DIR)
    puts "Wiki directory already exists, removing..."
    FileUtils.rm_rf(WIKI_DIR)
  end

  success = system("git clone #{wiki_url} \"#{WIKI_DIR}\" 2>&1")
  unless success
    puts "Error: Failed to clone wiki from #{wiki_url}"
    exit 1
  end

  puts "Wiki cloned successfully"
end

#extract_headers(html_content) ⇒ Object

Rouge CSS is now included in shared_styles.css



251
252
253
254
255
256
257
258
259
260
# File 'ext/apex_ext/apex_src/documentation/generate_docset.rb', line 251

def extract_headers(html_content)
  headers = []
  # Match headers with IDs, handling multi-line formatting
  html_content.scan(/<h([1-6])[^>]*id=["']([^"']+)["'][^>]*>([\s\S]*?)<\/h[1-6]>/i) do |level, id, text|
    # Remove HTML tags and normalize whitespace
    text = text.gsub(/<[^>]+>/, '').gsub(/\s+/, ' ').strip
    headers << { level: level.to_i, id: id, text: text } unless text.empty?
  end
  headers
end

#find_ai_agentObject

Check if cursor-agent or similar tool is available



52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'ext/apex_ext/apex_src/documentation/generate_app_docs_ai.rb', line 52

def find_ai_agent
  # Try cursor-agent first
  cursor_agent = `which cursor-agent 2>/dev/null`.strip
  return ['cursor-agent'] if cursor_agent != '' && File.exist?(cursor_agent)

  # Try other possible AI tools
  ['claude', 'gpt', 'ai'].each do |tool|
    which = `which #{tool} 2>/dev/null`.strip
    return [tool] if which != '' && File.exist?(which)
  end

  nil
end

#find_apex_binaryObject

Find Apex binary



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'ext/apex_ext/apex_src/documentation/generate_docset.rb', line 46

def find_apex_binary
  # Prioritize build/apex if it exists (most recent build)
  build_apex = File.expand_path('../build/apex', __dir__)
  return build_apex if File.exist?(build_apex)

  # Try build-release (relative to repo root, not script dir)
  build_release_apex = File.expand_path('../build-release/apex', __dir__)
  return build_release_apex if File.exist?(build_release_apex)

  # Try other common build directories
  ['../build-debug/apex'].each do |path|
    full_path = File.expand_path(path, __dir__)
    return full_path if File.exist?(full_path)
  end

  # Fall back to system-installed apex
  system_apex = `which apex 2>/dev/null`.strip
  return system_apex if system_apex != '' && File.exist?(system_apex)

  nil
end

No longer needed - files are pre-transformed



527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
# File 'ext/apex_ext/apex_src/documentation/generate_app_docs.rb', line 527

def fix_links_in_html(html_content, available_files)
  # Create a mapping of page names to HTML files
  file_map = {}
  available_files.each do |file|
    basename = File.basename(file, '.html')
    file_map[basename] = "#{basename}.html"
    # Also map with different cases/spaces
    file_map[basename.gsub('-', ' ')] = "#{basename}.html"
    file_map[basename.gsub('-', '_')] = "#{basename}.html"
  end

  # Fix relative links that don't have .html extension
  # Match href="PageName" or href="Page-Name" (but not external links, anchors, or already .html)
  html_content.gsub(/<a\s+([^>]*\s+)?href=["']([^"']+)["']([^>]*)>/i) do |match|
    attrs_before = $1 || ''
    href = $2
    attrs_after = $3 || ''

    # Skip if it's already an external link, anchor, or has extension
    if href =~ /^(https?:\/\/|mailto:|#|.*\.(html|md|pdf|png|jpg|jpeg|gif|svg|webp))/i
      match
    elsif file_map[href]
      # Found a matching file, add .html extension
      "<a #{attrs_before}href=\"#{file_map[href]}\"#{attrs_after}>"
    elsif file_map[href.gsub(/\s+/, '-')]
      # Try with spaces converted to dashes
      fixed_href = file_map[href.gsub(/\s+/, '-')]
      "<a #{attrs_before}href=\"#{fixed_href}\"#{attrs_after}>"
    else
      # Try to find a case-insensitive match
      found = available_files.find { |f| File.basename(f, '.html').downcase == href.downcase }
      if found
        "<a #{attrs_before}href=\"#{File.basename(found)}\"#{attrs_after}>"
      else
        match  # Keep original if no match found
      end
    end
  end
end

#generate_cssObject

Rouge CSS is now included in shared_styles.css



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'ext/apex_ext/apex_src/documentation/generate_single_html.rb', line 85

def generate_css
  # Load shared CSS
  shared_css_file = File.join(SCRIPT_DIR, 'shared_styles.css')
  shared_css = File.exist?(shared_css_file) ? File.read(shared_css_file) : ''

  # Additional styles specific to app docs HTML
  additional_css = <<~CSS
    <style>
      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }
      #{shared_css}
      .content {
        padding: 2rem;
        max-width: 900px;
      }
      .page {
        display: none;
      }
      .page.active {
        display: block;
      }
      h1, h2, h3, h4, h5, h6 {
        margin-top: 1.5em;
        margin-bottom: 0.5em;
      }
      h1 {
        font-size: 2em;
        border-bottom: 2px solid #eee;
        padding-bottom: 0.3em;
      }
      h2 {
        font-size: 1.5em;
        border-bottom: 1px solid #eee;
        padding-bottom: 0.3em;
      }
      blockquote {
        border-left: 4px solid #ddd;
        margin: 0;
        padding-left: 1rem;
        color: #666;
      }
      table {
        border-collapse: collapse;
        width: 100%;
      }
      th, td {
        border: 1px solid #ddd;
        padding: 0.5rem;
      }
      th {
        background: #f5f5f5;
      }
    </style>
  CSS
end

#generate_javascriptObject



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
# File 'ext/apex_ext/apex_src/documentation/generate_app_docs.rb', line 140

def generate_javascript
  # Load shared JavaScript
  shared_js_file = File.join(SCRIPT_DIR, 'shared_scripts.js')
  shared_js = File.exist?(shared_js_file) ? File.read(shared_js_file) : ''

  <<~JS
    <script>
      #{shared_js}
      function showPage(pageId) {
        var pages = document.querySelectorAll('.page');
        pages.forEach(function(page) {
          page.classList.remove('active');
        });

        var selectedPage = document.getElementById('page-' + pageId);
        if (selectedPage) {
          selectedPage.classList.add('active');
        }

        var links = document.querySelectorAll('.sidebar a');
        links.forEach(function(link) {
          link.classList.remove('active');
        });
        var activeLink = document.querySelector('.sidebar a[data-page="' + pageId + '"]');
        if (activeLink) {
          activeLink.classList.add('active');
        }

        window.scrollTo(0, 0);

        if (history.pushState) {
          history.pushState(null, null, '#' + pageId);
        } else {
          window.location.hash = pageId;
        }

      }

      // Initialize floating TOC for a page
      function initFloatingTOC(pageElement) {
        if (!pageElement) return;

        var pageTOC = pageElement.querySelector('.page-toc');
        if (!pageTOC) return;

        // Add ID to page TOC if it doesn't have one
        if (!pageTOC.id) {
          pageTOC.id = 'page-toc-top-' + pageElement.id;
        }

        // Find or create floating TOC for this page
        var floatingTOC = pageElement.querySelector('.floating-toc');
        if (!floatingTOC) {
          floatingTOC = document.createElement('div');
          floatingTOC.className = 'floating-toc';
          floatingTOC.id = 'floating-toc-' + pageElement.id;
          floatingTOC.innerHTML = '<div class="floating-toc-container"><div class="floating-toc-header"><span>Table of Contents 🔻</span></div><div class="floating-toc-content" id="floating-toc-content-' + pageElement.id + '"></div></div>';
          pageElement.insertBefore(floatingTOC, pageElement.firstChild);
        }

        var floatingTOCContent = floatingTOC.querySelector('.floating-toc-content');
        if (!floatingTOCContent) return;

        // Clone the TOC structure
        var tocClone = pageTOC.cloneNode(true);
        tocClone.id = 'floating-toc-clone-' + pageElement.id;
        floatingTOCContent.innerHTML = '';
        floatingTOCContent.appendChild(tocClone);

        // Update all links to use smooth scrolling
        var allTOCLinks = pageElement.querySelectorAll('.page-toc a, .floating-toc-content a');
        allTOCLinks.forEach(function(link) {
          link.addEventListener('click', function(e) {
            var href = this.getAttribute('href');
            if (href && href.startsWith('#')) {
              e.preventDefault();
              var targetId = href.substring(1);
              var targetElement = document.getElementById(targetId);
              if (targetElement) {
                var offset = 20; // Offset from top

                // Function to calculate absolute position from document top
                function getAbsoluteTop(element) {
                  var top = 0;
                  while (element) {
                    top += element.offsetTop;
                    element = element.offsetParent;
                  }
                  return top;
                }

                // Ensure the target element's page is visible
                var targetPage = targetElement.closest('.page');
                if (targetPage && !targetPage.classList.contains('active')) {
                  // If target is in a different page, switch to that page first
                  var pageId = targetPage.id.replace('page-', '');
                  if (typeof showPage === 'function') {
                    showPage(pageId);
                    // Wait for page to be visible, then scroll
                    setTimeout(function() {
                      var absoluteTop = getAbsoluteTop(targetElement);
                      window.scrollTo({
                        top: Math.max(0, absoluteTop - offset),
                        behavior: 'smooth'
                      });
                    }, 150);
                  }
                } else {
                  // Target is in current page, calculate and scroll
                  var absoluteTop = getAbsoluteTop(targetElement);
                  var scrollTop = window.pageYOffset || document.documentElement.scrollTop;
                  var offsetPosition = absoluteTop - offset;

                  if (Math.abs(scrollTop - offsetPosition) > 10) {
                    window.scrollTo({
                      top: Math.max(0, offsetPosition),
                      behavior: 'smooth'
                    });
                  }
                }

                // Update URL hash without triggering scroll
                if (history.pushState) {
                  history.pushState(null, null, href);
                }
              }
            }
          });
        });

        // Handle scroll to show/hide floating TOC
        var tocTop = pageTOC.getBoundingClientRect().top + window.pageYOffset;
        var tocBottom = tocTop + pageTOC.offsetHeight;

        function updateFloatingTOC() {
          if (!pageElement.classList.contains('active')) {
            floatingTOC.classList.remove('visible');
            return;
          }

          var scrollY = window.pageYOffset || document.documentElement.scrollTop;

          if (scrollY > tocBottom) {
            floatingTOC.classList.add('visible');
          } else {
            floatingTOC.classList.remove('visible');
          }
        }

        // Throttle scroll events
        var ticking = false;
        function handleScroll() {
          if (!ticking) {
            window.requestAnimationFrame(function() {
              updateFloatingTOC();
              ticking = false;
            });
            ticking = true;
          }
        }

        window.addEventListener('scroll', handleScroll);

        // Initial check
        updateFloatingTOC();
      }

      document.addEventListener('DOMContentLoaded', function() {
        var links = document.querySelectorAll('.sidebar a');
        links.forEach(function(link) {
          link.addEventListener('click', function(e) {
            e.preventDefault();
            var pageId = this.getAttribute('data-page');
            if (pageId) {
              showPage(pageId);
              // Initialize floating TOC for the new page
              setTimeout(function() {
                var pageElement = document.getElementById('page-' + pageId);
                if (pageElement) {
                  initFloatingTOC(pageElement);
                }
              }, 100);
            }
          });
        });

        // Function to handle hash navigation
        function handleHashNavigation() {
          var hash = window.location.hash.substring(1);
          if (!hash) {
            // No hash, show first page by default
            var firstLink = document.querySelector('.sidebar a[data-page]');
            if (firstLink) {
              var firstPageId = firstLink.getAttribute('data-page');
              showPage(firstPageId);
              setTimeout(function() {
                var firstPageElement = document.getElementById('page-' + firstPageId);
                if (firstPageElement) {
                  initFloatingTOC(firstPageElement);
                }
              }, 100);
            }
            return;
          }

          // Check if hash is a page ID
          var pageElement = document.getElementById('page-' + hash);
          if (pageElement) {
            // Hash is a page ID, show that page
            showPage(hash);
            setTimeout(function() {
              initFloatingTOC(pageElement);
            }, 100);
            return;
          }

          // Hash might be a section ID, find which page contains it
          var targetElement = document.getElementById(hash);
          if (targetElement) {
            // Find the page that contains this element
            var containingPage = targetElement.closest('.page');
            if (containingPage) {
              var pageId = containingPage.id.replace('page-', '');
              showPage(pageId);
              setTimeout(function() {
                initFloatingTOC(containingPage);
                // Scroll to the target element
                function getAbsoluteTop(element) {
                  var top = 0;
                  while (element) {
                    top += element.offsetTop;
                    element = element.offsetParent;
                  }
                  return top;
                }
                var absoluteTop = getAbsoluteTop(targetElement);
                window.scrollTo({
                  top: Math.max(0, absoluteTop - 20),
                  behavior: 'smooth'
                });
              }, 200);
              return;
            }
          }

          // Hash doesn't match anything, show first page
          var firstLink = document.querySelector('.sidebar a[data-page]');
          if (firstLink) {
            var firstPageId = firstLink.getAttribute('data-page');
            showPage(firstPageId);
            setTimeout(function() {
              var firstPageElement = document.getElementById('page-' + firstPageId);
              if (firstPageElement) {
                initFloatingTOC(firstPageElement);
              }
            }, 100);
          }
        }

        // Handle hash on load
        handleHashNavigation();

        // Also handle hash changes
        window.addEventListener('hashchange', function() {
          handleHashNavigation();
        });

        // Initialize floating TOC for all pages
        var allPages = document.querySelectorAll('.page');
        allPages.forEach(function(page) {
          initFloatingTOC(page);
        });
      });
    </script>
  JS
end

#generate_multi_page_docsetObject



601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
# File 'ext/apex_ext/apex_src/documentation/generate_docset.rb', line 601

def generate_multi_page_docset
  require_sqlite3  # Only needed for multi-page mode

  puts "Generating multi-page docset from wiki files..."

  # Clone wiki if needed
  clone_wiki unless File.exist?(WIKI_DIR)

  unless File.exist?(WIKI_DIR)
    puts "Error: Wiki directory not found at #{WIKI_DIR}"
    exit 1
  end

  unless File.exist?(APEX_BIN)
    puts "Error: Apex binary not found at #{APEX_BIN}"
    puts "Please build Apex first: cd build-release && make"
    puts "Or ensure 'apex' is in your PATH"
    cleanup_wiki
    exit 1
  end

  # Ensure output directory exists
  FileUtils.mkdir_p(DOCSETS_DIR)

  docset_name = 'Apex.docset'
  docset_path = File.join(DOCSETS_DIR, docset_name)
  contents_path = File.join(docset_path, 'Contents')
  resources_path = File.join(contents_path, 'Resources')
  documents_path = File.join(resources_path, 'Documents')

  # Clean up existing docset
  FileUtils.rm_rf(docset_path) if File.exist?(docset_path)

  # Create directory structure
  FileUtils.mkdir_p(documents_path)

  # Get all markdown files from wiki (excluding special files)
  wiki_files = Dir.glob(File.join(WIKI_DIR, '*.md')).reject do |f|
    basename = File.basename(f)
    basename =~ /^(_|\.)/ || basename == 'commit_message.txt'
  end.sort

  # Ensure Home.md is first (it's the index file)
  home_file = wiki_files.find { |f| File.basename(f) == 'Home.md' }
  if home_file
    wiki_files.delete(home_file)
    wiki_files.unshift(home_file)
  end

  puts "Found #{wiki_files.length} wiki files to process..."

  # Process each wiki file - first pass: convert to HTML
  html_files = []
  wiki_files.each do |md_file|
    basename = File.basename(md_file, '.md')
    html_file = File.join(documents_path, "#{basename}.html")
    html_files << html_file

    puts "Processing #{basename}..."

    # Convert markdown to HTML using Apex
    html_content = `#{APEX_BIN} "#{md_file}" --standalone --pretty 2>/dev/null`

    if $?.success? && !html_content.empty?
      # Write HTML file (will fix links in second pass)
      File.write(html_file, html_content)
    else
      puts "  Warning: Failed to process #{basename}"
    end
  end

  # Generate main TOC from sidebar
  puts "\nGenerating main TOC from sidebar..."
  main_toc_html = parse_sidebar_toc
  puts "Main TOC generated (#{main_toc_html.length} chars)"
  if main_toc_html.length < 100
    puts "Warning: TOC seems too short, checking..."
    puts "First 200 chars: #{main_toc_html[0..200]}"
  end

  # Generate footer
  puts "Generating footer..."
  footer_html = parse_footer
  puts "Footer generated (#{footer_html.length} chars)"

  # Second pass: fix links in all HTML files and add TOCs
  puts "Fixing links and adding TOCs..."
  entries = []
  all_guides = []  # Collect all guide entries for TOC

  html_files.each do |html_file|
    next unless File.exist?(html_file)

    basename = File.basename(html_file, '.html')
    html_content = File.read(html_file)

    # Fix links to add .html extensions
    html_content = fix_links_in_html(html_content, html_files.map { |f| File.basename(f) })

    # Extract title from first h1 or use filename
    title_match = html_content.match(/<h1[^>]*>(.*?)<\/h1>/i)
    title = title_match ? title_match[1].gsub(/<[^>]+>/, '').strip : basename

    # Collect guide info for TOC
    all_guides << { basename: basename, title: title, path: "#{basename}.html" }

    # Extract headers for index and TOC
    headers = extract_headers(html_content)

    # Generate TOC for this page
    page_toc_html = generate_page_toc(headers)
    if page_toc_html.empty?
      puts "  No page TOC for #{basename} (no headers found)"
    else
      puts "  Generated page TOC for #{basename} (#{headers.length} headers)"
    end

    # Highlight code blocks before injecting TOC
    html_content = highlight_code_blocks(html_content)

    # Inject both TOCs and footer into HTML
    html_content = inject_toc_into_html(html_content, main_toc_html, page_toc_html)
    html_content = inject_footer_into_html(html_content, footer_html)

    # Write updated HTML file with TOCs and footer
    File.write(html_file, html_content)

    # Add main entry
    entries << {
      name: title,
      type: 'Guide',
      path: "#{basename}.html"
    }

    # Add header entries
    headers.each do |header|
      entries << {
        name: header[:text],
        type: 'Section',
        path: "#{basename}.html##{header[:id]}"
      }
    end
  end

  # Add table of contents to Home.html for Dash index
  home_html_file = html_files.find { |f| File.basename(f, '.html') == 'Home' }
  if home_html_file && File.exist?(home_html_file)
    puts "Adding table of contents to index page..."
    html_content = File.read(home_html_file)

    # Generate TOC HTML
    toc_html = "<nav class=\"dash-toc\">\n<h2>Documentation</h2>\n<ul>\n"
    all_guides.each do |guide|
      toc_html += "  <li><a href=\"#{guide[:path]}\">#{guide[:title]}</a></li>\n"
    end
    toc_html += "</ul>\n</nav>\n"

    # Insert TOC after the first h1 or at the beginning of body
    if html_content =~ /(<h1[^>]*>.*?<\/h1>)/i
      html_content = html_content.sub(/(<h1[^>]*>.*?<\/h1>)/i, "\\1\n#{toc_html}")
    elsif html_content =~ /(<body[^>]*>)/i
      html_content = html_content.sub(/(<body[^>]*>)/i, "\\1\n#{toc_html}")
    end

    File.write(home_html_file, html_content)
  end

  # Determine index file
  index_file = if entries.any? { |e| e[:path] == 'Home.html' }
    'Home.html'
  elsif entries.first
    entries.first[:path]
  else
    'index.html'
  end

  # Create Info.plist
  info_plist = <<~PLIST
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
      <key>CFBundleIdentifier</key>
      <string>apex</string>
      <key>CFBundleName</key>
      <string>Apex</string>
      <key>DocSetPlatformFamily</key>
      <string>apex</string>
      <key>isDashDocset</key>
      <true/>
      <key>dashIndexFilePath</key>
      <string>#{index_file}</string>
      <key>DashDocSetFamily</key>
      <string>dashtoc</string>
      <key>DashDocSetPluginKeyword</key>
      <string>apex</string>
      <key>DashDocSetFallbackURL</key>
      <string>#{index_file}</string>
      <key>DashDocSetDeclaredInStyle</key>
      <string>originalName</string>
    </dict>
    </plist>
  PLIST

  File.write(File.join(contents_path, 'Info.plist'), info_plist)

  # Create SQLite index
  db_path = File.join(resources_path, 'docSet.dsidx')
  # Open database with busy timeout and retry logic
  db = nil
  max_retries = 5
  retry_count = 0

  begin
    db = SQLite3::Database.new(db_path)
    db.busy_timeout = 5000  # Wait up to 5 seconds for database to be available

    db.execute <<~SQL
      CREATE TABLE IF NOT EXISTS searchIndex(
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT,
        type TEXT,
        path TEXT
      );
      CREATE UNIQUE INDEX IF NOT EXISTS anchor ON searchIndex(name, type, path);
    SQL

    # Clear existing entries if regenerating
    db.execute("DELETE FROM searchIndex")

    # Use a transaction for better performance and atomicity
    db.transaction do
      entries.each do |entry|
        db.execute(
          "INSERT OR IGNORE INTO searchIndex(name, type, path) VALUES(?, ?, ?)",
          [entry[:name], entry[:type], entry[:path]]
        )
      end
    end
  rescue SQLite3::BusyException => e
    retry_count += 1
    if retry_count < max_retries
      puts "Database busy, retrying (#{retry_count}/#{max_retries})..."
      sleep(0.5 * retry_count)  # Exponential backoff
      db.close if db
      retry
    else
      puts "\nError: Database is locked after #{max_retries} retries."
      puts "Please close Dash if it's open with this docset, then try again."
      raise
    end
  ensure
    db.close if db
  end

  puts "\nMulti-page docset generated successfully!"
  puts "Docset location: #{docset_path}"
  puts "Total entries: #{entries.length}"

  # Clean up wiki clone
  cleanup_wiki
end

#generate_page_toc(headers) ⇒ Object



287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# File 'ext/apex_ext/apex_src/documentation/generate_docset.rb', line 287

def generate_page_toc(headers)
  return '' if headers.empty?

  toc_html = '<nav class="page-toc">'
  toc_html += '<ul>'
  stack = []  # Track open list items that need closing

  headers.each_with_index do |header, idx|
    level = header[:level]
    next_header = headers[idx + 1]
    next_level = next_header ? next_header[:level] : 1

    # Close lists if we're going up in level
    while !stack.empty? && stack.last >= level
      toc_html += '</ul></li>'
      stack.pop
    end

    # If next item is deeper, this item will have children
    if next_level > level
      toc_html += "<li><a href=\"##{header[:id]}\">#{header[:text]}</a><ul>"
      stack.push(level)
    else
      toc_html += "<li><a href=\"##{header[:id]}\">#{header[:text]}</a></li>"
    end
  end

  # Close all remaining lists
  while !stack.empty?
    toc_html += '</ul></li>'
    stack.pop
  end

  toc_html += '</ul>'
  toc_html += '</nav>'
  toc_html
end

#generate_settings_tableObject



547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
# File 'ext/apex_ext/apex_src/documentation/generate_app_docs.rb', line 547

def generate_settings_table
  # Extract settings from transformed files by scanning for "Settings->" references
  settings_referenced = Set.new

  APP_PAGES.each do |page_name|
    file_name = "#{page_name}.md"
    md_file = File.join(TRANSFORMED_DIR, file_name)
    next unless File.exist?(md_file)

    content = File.read(md_file)
    # Find all Settings-> references
    content.scan(/Settings->[^\s\)\]]+/i) do |match|
      settings_referenced.add(match)
    end
  end

  settings_paths = settings_referenced.to_a.sort

  # Organize by category
  categories = {
    'General' => [],
    'Processor' => [],
    'Output' => [],
    'Developer' => [],
    'About/Help' => [],
    'Other' => []
  }

  settings_paths.each do |setting|
    if setting =~ /^Settings->General/
      categories['General'] << setting.gsub('Settings->General->', '')
    elsif setting =~ /^Settings->Processor/
      categories['Processor'] << setting.gsub('Settings->Processor->', '')
    elsif setting =~ /^Settings->Output/
      categories['Output'] << setting.gsub('Settings->Output->', '')
    elsif setting =~ /^Settings->Developer/
      categories['Developer'] << setting.gsub('Settings->Developer->', '')
    elsif setting =~ /^(About Apex|Help->)/
      categories['About/Help'] << setting
    else
      categories['Other'] << setting
    end
  end

  # Also add menu items
  menu_items = ['Plugins menu', 'Help menu', 'About window']

  markdown = <<~MD
# Apex App Settings Reference

This document lists all Settings referenced in the app-focused documentation that would need to be implemented in the Apex app.

## Settings Structure

### General

| Setting | Type | Description |
|---------|------|-------------|
#{categories['General'].map { |s| "| #{s} | Checkbox/Toggle | Enable or disable #{s.downcase} |" }.join("\n")}

### Processor

| Setting | Type | Description |
|---------|------|-------------|
#{categories['Processor'].map { |s| "| #{s} | Varies | Configure #{s.downcase} |" }.join("\n")}

### Output

| Setting | Type | Description |
|---------|------|-------------|
#{categories['Output'].map { |s| "| #{s} | Varies | Configure #{s.downcase} |" }.join("\n")}

#{categories['Developer'].any? ? "### Developer\n\n| Setting | Type | Description |\n|---------|------|-------------|\n#{categories['Developer'].map { |s| "| #{s} | Varies | Configure #{s.downcase} |" }.join("\n")}\n" : ""}
#{categories['About/Help'].any? ? "### About/Help\n\n| Setting | Type | Description |\n|---------|------|-------------|\n#{categories['About/Help'].map { |s| "| #{s} | Menu | Access #{s.downcase} |" }.join("\n")}\n" : ""}
#{categories['Other'].any? ? "### Other\n\n| Setting | Type | Description |\n|---------|------|-------------|\n#{categories['Other'].map { |s| "| #{s} | Varies | Configure #{s.downcase} |" }.join("\n")}\n" : ""}
## Menu Items

| Menu Item | Location | Description |
|-----------|---------|-------------|
#{menu_items.map { |m| "| #{m} | Main menu | Access #{m.downcase} |" }.join("\n")}

## Notes

- Settings should be organized in a Settings window with the structure: **Settings->Category->Setting Name**
- Boolean settings (checkboxes/toggles) should have clear on/off states
- File selection settings should provide a file picker dialog
- Text input settings should have appropriate validation
- Settings should be saved per-document or globally (user preference)

MD

  markdown
end

#generate_single_page_docsetObject



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'ext/apex_ext/apex_src/documentation/generate_docset.rb', line 70

def generate_single_page_docset
  puts "Generating single-page docset using mmd2cheatset..."

  # Ensure output directory exists
  FileUtils.mkdir_p(DOCSETS_DIR)

  # Change to docsets directory for output
  original_dir = Dir.pwd
  Dir.chdir(DOCSETS_DIR)

  # Create a cheatsheet markdown file for command-line options
  cheatsheet_md = <<~MARKDOWN
    title: Apex Command Line Options
    name: ApexCLI
    keyword: apex

    Complete reference for all Apex command-line flags and options.

    | name | command | note |
    |------|---------|------|
    | Help | -h, --help | Display help message and exit |
    | Version | -v, --version | Display version information and exit |
    | Progress | --[no-]progress | Show progress indicator during processing |
    | Combine | --combine | Concatenate Markdown files into a single stream |
    | MMD Merge | --mmd-merge | Merge files from MultiMarkdown index files |
    | Mode | -m, --mode MODE | Set processor mode (commonmark, gfm, mmd, kramdown, unified) |
    | Output | -o, --output FILE | Write output to a file instead of stdout |
    | Standalone | -s, --standalone | Generate complete HTML document |
    | Style | --style FILE, --css FILE | Link to a CSS file in document head |
    | Embed CSS | --embed-css | Embed CSS file contents into style tag |
    | Script | --script VALUE | Inject script tags (mermaid, mathjax, katex, etc.) |
    | Title | --title TITLE | Set the document title |
    | Pretty | --pretty | Pretty-print HTML with indentation |
    | ARIA | --aria | Add ARIA labels and accessibility attributes |
    | Plugins | --plugins, --no-plugins | Enable or disable plugin processing |
    | List Plugins | --list-plugins | List installed and available plugins |
    | Install Plugin | --install-plugin ID-or-URL | Install a plugin by ID or URL |
    | Uninstall Plugin | --uninstall-plugin ID | Uninstall a locally installed plugin |
    | ID Format | --id-format FORMAT | Set header ID generation format (gfm, mmd, kramdown) |
    | No IDs | --no-ids | Disable automatic header ID generation |
    | Header Anchors | --header-anchors | Generate anchor tags instead of id attributes |
    | Relaxed Tables | --relaxed-tables | Enable relaxed table parsing |
    | No Relaxed Tables | --no-relaxed-tables | Disable relaxed table parsing |
    | Captions | --captions POSITION | Control table caption position (above, below) |
    | No Tables | --no-tables | Disable table support entirely |
    | Alpha Lists | --[no-]alpha-lists | Control alphabetic list markers (a., b., c.) |
    | Mixed Lists | --[no-]mixed-lists | Control mixed list marker types |
    | No Footnotes | --no-footnotes | Disable footnote support |
    | No Smart | --no-smart | Disable smart typography |
    | No Math | --no-math | Disable math support |
    | Autolink | --[no-]autolink | Control automatic linking of URLs and email addresses |
    | Obfuscate Emails | --obfuscate-emails | Obfuscate email links by hex-encoding |
    | Includes | --includes, --no-includes | Enable or disable file inclusion features |
    | Embed Images | --embed-images | Embed local images as base64 data URLs |
    | Base Dir | --base-dir DIR | Set base directory for resolving relative paths |
    | Transforms | --[no-]transforms | Control metadata variable transforms |
    | Meta File | --meta-file FILE | Load metadata from external file |
    | Meta | --meta KEY=VALUE | Set metadata from command line |
    | Bibliography | --bibliography FILE | Specify bibliography file (BibTeX, CSL JSON, CSL YAML) |
    | CSL | --csl FILE | Specify Citation Style Language file |
    | No Bibliography | --no-bibliography | Suppress bibliography output |
    | Link Citations | --link-citations | Link citations to bibliography entries |
    | Show Tooltips | --show-tooltips | Enable tooltips on citations when hovering |
    | Indices | --indices | Enable index processing |
    | No Indices | --no-indices | Disable index processing entirely |
    | No Index | --no-index | Suppress index generation while creating markers |
    | Hardbreaks | --hardbreaks | Treat newlines as hard breaks (GFM style) |
    | Sup Sub | --[no-]sup-sub | Control MultiMarkdown-style superscript and subscript |
    | Divs | --[no-]divs | Control Pandoc fenced divs syntax |
    | Spans | --[no-]spans | Control Pandoc-style bracketed spans |
    | Emoji Autocorrect | --[no-]emoji-autocorrect | Control emoji name autocorrect |
    | Unsafe | --[no-]unsafe | Control whether raw HTML is allowed |
    | Wikilinks | --[no-]wikilinks | Control wiki link syntax |
    | Wikilink Space | --wikilink-space MODE | Control how spaces in wiki links are handled |
    | Wikilink Extension | --wikilink-extension EXT | Add file extension to wiki link URLs |
    | Accept | --accept | Accept all Critic Markup changes |
    | Reject | --reject | Reject all Critic Markup changes |
    [Command Line Options]

    ---

    For complete documentation, see the Apex wiki at https://github.com/ApexMarkdown/apex/wiki
  MARKDOWN

  File.write('Apex_Command_Line_Options.md', cheatsheet_md)

  if File.exist?(MMD2CHEATSET)
    system("#{MMD2CHEATSET} Apex_Command_Line_Options.md")
    FileUtils.rm('Apex_Command_Line_Options.md')
    puts "\nSingle-page docset generated successfully!"
    puts "Docset location: #{File.join(DOCSETS_DIR, "ApexCLI.docset")}"
  else
    puts "Error: mmd2cheatset.rb not found at #{MMD2CHEATSET}"
    puts "Please check the path or install mmd2cheatset"
    Dir.chdir(original_dir)
    exit 1
  end

  # Return to original directory
  Dir.chdir(original_dir)
end

#highlight_code_blocks(html_content) ⇒ Object



172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# File 'ext/apex_ext/apex_src/documentation/generate_docset.rb', line 172

def highlight_code_blocks(html_content)
  return html_content unless ROUGE_AVAILABLE

  code_block_count = 0
  highlighted_count = 0
  error_count = 0

  begin
    # Find all code blocks - handle various formats
    # Apex outputs: <pre lang="language"><code>...</code></pre>
    # Or: <pre><code class="language-xxx">...</code></pre>
    # Use non-greedy matching and ensure we match complete code blocks
    # Match <pre> tag (with optional lang attribute), then <code> tag (with optional class), then content, then closing tags
    result = html_content.gsub(/<pre(\s+lang=["']([^"']+)["'])?[^>]*>\s*<code(\s+class=["'](?:language-)?([^"'\s]+)["'])?[^>]*>([\s\S]*?)<\/code>\s*<\/pre>/i) do |match|
      code_block_count += 1
      lang = $2 || $4  # Get lang from <pre lang="..."> ($2) or class from <code class="..."> ($4)
      code = $5

      # Skip if code is empty or suspiciously large (might be a regex error)
      if code.nil? || code.empty? || code.length > 100000
        match
      else
        begin
          # Unescape HTML entities
          code = code.gsub(/&lt;/, '<').gsub(/&gt;/, '>').gsub(/&amp;/, '&').gsub(/&quot;/, '"')

          if lang && !lang.empty?
            lang_normalized = lang.downcase
            # Handle common language aliases
            lang_aliases = {
              'yml' => 'yaml',
              'md' => 'markdown',
              'mkd' => 'markdown',
              'mkdn' => 'markdown',
              'mdown' => 'markdown'
            }
            lang_normalized = lang_aliases[lang_normalized] || lang_normalized

            lexer = Rouge::Lexer.find(lang_normalized)
            if lexer.nil?
              puts "  Warning: No lexer found for language '#{lang}' (normalized: '#{lang_normalized}'), using PlainText"
              lexer = Rouge::Lexers::PlainText
            end
          else
            lexer = Rouge::Lexers::PlainText
          end

          formatter = Rouge::Formatters::HTML.new
          highlighted = formatter.format(lexer.lex(code))
          highlighted_count += 1

          # Return highlighted code with proper classes
          "<pre><code class=\"highlight #{lang ? "language-#{lang}" : ''}\">#{highlighted}</code></pre>"
        rescue => e
          error_count += 1
          # If highlighting fails, return original
          puts "  Warning: Failed to highlight code block (lang: #{lang || 'none'}): #{e.class}: #{e.message}"
          puts "  Backtrace: #{e.backtrace.first(2).join(' | ')}" if $DEBUG
          match
        end
      end
    end

    if code_block_count > 0
      puts "  Highlighted #{highlighted_count}/#{code_block_count} code blocks"
      puts "  Errors: #{error_count}" if error_count > 0
    end

    result
  rescue => e
    puts "Warning: Error in highlight_code_blocks: #{e.class}: #{e.message}"
    puts "Backtrace: #{e.backtrace.first(3).join("\n")}" if $DEBUG
    puts "Returning original HTML without highlighting"
    html_content
  end
end


524
525
526
527
528
529
530
531
532
533
# File 'ext/apex_ext/apex_src/documentation/generate_docset.rb', line 524

def inject_footer_into_html(html_content, footer_html)
  return html_content if footer_html.empty?

  # Inject footer before closing body tag
  if html_content =~ /(<\/body>)/i
    html_content = html_content.sub(/(<\/body>)/i, "#{footer_html}\\1")
  end

  html_content
end

#inject_toc_into_html(html_content, main_toc_html, page_toc_html) ⇒ Object



325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
# File 'ext/apex_ext/apex_src/documentation/generate_docset.rb', line 325

def inject_toc_into_html(html_content, main_toc_html, page_toc_html)
  # Load shared CSS
  shared_css_file = File.join(SCRIPT_DIR, 'shared_styles.css')
  shared_css = File.exist?(shared_css_file) ? File.read(shared_css_file) : ''
  toc_css = "<style>\n#{shared_css}\n</style>"

  # Inject CSS into head (Rouge CSS is included in shared_styles.css)
  combined_css = toc_css
  if html_content =~ /(<\/head>)/i
    html_content = html_content.sub(/(<\/head>)/i, "#{combined_css}\\1")
  elsif html_content =~ /(<\/style>)/i
    html_content = html_content.sub(/(<\/style>)/i, "\\1\n#{combined_css}")
  end

  # Inject hamburger menu and mobile overlay
  hamburger_html = <<~HTML
    <button class="hamburger-menu" id="hamburger-menu" aria-label="Toggle navigation"></button>
    <div class="mobile-menu-overlay" id="mobile-menu-overlay"></div>
  HTML

  # Inject main TOC into body (left sidebar)
  if html_content =~ /(<body[^>]*>)/i
    html_content = html_content.sub(/(<body[^>]*>)/i, "\\1\n#{hamburger_html}\n#{main_toc_html}")
  else
    puts "Warning: Could not find <body> tag to inject main TOC"
  end

  # Inject page TOC at top of content (after first h1, or at start of body if no h1)
  # Handle multi-line h1 tags (from pretty printing)
  # Add ID to page TOC for scroll detection
  unless page_toc_html.empty?
    page_toc_with_id = page_toc_html.sub(/<nav class="page-toc">/, '<nav class="page-toc" id="page-toc-top">')

    if html_content =~ /<h1[^>]*>[\s\S]*?<\/h1>/i
      html_content = html_content.sub(/(<h1[^>]*>[\s\S]*?<\/h1>)/i, "\\1\n#{page_toc_with_id}")
    elsif html_content =~ /(<body[^>]*>)/i
      # If no h1, inject TOC right after body tag
      html_content = html_content.sub(/(<body[^>]*>)/i, "\\1\n#{page_toc_with_id}")
    else
      puts "Warning: Could not find <h1> or <body> tag to inject page TOC"
    end
  end

  # Add floating TOC HTML structure
  floating_toc_html = <<~HTML
    <div class="floating-toc" id="floating-toc">
      <div class="floating-toc-container">
        <div class="floating-toc-header">
          <span>Table of Contents 🔻</span>
        </div>
        <div class="floating-toc-content" id="floating-toc-content">
          <!-- Content will be populated by JavaScript -->
        </div>
      </div>
    </div>
  HTML

  # Inject floating TOC after body tag
  if html_content =~ /(<body[^>]*>)/i
    html_content = html_content.sub(/(<body[^>]*>)/i, "\\1\n#{floating_toc_html}")
  end

  # Add JavaScript for floating TOC
  floating_toc_js = <<~JS
    <script>
      (function() {
        // Clone the page TOC for floating TOC
        function initFloatingTOC() {
          var pageTOC = document.getElementById('page-toc-top');
          var floatingTOCContent = document.getElementById('floating-toc-content');
          var floatingTOC = document.getElementById('floating-toc');

          if (!pageTOC || !floatingTOCContent || !floatingTOC) return;

          // Clone the TOC structure
          var tocClone = pageTOC.cloneNode(true);
          tocClone.id = 'floating-toc-clone';
          floatingTOCContent.appendChild(tocClone);

          // Update all links to use smooth scrolling
          var allTOCLinks = document.querySelectorAll('.page-toc a, .floating-toc-content a');
          allTOCLinks.forEach(function(link) {
            link.addEventListener('click', function(e) {
              var href = this.getAttribute('href');
              if (href && href.startsWith('#')) {
                e.preventDefault();
                var targetId = href.substring(1);
                var targetElement = document.getElementById(targetId);
                if (targetElement) {
                  var offset = 20; // Offset from top

                  // Function to calculate absolute position from document top
                  function getAbsoluteTop(element) {
                    var top = 0;
                    while (element) {
                      top += element.offsetTop;
                      element = element.offsetParent;
                    }
                    return top;
                  }

                  var absoluteTop = getAbsoluteTop(targetElement);
                  var scrollTop = window.pageYOffset || document.documentElement.scrollTop;
                  var offsetPosition = absoluteTop - offset;

                  // Only scroll if we're not already at the target position
                  if (Math.abs(scrollTop - offsetPosition) > 10) {
                    window.scrollTo({
                      top: Math.max(0, offsetPosition),
                      behavior: 'smooth'
                    });
                  }

                  // Update URL hash without triggering scroll
                  if (history.pushState) {
                    history.pushState(null, null, href);
                  }
                }
              }
            });
          });

          // Handle scroll to show/hide floating TOC
          var tocTop = pageTOC.getBoundingClientRect().top + window.pageYOffset;
          var tocBottom = tocTop + pageTOC.offsetHeight;

          function updateFloatingTOC() {
            var scrollY = window.pageYOffset || document.documentElement.scrollTop;

            if (scrollY > tocBottom) {
              floatingTOC.classList.add('visible');
            } else {
              floatingTOC.classList.remove('visible');
            }
          }

          // Throttle scroll events
          var ticking = false;
          window.addEventListener('scroll', function() {
            if (!ticking) {
              window.requestAnimationFrame(function() {
                updateFloatingTOC();
                ticking = false;
              });
              ticking = true;
            }
          });

          // Initial check
          updateFloatingTOC();
        }

        // Initialize when DOM is ready
        if (document.readyState === 'loading') {
          document.addEventListener('DOMContentLoaded', initFloatingTOC);
        } else {
          initFloatingTOC();
        }
      })();
    </script>
  JS

  # Load shared JavaScript
  shared_js_file = File.join(SCRIPT_DIR, 'shared_scripts.js')
  shared_js = File.exist?(shared_js_file) ? File.read(shared_js_file) : ''

  # Combine shared JS and floating TOC JS
  combined_js = "<script>\n#{shared_js}\n</script>\n#{floating_toc_js}"

  # Inject JavaScript before closing body tag
  if html_content =~ /(<\/body>)/i
    html_content = html_content.sub(/(<\/body>)/i, "#{combined_js}\\1")
  end

  html_content
end

#markdown_to_html(s) ⇒ Object



10
11
12
13
# File 'ext/apex_ext/apex_src/vendor/cmark-gfm/wrappers/wrapper.rb', line 10

def markdown_to_html(s)
  len = s.bytesize
  CMark::cmark_markdown_to_html(s, len, 0)
end

#normalize_suite(suite) ⇒ Object



9
10
11
12
13
14
15
16
# File 'ext/apex_ext/apex_src/tests.rb', line 9

def normalize_suite(suite)
  files = Dir.glob("tests/test_*.rb")
  suite = suite.split('').join('.*')
  if suite && !suite.empty?
    files.delete_if { |f| f !~ /#{suite}/  }
  end
  files.map { |f| File.basename(f, '.rb')  }
end


502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
# File 'ext/apex_ext/apex_src/documentation/generate_docset.rb', line 502

def parse_footer
  footer_file = File.join(WIKI_DIR, '_Footer.md')
  return '' unless File.exist?(footer_file)

  # Convert markdown to HTML using Apex
  footer_html_content = `#{APEX_BIN} "#{footer_file}" 2>/dev/null`

  if $?.success? && !footer_html_content.empty?
    footer_html = '<footer class="page-footer">'
    footer_html += footer_html_content.strip
    footer_html += '</footer>'
    footer_html
  else
    # Fallback: simple text conversion
    footer_content = File.read(footer_file)
    footer_html = '<footer class="page-footer">'
    footer_html += '<p>' + footer_content.strip.gsub(/\n/, '<br>') + '</p>'
    footer_html += '</footer>'
    footer_html
  end
end

#parse_sidebarObject



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'ext/apex_ext/apex_src/documentation/generate_single_html.rb', line 51

def parse_sidebar
  sidebar_file = File.join(WIKI_DIR, '_Sidebar.md')
  pages = []

  # Always add Home first
  pages << { name: 'Home', title: 'Home', file: 'Home.md' }

  if File.exist?(sidebar_file)
    sidebar_content = File.read(sidebar_file)
    sidebar_content.scan(/\[([^\]]+)\]\(([^)]+)\)/) do |text, link|
      page_name = link.gsub(/\.md$/, '')
      next if page_name.downcase == 'home'
      pages << { name: page_name, title: text, file: "#{page_name}.md" }
    end
  end

  pages
end

#parse_sidebar_tocObject



262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
# File 'ext/apex_ext/apex_src/documentation/generate_docset.rb', line 262

def parse_sidebar_toc
  sidebar_file = File.join(WIKI_DIR, '_Sidebar.md')
  toc_html = '<nav class="main-toc">'
  toc_html += '<ul>'

  # Always add Home first
  toc_html += '<li><a href="Home.html">Home</a></li>'

  if File.exist?(sidebar_file)
    sidebar_content = File.read(sidebar_file)
    # Parse markdown links: [Text](PageName) or [Text](Page-Name)
    sidebar_content.scan(/\[([^\]]+)\]\(([^)]+)\)/) do |text, link|
      # Remove .md extension if present, add .html
      page_name = link.gsub(/\.md$/, '')
      # Skip if it's Home (already added)
      next if page_name.downcase == 'home'
      toc_html += "<li><a href=\"#{page_name}.html\">#{text}</a></li>"
    end
  end

  toc_html += '</ul>'
  toc_html += '</nav>'
  toc_html
end

#render_kbd(markup, use_key_symbol: true, use_mod_symbol: true, use_plus: false) ⇒ Object



174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'ext/apex_ext/apex_src/examples/kbd_plugin.rb', line 174

def render_kbd(markup, use_key_symbol: true, use_mod_symbol: true, use_plus: false)
  combos = []

  markup.split(%r{ / }).each do |combo|
    mods = []
    key = ''
    combo.clean_combo!
    combo.strip.split(//).each do |char|
      next if char == ' '
      case char
      when /[⌃⇧⌥⌘]/
        mods << char
      when /[*\^$@~%]/
        mods << char.to_mod
      else
        key << char
      end
    end
    mods = sort_mods(mods)
    title = ''
    if key.length == 1
      if mods.empty? && (key =~ /[A-Z]/ || key.upper?)
        mods << '$'.to_mod
      end
      key = key.lower_to_upper if mods.include?('$'.to_mod)
      key = key.upcase
      title = key.clarify_characters
    elsif mods.include?('$'.to_mod)
      key = key.lower_to_upper
    end
    key.gsub!(/"/, '&quot;')
    combos << { mods: mods, key: key, title: title }
  end

  outputs = combos.map do |combo|
    next if combo[:mods].empty? && combo[:key].empty?
    kbds = []
    title = []

    combo[:mods].each do |mod|
      mod_class = use_mod_symbol ? 'mod symbol' : 'mod'
      kbds << %(<kbd class="#{mod_class}">#{mod.mod_to_ent(use_mod_symbol)}</kbd>)
      title << mod.mod_to_title
    end

    unless combo[:key].empty?
      key, keytitle = combo[:key].name_to_ent(use_key_symbol)
      key_class = use_key_symbol ? 'key symbol' : 'key'
      keytitle = keytitle.clarify_characters if keytitle.length == 1
      kbds << %(<kbd class="#{key_class}">#{key}</kbd>)
      title << keytitle
    end

    kbd = if use_mod_symbol
            use_plus ? kbds.join('<span class="keycombo combiner">+</span>') : kbds.join
          else
            kbds.join('-')
          end
    span_class = "keycombo #{use_mod_symbol && !use_plus ? 'combined' : 'separated'}"
    %(<span class="#{span_class}" title="#{title.join('-')}">#{kbd}</span>)
  end.compact

  outputs.join('<span class="keycombo separator">/</span>')
end

#require_cmark_gfm!Object



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'ext/apex_ext/extconf.rb', line 74

def require_cmark_gfm!
  if ENV['APEX_SHOW_CMARK_ERROR']
    warn CMARK_NOT_FOUND_MSG
    abort
  end

  if pkg_config('cmark-gfm')
    # pkg-config has already added the right flags (and usually rpath).
    return true
  end

  have_header('cmark-gfm-core-extensions.h')
  unless have_library('cmark-gfm')
    abort CMARK_NOT_FOUND_MSG
  end

  # Embed rpath so the .bundle finds libcmark-gfm at load time (macOS often
  # ignores DYLD_LIBRARY_PATH for dlopen'd dependencies).
  cmark_lib = `brew --prefix cmark-gfm 2>/dev/null`.strip
  cmark_lib = File.join(cmark_lib, 'lib') if !cmark_lib.empty? && Dir.exist?(File.join(cmark_lib, 'lib'))
  if RbConfig::CONFIG['host_os'].to_s.include?('darwin') && !cmark_lib.empty?
    $LDFLAGS << " -Wl,-rpath,#{cmark_lib}"
  end

  true
end

#require_sqlite3Object

Only require sqlite3 for multi-page docset



30
31
32
33
34
35
36
# File 'ext/apex_ext/apex_src/documentation/generate_docset.rb', line 30

def require_sqlite3
  require 'sqlite3'
rescue LoadError
  puts "Error: sqlite3 gem is required for multi-page docset generation"
  puts "Install it with: gem install sqlite3"
  exit 1
end

#sort_mods(mods) ⇒ Object



239
240
241
242
# File 'ext/apex_ext/apex_src/examples/kbd_plugin.rb', line 239

def sort_mods(mods)
  order = ['Fn', '', '', '', '']
  mods.uniq.sort { |a, b| order.index(a) < order.index(b) ? -1 : 1 }
end

#transform_with_ai(markdown_content, page_name) ⇒ Object



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'ext/apex_ext/apex_src/documentation/generate_app_docs_ai.rb', line 66

def transform_with_ai(markdown_content, page_name)
  ai_tool = find_ai_agent

  if ai_tool.nil?
    puts "  Warning: No AI agent found. Falling back to basic transformation."
    return basic_transform(markdown_content)
  end

  puts "  Using #{ai_tool[0]} to transform content..."

  # Read the prompt template
  prompt_template = File.read(PROMPT_FILE)

  # Create full prompt
  full_prompt = "#{prompt_template}\n\n---\n\n#{markdown_content}"

  # Write prompt to temp file
  temp_prompt = File.join(ENV['TMPDIR'] || ENV['TMP'] || ENV['TEMP'] || '/tmp', "apex_prompt_#{Process.pid}.md")
  File.write(temp_prompt, full_prompt)

  # Try to use the AI tool
  # This is a generic approach - adjust based on your AI tool's interface
  begin
    # For cursor-agent, you might need to adjust this command
    result = `#{ai_tool[0]} "#{temp_prompt}" 2>&1`

    if $?.success? && !result.empty?
      File.delete(temp_prompt) if File.exist?(temp_prompt)
      return result
    else
      puts "  Warning: AI transformation failed, falling back to basic transformation."
      File.delete(temp_prompt) if File.exist?(temp_prompt)
      return basic_transform(markdown_content)
    end
  rescue => e
    puts "  Error using AI tool: #{e.message}"
    puts "  Falling back to basic transformation."
    File.delete(temp_prompt) if File.exist?(temp_prompt)
    return basic_transform(markdown_content)
  end
end