soumaeda 2 лет назад
Родитель
Сommit
da943486b4

+ 2 - 0
packages/micromark-extension-gfm-table/dev/index.d.ts

@@ -0,0 +1,2 @@
+export { gfmTableHtml } from "./lib/html.js";
+export { gfmTable } from "./lib/syntax.js";

+ 8 - 0
packages/micromark-extension-gfm-table/dev/lib/html.d.ts

@@ -0,0 +1,8 @@
+/**
+ * HTML extension for micromark (passed in `htmlExtensions`).
+ *
+ * @type {HtmlExtension}
+ */
+export const gfmTableHtml: HtmlExtension;
+export type HtmlExtension = import('micromark-util-types').HtmlExtension;
+export type Align = import('./syntax.js').Align;

+ 12 - 0
packages/micromark-extension-gfm-table/dev/lib/syntax.d.ts

@@ -0,0 +1,12 @@
+/**
+ * Syntax extension for micromark (passed in `extensions`).
+ *
+ * @type {Extension}
+ */
+export const gfmTable: Extension;
+export type Extension = import('micromark-util-types').Extension;
+export type Resolver = import('micromark-util-types').Resolver;
+export type Tokenizer = import('micromark-util-types').Tokenizer;
+export type State = import('micromark-util-types').State;
+export type Token = import('micromark-util-types').Token;
+export type Align = 'left' | 'center' | 'right' | 'none';

+ 2 - 0
packages/micromark-extension-gfm-table/index.d.ts

@@ -0,0 +1,2 @@
+export { gfmTableHtml } from "./lib/html.js";
+export { gfmTable } from "./lib/syntax.js";

+ 2 - 0
packages/micromark-extension-gfm-table/index.js

@@ -0,0 +1,2 @@
+export { gfmTableHtml } from './lib/html.js';
+export { gfmTable } from './lib/syntax.js';

+ 8 - 0
packages/micromark-extension-gfm-table/lib/html.d.ts

@@ -0,0 +1,8 @@
+/**
+ * HTML extension for micromark (passed in `htmlExtensions`).
+ *
+ * @type {HtmlExtension}
+ */
+export const gfmTableHtml: HtmlExtension;
+export type HtmlExtension = import('micromark-util-types').HtmlExtension;
+export type Align = import('./syntax.js').Align;

+ 134 - 0
packages/micromark-extension-gfm-table/lib/html.js

