All files / src/pipeline/rendering render-spec-document.js

0% Statements 0/79
0% Branches 0/27
0% Functions 0/6
0% Lines 0/76

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 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 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 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                                                                                                                                                                                                                                                                                                                                                                                                                         
/**
 * Handles the core rendering logic for a single spec.
 * Processes markdown files, renders HTML, applies fixes, and writes output.
 * It takes shared variables and config as parameters to maintain modularity.
 */
 
const fs = require('fs-extra');
const path = require('node:path');
 
const { fetchExternalSpecs, validateReferences, mergeXrefTermsIntoAllXTrefs } = require('../references/external-references-service.js');
const { processEscapedTags, restoreEscapedTags } = require('../preprocessing/escape-placeholder-utils.js');
const { sortDefinitionTermsInHtml, fixDefinitionListStructure } = require('../postprocessing/definition-list-postprocessor.js');
const { getGithubRepoInfo, getCurrentBranch } = require('../../utils/git-info.js');
const { templateTags } = require('../../utils/regex-patterns.js');
 
const { createScriptElementWithXTrefDataForEmbeddingInHtml, applyReplacers } = require('./render-utils.js');
const { warnOnHeadingHierarchyViolations } = require('./heading-hierarchy-validator.js');
const { copyStaticRoot } = require('./copy-static-root.js');
 
