All files / src/lib/markdown-it/highlight HighlightRule.ts

100% Statements 76/76
100% Branches 45/45
100% Functions 16/16
100% Lines 73/73

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 206 207 208 209    22x 22x 22x 22x       22x                             25x 25x       6x 6x 6x 6x 13x 13x         27x 27x 27x 27x 137x 18x 18x     137x 4x     27x 27x   27x         18x   18x 24x 24x     4x     14x 14x       11x 7x 4x 4x 4x     3x 3x                       14x     14x 7x 7x       14x 14x   14x                       12x 12x 12x 12x   12x   10x 3x         7x   5x   5x   1x           4x               2x             12x                       19x   19x 26x 17x     8x 4x 4x 3x   1x     8x           9x                 19x       23x      
import { HighlightRuleComponent } from './HighlightRuleComponent.js';
 
export enum HIGHLIGHT_TYPES {
  WholeLine,
  WholeText,
  PartialText,
}
 
// Define color mappings
const COLOR_MAPPING: { [key: string]: string } = {
  r: 'var(--red)',
  g: 'var(--green)',
  b: 'var(--blue)',
  c: 'var(--cyan)',
  m: 'var(--magenta)',
  y: 'var(--yellow)',
  k: 'var(--black)',
  w: 'var(--white)',
};
 
export class HighlightRule {
  ruleComponents: HighlightRuleComponent[];
  color?: string;
  constructor(ruleComponents: HighlightRuleComponent[], color?: string) {
    this.ruleComponents = ruleComponents;
    this.color = color;
  }
 
  static parseAllRules(allRules: string, lineOffset: number, tokenContent: string) {
    const highlightLines = this.splitByChar(allRules, ',');
    const strArray = tokenContent.split('\n');
    strArray.pop(); // removes the last empty string
    return highlightLines
      .map(ruleStr => HighlightRule.parseRule(ruleStr, lineOffset, strArray))
      .filter(rule => rule) as HighlightRule[]; // discards invalid rules
  }
 
  // this function splits allRules by a splitter while ignoring the splitter if it is within quotes
  static splitByChar(allRules: string, splitter: string) {
    const highlightRules = [];
    let isWithinQuote = false;
    let currentPosition = 0;
    for (let i = 0; i < allRules.length; i += 1) {
      if (allRules.charAt(i) === splitter && !isWithinQuote) {
        highlightRules.push(allRules.substring(currentPosition, i));
        currentPosition = i + 1;
      }
      // Checks if the current character is not an unescaped quotation mark
      if (allRules.charAt(i) === '\'' && (i === 0 || allRules.charAt(i - 1) !== '\\')) {
        isWithinQuote = !isWithinQuote;
      }
    }
    if (currentPosition !== allRules.length) {
      highlightRules.push(allRules.substring(currentPosition));
    }
    return highlightRules;
  }
 
  static parseRule(ruleString: string, lineOffset: number, lines: string[]) {
    // Split by @ (e.g "1[:]@blue" -> ["1[:]", "blue"])
    const [rulePart, inputColor] = ruleString.split('@');
 
    const components = this.splitByChar(rulePart, '-')
      .map(compString => HighlightRuleComponent.parseRuleComponent(compString, lineOffset, lines));
    if (components.some(c => !c)) {
      // Not all components are properly parsed, which means
      // the rule itself is not proper
      return null;
    }
 
    const color: string = inputColor && COLOR_MAPPING[inputColor] ? COLOR_MAPPING[inputColor] : inputColor;
    return new HighlightRule(components as HighlightRuleComponent[], color);
  }
 
  shouldApplyHighlight(lineNumber: number) {
    const compares = this.ruleComponents.map(comp => comp.compareLine(lineNumber));
    if (this.isLineRange()) {
      const withinRangeStart = compares[0] <= 0;
      const withinRangeEnd = compares[1] >= 0;
      return withinRangeStart && withinRangeEnd;
    }
 
    const atLineNumber = compares[0] === 0;
    return atLineNumber;
  }
 