@@ -0,0 +1,134 @@
+/**
+ * @typedef {import('micromark-util-types').HtmlExtension} HtmlExtension
+ * @typedef {import('./syntax.js').Align} Align
+ */
+
+const alignment = {
+  none: '',
+  left: ' align="left"',
+  right: ' align="right"',
+  center: ' align="center"'
+};
+
+/**
+ * HTML extension for micromark (passed in `htmlExtensions`).
+ *
+ * @type {HtmlExtension}
+ */
+export const gfmTableHtml = {
+  enter: {
+    table(token) {
+      /** @type {Array<Align>} */
+      // @ts-expect-error Custom.
+      const tableAlign = token._align;
+      this.lineEndingIfNeeded();
+      this.tag('<table>');
+      this.setData('tableAlign', tableAlign);
+    },
+    tableBody() {
+      // Clear slurping line ending from the delimiter row.
+      this.setData('slurpOneLineEnding');
+      this.tag('<tbody>');
+    },
+    tableData() {
+      const tableAlign = /** @type {Array<Align>} */
+      this.getData('tableAlign');
+      const tableColumn = /** @type {number} */this.getData('tableColumn');
+      const align = alignment[tableAlign[tableColumn]];
+      if (align === undefined) {
+        // Capture results to ignore them.
+        this.buffer();
+      } else {
+        this.lineEndingIfNeeded();
+        this.tag('<td' + align + '>');
+      }
+    },
+    tableHead() {
+      this.lineEndingIfNeeded();
+      this.tag('<thead>');
+    },
+    tableHeader() {
+      const tableAlign = /** @type {Array<Align>} */
+      this.getData('tableAlign');
+      const tableColumn = /** @type {number} */this.getData('tableColumn');
+      const align = alignment[tableAlign[tableColumn]];
+      this.lineEndingIfNeeded();
+      this.tag('<th' + align + '>');
+    },
+    tableRow() {
+      this.setData('tableColumn', 0);
+      this.lineEndingIfNeeded();
+      this.tag('<tr>');
+    }
+  },
+  exit: {
+    // Overwrite the default code text data handler to unescape escaped pipes when
+    // they are in tables.
+    codeTextData(token) {
+      let value = this.sliceSerialize(token);
+      if (this.getData('tableAlign')) {
+        value = value.replace(/\\([\\|])/g, replace);
+      }
+      this.raw(this.encode(value));
+    },
+    table() {
+      this.setData('tableAlign');
+      // If there was no table body, make sure the slurping from the delimiter row
+      // is cleared.
+      this.setData('slurpAllLineEndings');
+      this.lineEndingIfNeeded();
+      this.tag('</table>');
+    },
+    tableBody() {
+      this.lineEndingIfNeeded();
+      this.tag('</tbody>');
+    },
+    tableData() {
+      const tableAlign = /** @type {Array<Align>} */
+      this.getData('tableAlign');
+      const tableColumn = /** @type {number} */this.getData('tableColumn');
+      if (tableColumn in tableAlign) {
+        this.tag('</td>');
+        this.setData('tableColumn', tableColumn + 1);
+      } else {
+        // Stop capturing.
+        this.resume();
+      }
+    },
+    tableHead() {
+      this.lineEndingIfNeeded();
+      this.tag('</thead>');
+      this.setData('slurpOneLineEnding', true);
+      // Slurp the line ending from the delimiter row.
+    },
+
+    tableHeader() {
+      const tableColumn = /** @type {number} */this.getData('tableColumn');
+      this.tag('</th>');
+      this.setData('tableColumn', tableColumn + 1);
+    },
+    tableRow() {
+      const tableAlign = /** @type {Array<Align>} */
+      this.getData('tableAlign');
+      let tableColumn = /** @type {number} */this.getData('tableColumn');
+      while (tableColumn < tableAlign.length) {
+        this.lineEndingIfNeeded();
+        this.tag('<td' + alignment[tableAlign[tableColumn]] + '></td>');
+        tableColumn++;
+      }
+      this.setData('tableColumn', tableColumn);
+      this.lineEndingIfNeeded();
+      this.tag('</tr>');
+    }
+  }
+};
+
+/**
+ * @param {string} $0
+ * @param {string} $1
+ * @returns {string}
+ */
+function replace($0, $1) {
+  // Pipes work, backslashes don’t (but can’t escape pipes).
+  return $1 === '|' ? $1 : $0;
+}

+ 12 - 0
packages/micromark-extension-gfm-table/lib/syntax.d.ts

@@ -0,0 +1,12 @@
+/**
+ * Syntax extension for micromark (passed in `extensions`).
+ *
+ * @type {Extension}
+ */
+export const gfmTable: Extension;
+export type Extension = import('micromark-util-types').Extension;
+export type Resolver = import('micromark-util-types').Resolver;
+export type Tokenizer = import('micromark-util-types').Tokenizer;
+export type State = import('micromark-util-types').State;
+export type Token = import('micromark-util-types').Token;
+export type Align = 'left' | 'center' | 'right' | 'none';

+ 552 - 0
packages/micromark-extension-gfm-table/lib/syntax.js

