UNPKG

@adobe/micromark-extension-gridtables

Version:
363 lines (330 loc) 10 kB
/* * Copyright 2022 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ /* eslint-disable no-use-before-define,no-underscore-dangle,no-param-reassign */ import { codes, types } from 'micromark-util-symbol'; import { markdownLineEnding, markdownSpace, } from 'micromark-util-character'; import { TYPE_BODY, TYPE_CELL, TYPE_HEADER, TYPE_FOOTER, TYPE_TABLE, TYPE_GRID_DIVIDER, TYPE_ROW_LINE, } from './types.js'; // the cell divider: | or + const TYPE_CELL_DIVIDER = 'cellDivider'; const V_ALIGN_CODES = { [codes.lowercaseV]: 'bottom', [codes.lowercaseX]: 'middle', [codes.caret]: 'top', }; function parse() { return { tokenize: tokenizeTable, resolve: resolveTable, resolveAll: resolveAllTable, concrete: true, }; // special tokenizer to check for the line start. this is needed to we can avoid consuming // the line ending after the table function tokenizeLineStart(effects, ok, nok) { function lineStart(code) { if (code === codes.plusSign || code === codes.verticalBar) { return ok(code); } return nok(code); } function lineEnding(code) { // consume line ending effects.enter(types.lineEnding); effects.consume(code); effects.exit(types.lineEnding); return lineStart; } return lineEnding; } function tokenizeTable(effects, ok, nok) { // positions of columns const cols = [0]; let numRows = 0; let numCols = 0; let colPos = 0; let rowLine = null; let align = ''; let valign = ''; return start; function start(code) { effects.enter(TYPE_TABLE)._cols = cols; effects.enter(TYPE_BODY); return lineStart(code); } function lineStart(code) { rowLine = effects.enter(TYPE_ROW_LINE); effects.enter(TYPE_CELL_DIVIDER); effects.consume(code); effects.exit(TYPE_CELL_DIVIDER); colPos = 0; numCols = 0; return cellOrGridStart; } function cellOrGridStart(code) { align = ''; valign = ''; if (code === codes.dash || code === codes.equalsTo || code === codes.colon || code === codes.greaterThan) { effects.enter(TYPE_GRID_DIVIDER)._colStart = colPos; colPos += 1; if (code === codes.colon) { align = 'left'; } else if (code === codes.greaterThan) { align = 'justify'; } effects.consume(code); return gridDivider; } if (code === codes.eof || markdownLineEnding(code)) { return lineEnd(code); } effects.enter(TYPE_CELL)._colStart = colPos; colPos += 1; effects.consume(code); if (markdownSpace(code)) { return cellSpace; } return cell; } function cellSpace(code) { if (code === codes.eof || markdownLineEnding(code)) { // mark as discarded, will be filtered out in transform effects.exit(TYPE_CELL)._discard = true; return lineEnd(code); } if (markdownSpace(code)) { colPos += 1; effects.consume(code); return cellSpace; } return cell(code); } function endTable(code) { if (numRows < 3) { return nok(code); } effects.exit(TYPE_BODY); effects.exit(TYPE_TABLE); return ok(code); } function lineEnd(code) { if (numCols === 0) { return nok(code); } effects.exit(TYPE_ROW_LINE); numRows += 1; if (code === codes.eof) { return endTable(code); } // since this function is only called for lineEnding or EOF, we can assume EOL here // let's check if the grid table is finished and if not, go to line start // this is needed so that we don't consume the line ending after the table return effects.check( { tokenize: tokenizeLineStart }, (cd) => { effects.enter(types.lineEnding); effects.consume(cd); effects.exit(types.lineEnding); return lineStart; }, endTable, )(code); } function gridDivider(code) { colPos += 1; if (code === codes.dash || code === codes.equalsTo) { if (!rowLine._type) { rowLine._type = code; } effects.consume(code); return gridDivider; } if (code === codes.colon) { if (!align) { align = 'right'; } else if (align === 'left') { align = 'center'; } else { return nok(code); } effects.consume(code); return gridDividerEnd; } if (code === codes.lessThan) { if (align !== 'justify') { return nok(code); } effects.consume(code); return gridDividerEnd; } if (V_ALIGN_CODES[code]) { if (valign) { return nok(code); } valign = V_ALIGN_CODES[code]; effects.consume(code); return gridDivider; } if (code === codes.plusSign || code === codes.verticalBar) { colPos -= 1; return gridDividerEnd(code); } return nok(code); } function gridDividerEnd(code) { if (code !== codes.plusSign && code !== codes.verticalBar) { return nok(code); } // for a super small column, assume dash if (!rowLine._type) { rowLine._type = code.dash; } colPos += 1; // remember cols const idx = cols.indexOf(colPos); if (idx < 0) { cols.push(colPos); cols.sort((c0, c1) => c0 - c1); } const token = effects.exit(TYPE_GRID_DIVIDER); token._colEnd = colPos; token._align = align; token._valign = valign; effects.enter(TYPE_CELL_DIVIDER); effects.consume(code); effects.exit(TYPE_CELL_DIVIDER); numCols += 1; return cellOrGridStart; } function cell(code) { colPos += 1; // find existing col if (code === codes.verticalBar || code === codes.plusSign) { const idx = cols.indexOf(colPos); if (idx >= 0) { effects.exit(TYPE_CELL)._colEnd = colPos; effects.enter(TYPE_CELL_DIVIDER); effects.consume(code); effects.exit(TYPE_CELL_DIVIDER); numCols += 1; return cellOrGridStart; } effects.consume(code); return cell; } if (code === codes.eof) { // row with cells never terminate eof return nok(code); } effects.consume(code); return (code === codes.backslash) ? cellEscaped : cell; } function cellEscaped(code) { if (code === codes.backslash || code === codes.verticalBar || code === codes.plusSign) { colPos += 1; effects.consume(code); return cell; } return cell(code); } } function resolveHeaderAndFooter(events, context) { // detect headers: // no `=` lines -> only body // 1 `=` line -> header + body // 2 `=` lines -> header + body + footer const fatLines = []; let bodyStart = -1; // should default to 1. but just be sure for (let idx = 0; idx < events.length; idx += 1) { const [e, node] = events[idx]; const { type } = node; if (type === TYPE_BODY) { if (e === 'enter') { bodyStart = idx; } else { // eslint-disable-next-line prefer-const let [hdrIdx, ftrIdx] = fatLines; const bdy = node; if (hdrIdx > bodyStart + 1) { // insert header above body const hdr = { type: TYPE_HEADER, start: bdy.start, end: events[hdrIdx][1].end, }; bdy.start = hdr.end; events[bodyStart][1] = hdr; events.splice( hdrIdx, 0, ['exit', hdr, context], ['enter', bdy, context], ); idx += 2; ftrIdx += 2; } if (ftrIdx) { // insert footer below body const ftr = { type: TYPE_FOOTER, start: events[ftrIdx][1].start, end: bdy.end, }; bdy.end = ftr.start; events.splice( ftrIdx, 0, ['exit', bdy, context], ['enter', ftr, context], ); idx += 2; events[idx][1] = ftr; } } } else if (type === TYPE_ROW_LINE && e === 'enter' && node._type === codes.equalsTo) { fatLines.push(idx); } } return events; } function resolveTable(events, context) { // remove discarded events = events.filter(([, node]) => !node._discard); events = resolveHeaderAndFooter(events, context); return events; } function resolveAllTable(events, context) { // since we create a detached parser for each cell content later (in from-markdown.js) // we need to remember the definitions of the overall document. otherwise the cell parsers // would not detect the image and link references. const { defined } = context.parser; // find all grid tables and remember the definitions for (const [evt, node] of events) { if (evt === 'enter' && node.type === TYPE_TABLE) { node._definitions = defined; } } return events; } } export const gridTables = { flow: { [codes.plusSign]: parse(), }, };