async function render(spec, assets, sharedVars, config, template, assetsGlobal, Logger, md, externalSpecsList) {
  let { externalReferences } = sharedVars;
  let renderedHtml;
 
  try {
    globalThis.noticeTitles = {};
    globalThis.specGroups = {};
    Logger.info('Rendering: ' + spec.title);
 
    function interpolate(template, variables) {
      return template.replace(templateTags.variableInterpolation, (match, p1) => variables[p1.trim()]);
    }
 
    // Add current date in 'DD Month YYYY' format for template injection
    const date = new Date();
    const day = String(date.getDate()).padStart(2, '0');
    const month = date.toLocaleString('en-US', { month: 'long' });
    const year = date.getFullYear();
    const currentDate = `${day} ${month} ${year}`;
 
    // Add universal timestamp in ISO 8601 format for template injection
    const universalTimestamp = date.toISOString();
 
    // Get the branch name from which the index.html was generated
    const buildBranch = getCurrentBranch();
 
    // Read spec-up-t version from its own package.json for build info display
    const specUpTVersion = require('../../../package.json').version;
 
    // Capture Node.js runtime version for build info display
    const nodeVersion = process.version;
 
    // Read all markdown files into an array
    const docs = await Promise.all(
      (spec.markdown_paths || ['spec.md']).map(_path =>
        fs.readFile(spec.spec_directory + _path, 'utf8')
      )
    );
 
    const features = (({ source, logo }) => ({ source, logo }))(spec);
    if (spec.external_specs && !externalReferences) {
      // Fetch xref terms and merge them into allXTrefs instead of creating DOM HTML
      const xrefTerms = await fetchExternalSpecs(spec);
 
      // Define paths for the xtrefs data files
      const outputPathJSON = path.join('.cache', 'xtrefs-data.json');
      const outputPathJS = path.join('.cache', 'xtrefs-data.js');
 
      // Merge xref terms into the unified allXTrefs structure
      await mergeXrefTermsIntoAllXTrefs(xrefTerms, outputPathJSON, outputPathJS);
 
      // Set flag to indicate external references have been processed
      externalReferences = true; // Changed from HTML array to boolean flag
    }
 
    // Find the index of the terms-and-definitions-intro.md file
    const termsIndex = (spec.markdown_paths || ['spec.md']).indexOf('terms-and-definitions-intro.md');
    if (termsIndex !== -1) {
      // Marker used as the starting point of the terminology index. The newlines are essential for the correct rendering of the markdown.
      docs[termsIndex] += '\n\n<div id="terminology-section-start"></div>\n\n';
    }
 
    // Set up file tracking for definitions before rendering
    for (let i = 0; i < docs.length; i++) {
      globalThis.currentFile = spec.markdown_paths[i] || 'unknown';
      docs[i] = `<!-- file: ${globalThis.currentFile} -->\n${docs[i]}`;
    }
 
    // Concatenate all file contents into one string, separated by newlines
    let doc = docs.join("\n");
 
    // Handles backslash escape mechanism for substitution tags
    // Phase 1: Pre-processing - Handle escaped tags
    doc = processEscapedTags(doc);
 
    // Handles backslash escape mechanism for substitution tags
    // Phase 2: Tag Processing - Apply normal substitution logic
    doc = applyReplacers(doc);
 
    md[spec.katex ? "enable" : "disable"](['math_block', 'math_inline']);
 
    // Suppress markdown-it-attrs table errors during rendering
    // 
    // Background: markdown-it-attrs can throw "Cannot read properties of null (reading 'colsnum')" 
    // errors when processing malformed tables. These errors are:
    // 1. Non-blocking: The rendering continues and index.html is generated successfully
    // 2. Caught proactively: The markdown-tables healthcheck detects these issues
    // 3. Noisy: They clutter the console output without providing actionable information
    //
    // Solution: Intercept console.error during md.render() to suppress only these specific
    // table-related errors while allowing all other errors to pass through normally.
    const originalConsoleError = console.error;
    let suppressNext = false;
    console.error = (...args) => {
      const message = args[0]?.toString() || '';
 
      // Check if this is the first console.error call with the error message
      if (message.includes('markdown-it-attrs: Error in pattern') &&
        message.includes('tables') &&
        message.includes('colsnum')) {
        // Suppress this error and the next stack trace
        suppressNext = true;
        return;
      }
 
      // Check if this is the stack trace following the suppressed error
      if (suppressNext) {
        suppressNext = false;
        // Also suppress if this looks like a stack trace for colsnum error
        if (message.includes('colsnum') || message.includes('at Object.transform')) {
          return;
        }
      }
 
      // Let all other errors through
      originalConsoleError.apply(console, args);
    };
 
    try {
      // `render` is the rendered HTML
      renderedHtml = md.render(doc);
    } finally {
      // Always restore console.error
      console.error = originalConsoleError;
    }
 
    // Apply the fix for broken definition list structures
    renderedHtml = fixDefinitionListStructure(renderedHtml);
 
    // Sort definition terms case-insensitively before final rendering
    renderedHtml = sortDefinitionTermsInHtml(renderedHtml);
 
    // Handles backslash escape mechanism for substitution tags
    // Phase 3: Post-processing - Restore escaped sequences as literals
    renderedHtml = restoreEscapedTags(renderedHtml);
 
    // Warn about heading hierarchy violations (W3C accessibility)
    warnOnHeadingHierarchyViolations(renderedHtml, Logger);
 
    const templateInterpolated = interpolate(template, {
      title: spec.title,
      description: spec.description,
      author: spec.author,
      toc: globalThis.toc,
      render: renderedHtml,
      assetsHead: assets.head,
      assetsBody: assets.body,
      assetsSvg: assets.svg,
      xtrefsData: createScriptElementWithXTrefDataForEmbeddingInHtml(),
      specLogo: spec.logo,
      specFavicon: spec.favicon,
      specLogoLink: spec.logo_link,
      spec: JSON.stringify(spec),
      externalSpecsList: externalSpecsList,
      currentDate: currentDate,
      universalTimestamp: universalTimestamp,
      buildBranch: buildBranch,
      specUpTVersion: specUpTVersion,
      nodeVersion: nodeVersion,
      githubRepoInfo: getGithubRepoInfo(spec)
    });
 
    const outputPath = path.join(spec.destination, 'index.html');
    Logger.info('Attempting to write to:', outputPath);
 
    // Use promisified version instead of callback
    await fs.promises.writeFile(outputPath, templateInterpolated, 'utf8');
    Logger.success(`Successfully wrote ${outputPath}`);
 
    // Copy any deployment root files (CNAME, robots.txt, …) from spec/static-root/
    copyStaticRoot(spec.spec_directory, spec.destination);
 
    validateReferences(globalThis.references, globalThis.definitions, renderedHtml);
    globalThis.references = [];
    globalThis.definitions = [];
  } catch (e) {
    Logger.error("Render error: " + e.message);
    throw e;
  }
 
  // Update sharedVars
  sharedVars.externalReferences = externalReferences;
}
 
module.exports = { render };