294 lines
7.6 KiB
JavaScript
294 lines
7.6 KiB
JavaScript
import { get, isEmpty, isArray } from 'lodash';
|
|
import u from 'unist-builder';
|
|
import modifyChildren from 'unist-util-modify-children';
|
|
|
|
/**
|
|
* Map of MDAST node types to Slate node types.
|
|
*/
|
|
const typeMap = {
|
|
root: 'root',
|
|
paragraph: 'paragraph',
|
|
blockquote: 'quote',
|
|
code: 'code',
|
|
listItem: 'list-item',
|
|
table: 'table',
|
|
tableRow: 'table-row',
|
|
tableCell: 'table-cell',
|
|
thematicBreak: 'thematic-break',
|
|
link: 'link',
|
|
image: 'image',
|
|
shortcode: 'shortcode',
|
|
};
|
|
|
|
|
|
/**
|
|
* Map of MDAST node types to Slate mark types.
|
|
*/
|
|
const markMap = {
|
|
strong: 'bold',
|
|
emphasis: 'italic',
|
|
delete: 'strikethrough',
|
|
inlineCode: 'code',
|
|
};
|
|
|
|
|
|
/**
|
|
* Create a Slate Inline node.
|
|
*/
|
|
function createBlock(type, nodes, props = {}) {
|
|
if (!isArray(nodes)) {
|
|
props = nodes;
|
|
nodes = undefined;
|
|
}
|
|
|
|
return { kind: 'block', type, nodes, ...props };
|
|
}
|
|
|
|
|
|
/**
|
|
* Create a Slate Block node.
|
|
*/
|
|
function createInline(type, nodes, props = {}) {
|
|
return { kind: 'inline', type, nodes, ...props };
|
|
}
|
|
|
|
|
|
/**
|
|
* Create a Slate Raw text node.
|
|
*/
|
|
function createText(value, data) {
|
|
const node = { kind: 'text', data };
|
|
if (isArray(value)) {
|
|
return { ...node, ranges: value };
|
|
}
|
|
return {...node, text: value };
|
|
}
|
|
|
|
function convertMarkNode(node, parentMarks = []) {
|
|
|
|
/**
|
|
* Add the current node's mark type to the marks collected from parent
|
|
* mark nodes, if any.
|
|
*/
|
|
const marks = [...parentMarks, { type: markMap[node.type] }];
|
|
|
|
/**
|
|
* Set an array to collect sections of text.
|
|
*/
|
|
const ranges = [];
|
|
|
|
node.children.forEach(childNode => {
|
|
|
|
/**
|
|
* If a text node is a direct child of the current node, it should be
|
|
* set aside as a range, and all marks that have been collected in the
|
|
* `marks` array should apply to that specific range.
|
|
*/
|
|
if (['html', 'text'].includes(childNode.type)) {
|
|
ranges.push({ text: childNode.value, marks });
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Any non-text child node should be processed as a parent node. The
|
|
* recursive results should be pushed into the ranges array. This way,
|
|
* every MDAST nested text structure becomes a flat array of ranges
|
|
* that can serve as the value of a single Slate Raw text node.
|
|
*/
|
|
const nestedRanges = convertMarkNode(childNode, marks);
|
|
ranges.push(...nestedRanges);
|
|
});
|
|
|
|
return ranges;
|
|
}
|
|
|
|
/**
|
|
* Convert a single MDAST node to a Slate Raw node. Uses local node factories
|
|
* that mimic the unist-builder function utilized in the slateRemark
|
|
* transformer.
|
|
*/
|
|
function convertNode(node, nodes) {
|
|
|
|
/**
|
|
* Unified/Remark processors use mutable operations, so we don't want to
|
|
* change a node's type directly for conversion purposes, as that tends to
|
|
* unexpected errors.
|
|
*/
|
|
const type = get(node, ['data', 'shortcode']) ? 'shortcode' : node.type;
|
|
|
|
switch (type) {
|
|
|
|
/**
|
|
* General
|
|
*
|
|
* Convert simple cases that only require a type and children, with no
|
|
* additional properties.
|
|
*/
|
|
case 'root':
|
|
case 'paragraph':
|
|
case 'listItem':
|
|
case 'blockquote':
|
|
case 'tableRow':
|
|
case 'tableCell': {
|
|
return createBlock(typeMap[type], nodes);
|
|
}
|
|
|
|
|
|
/**
|
|
* Shortcodes
|
|
*
|
|
* Shortcode nodes are represented as "void" blocks in the Slate AST. They
|
|
* maintain the same data as MDAST shortcode nodes. Slate void blocks must
|
|
* contain a blank text node.
|
|
*/
|
|
case 'shortcode': {
|
|
const { data } = node;
|
|
const nodes = [ createText('') ];
|
|
return createBlock(typeMap[type], nodes, { data, isVoid: true });
|
|
}
|
|
|
|
/**
|
|
* Text
|
|
*
|
|
* Text and HTML nodes are both used to render text, and should be treated
|
|
* the same. HTML is treated as text because we never want to escape or
|
|
* encode it.
|
|
*/
|
|
case 'text':
|
|
case 'html': {
|
|
return createText(node.value, node.data);
|
|
}
|
|
|
|
/**
|
|
* Inline Code
|
|
*
|
|
* Inline code nodes from an MDAST are represented in our Slate schema as
|
|
* text nodes with a "code" mark. We manually create the "range" containing
|
|
* the inline code value and a "code" mark, and place it in an array for use
|
|
* as a Slate text node's children array.
|
|
*/
|
|
case 'inlineCode': {
|
|
const range = {
|
|
text: node.value,
|
|
marks: [{ type: 'code' }],
|
|
};
|
|
return createText([ range ]);
|
|
}
|
|
|
|
/**
|
|
* Marks
|
|
*
|
|
* Marks are typically decorative sub-types that apply to text nodes. In an
|
|
* MDAST, marks are nodes that can contain other nodes. This nested
|
|
* hierarchy has to be flattened and split into distinct text nodes with
|
|
* their own set of marks.
|
|
*/
|
|
case 'strong':
|
|
case 'emphasis':
|
|
case 'delete': {
|
|
return createText(convertMarkNode(node));
|
|
}
|
|
|
|
/**
|
|
* Headings
|
|
*
|
|
* MDAST headings use a single type with a separate "depth" property to
|
|
* indicate the heading level, while the Slate schema uses a separate node
|
|
* type for each heading level. Here we get the proper Slate node name based
|
|
* on the MDAST node depth.
|
|
*/
|
|
case 'heading': {
|
|
const depthMap = { 1: 'one', 2: 'two', 3: 'three', 4: 'four', 5: 'five', 6: 'six' };
|
|
const slateType = `heading-${depthMap[node.depth]}`;
|
|
return createBlock(slateType, nodes);
|
|
}
|
|
|
|
/**
|
|
* Code Blocks
|
|
*
|
|
* MDAST code blocks are a distinct node type with a simple text value. We
|
|
* convert that value into a nested child text node for Slate. We also carry
|
|
* over the "lang" data property if it's defined.
|
|
*/
|
|
case 'code': {
|
|
const data = { lang: node.lang };
|
|
const text = createText(node.value);
|
|
const nodes = [text];
|
|
return createBlock(typeMap[type], nodes, { data });
|
|
}
|
|
|
|
/**
|
|
* Lists
|
|
*
|
|
* MDAST has a single list type and an "ordered" property. We derive that
|
|
* information into the Slate schema's distinct list node types. We also
|
|
* include the "start" property, which indicates the number an ordered list
|
|
* starts at, if defined.
|
|
*/
|
|
case 'list': {
|
|
const slateType = node.ordered ? 'numbered-list' : 'bulleted-list';
|
|
const data = { start: node.start };
|
|
return createBlock(slateType, nodes, { data });
|
|
}
|
|
|
|
|
|
/**
|
|
* Thematic Breaks
|
|
*
|
|
* Thematic breaks are void nodes in the Slate schema.
|
|
*/
|
|
case 'thematicBreak': {
|
|
return createBlock(typeMap[type], { isVoid: true });
|
|
}
|
|
|
|
/**
|
|
* Links
|
|
*
|
|
* MDAST stores the link attributes directly on the node, while our Slate
|
|
* schema references them in the data object.
|
|
*/
|
|
case 'link': {
|
|
const { title, url } = node;
|
|
const data = { title, url };
|
|
return createInline(typeMap[type], nodes, { data });
|
|
}
|
|
|
|
/**
|
|
* Tables
|
|
*
|
|
* Tables are parsed separately because they may include an "align"
|
|
* property, which should be passed to the Slate node.
|
|
*/
|
|
case 'table': {
|
|
const data = { align: node.align };
|
|
return createBlock(typeMap[type], nodes, { data });
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* A Remark plugin for converting an MDAST to Slate Raw AST. Remark plugins
|
|
* return a `transform` function that receives the MDAST as it's first argument.
|
|
*/
|
|
export default function remarkToSlatePlugin() {
|
|
function transform(node) {
|
|
|
|
/**
|
|
* Call `transform` recursively on child nodes.
|
|
*
|
|
* If a node returns a falsey value, filter it out. Some nodes do not
|
|
* translate from MDAST to Slate, such as definitions for link/image
|
|
* references or footnotes.
|
|
*/
|
|
const children = !isEmpty(node.children) && node.children.map(transform).filter(val => val);
|
|
|
|
/**
|
|
* Run individual nodes through the conversion factory.
|
|
*/
|
|
return convertNode(node, children);
|
|
}
|
|
|
|
return transform;
|
|
}
|