diff options
Diffstat (limited to 'packages/markdown-it-14.1.0/lib/rules_block')
12 files changed, 1612 insertions, 0 deletions
| diff --git a/packages/markdown-it-14.1.0/lib/rules_block/blockquote.mjs b/packages/markdown-it-14.1.0/lib/rules_block/blockquote.mjs new file mode 100644 index 0000000..b61da02 --- /dev/null +++ b/packages/markdown-it-14.1.0/lib/rules_block/blockquote.mjs @@ -0,0 +1,209 @@ +// Block quotes + +import { isSpace } from '../common/utils.mjs' + +export default function blockquote (state, startLine, endLine, silent) { +  let pos = state.bMarks[startLine] + state.tShift[startLine] +  let max = state.eMarks[startLine] + +  const oldLineMax = state.lineMax + +  // if it's indented more than 3 spaces, it should be a code block +  if (state.sCount[startLine] - state.blkIndent >= 4) { return false } + +  // check the block quote marker +  if (state.src.charCodeAt(pos) !== 0x3E/* > */) { return false } + +  // we know that it's going to be a valid blockquote, +  // so no point trying to find the end of it in silent mode +  if (silent) { return true } + +  const oldBMarks  = [] +  const oldBSCount = [] +  const oldSCount  = [] +  const oldTShift  = [] + +  const terminatorRules = state.md.block.ruler.getRules('blockquote') + +  const oldParentType = state.parentType +  state.parentType = 'blockquote' +  let lastLineEmpty = false +  let nextLine + +  // Search the end of the block +  // +  // Block ends with either: +  //  1. an empty line outside: +  //     ``` +  //     > test +  // +  //     ``` +  //  2. an empty line inside: +  //     ``` +  //     > +  //     test +  //     ``` +  //  3. another tag: +  //     ``` +  //     > test +  //      - - - +  //     ``` +  for (nextLine = startLine; nextLine < endLine; nextLine++) { +    // check if it's outdented, i.e. it's inside list item and indented +    // less than said list item: +    // +    // ``` +    // 1. anything +    //    > current blockquote +    // 2. checking this line +    // ``` +    const isOutdented = state.sCount[nextLine] < state.blkIndent + +    pos = state.bMarks[nextLine] + state.tShift[nextLine] +    max = state.eMarks[nextLine] + +    if (pos >= max) { +      // Case 1: line is not inside the blockquote, and this line is empty. +      break +    } + +    if (state.src.charCodeAt(pos++) === 0x3E/* > */ && !isOutdented) { +      // This line is inside the blockquote. + +      // set offset past spaces and ">" +      let initial = state.sCount[nextLine] + 1 +      let spaceAfterMarker +      let adjustTab + +      // skip one optional space after '>' +      if (state.src.charCodeAt(pos) === 0x20 /* space */) { +        // ' >   test ' +        //     ^ -- position start of line here: +        pos++ +        initial++ +        adjustTab = false +        spaceAfterMarker = true +      } else if (state.src.charCodeAt(pos) === 0x09 /* tab */) { +        spaceAfterMarker = true + +        if ((state.bsCount[nextLine] + initial) % 4 === 3) { +          // '  >\t  test ' +          //       ^ -- position start of line here (tab has width===1) +          pos++ +          initial++ +          adjustTab = false +        } else { +          // ' >\t  test ' +          //    ^ -- position start of line here + shift bsCount slightly +          //         to make extra space appear +          adjustTab = true +        } +      } else { +        spaceAfterMarker = false +      } + +      let offset = initial +      oldBMarks.push(state.bMarks[nextLine]) +      state.bMarks[nextLine] = pos + +      while (pos < max) { +        const ch = state.src.charCodeAt(pos) + +        if (isSpace(ch)) { +          if (ch === 0x09) { +            offset += 4 - (offset + state.bsCount[nextLine] + (adjustTab ? 1 : 0)) % 4 +          } else { +            offset++ +          } +        } else { +          break +        } + +        pos++ +      } + +      lastLineEmpty = pos >= max + +      oldBSCount.push(state.bsCount[nextLine]) +      state.bsCount[nextLine] = state.sCount[nextLine] + 1 + (spaceAfterMarker ? 1 : 0) + +      oldSCount.push(state.sCount[nextLine]) +      state.sCount[nextLine] = offset - initial + +      oldTShift.push(state.tShift[nextLine]) +      state.tShift[nextLine] = pos - state.bMarks[nextLine] +      continue +    } + +    // Case 2: line is not inside the blockquote, and the last line was empty. +    if (lastLineEmpty) { break } + +    // Case 3: another tag found. +    let terminate = false +    for (let i = 0, l = terminatorRules.length; i < l; i++) { +      if (terminatorRules[i](state, nextLine, endLine, true)) { +        terminate = true +        break +      } +    } + +    if (terminate) { +      // Quirk to enforce "hard termination mode" for paragraphs; +      // normally if you call `tokenize(state, startLine, nextLine)`, +      // paragraphs will look below nextLine for paragraph continuation, +      // but if blockquote is terminated by another tag, they shouldn't +      state.lineMax = nextLine + +      if (state.blkIndent !== 0) { +        // state.blkIndent was non-zero, we now set it to zero, +        // so we need to re-calculate all offsets to appear as +        // if indent wasn't changed +        oldBMarks.push(state.bMarks[nextLine]) +        oldBSCount.push(state.bsCount[nextLine]) +        oldTShift.push(state.tShift[nextLine]) +        oldSCount.push(state.sCount[nextLine]) +        state.sCount[nextLine] -= state.blkIndent +      } + +      break +    } + +    oldBMarks.push(state.bMarks[nextLine]) +    oldBSCount.push(state.bsCount[nextLine]) +    oldTShift.push(state.tShift[nextLine]) +    oldSCount.push(state.sCount[nextLine]) + +    // A negative indentation means that this is a paragraph continuation +    // +    state.sCount[nextLine] = -1 +  } + +  const oldIndent = state.blkIndent +  state.blkIndent = 0 + +  const token_o  = state.push('blockquote_open', 'blockquote', 1) +  token_o.markup = '>' +  const lines = [startLine, 0] +  token_o.map    = lines + +  state.md.block.tokenize(state, startLine, nextLine) + +  const token_c  = state.push('blockquote_close', 'blockquote', -1) +  token_c.markup = '>' + +  state.lineMax = oldLineMax +  state.parentType = oldParentType +  lines[1] = state.line + +  // Restore original tShift; this might not be necessary since the parser +  // has already been here, but just to make sure we can do that. +  for (let i = 0; i < oldTShift.length; i++) { +    state.bMarks[i + startLine] = oldBMarks[i] +    state.tShift[i + startLine] = oldTShift[i] +    state.sCount[i + startLine] = oldSCount[i] +    state.bsCount[i + startLine] = oldBSCount[i] +  } +  state.blkIndent = oldIndent + +  return true +} diff --git a/packages/markdown-it-14.1.0/lib/rules_block/code.mjs b/packages/markdown-it-14.1.0/lib/rules_block/code.mjs new file mode 100644 index 0000000..e45e6f9 --- /dev/null +++ b/packages/markdown-it-14.1.0/lib/rules_block/code.mjs @@ -0,0 +1,30 @@ +// Code block (4 spaces padded) + +export default function code (state, startLine, endLine/*, silent */) { +  if (state.sCount[startLine] - state.blkIndent < 4) { return false } + +  let nextLine = startLine + 1 +  let last = nextLine + +  while (nextLine < endLine) { +    if (state.isEmpty(nextLine)) { +      nextLine++ +      continue +    } + +    if (state.sCount[nextLine] - state.blkIndent >= 4) { +      nextLine++ +      last = nextLine +      continue +    } +    break +  } + +  state.line = last + +  const token   = state.push('code_block', 'code', 0) +  token.content = state.getLines(startLine, last, 4 + state.blkIndent, false) + '\n' +  token.map     = [startLine, state.line] + +  return true +} diff --git a/packages/markdown-it-14.1.0/lib/rules_block/fence.mjs b/packages/markdown-it-14.1.0/lib/rules_block/fence.mjs new file mode 100644 index 0000000..930f7b3 --- /dev/null +++ b/packages/markdown-it-14.1.0/lib/rules_block/fence.mjs @@ -0,0 +1,94 @@ +// fences (``` lang, ~~~ lang) + +export default function fence (state, startLine, endLine, silent) { +  let pos = state.bMarks[startLine] + state.tShift[startLine] +  let max = state.eMarks[startLine] + +  // if it's indented more than 3 spaces, it should be a code block +  if (state.sCount[startLine] - state.blkIndent >= 4) { return false } + +  if (pos + 3 > max) { return false } + +  const marker = state.src.charCodeAt(pos) + +  if (marker !== 0x7E/* ~ */ && marker !== 0x60 /* ` */) { +    return false +  } + +  // scan marker length +  let mem = pos +  pos = state.skipChars(pos, marker) + +  let len = pos - mem + +  if (len < 3) { return false } + +  const markup = state.src.slice(mem, pos) +  const params = state.src.slice(pos, max) + +  if (marker === 0x60 /* ` */) { +    if (params.indexOf(String.fromCharCode(marker)) >= 0) { +      return false +    } +  } + +  // Since start is found, we can report success here in validation mode +  if (silent) { return true } + +  // search end of block +  let nextLine = startLine +  let haveEndMarker = false + +  for (;;) { +    nextLine++ +    if (nextLine >= endLine) { +      // unclosed block should be autoclosed by end of document. +      // also block seems to be autoclosed by end of parent +      break +    } + +    pos = mem = state.bMarks[nextLine] + state.tShift[nextLine] +    max = state.eMarks[nextLine] + +    if (pos < max && state.sCount[nextLine] < state.blkIndent) { +      // non-empty line with negative indent should stop the list: +      // - ``` +      //  test +      break +    } + +    if (state.src.charCodeAt(pos) !== marker) { continue } + +    if (state.sCount[nextLine] - state.blkIndent >= 4) { +      // closing fence should be indented less than 4 spaces +      continue +    } + +    pos = state.skipChars(pos, marker) + +    // closing code fence must be at least as long as the opening one +    if (pos - mem < len) { continue } + +    // make sure tail has spaces only +    pos = state.skipSpaces(pos) + +    if (pos < max) { continue } + +    haveEndMarker = true +    // found! +    break +  } + +  // If a fence has heading spaces, they should be removed from its inner block +  len = state.sCount[startLine] + +  state.line = nextLine + (haveEndMarker ? 1 : 0) + +  const token   = state.push('fence', 'code', 0) +  token.info    = params +  token.content = state.getLines(startLine + 1, nextLine, len, true) +  token.markup  = markup +  token.map     = [startLine, state.line] + +  return true +} diff --git a/packages/markdown-it-14.1.0/lib/rules_block/heading.mjs b/packages/markdown-it-14.1.0/lib/rules_block/heading.mjs new file mode 100644 index 0000000..d2f7b79 --- /dev/null +++ b/packages/markdown-it-14.1.0/lib/rules_block/heading.mjs @@ -0,0 +1,51 @@ +// heading (#, ##, ...) + +import { isSpace } from '../common/utils.mjs' + +export default function heading (state, startLine, endLine, silent) { +  let pos = state.bMarks[startLine] + state.tShift[startLine] +  let max = state.eMarks[startLine] + +  // if it's indented more than 3 spaces, it should be a code block +  if (state.sCount[startLine] - state.blkIndent >= 4) { return false } + +  let ch  = state.src.charCodeAt(pos) + +  if (ch !== 0x23/* # */ || pos >= max) { return false } + +  // count heading level +  let level = 1 +  ch = state.src.charCodeAt(++pos) +  while (ch === 0x23/* # */ && pos < max && level <= 6) { +    level++ +    ch = state.src.charCodeAt(++pos) +  } + +  if (level > 6 || (pos < max && !isSpace(ch))) { return false } + +  if (silent) { return true } + +  // Let's cut tails like '    ###  ' from the end of string + +  max = state.skipSpacesBack(max, pos) +  const tmp = state.skipCharsBack(max, 0x23, pos) // # +  if (tmp > pos && isSpace(state.src.charCodeAt(tmp - 1))) { +    max = tmp +  } + +  state.line = startLine + 1 + +  const token_o  = state.push('heading_open', 'h' + String(level), 1) +  token_o.markup = '########'.slice(0, level) +  token_o.map    = [startLine, state.line] + +  const token_i    = state.push('inline', '', 0) +  token_i.content  = state.src.slice(pos, max).trim() +  token_i.map      = [startLine, state.line] +  token_i.children = [] + +  const token_c  = state.push('heading_close', 'h' + String(level), -1) +  token_c.markup = '########'.slice(0, level) + +  return true +} diff --git a/packages/markdown-it-14.1.0/lib/rules_block/hr.mjs b/packages/markdown-it-14.1.0/lib/rules_block/hr.mjs new file mode 100644 index 0000000..d467b21 --- /dev/null +++ b/packages/markdown-it-14.1.0/lib/rules_block/hr.mjs @@ -0,0 +1,40 @@ +// Horizontal rule + +import { isSpace } from '../common/utils.mjs' + +export default function hr (state, startLine, endLine, silent) { +  const max = state.eMarks[startLine] +  // if it's indented more than 3 spaces, it should be a code block +  if (state.sCount[startLine] - state.blkIndent >= 4) { return false } + +  let pos = state.bMarks[startLine] + state.tShift[startLine] +  const marker = state.src.charCodeAt(pos++) + +  // Check hr marker +  if (marker !== 0x2A/* * */ && +      marker !== 0x2D/* - */ && +      marker !== 0x5F/* _ */) { +    return false +  } + +  // markers can be mixed with spaces, but there should be at least 3 of them + +  let cnt = 1 +  while (pos < max) { +    const ch = state.src.charCodeAt(pos++) +    if (ch !== marker && !isSpace(ch)) { return false } +    if (ch === marker) { cnt++ } +  } + +  if (cnt < 3) { return false } + +  if (silent) { return true } + +  state.line = startLine + 1 + +  const token  = state.push('hr', 'hr', 0) +  token.map    = [startLine, state.line] +  token.markup = Array(cnt + 1).join(String.fromCharCode(marker)) + +  return true +} diff --git a/packages/markdown-it-14.1.0/lib/rules_block/html_block.mjs b/packages/markdown-it-14.1.0/lib/rules_block/html_block.mjs new file mode 100644 index 0000000..197520f --- /dev/null +++ b/packages/markdown-it-14.1.0/lib/rules_block/html_block.mjs @@ -0,0 +1,69 @@ +// HTML block + +import block_names from '../common/html_blocks.mjs' +import { HTML_OPEN_CLOSE_TAG_RE } from '../common/html_re.mjs' + +// An array of opening and corresponding closing sequences for html tags, +// last argument defines whether it can terminate a paragraph or not +// +const HTML_SEQUENCES = [ +  [/^<(script|pre|style|textarea)(?=(\s|>|$))/i, /<\/(script|pre|style|textarea)>/i, true], +  [/^<!--/,        /-->/,   true], +  [/^<\?/,         /\?>/,   true], +  [/^<![A-Z]/,     />/,     true], +  [/^<!\[CDATA\[/, /\]\]>/, true], +  [new RegExp('^</?(' + block_names.join('|') + ')(?=(\\s|/?>|$))', 'i'), /^$/, true], +  [new RegExp(HTML_OPEN_CLOSE_TAG_RE.source + '\\s*$'),  /^$/, false] +] + +export default function html_block (state, startLine, endLine, silent) { +  let pos = state.bMarks[startLine] + state.tShift[startLine] +  let max = state.eMarks[startLine] + +  // if it's indented more than 3 spaces, it should be a code block +  if (state.sCount[startLine] - state.blkIndent >= 4) { return false } + +  if (!state.md.options.html) { return false } + +  if (state.src.charCodeAt(pos) !== 0x3C/* < */) { return false } + +  let lineText = state.src.slice(pos, max) + +  let i = 0 +  for (; i < HTML_SEQUENCES.length; i++) { +    if (HTML_SEQUENCES[i][0].test(lineText)) { break } +  } +  if (i === HTML_SEQUENCES.length) { return false } + +  if (silent) { +    // true if this sequence can be a terminator, false otherwise +    return HTML_SEQUENCES[i][2] +  } + +  let nextLine = startLine + 1 + +  // If we are here - we detected HTML block. +  // Let's roll down till block end. +  if (!HTML_SEQUENCES[i][1].test(lineText)) { +    for (; nextLine < endLine; nextLine++) { +      if (state.sCount[nextLine] < state.blkIndent) { break } + +      pos = state.bMarks[nextLine] + state.tShift[nextLine] +      max = state.eMarks[nextLine] +      lineText = state.src.slice(pos, max) + +      if (HTML_SEQUENCES[i][1].test(lineText)) { +        if (lineText.length !== 0) { nextLine++ } +        break +      } +    } +  } + +  state.line = nextLine + +  const token   = state.push('html_block', '', 0) +  token.map     = [startLine, nextLine] +  token.content = state.getLines(startLine, nextLine, state.blkIndent, true) + +  return true +} diff --git a/packages/markdown-it-14.1.0/lib/rules_block/lheading.mjs b/packages/markdown-it-14.1.0/lib/rules_block/lheading.mjs new file mode 100644 index 0000000..ee3b9a3 --- /dev/null +++ b/packages/markdown-it-14.1.0/lib/rules_block/lheading.mjs @@ -0,0 +1,82 @@ +// lheading (---, ===) + +export default function lheading (state, startLine, endLine/*, silent */) { +  const terminatorRules = state.md.block.ruler.getRules('paragraph') + +  // if it's indented more than 3 spaces, it should be a code block +  if (state.sCount[startLine] - state.blkIndent >= 4) { return false } + +  const oldParentType = state.parentType +  state.parentType = 'paragraph' // use paragraph to match terminatorRules + +  // jump line-by-line until empty one or EOF +  let level = 0 +  let marker +  let nextLine = startLine + 1 + +  for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) { +    // this would be a code block normally, but after paragraph +    // it's considered a lazy continuation regardless of what's there +    if (state.sCount[nextLine] - state.blkIndent > 3) { continue } + +    // +    // Check for underline in setext header +    // +    if (state.sCount[nextLine] >= state.blkIndent) { +      let pos = state.bMarks[nextLine] + state.tShift[nextLine] +      const max = state.eMarks[nextLine] + +      if (pos < max) { +        marker = state.src.charCodeAt(pos) + +        if (marker === 0x2D/* - */ || marker === 0x3D/* = */) { +          pos = state.skipChars(pos, marker) +          pos = state.skipSpaces(pos) + +          if (pos >= max) { +            level = (marker === 0x3D/* = */ ? 1 : 2) +            break +          } +        } +      } +    } + +    // quirk for blockquotes, this line should already be checked by that rule +    if (state.sCount[nextLine] < 0) { continue } + +    // Some tags can terminate paragraph without empty line. +    let terminate = false +    for (let i = 0, l = terminatorRules.length; i < l; i++) { +      if (terminatorRules[i](state, nextLine, endLine, true)) { +        terminate = true +        break +      } +    } +    if (terminate) { break } +  } + +  if (!level) { +    // Didn't find valid underline +    return false +  } + +  const content = state.getLines(startLine, nextLine, state.blkIndent, false).trim() + +  state.line = nextLine + 1 + +  const token_o    = state.push('heading_open', 'h' + String(level), 1) +  token_o.markup   = String.fromCharCode(marker) +  token_o.map      = [startLine, state.line] + +  const token_i    = state.push('inline', '', 0) +  token_i.content  = content +  token_i.map      = [startLine, state.line - 1] +  token_i.children = [] + +  const token_c    = state.push('heading_close', 'h' + String(level), -1) +  token_c.markup   = String.fromCharCode(marker) + +  state.parentType = oldParentType + +  return true +} diff --git a/packages/markdown-it-14.1.0/lib/rules_block/list.mjs b/packages/markdown-it-14.1.0/lib/rules_block/list.mjs new file mode 100644 index 0000000..fb53abd --- /dev/null +++ b/packages/markdown-it-14.1.0/lib/rules_block/list.mjs @@ -0,0 +1,331 @@ +// Lists + +import { isSpace } from '../common/utils.mjs' + +// Search `[-+*][\n ]`, returns next pos after marker on success +// or -1 on fail. +function skipBulletListMarker (state, startLine) { +  const max = state.eMarks[startLine] +  let pos = state.bMarks[startLine] + state.tShift[startLine] + +  const marker = state.src.charCodeAt(pos++) +  // Check bullet +  if (marker !== 0x2A/* * */ && +      marker !== 0x2D/* - */ && +      marker !== 0x2B/* + */) { +    return -1 +  } + +  if (pos < max) { +    const ch = state.src.charCodeAt(pos) + +    if (!isSpace(ch)) { +      // " -test " - is not a list item +      return -1 +    } +  } + +  return pos +} + +// Search `\d+[.)][\n ]`, returns next pos after marker on success +// or -1 on fail. +function skipOrderedListMarker (state, startLine) { +  const start = state.bMarks[startLine] + state.tShift[startLine] +  const max = state.eMarks[startLine] +  let pos = start + +  // List marker should have at least 2 chars (digit + dot) +  if (pos + 1 >= max) { return -1 } + +  let ch = state.src.charCodeAt(pos++) + +  if (ch < 0x30/* 0 */ || ch > 0x39/* 9 */) { return -1 } + +  for (;;) { +    // EOL -> fail +    if (pos >= max) { return -1 } + +    ch = state.src.charCodeAt(pos++) + +    if (ch >= 0x30/* 0 */ && ch <= 0x39/* 9 */) { +      // List marker should have no more than 9 digits +      // (prevents integer overflow in browsers) +      if (pos - start >= 10) { return -1 } + +      continue +    } + +    // found valid marker +    if (ch === 0x29/* ) */ || ch === 0x2e/* . */) { +      break +    } + +    return -1 +  } + +  if (pos < max) { +    ch = state.src.charCodeAt(pos) + +    if (!isSpace(ch)) { +      // " 1.test " - is not a list item +      return -1 +    } +  } +  return pos +} + +function markTightParagraphs (state, idx) { +  const level = state.level + 2 + +  for (let i = idx + 2, l = state.tokens.length - 2; i < l; i++) { +    if (state.tokens[i].level === level && state.tokens[i].type === 'paragraph_open') { +      state.tokens[i + 2].hidden = true +      state.tokens[i].hidden = true +      i += 2 +    } +  } +} + +export default function list (state, startLine, endLine, silent) { +  let max, pos, start, token +  let nextLine = startLine +  let tight = true + +  // if it's indented more than 3 spaces, it should be a code block +  if (state.sCount[nextLine] - state.blkIndent >= 4) { return false } + +  // Special case: +  //  - item 1 +  //   - item 2 +  //    - item 3 +  //     - item 4 +  //      - this one is a paragraph continuation +  if (state.listIndent >= 0 && +      state.sCount[nextLine] - state.listIndent >= 4 && +      state.sCount[nextLine] < state.blkIndent) { +    return false +  } + +  let isTerminatingParagraph = false + +  // limit conditions when list can interrupt +  // a paragraph (validation mode only) +  if (silent && state.parentType === 'paragraph') { +    // Next list item should still terminate previous list item; +    // +    // This code can fail if plugins use blkIndent as well as lists, +    // but I hope the spec gets fixed long before that happens. +    // +    if (state.sCount[nextLine] >= state.blkIndent) { +      isTerminatingParagraph = true +    } +  } + +  // Detect list type and position after marker +  let isOrdered +  let markerValue +  let posAfterMarker +  if ((posAfterMarker = skipOrderedListMarker(state, nextLine)) >= 0) { +    isOrdered = true +    start = state.bMarks[nextLine] + state.tShift[nextLine] +    markerValue = Number(state.src.slice(start, posAfterMarker - 1)) + +    // If we're starting a new ordered list right after +    // a paragraph, it should start with 1. +    if (isTerminatingParagraph && markerValue !== 1) return false +  } else if ((posAfterMarker = skipBulletListMarker(state, nextLine)) >= 0) { +    isOrdered = false +  } else { +    return false +  } + +  // If we're starting a new unordered list right after +  // a paragraph, first line should not be empty. +  if (isTerminatingParagraph) { +    if (state.skipSpaces(posAfterMarker) >= state.eMarks[nextLine]) return false +  } + +  // For validation mode we can terminate immediately +  if (silent) { return true } + +  // We should terminate list on style change. Remember first one to compare. +  const markerCharCode = state.src.charCodeAt(posAfterMarker - 1) + +  // Start list +  const listTokIdx = state.tokens.length + +  if (isOrdered) { +    token       = state.push('ordered_list_open', 'ol', 1) +    if (markerValue !== 1) { +      token.attrs = [['start', markerValue]] +    } +  } else { +    token       = state.push('bullet_list_open', 'ul', 1) +  } + +  const listLines = [nextLine, 0] +  token.map    = listLines +  token.markup = String.fromCharCode(markerCharCode) + +  // +  // Iterate list items +  // + +  let prevEmptyEnd = false +  const terminatorRules = state.md.block.ruler.getRules('list') + +  const oldParentType = state.parentType +  state.parentType = 'list' + +  while (nextLine < endLine) { +    pos = posAfterMarker +    max = state.eMarks[nextLine] + +    const initial = state.sCount[nextLine] + posAfterMarker - (state.bMarks[nextLine] + state.tShift[nextLine]) +    let offset = initial + +    while (pos < max) { +      const ch = state.src.charCodeAt(pos) + +      if (ch === 0x09) { +        offset += 4 - (offset + state.bsCount[nextLine]) % 4 +      } else if (ch === 0x20) { +        offset++ +      } else { +        break +      } + +      pos++ +    } + +    const contentStart = pos +    let indentAfterMarker + +    if (contentStart >= max) { +      // trimming space in "-    \n  3" case, indent is 1 here +      indentAfterMarker = 1 +    } else { +      indentAfterMarker = offset - initial +    } + +    // If we have more than 4 spaces, the indent is 1 +    // (the rest is just indented code block) +    if (indentAfterMarker > 4) { indentAfterMarker = 1 } + +    // "  -  test" +    //  ^^^^^ - calculating total length of this thing +    const indent = initial + indentAfterMarker + +    // Run subparser & write tokens +    token        = state.push('list_item_open', 'li', 1) +    token.markup = String.fromCharCode(markerCharCode) +    const itemLines = [nextLine, 0] +    token.map    = itemLines +    if (isOrdered) { +      token.info = state.src.slice(start, posAfterMarker - 1) +    } + +    // change current state, then restore it after parser subcall +    const oldTight = state.tight +    const oldTShift = state.tShift[nextLine] +    const oldSCount = state.sCount[nextLine] + +    //  - example list +    // ^ listIndent position will be here +    //   ^ blkIndent position will be here +    // +    const oldListIndent = state.listIndent +    state.listIndent = state.blkIndent +    state.blkIndent = indent + +    state.tight = true +    state.tShift[nextLine] = contentStart - state.bMarks[nextLine] +    state.sCount[nextLine] = offset + +    if (contentStart >= max && state.isEmpty(nextLine + 1)) { +      // workaround for this case +      // (list item is empty, list terminates before "foo"): +      // ~~~~~~~~ +      //   - +      // +      //     foo +      // ~~~~~~~~ +      state.line = Math.min(state.line + 2, endLine) +    } else { +      state.md.block.tokenize(state, nextLine, endLine, true) +    } + +    // If any of list item is tight, mark list as tight +    if (!state.tight || prevEmptyEnd) { +      tight = false +    } +    // Item become loose if finish with empty line, +    // but we should filter last element, because it means list finish +    prevEmptyEnd = (state.line - nextLine) > 1 && state.isEmpty(state.line - 1) + +    state.blkIndent = state.listIndent +    state.listIndent = oldListIndent +    state.tShift[nextLine] = oldTShift +    state.sCount[nextLine] = oldSCount +    state.tight = oldTight + +    token        = state.push('list_item_close', 'li', -1) +    token.markup = String.fromCharCode(markerCharCode) + +    nextLine = state.line +    itemLines[1] = nextLine + +    if (nextLine >= endLine) { break } + +    // +    // Try to check if list is terminated or continued. +    // +    if (state.sCount[nextLine] < state.blkIndent) { break } + +    // if it's indented more than 3 spaces, it should be a code block +    if (state.sCount[nextLine] - state.blkIndent >= 4) { break } + +    // fail if terminating block found +    let terminate = false +    for (let i = 0, l = terminatorRules.length; i < l; i++) { +      if (terminatorRules[i](state, nextLine, endLine, true)) { +        terminate = true +        break +      } +    } +    if (terminate) { break } + +    // fail if list has another type +    if (isOrdered) { +      posAfterMarker = skipOrderedListMarker(state, nextLine) +      if (posAfterMarker < 0) { break } +      start = state.bMarks[nextLine] + state.tShift[nextLine] +    } else { +      posAfterMarker = skipBulletListMarker(state, nextLine) +      if (posAfterMarker < 0) { break } +    } + +    if (markerCharCode !== state.src.charCodeAt(posAfterMarker - 1)) { break } +  } + +  // Finalize list +  if (isOrdered) { +    token = state.push('ordered_list_close', 'ol', -1) +  } else { +    token = state.push('bullet_list_close', 'ul', -1) +  } +  token.markup = String.fromCharCode(markerCharCode) + +  listLines[1] = nextLine +  state.line = nextLine + +  state.parentType = oldParentType + +  // mark paragraphs tight if needed +  if (tight) { +    markTightParagraphs(state, listTokIdx) +  } + +  return true +} diff --git a/packages/markdown-it-14.1.0/lib/rules_block/paragraph.mjs b/packages/markdown-it-14.1.0/lib/rules_block/paragraph.mjs new file mode 100644 index 0000000..6ecdcef --- /dev/null +++ b/packages/markdown-it-14.1.0/lib/rules_block/paragraph.mjs @@ -0,0 +1,46 @@ +// Paragraph + +export default function paragraph (state, startLine, endLine) { +  const terminatorRules = state.md.block.ruler.getRules('paragraph') +  const oldParentType = state.parentType +  let nextLine = startLine + 1 +  state.parentType = 'paragraph' + +  // jump line-by-line until empty one or EOF +  for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) { +    // this would be a code block normally, but after paragraph +    // it's considered a lazy continuation regardless of what's there +    if (state.sCount[nextLine] - state.blkIndent > 3) { continue } + +    // quirk for blockquotes, this line should already be checked by that rule +    if (state.sCount[nextLine] < 0) { continue } + +    // Some tags can terminate paragraph without empty line. +    let terminate = false +    for (let i = 0, l = terminatorRules.length; i < l; i++) { +      if (terminatorRules[i](state, nextLine, endLine, true)) { +        terminate = true +        break +      } +    } +    if (terminate) { break } +  } + +  const content = state.getLines(startLine, nextLine, state.blkIndent, false).trim() + +  state.line = nextLine + +  const token_o    = state.push('paragraph_open', 'p', 1) +  token_o.map      = [startLine, state.line] + +  const token_i    = state.push('inline', '', 0) +  token_i.content  = content +  token_i.map      = [startLine, state.line] +  token_i.children = [] + +  state.push('paragraph_close', 'p', -1) + +  state.parentType = oldParentType + +  return true +} diff --git a/packages/markdown-it-14.1.0/lib/rules_block/reference.mjs b/packages/markdown-it-14.1.0/lib/rules_block/reference.mjs new file mode 100644 index 0000000..4166286 --- /dev/null +++ b/packages/markdown-it-14.1.0/lib/rules_block/reference.mjs @@ -0,0 +1,212 @@ +import { isSpace, normalizeReference } from '../common/utils.mjs' + +export default function reference (state, startLine, _endLine, silent) { +  let pos = state.bMarks[startLine] + state.tShift[startLine] +  let max = state.eMarks[startLine] +  let nextLine = startLine + 1 + +  // if it's indented more than 3 spaces, it should be a code block +  if (state.sCount[startLine] - state.blkIndent >= 4) { return false } + +  if (state.src.charCodeAt(pos) !== 0x5B/* [ */) { return false } + +  function getNextLine (nextLine) { +    const endLine = state.lineMax + +    if (nextLine >= endLine || state.isEmpty(nextLine)) { +      // empty line or end of input +      return null +    } + +    let isContinuation = false + +    // this would be a code block normally, but after paragraph +    // it's considered a lazy continuation regardless of what's there +    if (state.sCount[nextLine] - state.blkIndent > 3) { isContinuation = true } + +    // quirk for blockquotes, this line should already be checked by that rule +    if (state.sCount[nextLine] < 0) { isContinuation = true } + +    if (!isContinuation) { +      const terminatorRules = state.md.block.ruler.getRules('reference') +      const oldParentType = state.parentType +      state.parentType = 'reference' + +      // Some tags can terminate paragraph without empty line. +      let terminate = false +      for (let i = 0, l = terminatorRules.length; i < l; i++) { +        if (terminatorRules[i](state, nextLine, endLine, true)) { +          terminate = true +          break +        } +      } + +      state.parentType = oldParentType +      if (terminate) { +        // terminated by another block +        return null +      } +    } + +    const pos = state.bMarks[nextLine] + state.tShift[nextLine] +    const max = state.eMarks[nextLine] + +    // max + 1 explicitly includes the newline +    return state.src.slice(pos, max + 1) +  } + +  let str = state.src.slice(pos, max + 1) + +  max = str.length +  let labelEnd = -1 + +  for (pos = 1; pos < max; pos++) { +    const ch = str.charCodeAt(pos) +    if (ch === 0x5B /* [ */) { +      return false +    } else if (ch === 0x5D /* ] */) { +      labelEnd = pos +      break +    } else if (ch === 0x0A /* \n */) { +      const lineContent = getNextLine(nextLine) +      if (lineContent !== null) { +        str += lineContent +        max = str.length +        nextLine++ +      } +    } else if (ch === 0x5C /* \ */) { +      pos++ +      if (pos < max && str.charCodeAt(pos) === 0x0A) { +        const lineContent = getNextLine(nextLine) +        if (lineContent !== null) { +          str += lineContent +          max = str.length +          nextLine++ +        } +      } +    } +  } + +  if (labelEnd < 0 || str.charCodeAt(labelEnd + 1) !== 0x3A/* : */) { return false } + +  // [label]:   destination   'title' +  //         ^^^ skip optional whitespace here +  for (pos = labelEnd + 2; pos < max; pos++) { +    const ch = str.charCodeAt(pos) +    if (ch === 0x0A) { +      const lineContent = getNextLine(nextLine) +      if (lineContent !== null) { +        str += lineContent +        max = str.length +        nextLine++ +      } +    } else if (isSpace(ch)) { +      /* eslint no-empty:0 */ +    } else { +      break +    } +  } + +  // [label]:   destination   'title' +  //            ^^^^^^^^^^^ parse this +  const destRes = state.md.helpers.parseLinkDestination(str, pos, max) +  if (!destRes.ok) { return false } + +  const href = state.md.normalizeLink(destRes.str) +  if (!state.md.validateLink(href)) { return false } + +  pos = destRes.pos + +  // save cursor state, we could require to rollback later +  const destEndPos = pos +  const destEndLineNo = nextLine + +  // [label]:   destination   'title' +  //                       ^^^ skipping those spaces +  const start = pos +  for (; pos < max; pos++) { +    const ch = str.charCodeAt(pos) +    if (ch === 0x0A) { +      const lineContent = getNextLine(nextLine) +      if (lineContent !== null) { +        str += lineContent +        max = str.length +        nextLine++ +      } +    } else if (isSpace(ch)) { +      /* eslint no-empty:0 */ +    } else { +      break +    } +  } + +  // [label]:   destination   'title' +  //                          ^^^^^^^ parse this +  let titleRes = state.md.helpers.parseLinkTitle(str, pos, max) +  while (titleRes.can_continue) { +    const lineContent = getNextLine(nextLine) +    if (lineContent === null) break +    str += lineContent +    pos = max +    max = str.length +    nextLine++ +    titleRes = state.md.helpers.parseLinkTitle(str, pos, max, titleRes) +  } +  let title + +  if (pos < max && start !== pos && titleRes.ok) { +    title = titleRes.str +    pos = titleRes.pos +  } else { +    title = '' +    pos = destEndPos +    nextLine = destEndLineNo +  } + +  // skip trailing spaces until the rest of the line +  while (pos < max) { +    const ch = str.charCodeAt(pos) +    if (!isSpace(ch)) { break } +    pos++ +  } + +  if (pos < max && str.charCodeAt(pos) !== 0x0A) { +    if (title) { +      // garbage at the end of the line after title, +      // but it could still be a valid reference if we roll back +      title = '' +      pos = destEndPos +      nextLine = destEndLineNo +      while (pos < max) { +        const ch = str.charCodeAt(pos) +        if (!isSpace(ch)) { break } +        pos++ +      } +    } +  } + +  if (pos < max && str.charCodeAt(pos) !== 0x0A) { +    // garbage at the end of the line +    return false +  } + +  const label = normalizeReference(str.slice(1, labelEnd)) +  if (!label) { +    // CommonMark 0.20 disallows empty labels +    return false +  } + +  // Reference can not terminate anything. This check is for safety only. +  /* istanbul ignore if */ +  if (silent) { return true } + +  if (typeof state.env.references === 'undefined') { +    state.env.references = {} +  } +  if (typeof state.env.references[label] === 'undefined') { +    state.env.references[label] = { title, href } +  } + +  state.line = nextLine +  return true +} diff --git a/packages/markdown-it-14.1.0/lib/rules_block/state_block.mjs b/packages/markdown-it-14.1.0/lib/rules_block/state_block.mjs new file mode 100644 index 0000000..3c2a876 --- /dev/null +++ b/packages/markdown-it-14.1.0/lib/rules_block/state_block.mjs @@ -0,0 +1,220 @@ +// Parser state class + +import Token from '../token.mjs' +import { isSpace } from '../common/utils.mjs' + +function StateBlock (src, md, env, tokens) { +  this.src = src + +  // link to parser instance +  this.md     = md + +  this.env = env + +  // +  // Internal state vartiables +  // + +  this.tokens = tokens + +  this.bMarks = []  // line begin offsets for fast jumps +  this.eMarks = []  // line end offsets for fast jumps +  this.tShift = []  // offsets of the first non-space characters (tabs not expanded) +  this.sCount = []  // indents for each line (tabs expanded) + +  // An amount of virtual spaces (tabs expanded) between beginning +  // of each line (bMarks) and real beginning of that line. +  // +  // It exists only as a hack because blockquotes override bMarks +  // losing information in the process. +  // +  // It's used only when expanding tabs, you can think about it as +  // an initial tab length, e.g. bsCount=21 applied to string `\t123` +  // means first tab should be expanded to 4-21%4 === 3 spaces. +  // +  this.bsCount = [] + +  // block parser variables + +  // required block content indent (for example, if we are +  // inside a list, it would be positioned after list marker) +  this.blkIndent  = 0 +  this.line       = 0 // line index in src +  this.lineMax    = 0 // lines count +  this.tight      = false  // loose/tight mode for lists +  this.ddIndent   = -1 // indent of the current dd block (-1 if there isn't any) +  this.listIndent = -1 // indent of the current list block (-1 if there isn't any) + +  // can be 'blockquote', 'list', 'root', 'paragraph' or 'reference' +  // used in lists to determine if they interrupt a paragraph +  this.parentType = 'root' + +  this.level = 0 + +  // Create caches +  // Generate markers. +  const s = this.src + +  for (let start = 0, pos = 0, indent = 0, offset = 0, len = s.length, indent_found = false; pos < len; pos++) { +    const ch = s.charCodeAt(pos) + +    if (!indent_found) { +      if (isSpace(ch)) { +        indent++ + +        if (ch === 0x09) { +          offset += 4 - offset % 4 +        } else { +          offset++ +        } +        continue +      } else { +        indent_found = true +      } +    } + +    if (ch === 0x0A || pos === len - 1) { +      if (ch !== 0x0A) { pos++ } +      this.bMarks.push(start) +      this.eMarks.push(pos) +      this.tShift.push(indent) +      this.sCount.push(offset) +      this.bsCount.push(0) + +      indent_found = false +      indent = 0 +      offset = 0 +      start = pos + 1 +    } +  } + +  // Push fake entry to simplify cache bounds checks +  this.bMarks.push(s.length) +  this.eMarks.push(s.length) +  this.tShift.push(0) +  this.sCount.push(0) +  this.bsCount.push(0) + +  this.lineMax = this.bMarks.length - 1 // don't count last fake line +} + +// Push new token to "stream". +// +StateBlock.prototype.push = function (type, tag, nesting) { +  const token = new Token(type, tag, nesting) +  token.block = true + +  if (nesting < 0) this.level-- // closing tag +  token.level = this.level +  if (nesting > 0) this.level++ // opening tag + +  this.tokens.push(token) +  return token +} + +StateBlock.prototype.isEmpty = function isEmpty (line) { +  return this.bMarks[line] + this.tShift[line] >= this.eMarks[line] +} + +StateBlock.prototype.skipEmptyLines = function skipEmptyLines (from) { +  for (let max = this.lineMax; from < max; from++) { +    if (this.bMarks[from] + this.tShift[from] < this.eMarks[from]) { +      break +    } +  } +  return from +} + +// Skip spaces from given position. +StateBlock.prototype.skipSpaces = function skipSpaces (pos) { +  for (let max = this.src.length; pos < max; pos++) { +    const ch = this.src.charCodeAt(pos) +    if (!isSpace(ch)) { break } +  } +  return pos +} + +// Skip spaces from given position in reverse. +StateBlock.prototype.skipSpacesBack = function skipSpacesBack (pos, min) { +  if (pos <= min) { return pos } + +  while (pos > min) { +    if (!isSpace(this.src.charCodeAt(--pos))) { return pos + 1 } +  } +  return pos +} + +// Skip char codes from given position +StateBlock.prototype.skipChars = function skipChars (pos, code) { +  for (let max = this.src.length; pos < max; pos++) { +    if (this.src.charCodeAt(pos) !== code) { break } +  } +  return pos +} + +// Skip char codes reverse from given position - 1 +StateBlock.prototype.skipCharsBack = function skipCharsBack (pos, code, min) { +  if (pos <= min) { return pos } + +  while (pos > min) { +    if (code !== this.src.charCodeAt(--pos)) { return pos + 1 } +  } +  return pos +} + +// cut lines range from source. +StateBlock.prototype.getLines = function getLines (begin, end, indent, keepLastLF) { +  if (begin >= end) { +    return '' +  } + +  const queue = new Array(end - begin) + +  for (let i = 0, line = begin; line < end; line++, i++) { +    let lineIndent = 0 +    const lineStart = this.bMarks[line] +    let first = lineStart +    let last + +    if (line + 1 < end || keepLastLF) { +      // No need for bounds check because we have fake entry on tail. +      last = this.eMarks[line] + 1 +    } else { +      last = this.eMarks[line] +    } + +    while (first < last && lineIndent < indent) { +      const ch = this.src.charCodeAt(first) + +      if (isSpace(ch)) { +        if (ch === 0x09) { +          lineIndent += 4 - (lineIndent + this.bsCount[line]) % 4 +        } else { +          lineIndent++ +        } +      } else if (first - lineStart < this.tShift[line]) { +        // patched tShift masked characters to look like spaces (blockquotes, list markers) +        lineIndent++ +      } else { +        break +      } + +      first++ +    } + +    if (lineIndent > indent) { +      // partially expanding tabs in code blocks, e.g '\t\tfoobar' +      // with indent=2 becomes '  \tfoobar' +      queue[i] = new Array(lineIndent - indent + 1).join(' ') + this.src.slice(first, last) +    } else { +      queue[i] = this.src.slice(first, last) +    } +  } + +  return queue.join('') +} + +// re-export Token class to use in block rules +StateBlock.prototype.Token = Token + +export default StateBlock diff --git a/packages/markdown-it-14.1.0/lib/rules_block/table.mjs b/packages/markdown-it-14.1.0/lib/rules_block/table.mjs new file mode 100644 index 0000000..be0ba0a --- /dev/null +++ b/packages/markdown-it-14.1.0/lib/rules_block/table.mjs @@ -0,0 +1,228 @@ +// GFM table, https://github.github.com/gfm/#tables-extension- + +import { isSpace } from '../common/utils.mjs' + +// Limit the amount of empty autocompleted cells in a table, +// see https://github.com/markdown-it/markdown-it/issues/1000, +// +// Both pulldown-cmark and commonmark-hs limit the number of cells this way to ~200k. +// We set it to 65k, which can expand user input by a factor of x370 +// (256x256 square is 1.8kB expanded into 650kB). +const MAX_AUTOCOMPLETED_CELLS = 0x10000 + +function getLine (state, line) { +  const pos = state.bMarks[line] + state.tShift[line] +  const max = state.eMarks[line] + +  return state.src.slice(pos, max) +} + +function escapedSplit (str) { +  const result = [] +  const max = str.length + +  let pos = 0 +  let ch = str.charCodeAt(pos) +  let isEscaped = false +  let lastPos = 0 +  let current = '' + +  while (pos < max) { +    if (ch === 0x7c/* | */) { +      if (!isEscaped) { +        // pipe separating cells, '|' +        result.push(current + str.substring(lastPos, pos)) +        current = '' +        lastPos = pos + 1 +      } else { +        // escaped pipe, '\|' +        current += str.substring(lastPos, pos - 1) +        lastPos = pos +      } +    } + +    isEscaped = (ch === 0x5c/* \ */) +    pos++ + +    ch = str.charCodeAt(pos) +  } + +  result.push(current + str.substring(lastPos)) + +  return result +} + +export default function table (state, startLine, endLine, silent) { +  // should have at least two lines +  if (startLine + 2 > endLine) { return false } + +  let nextLine = startLine + 1 + +  if (state.sCount[nextLine] < state.blkIndent) { return false } + +  // if it's indented more than 3 spaces, it should be a code block +  if (state.sCount[nextLine] - state.blkIndent >= 4) { return false } + +  // first character of the second line should be '|', '-', ':', +  // and no other characters are allowed but spaces; +  // basically, this is the equivalent of /^[-:|][-:|\s]*$/ regexp + +  let pos = state.bMarks[nextLine] + state.tShift[nextLine] +  if (pos >= state.eMarks[nextLine]) { return false } + +  const firstCh = state.src.charCodeAt(pos++) +  if (firstCh !== 0x7C/* | */ && firstCh !== 0x2D/* - */ && firstCh !== 0x3A/* : */) { return false } + +  if (pos >= state.eMarks[nextLine]) { return false } + +  const secondCh = state.src.charCodeAt(pos++) +  if (secondCh !== 0x7C/* | */ && secondCh !== 0x2D/* - */ && secondCh !== 0x3A/* : */ && !isSpace(secondCh)) { +    return false +  } + +  // if first character is '-', then second character must not be a space +  // (due to parsing ambiguity with list) +  if (firstCh === 0x2D/* - */ && isSpace(secondCh)) { return false } + +  while (pos < state.eMarks[nextLine]) { +    const ch = state.src.charCodeAt(pos) + +    if (ch !== 0x7C/* | */ && ch !== 0x2D/* - */ && ch !== 0x3A/* : */ && !isSpace(ch)) { return false } + +    pos++ +  } + +  let lineText = getLine(state, startLine + 1) +  let columns = lineText.split('|') +  const aligns = [] +  for (let i = 0; i < columns.length; i++) { +    const t = columns[i].trim() +    if (!t) { +      // allow empty columns before and after table, but not in between columns; +      // e.g. allow ` |---| `, disallow ` ---||--- ` +      if (i === 0 || i === columns.length - 1) { +        continue +      } else { +        return false +      } +    } + +    if (!/^:?-+:?$/.test(t)) { return false } +    if (t.charCodeAt(t.length - 1) === 0x3A/* : */) { +      aligns.push(t.charCodeAt(0) === 0x3A/* : */ ? 'center' : 'right') +    } else if (t.charCodeAt(0) === 0x3A/* : */) { +      aligns.push('left') +    } else { +      aligns.push('') +    } +  } + +  lineText = getLine(state, startLine).trim() +  if (lineText.indexOf('|') === -1) { return false } +  if (state.sCount[startLine] - state.blkIndent >= 4) { return false } +  columns = escapedSplit(lineText) +  if (columns.length && columns[0] === '') columns.shift() +  if (columns.length && columns[columns.length - 1] === '') columns.pop() + +  // header row will define an amount of columns in the entire table, +  // and align row should be exactly the same (the rest of the rows can differ) +  const columnCount = columns.length +  if (columnCount === 0 || columnCount !== aligns.length) { return false } + +  if (silent) { return true } + +  const oldParentType = state.parentType +  state.parentType = 'table' + +  // use 'blockquote' lists for termination because it's +  // the most similar to tables +  const terminatorRules = state.md.block.ruler.getRules('blockquote') + +  const token_to = state.push('table_open', 'table', 1) +  const tableLines = [startLine, 0] +  token_to.map = tableLines + +  const token_tho = state.push('thead_open', 'thead', 1) +  token_tho.map = [startLine, startLine + 1] + +  const token_htro = state.push('tr_open', 'tr', 1) +  token_htro.map = [startLine, startLine + 1] + +  for (let i = 0; i < columns.length; i++) { +    const token_ho = state.push('th_open', 'th', 1) +    if (aligns[i]) { +      token_ho.attrs  = [['style', 'text-align:' + aligns[i]]] +    } + +    const token_il = state.push('inline', '', 0) +    token_il.content  = columns[i].trim() +    token_il.children = [] + +    state.push('th_close', 'th', -1) +  } + +  state.push('tr_close', 'tr', -1) +  state.push('thead_close', 'thead', -1) + +  let tbodyLines +  let autocompletedCells = 0 + +  for (nextLine = startLine + 2; nextLine < endLine; nextLine++) { +    if (state.sCount[nextLine] < state.blkIndent) { break } + +    let terminate = false +    for (let i = 0, l = terminatorRules.length; i < l; i++) { +      if (terminatorRules[i](state, nextLine, endLine, true)) { +        terminate = true +        break +      } +    } + +    if (terminate) { break } +    lineText = getLine(state, nextLine).trim() +    if (!lineText) { break } +    if (state.sCount[nextLine] - state.blkIndent >= 4) { break } +    columns = escapedSplit(lineText) +    if (columns.length && columns[0] === '') columns.shift() +    if (columns.length && columns[columns.length - 1] === '') columns.pop() + +    // note: autocomplete count can be negative if user specifies more columns than header, +    // but that does not affect intended use (which is limiting expansion) +    autocompletedCells += columnCount - columns.length +    if (autocompletedCells > MAX_AUTOCOMPLETED_CELLS) { break } + +    if (nextLine === startLine + 2) { +      const token_tbo = state.push('tbody_open', 'tbody', 1) +      token_tbo.map = tbodyLines = [startLine + 2, 0] +    } + +    const token_tro = state.push('tr_open', 'tr', 1) +    token_tro.map = [nextLine, nextLine + 1] + +    for (let i = 0; i < columnCount; i++) { +      const token_tdo = state.push('td_open', 'td', 1) +      if (aligns[i]) { +        token_tdo.attrs  = [['style', 'text-align:' + aligns[i]]] +      } + +      const token_il = state.push('inline', '', 0) +      token_il.content  = columns[i] ? columns[i].trim() : '' +      token_il.children = [] + +      state.push('td_close', 'td', -1) +    } +    state.push('tr_close', 'tr', -1) +  } + +  if (tbodyLines) { +    state.push('tbody_close', 'tbody', -1) +    tbodyLines[1] = nextLine +  } + +  state.push('table_close', 'table', -1) +  tableLines[1] = nextLine + +  state.parentType = oldParentType +  state.line = nextLine +  return true +} | 
