All files / src/markdown-it template-tag-syntax.js

82.92% Statements 34/41
47.82% Branches 11/23
100% Functions 4/4
82.5% Lines 33/40

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    1x                                             1x   1x 1x 1x         1x                                                     1x     1x         1x 1x         1x   1x   1x     1x 1x         1x 1x   1x         1x       1x 1x 1x       1x 1x       1x 1x               4x                             4x 1x 1x     1x             1x       1x
'use strict';
 
const { ESCAPED_PLACEHOLDER } = require('../pipeline/preprocessing/escape-processor.js');
 
/**
 * Markdown-it Template-Tag Syntax Module
 * 
 * This module adds support for custom template-tag syntax like [[ref:spec,term]] in markdown.
 * It handles the parsing and rendering of these special constructs.
 * 
 * Template-tag syntax format: [[type:arg1,arg2,arg3]]
 * Examples:
 * - [[def:term,alias]] - Define a term with an alias
 * - [[tref:external-spec,term]] - Reference a term from external specification
 * - [[xref:spec,term]] - Cross-reference to another specification
 * 
 * The module works in two phases:
 * 1. PARSING: Scans markdown text for [[...]] patterns and creates template-tag tokens
 * 2. RENDERING: Converts template-tag tokens to final HTML using template-tag handlers
 */
 
/**
 * Configuration constants for template-tag syntax
 * These define the delimiters and parsing rules for template-tag markers
 */
const { templateTags } = require('../utils/regex-patterns');
 
const levels = 2;                         // Number of bracket characters (e.g., 2 = [[]])
const openString = '['.repeat(levels);    // Opening delimiter: '[['
const closeString = ']'.repeat(levels);   // Closing delimiter: ']]'
 
// Regular expression to extract template-tag components from [[type:args]] syntax
// Group 1: template-tag type (e.g., 'ref', 'tref', 'def')
// Group 2: arguments (everything after the colon, comma-separated)
const contentRegex = templateTags.content;
 
/**
 * Applies template-tag syntax support to a markdown-it instance
 * 
 * @param {Object} md - The markdown-it instance to enhance
 * @param {Array} templates - Array of template-tag handler objects
 * 
 * Each template-tag handler should have:
 * - filter(type): function that returns true if this handler processes the given type
 * - parse(token, type, ...args): optional preprocessing function
 * - render(token, type, ...args): function that returns HTML string
 */
function applyTemplateTagSyntax(md, templates = []) {
  
  /**
   * Custom inline parsing rule for template-tag syntax
   * 
   * This rule is added to markdown-it's inline ruler (which processes inline elements)
   * and runs after the 'emphasis' rule to handle our [[template-tag]] syntax.
   * 
   * @param {Object} state - Parser state object containing source text and position
   * @param {Boolean} silent - If true, don't modify state (used for validation)
   * @returns {Boolean} true if template-tag was found and processed, false otherwise
   */
  function templates_ruler(state, silent) {
    // Get current position in the source text
    var start = state.pos;
 
    // Skip processing if we're at an escaped placeholder (handled by escape mechanism)
    Iif (state.src.slice(start, start + ESCAPED_PLACEHOLDER.length) === ESCAPED_PLACEHOLDER) {
      return false;
    }
 
    // Check if we're at the start of a template-tag marker [[
    let prefix = state.src.slice(start, start + levels);
    Iif (prefix !== openString) {
      return false; // Not a template-tag, let other rules handle it
    }
    
    // Find the matching closing marker ]]
    const indexOfClosingBrace = state.src.indexOf(closeString, start);
    
    Eif (indexOfClosingBrace > 0) {
      // Extract the content between [[ and ]]
      let templateTagContent = state.src.slice(start + levels, indexOfClosingBrace);
      
      // Parse the template-tag content using regex
      let match = contentRegex.exec(templateTagContent);
      Iif (!match) {
        return false; // Invalid template-tag syntax
      }
 
      // Extract template-tag type and arguments
      let type = match[1];
      let template = templates.find(t => t.filter(type) && t);
      
      Iif (!template) {
        return false; // No handler found for this template-tag type
      }
 
      // Parse arguments (comma-separated list)
      let args = match[2] ? match[2].trim().split(/\s*,+\s*/) : [];
      
      // Create a new template-tag token in the token stream
      // This token will be processed later during rendering
      let token = state.push('template', '', 0);
      token.content = match[0]; // Store original matched content
      token.info = { type, template, args }; // Store parsed information
 
      // If the template-tag has a parse function, call it for preprocessing
      // This allows template-tags to modify their content during parsing
      Eif (template.parse) {
        token.content = template.parse(token, type, ...args) || token.content;
      }
 
      // Move parser position past the template-tag
      state.pos = indexOfClosingBrace + levels;
      return true; // Template-tag successfully processed
    }
 
    return false; // No closing marker found
  }
 
  // Register the template-tag parsing rule with markdown-it
  // It runs after 'emphasis' to ensure proper precedence
  md.inline.ruler.after('emphasis', 'templates', templates_ruler);
 
  /**
   * Renderer for template-tag tokens
   * 
   * This function is called during the rendering phase to convert
   * template-tag tokens into their final HTML representation.
   * 
   * @param {Array} tokens - Array of all tokens being rendered
   * @param {Number} idx - Index of current template-tag token
   * @param {Object} options - Markdown-it options
   * @param {Object} env - Environment/context object
   * @param {Object} renderer - The renderer instance
   * @returns {String} HTML string for this template-tag
   */
  md.renderer.rules.template = function (tokens, idx, options, env, renderer) {
    let token = tokens[idx];
    let template = token.info.template;
    
    // Call the template-tag's render function if it exists
    Iif (template.render) {
      let result = template.render(token, token.info.type, ...token.info.args);
      // Return the rendered result, or fall back to original syntax if render fails
      return result || (openString + token.content + closeString);
    }
    
    // No render function - return the content as-is
    return token.content;
  };
}
 
module.exports = applyTemplateTagSyntax;