@@ -0,0 +1,552 @@
+/**
+ * @typedef {import('micromark-util-types').Extension} Extension
+ * @typedef {import('micromark-util-types').Resolver} Resolver
+ * @typedef {import('micromark-util-types').Tokenizer} Tokenizer
+ * @typedef {import('micromark-util-types').State} State
+ * @typedef {import('micromark-util-types').Token} Token
+ */
+
+/**
+ * @typedef {'left'|'center'|'right'|'none'} Align
+ */
+
+import { factorySpace } from 'micromark-factory-space';
+import { markdownLineEnding, markdownLineEndingOrSpace, markdownSpace } from 'micromark-util-character';
+/**
+ * Syntax extension for micromark (passed in `extensions`).
+ *
+ * @type {Extension}
+ */
+export const gfmTable = {
+  flow: {
+    null: {
+      tokenize: tokenizeTable,
+      resolve: resolveTable
+    }
+  }
+};
+const nextPrefixedOrBlank = {
+  tokenize: tokenizeNextPrefixedOrBlank,
+  partial: true
+};
+
+/** @type {Resolver} */
+// eslint-disable-next-line complexity
+function resolveTable(events, context) {
+  let index = -1;
+  /** @type {boolean|undefined} */
+  let inHead;
+  /** @type {boolean|undefined} */
+  let inDelimiterRow;
+  /** @type {boolean|undefined} */
+  let inRow;
+  /** @type {number|undefined} */
+  let contentStart;
+  /** @type {number|undefined} */
+  let contentEnd;
+  /** @type {number|undefined} */
+  let cellStart;
+  /** @type {boolean|undefined} */
+  let seenCellInRow;
+  while (++index < events.length) {
+    const token = events[index][1];
+    if (inRow) {
+      if (token.type === 'temporaryTableCellContent') {
+        contentStart = contentStart || index;
+        contentEnd = index;
+      }
+      if (
+      // Combine separate content parts into one.
+      (token.type === 'tableCellDivider' || token.type === 'tableRow') && contentEnd) {
+        const content = {
+          type: 'tableContent',
+          start: events[contentStart][1].start,
+          end: events[contentEnd][1].end
+        };
+        /** @type {Token} */
+        const text = {
+          type: "chunkText",
+          start: content.start,
+          end: content.end,
+          // @ts-expect-error It’s fine.
+          contentType: "text"
+        };
+        events.splice(contentStart, contentEnd - contentStart + 1, ['enter', content, context], ['enter', text, context], ['exit', text, context], ['exit', content, context]);
+        index -= contentEnd - contentStart - 3;
+        contentStart = undefined;
+        contentEnd = undefined;
+      }
+    }
+    if (events[index][0] === 'exit' && cellStart !== undefined && cellStart + (seenCellInRow ? 0 : 1) < index && (token.type === 'tableCellDivider' || token.type === 'tableRow' && (cellStart + 3 < index || events[cellStart][1].type !== "whitespace"))) {
+      const cell = {
+        // eslint-disable-next-line no-nested-ternary
+        type: inDelimiterRow ? 'tableDelimiter' : inHead ? 'tableHeader' : 'tableData',
+        start: events[cellStart][1].start,
+        end: events[index][1].end
+      };
+      events.splice(index + (token.type === 'tableCellDivider' ? 1 : 0), 0, ['exit', cell, context]);
+      events.splice(cellStart, 0, ['enter', cell, context]);
+      index += 2;
+      cellStart = index + 1;
+      seenCellInRow = true;
+    }
+    if (token.type === 'tableRow') {
+      inRow = events[index][0] === 'enter';
+      if (inRow) {
+        cellStart = index + 1;
+        seenCellInRow = false;
+      }
+    }
+    if (token.type === 'tableDelimiterRow') {
+      inDelimiterRow = events[index][0] === 'enter';
+      if (inDelimiterRow) {
+        cellStart = index + 1;
+        seenCellInRow = false;
+      }
+    }
+    if (token.type === 'tableHead') {
+      inHead = events[index][0] === 'enter';
+    }
+  }
+  return events;
+}
+
+/** @type {Tokenizer} */
+function tokenizeTable(effects, ok, nok) {
+  // eslint-disable-next-line @typescript-eslint/no-this-alias
+  const self = this;
+  /** @type {Array<Align>} */
+  const align = [];
+  let tableHeaderCount = 0;
+  /** @type {boolean|undefined} */
+  let seenDelimiter;
+  /** @type {boolean|undefined} */
+  let hasDash;
+  return start;
+
+  /** @type {State} */
+  function start(code) {
+    const {
+      containerState
+    } = self;
+    const prevRowCount = containerState.rowCount ?? 0;
+    const hasDelimiterRow = containerState.hasDelimiterRow ?? false;
+
+    // @ts-expect-error Custom.
+    effects.enter('table')._align = align;
+    effects.enter('tableHead');
+    effects.enter('tableRow');
+
+    // increment row count
+    const crrRowCount = prevRowCount + 1;
+    containerState.rowCount = crrRowCount;
+
+    // Max 2 rows processing before delimiter row
+    if (hasDelimiterRow || crrRowCount > 2) {
+      return nok(code);
+    }
+
+    // If we start with a pipe, we open a cell marker.
+    if (code === 124) {
+      return cellDividerHead(code);
+    }
+    tableHeaderCount++;
+    effects.enter('temporaryTableCellContent');
+    // Can’t be space or eols at the start of a construct, so we’re in a cell.
+
+    return inCellContentHead(code);
+  }
+
+  /** @type {State} */
+  function cellDividerHead(code) {
+    effects.enter('tableCellDivider');
+    effects.consume(code);
+    effects.exit('tableCellDivider');
+    seenDelimiter = true;
+    return cellBreakHead;
+  }
+
+  /** @type {State} */
+  function cellBreakHead(code) {
+    if (code === null || markdownLineEnding(code)) {
+      return atRowEndHead(code);
+    }
+    if (markdownSpace(code)) {
+      effects.enter("whitespace");
+      effects.consume(code);
+      return inWhitespaceHead;
+    }
+    if (seenDelimiter) {
+      seenDelimiter = undefined;
+      tableHeaderCount++;
+    }
+    if (code === 124) {
+      return cellDividerHead(code);
+    }
+
+    // Anything else is cell content.
+    effects.enter('temporaryTableCellContent');
+    return inCellContentHead(code);
+  }
+
+  /** @type {State} */
+  function inWhitespaceHead(code) {
+    if (markdownSpace(code)) {
+      effects.consume(code);
+      return inWhitespaceHead;
+    }
+    effects.exit("whitespace");
+    return cellBreakHead(code);
+  }
+
+  /** @type {State} */
+  function inCellContentHead(code) {
+    // EOF, whitespace, pipe
+    if (code === null || code === 124 || markdownLineEndingOrSpace(code)) {
+      effects.exit('temporaryTableCellContent');
+      return cellBreakHead(code);
+    }
+    effects.consume(code);
+    return code === 92 ? inCellContentEscapeHead : inCellContentHead;
+  }
+
+  /** @type {State} */
+  function inCellContentEscapeHead(code) {
+    if (code === 92 || code === 124) {
+      effects.consume(code);
+      return inCellContentHead;
+    }
+
+    // Anything else.
+    return inCellContentHead(code);
+  }
+
+  /** @type {State} */
+  function atRowEndHead(code) {
+    // for debug -- 2023.05.06 Yuki Takei
+    // const { containerState } = self;
+    // let atRowEndHeadCount = containerState.atRowEndHeadCount ?? 0;
+    // atRowEndHeadCount++;
+    // containerState.atRowEndHeadCount = atRowEndHeadCount;
+    // console.log({ atRowEndHeadCount });
+
+    if (code === null) {
+      return tableExit(code);
+    }
+    effects.exit('tableRow');
+    effects.exit('tableHead');
+    const originalInterrupt = self.interrupt;
+    self.interrupt = true;
+    return effects.attempt({
+      tokenize: tokenizeRowEnd,
+      partial: true
+    }, code => {
+      self.interrupt = originalInterrupt;
+      effects.enter('tableDelimiterRow');
+      return atDelimiterRowBreak(code);
+    }, code => {
+      self.interrupt = originalInterrupt;
+      return tableExit(code);
+    })(code);
+  }
+
+  /** @type {State} */
+  function atDelimiterRowBreak(code) {
+    // persist that the table has a delimiter row
+    self.containerState.hasDelimiterRow = true;
+    if (code === null || markdownLineEnding(code)) {
+      return rowEndDelimiter(code);
+    }
+    if (markdownSpace(code)) {
+      effects.enter("whitespace");
+      effects.consume(code);
+      return inWhitespaceDelimiter;
+    }
+    if (code === 45) {
+      effects.enter('tableDelimiterFiller');
+      effects.consume(code);
+      hasDash = true;
+      align.push('none');
+      return inFillerDelimiter;
+    }
+    if (code === 58) {
+      effects.enter('tableDelimiterAlignment');
+      effects.consume(code);
+      effects.exit('tableDelimiterAlignment');
+      align.push('left');
+      return afterLeftAlignment;
+    }
+
+    // If we start with a pipe, we open a cell marker.
+    if (code === 124) {
+      effects.enter('tableCellDivider');
+      effects.consume(code);
+      effects.exit('tableCellDivider');
+      return atDelimiterRowBreak;
+    }
+    return tableExit(code);
+  }
+
+  /** @type {State} */
+  function inWhitespaceDelimiter(code) {
+    if (markdownSpace(code)) {
+      effects.consume(code);
+      return inWhitespaceDelimiter;
+    }
+    effects.exit("whitespace");
+    return atDelimiterRowBreak(code);
+  }
+
+  /** @type {State} */
+  function inFillerDelimiter(code) {
+    if (code === 45) {
+      effects.consume(code);
+      return inFillerDelimiter;
+    }
+    effects.exit('tableDelimiterFiller');
+    if (code === 58) {
+      effects.enter('tableDelimiterAlignment');
+      effects.consume(code);
+      effects.exit('tableDelimiterAlignment');
+      align[align.length - 1] = align[align.length - 1] === 'left' ? 'center' : 'right';
+      return afterRightAlignment;
+    }
+    return atDelimiterRowBreak(code);
+  }
+
+  /** @type {State} */
+  function afterLeftAlignment(code) {
+    if (code === 45) {
+      effects.enter('tableDelimiterFiller');
+      effects.consume(code);
+      hasDash = true;
+      return inFillerDelimiter;
+    }
+
+    // Anything else is not ok.
+    return tableExit(code);
+  }
+
+  /** @type {State} */
+  function afterRightAlignment(code) {
+    if (code === null || markdownLineEnding(code)) {
+      return rowEndDelimiter(code);
+    }
+    if (markdownSpace(code)) {
+      effects.enter("whitespace");
+      effects.consume(code);
+      return inWhitespaceDelimiter;
+    }
+
+    // `|`
+    if (code === 124) {
+      effects.enter('tableCellDivider');
+      effects.consume(code);
+      effects.exit('tableCellDivider');
+      return atDelimiterRowBreak;
+    }
+    return tableExit(code);
+  }
+
+  /** @type {State} */
+  function rowEndDelimiter(code) {
+    effects.exit('tableDelimiterRow');
+
+    // Exit if there was no dash at all, or if the header cell count is not the
+    // delimiter cell count.
+    if (!hasDash || tableHeaderCount !== align.length) {
+      return tableExit(code);
+    }
+    if (code === null) {
+      return tableClose(code);
+    }
+    return effects.check(nextPrefixedOrBlank, tableClose, effects.attempt({
+      tokenize: tokenizeRowEnd,
+      partial: true
+    }, factorySpace(effects, bodyStart, "linePrefix", 4), tableClose))(code);
+  }
+
+  /** @type {State} */
+  function tableExit(code) {
+    // delete persisted states
+    delete self.containerState.rowCount;
+    delete self.containerState.hasDelimiterRow;
+    return nok(code);
+  }
+
+  /** @type {State} */
+  function tableClose(code) {
+    effects.exit('table');
+
+    // delete persisted states
+    delete self.containerState.rowCount;
+    delete self.containerState.hasDelimiterRow;
+    return ok(code);
+  }
+
+  /** @type {State} */
+  function bodyStart(code) {
+    effects.enter('tableBody');
+    return rowStartBody(code);
+  }
+
+  /** @type {State} */
+  function rowStartBody(code) {
+    effects.enter('tableRow');
+
+    // If we start with a pipe, we open a cell marker.
+    if (code === 124) {
+      return cellDividerBody(code);
+    }
+    effects.enter('temporaryTableCellContent');
+    // Can’t be space or eols at the start of a construct, so we’re in a cell.
+    return inCellContentBody(code);
+  }
+
+  /** @type {State} */
+  function cellDividerBody(code) {
+    effects.enter('tableCellDivider');
+    effects.consume(code);
+    effects.exit('tableCellDivider');
+    return cellBreakBody;
+  }
+
+  /** @type {State} */
+  function cellBreakBody(code) {
+    if (code === null || markdownLineEnding(code)) {
+      return atRowEndBody(code);
+    }
+    if (markdownSpace(code)) {
+      effects.enter("whitespace");
+      effects.consume(code);
+      return inWhitespaceBody;
+    }
+
+    // `|`
+    if (code === 124) {
+      return cellDividerBody(code);
+    }
+
+    // Anything else is cell content.
+    effects.enter('temporaryTableCellContent');
+    return inCellContentBody(code);
+  }
+
+  /** @type {State} */
+  function inWhitespaceBody(code) {
+    if (markdownSpace(code)) {
+      effects.consume(code);
+      return inWhitespaceBody;
+    }
+    effects.exit("whitespace");
+    return cellBreakBody(code);
+  }
+
+  /** @type {State} */
+  function inCellContentBody(code) {
+    // EOF, whitespace, pipe
+    if (code === null || code === 124 || markdownLineEndingOrSpace(code)) {
+      effects.exit('temporaryTableCellContent');
+      return cellBreakBody(code);
+    }
+    effects.consume(code);
+    return code === 92 ? inCellContentEscapeBody : inCellContentBody;
+  }
+
+  /** @type {State} */
+  function inCellContentEscapeBody(code) {
+    if (code === 92 || code === 124) {
+      effects.consume(code);
+      return inCellContentBody;
+    }
+
+    // Anything else.
+    return inCellContentBody(code);
+  }
+
+  /** @type {State} */
+  function atRowEndBody(code) {
+    effects.exit('tableRow');
+    if (code === null) {
+      return tableBodyClose(code);
+    }
+    return effects.check(nextPrefixedOrBlank, tableBodyClose, effects.attempt({
+      tokenize: tokenizeRowEnd,
+      partial: true
+    }, factorySpace(effects, rowStartBody, "linePrefix", 4), tableBodyClose))(code);
+  }
+
+  /** @type {State} */
+  function tableBodyClose(code) {
+    effects.exit('tableBody');
+    return tableClose(code);
+  }
+
+  /** @type {Tokenizer} */
+  function tokenizeRowEnd(effects, ok, nok) {
+    return start;
+
+    /** @type {State} */
+    function start(code) {
+      effects.enter("lineEnding");
+      effects.consume(code);
+      effects.exit("lineEnding");
+      return factorySpace(effects, prefixed, "linePrefix");
+    }
+
+    /** @type {State} */
+    function prefixed(code) {
+      // Blank or interrupting line.
+      if (self.parser.lazy[self.now().line] || code === null || markdownLineEnding(code)) {
+        return nok(code);
+      }
+      const tail = self.events[self.events.length - 1];
+
+      // Indented code can interrupt delimiter and body rows.
+      if (!self.parser.constructs.disable.null.includes('codeIndented') && tail && tail[1].type === "linePrefix" && tail[2].sliceSerialize(tail[1], true).length >= 4) {
+        return nok(code);
+      }
+      self._gfmTableDynamicInterruptHack = true;
+      return effects.check(self.parser.constructs.flow, code => {
+        self._gfmTableDynamicInterruptHack = false;
+        return nok(code);
+      }, code => {
+        self._gfmTableDynamicInterruptHack = false;
+        return ok(code);
+      })(code);
+    }
+  }
+}
+
+/** @type {Tokenizer} */
+function tokenizeNextPrefixedOrBlank(effects, ok, nok) {
+  let size = 0;
+  return start;
+
+  /** @type {State} */
+  function start(code) {
+    // This is a check, so we don’t care about tokens, but we open a bogus one
+    // so we’re valid.
+    effects.enter('check');
+    // EOL.
+    effects.consume(code);
+    return whitespace;
+  }
+
+  /** @type {State} */
+  function whitespace(code) {
+    if (code === -1 || code === 32) {
+      effects.consume(code);
+      size++;
+      return size === 4 ? ok : whitespace;
+    }
+
+    // EOF or whitespace
+    if (code === null || markdownLineEndingOrSpace(code)) {
+      return ok(code);
+    }
+
+    // Anything else.
+    return nok(code);
+  }
+}

+ 1 - 0
packages/micromark-extension-gfm-table/test/index.d.ts

@@ -0,0 +1 @@
+export {};