  getHighlightType(lineNumber: number): {
    highlightType: HIGHLIGHT_TYPES;
    bounds: Array<[number, number]> | null;
    color?: string;
  }[] {
    const results: {
      highlightType: HIGHLIGHT_TYPES;
      bounds: Array<[number, number]> | null;
      color?: string;
    }[] = [];
 
    // Handle line range logic if this is a line range
    if (this.isLineRange()) {
      const lineRangeResults = this.handleLineRange(lineNumber);
      results.push(...lineRangeResults);
    }
 
    // Now, handle rule components
    const ruleComponentResults = this.handleRuleComponent(lineNumber);
    results.push(...ruleComponentResults);
 
    return results;
  }
 
  handleLineRange(lineNumber: number): {
    highlightType: HIGHLIGHT_TYPES;
    bounds: Array<[number, number]> | null;
    color?: string;
  }[] {
    const results: {
      highlightType: HIGHLIGHT_TYPES;
      bounds: Array<[number, number]> | null;
      color?: string;
    }[] = [];
    const [startRule, endRule] = this.ruleComponents;
    const startLine = startRule.lineNumber;
    const endLine = endRule.lineNumber;
 
    if (lineNumber >= startLine && lineNumber <= endLine) {
      // If any component is an unbounded slice, highlight the whole line
      if (startRule.isUnboundedSlice() || endRule.isUnboundedSlice()) {
        results.push({
          highlightType: HIGHLIGHT_TYPES.WholeLine,
          bounds: null,
          color: this.color,
        });
      } else if (lineNumber === startLine || lineNumber === endLine) {
        // Apply the rule component for the start or end line
        const appliedRule = lineNumber === startLine ? startRule : endRule;
 
        if (appliedRule.isSlice && appliedRule.bounds.length > 0) {
          // If the rule has bounds, it's a PartialText highlight
          results.push({
            highlightType: HIGHLIGHT_TYPES.PartialText,
            bounds: appliedRule.bounds,
            color: this.color,
          });
        } else {
          results.push({
            highlightType: HIGHLIGHT_TYPES.WholeText,
            bounds: null,
            color: this.color,
          });
        }
      } else {
        // For lines within the range (not at the boundaries), apply WholeText
        results.push({
          highlightType: HIGHLIGHT_TYPES.WholeText,
          bounds: null,
          color: this.color,
        });
      }
    }
    return results;
  }
 
  handleRuleComponent(lineNumber: number): {
    highlightType: HIGHLIGHT_TYPES;
    bounds: Array<[number, number]> | null;
    color?: string;
  }[] {
    const results: {
      highlightType: HIGHLIGHT_TYPES;
      bounds: Array<[number, number]> | null;
      color?: string;
    }[] = [];
    // Iterate over all rule components to find matches for the current line
    this.ruleComponents.forEach((ruleComponent) => {
      if (ruleComponent.compareLine(lineNumber) === 0) {
        if (ruleComponent.isSlice) {
          let highlightType;
 
          if (ruleComponent.isUnboundedSlice()) {
            highlightType = HIGHLIGHT_TYPES.WholeLine;
          } else if (ruleComponent.bounds.length > 0) {
            highlightType = HIGHLIGHT_TYPES.PartialText;
          } else {
            highlightType = HIGHLIGHT_TYPES.WholeText;
          }
 
          results.push({
            highlightType,
            bounds: ruleComponent.isUnboundedSlice() ? null : ruleComponent.bounds,
            color: this.color,
          });
        } else {
          results.push({
            highlightType: HIGHLIGHT_TYPES.WholeText,
            bounds: null,
            color: this.color,
          });
        }
      }
    });
 
    return results;
  }
 
  isLineRange() {
    return this.ruleComponents.length === 2;
  }
}