# All files / src/lib/markdown-it/plugins markdown-it-radio-button.ts

95.52% Statements 64/67
89.18% Branches 33/37
100% Functions 12/12
98.41% Lines 62/63

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
    12922x

    22x

    22x
    43x
    43x

    43x
    328x
    328x
    1024x
    35x
    35x

    35x
    35x

    35x
    13x

    22x
    22x
    10x

    22x
    10x

    22x

    35x
    35x
    35x
    35x

    105x
    105x

    105x
    79x

    26x
    26x

    35x

    35x
    22x

    13x

    35x
    35x
    115x
    35x

    1024x

    35x

    35x
    35x
    35x

    35x

    32x
    32x

    32x
    32x

    35x
    35x

    35x
    35x

    35x
    29x
    6x
    6x

    35x

    1024x
    123x
    81x

    51x

     import crypto from 'crypto';
    import MarkdownIt from 'markdown-it';
    import Token from 'markdown-it/lib/token';
    import StateCore from 'markdown-it/lib/rules_core/state_core';

    interface RadioOptions {
      enabled?: boolean;
      label?: boolean;
    }

    /**
     * Modified from https://github.com/revin/markdown-it-task-lists/blob/master/index.js
     */
    export function radioButtonPlugin(md: MarkdownIt, options?: RadioOptions): void {
      const disableRadio = options ? !options.enabled : false;
      const useLabelWrapper = options ? !!options.label : true;

      md.core.ruler.after('inline', 'radio-lists', (state: StateCore) => {
        const tokens = state.tokens;
        for (let i = 2; i < tokens.length; i++) {
          if (isTodoItem(tokens, i)) {
            const parentIdx = parentToken(tokens, i - 2);
            Iif (parentIdx === -1) continue;

            const parent = tokens[parentIdx];
            let groupAttr = attrGet(parent, 'radio-group'); // try retrieve the group id
            let group: string;

            if (groupAttr) {
              group = groupAttr[1];
            } else {
              const hash = crypto.createHash('md5');
              if (i >= 5 && tokens[i-5]) {
                hash.update(tokens[i-5].content);
              }
              if (i >= 4 && tokens[i-4]) {
                hash.update(tokens[i-4].content);
              }
              group = hash.update(tokens[i].content).digest('hex').slice(2, 7); // generate a deterministic group id
            }
            radioify(tokens[i], group, disableRadio, useLabelWrapper);
            attrSet(tokens[i - 2], 'class', 'radio-list-item');
            attrSet(parent, 'radio-group', group); // save the group id to the top-level list
            attrSet(parent, 'class', 'radio-list');
          }
        }
      });
    }

    function attrSet(token: Token, name: string, value: string): void {
      const index = token.attrIndex(name);
      const attr: [string, string] = [name, value];

      if (index < 0) {
        token.attrPush(attr);
      } else {
        if (token.attrs) {
          token.attrs[index] = attr;
        }
      }
    }

    function attrGet(token: Token, name: string): [string, string] | undefined {
      const index = token.attrIndex(name);

      if (index < 0 || !token.attrs) {
        return undefined;
      }
      return token.attrs[index] as [string, string];
    }

    function parentToken(tokens: Token[], index: number): number {
      const targetLevel = tokens[index].level - 1;
      for (let i = index - 1; i >= 0; i--) {
        if (tokens[i].level === targetLevel) {
          return i;
        }
      }
      return -1;
    }

    function isTodoItem(tokens: Token[], index: number): boolean {
      return isInline(tokens[index]) &&
        isParagraph(tokens[index - 1]) &&
        isListItem(tokens[index - 2]) &&
        startsWithTodoMarkdown(tokens[index]);
    }

    function radioify(token: Token, radioId: string, disableRadio: boolean, useLabelWrapper: boolean): void {
      Iif (!token.children) token.children = [];

      token.children.unshift(makeRadioButton(token, radioId, disableRadio));
      token.children[1].content = token.children[1].content.slice(3);
      token.content = token.content.slice(3);

      if (useLabelWrapper) {
        // Removed beingLabel & endLabel functions since we can just use new Token(...) now.
        token.children.unshift(new Token('html_inline', '', 0));
        token.children[0].content = '<label>';

        token.children.push(new Token('html_inline', '', 0));
        token.children[token.children.length - 1].content = '</label>';
      }
    }

    function makeRadioButton(token: Token, radioId: string, disableRadio: boolean): Token {
      const radio = new Token('html_inline', '', 0);
      const disabledAttr = disableRadio ? ' disabled="" ' : '';

      const isUnchecked = token.content.indexOf('( ) ') === 0;
      const isChecked = token.content.indexOf('(x) ') === 0 ||
                        token.content.indexOf('(X) ') === 0;
      if (isUnchecked) {
        radio.content = `<input class="radio-list-input" name="${radioId}"${disabledAttr} type="radio">`;
      } else if (isChecked) {
        radio.content = `<input class="radio-list-input" checked="" name="${radioId}"${disabledAttr} type="radio">`;
      }
      return radio;
    }

    function isInline(token: Token): boolean { return token.type === 'inline'; }
    function isParagraph(token: Token): boolean { return token.type === 'paragraph_open'; }
    function isListItem(token: Token): boolean { return token.type === 'list_item_open'; }

    function startsWithTodoMarkdown(token: Token): boolean {
      // leading whitespace in a list item is already trimmed off by markdown-it
      return token.content.indexOf('( ) ') === 0 || token.content.indexOf('(x) ') === 0 || token.content.indexOf('(X) ') === 0;
    }