@atlaskit/editor-wikimarkup-transformer
Version:
Wiki markup transformer for JIRA and Confluence
326 lines (321 loc) • 10.1 kB
JavaScript
import { TableBuilder } from '../builder/table-builder';
import { parseString } from '../text';
import { normalizePMNodes } from '../utils/normalize';
import { linkFormat } from './links/link-format';
import { media } from './media';
import { emoji } from './emoji';
import { TokenType, parseToken } from './';
import { parseNewlineOnly } from './whitespace';
import { parseMacroKeyword } from './keyword';
import { hasAnyOfMarks } from '../utils/text';
/*
The following are currently NOT supported
1. Macros
2. Escape |
3. Table of table
*/
// Ignored via go/ees005
// eslint-disable-next-line require-unicode-regexp
const CELL_REGEXP = /^([ \t]*)([|]+)([ \t]*)/;
// Ignored via go/ees005
// eslint-disable-next-line require-unicode-regexp
const EMPTY_LINE_REGEXP = /^[ \t]*\r?\n/;
const processState = {
END_TABLE: 2,
BUFFER: 4,
CLOSE_ROW: 5,
NEW_ROW: 6,
LINE_BREAK: 7,
LINK: 8,
MEDIA: 9,
MACRO: 10,
EMOJI: 11
};
export const table = ({
input,
position,
schema,
context
}) => {
/**
* The following token types will be ignored in parsing
* the content of a table cell
*/
const ignoreTokenTypes = [TokenType.DOUBLE_DASH_SYMBOL, TokenType.TRIPLE_DASH_SYMBOL, TokenType.QUADRUPLE_DASH_SYMBOL, TokenType.TABLE, TokenType.RULER];
const output = [];
let index = position;
let currentState = processState.NEW_ROW;
let buffer = [];
let cellsBuffer = [];
let cellStyle = '';
let builder = null;
while (index < input.length) {
const char = input.charAt(index);
const substring = input.substring(index);
switch (currentState) {
case processState.NEW_ROW:
{
const tableMatch = substring.match(CELL_REGEXP);
if (tableMatch) {
if (!builder) {
builder = new TableBuilder(schema);
}
// Capture empty spaces
index += tableMatch[1].length;
cellStyle = tableMatch[2];
index += tableMatch[2].length;
currentState = processState.BUFFER;
continue;
}
currentState = processState.END_TABLE;
continue;
}
case processState.LINE_BREAK:
{
const emptyLineMatch = substring.match(EMPTY_LINE_REGEXP);
if (emptyLineMatch) {
// If we encounter an empty line, we should end the table
bufferToCells(cellStyle, buffer.join(''), cellsBuffer, schema, ignoreTokenTypes, context);
currentState = processState.END_TABLE;
continue;
}
// If we enconter a new row
const cellMatch = substring.match(CELL_REGEXP);
if (cellMatch) {
currentState = processState.CLOSE_ROW;
} else {
currentState = processState.BUFFER;
}
continue;
}
case processState.BUFFER:
{
const length = parseNewlineOnly(substring);
if (length) {
// Calculate the index of the end of the current cell,
// upto and including the new line
const endIndex = index;
// Calculate the index of the start of the current cell
const startIndex = input.lastIndexOf('|', endIndex) + 1;
const charsBefore = input.substring(startIndex, endIndex);
if (charsBefore === '' || charsBefore.match(EMPTY_LINE_REGEXP)) {
currentState = processState.CLOSE_ROW;
} else {
currentState = processState.LINE_BREAK;
buffer.push(input.substr(index, length));
}
index += length;
continue;
}
switch (char) {
case '|':
{
// This is now end of a cell, we should wrap the buffer into a cell
bufferToCells(cellStyle, buffer.join(''), cellsBuffer, schema, ignoreTokenTypes, context);
buffer = [];
// Update cells tyle
const cellMatch = substring.match(CELL_REGEXP);
// The below if statement should aways be true, we leave it here to prevent any future code changes fall into infinite loop
if (cellMatch) {
cellStyle = cellMatch[2];
// Move into the cell content
index += cellMatch[2].length;
continue;
}
break;
}
case ':':
case ';':
case '(':
{
currentState = processState.EMOJI;
continue;
}
case '[':
{
currentState = processState.LINK;
continue;
}
case '!':
{
currentState = processState.MEDIA;
continue;
}
case '{':
{
currentState = processState.MACRO;
continue;
}
default:
{
buffer.push(char);
index++;
continue;
}
}
break;
}
case processState.CLOSE_ROW:
{
const bufferOutput = buffer.join('');
if (bufferOutput.trim().length > 0) {
bufferToCells(cellStyle, bufferOutput, cellsBuffer, schema, ignoreTokenTypes, context);
buffer = [];
}
if (builder) {
builder.add(cellsBuffer);
cellsBuffer = [];
}
currentState = processState.NEW_ROW;
continue;
}
case processState.END_TABLE:
{
if (builder) {
if (cellsBuffer.length) {
builder.add(cellsBuffer);
}
output.push(builder.buildPMNode());
}
return {
type: 'pmnode',
nodes: output,
length: index - position
};
}
case processState.MEDIA:
{
const token = media({
input,
schema,
context,
position: index
});
buffer.push(input.substr(index, token.length));
index += token.length;
currentState = processState.BUFFER;
continue;
}
case processState.EMOJI:
{
const token = emoji({
input,
schema,
context,
position: index
});
buffer.push(input.substr(index, token.length));
index += token.length;
currentState = processState.BUFFER;
continue;
}
case processState.LINK:
{
/**
* We should "fly over" the link format and we dont want
* -awesome [link|https://www.atlass-ian.com] nice
* to be a strike through because of the '-' in link
*/
const token = linkFormat({
input,
schema,
context,
position: index
});
if (token.type === 'text') {
buffer.push(token.text);
index += token.length;
currentState = processState.BUFFER;
continue;
} else if (token.type === 'pmnode') {
buffer.push(input.substr(index, token.length));
index += token.length;
currentState = processState.BUFFER;
continue;
}
break;
}
case processState.MACRO:
{
const match = parseMacroKeyword(input.substring(index));
if (!match) {
buffer.push(char);
currentState = processState.BUFFER;
break;
}
const token = parseToken(input, match.type, index, schema, context);
buffer.push(input.substr(index, token.length));
index += token.length;
currentState = processState.BUFFER;
continue;
}
}
index++;
}
/**
* If there are left over content which didn't have a closing |
* For example
* |cell1|cell2|cell3
* we still want to create a new cell for the last cell3 if it's
* not empty.
*/
const bufferOutput = buffer.join('');
if (bufferOutput.trim().length > 0) {
bufferToCells(cellStyle, bufferOutput, cellsBuffer, schema, ignoreTokenTypes, context);
}
if (builder) {
if (cellsBuffer.length) {
builder.add(cellsBuffer);
}
output.push(builder.buildPMNode());
}
return {
type: 'pmnode',
nodes: output,
length: index - position
};
};
function bufferToCells(style, buffer, cellsBuffer, schema, ignoreTokenTypes, context) {
if (buffer.length) {
let contentNode = parseString({
schema,
context,
ignoreTokenTypes: ignoreTokenTypes,
input: buffer
});
if (style === '||') {
contentNode = contentNode.map(e => {
return createTableHeader(e, schema);
});
}
cellsBuffer.push({
style,
content: normalizePMNodes(contentNode, schema)
});
}
}
function createTableHeader(node, schema) {
const mark = schema.marks.strong.create();
return traverseNodeAndAddMarks(node, mark, schema);
}
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function traverseContent(node, mark, schema) {
if (node.content.childCount === 0 || !node.content.child(0) || !node.content.firstChild || node.type.name === 'codeBlock') {
return node;
}
for (let i = 0; i < node.content.childCount; i++) {
const child = node.content.child(i);
const markedChild = traverseNodeAndAddMarks(child, mark, schema);
const updatedContent = node.content.replaceChild(i, markedChild);
node = node.copy(updatedContent);
}
return node;
}
function traverseNodeAndAddMarks(node, mark, schema) {
if (node.type.name === 'text' && !hasAnyOfMarks(node, ['strong', 'code'])) {
const newNode = node.mark([...node.marks, mark]);
return newNode;
}
return traverseContent(node, mark, schema);
}