# All files / src/lib/markdown-it/plugins/markdown-it-block-embed tokenizer.ts

86.15% Statements 56/65
62.5% Branches 10/16
100% Functions 4/4
94.11% Lines 48/51

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

    22x
    110x

    40x
    40x

    40x

    39x
    39x

    39x
    39x
    39x
    39x

    22x
    35x
    600x
    600x

    600x

    600x
    305x
    305x
    305x

    330x
    329x

    22x
    21x

    20x

    20x

    20x
    20x

    19x
    19x

    19x

    19x

    19x
    19x

    19x

    19x
    3x
    3x
    3x

    19x

    19x
    19x
    19x
    19x
    19x

    19x

    19x

    19x

     // Copyright (c) Rotorz Limited and portions by original markdown-it-video authors
    // Licensed under the MIT license. See LICENSE file in the project root.

    import StateBlock from 'markdown-it/lib/rules_block/state_block';

    /**
     * Interface for the service configuration used by the tokenizer.
     */
    export interface EmbedService {
      extractVideoID: (reference: string) => string | null;
      [key: string]: any;
    }

    export interface EmbedServiceMap {
      [key: string]: EmbedService;
    }

    interface Pointer {
      line: number;
      pos: number;
      prevPos?: number;
    }

    const SYNTAX_CHARS = ['@', '[', ']', '(', ')'];
    const SYNTAX_CODES = SYNTAX_CHARS.map(char => char.charCodeAt(0));

    /**
     * Advances the pointer to the next occurrence of a specific symbol.
     */
    function advanceToSymbol(state: StateBlock, endLine: number, symbol: string, pointer: Pointer): boolean {
      let symbolLine = pointer.line;
      let symbolIndex = state.src.indexOf(symbol, pointer.pos);

      if (symbolIndex === -1) return false;

      let maxPos = state.eMarks[pointer.line];
      while (symbolIndex >= maxPos) {
        ++symbolLine;
        maxPos = state.eMarks[symbolLine];

        Iif (symbolLine >= endLine) return false;
      }

      pointer.prevPos = pointer.pos;
      pointer.pos = symbolIndex;
      pointer.line = symbolLine;
      return true;
    }

    /**
     * Factory function to create the tokenizer with access to configured services.
     */
    export function createTokenizer(services: EmbedServiceMap) {
      return function tokenizer(state: StateBlock, startLine: number, endLine: number, silent: boolean): boolean {
        let startPos = state.bMarks[startLine] + state.tShift[startLine];
        let maxPos = state.eMarks[startLine];

        let pointer: Pointer = { line: startLine, pos: startPos };

        // Block embed must be at start of input or the previous line must be blank.
        if (startLine !== 0) {
          let prevLineStartPos = state.bMarks[startLine - 1] + state.tShift[startLine - 1];
          let prevLineMaxPos = state.eMarks[startLine - 1];
          if (prevLineMaxPos > prevLineStartPos) return false;
        }

        // Identify as being a potential block embed: @[
        if (maxPos - startPos < 2) return false;
        if (SYNTAX_CODES[0] !== state.src.charCodeAt(pointer.pos++)) return false;

        // Read service name from within square brackets.
        if (SYNTAX_CODES[1] !== state.src.charCodeAt(pointer.pos++)) return false;
        if (!advanceToSymbol(state, endLine, ']', pointer)) return false;

        let serviceName = state.src
          .substring(pointer.prevPos!, pointer.pos)
          .trim()
          .toLowerCase();

        pointer.pos++;

        // Lookup service; if unknown, then this is not a known embed!
        const service = services[serviceName];
        if (!service) return false;

        // Read embed reference from within parenthesis: (reference)
        Iif (SYNTAX_CODES[3] !== state.src.charCodeAt(pointer.pos++)) return false;
        Iif (!advanceToSymbol(state, endLine, ')', pointer)) return false;

        let videoReference = state.src
          .substring(pointer.prevPos!, pointer.pos)
          .trim();

        pointer.pos++;

        // Do not recognize as block element when there is trailing text.
        maxPos = state.eMarks[pointer.line];
        let trailingText = state.src
          .substring(pointer.pos, maxPos)
          .trim();
        Iif (trailingText !== "") return false;

        // Block embed must be at end of input or the next line must be blank.
        if (endLine !== pointer.line + 1) {
          const nextLineStartPos = state.bMarks[pointer.line + 1] + state.tShift[pointer.line + 1];
          const nextLineMaxPos = state.eMarks[pointer.line + 1];
          Iif (nextLineMaxPos > nextLineStartPos) return false;
        }

        Iif (pointer.line >= endLine) return false;

        if (!silent) {
          const token = state.push('video', 'div', 0);
          token.markup = state.src.slice(startPos, pointer.pos);
          token.block = true;
          (token as any).info = {
            serviceName,
            service,
            videoReference,
            videoID: service.extractVideoID(videoReference),
          };
          token.map = [startLine, pointer.line + 1];

          state.line = pointer.line + 1;
        }

        return true;
      };
    }