"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = remarkToSlate; exports.mergeAdjacentTexts = mergeAdjacentTexts; var _isEqual2 = _interopRequireDefault(require("lodash/isEqual")); var _flatten2 = _interopRequireDefault(require("lodash/flatten")); var _map2 = _interopRequireDefault(require("lodash/map")); var _flatMap2 = _interopRequireDefault(require("lodash/flatMap")); var _isArray2 = _interopRequireDefault(require("lodash/isArray")); var _isEmpty2 = _interopRequireDefault(require("lodash/isEmpty")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : String(i); } function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } /** * Map of MDAST node types to Slate node types. */ const typeMap = { root: 'root', paragraph: 'paragraph', blockquote: 'quote', code: 'code-block', 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: 'delete', inlineCode: 'code' }; function isText(node) { return !!node.text; } function isMarksEqual(node1, node2) { return (0, _isEqual2.default)(node1.marks, node2.marks); } function mergeAdjacentTexts(children) { if (children.length <= 0) { return children; } const mergedChildren = []; let isMerging = false; let current; for (let i = 0; i < children.length - 1; i++) { if (!isMerging) { current = children[i]; } const next = children[i + 1]; if (isText(current) && isText(next) && isMarksEqual(current, next)) { isMerging = true; current = _objectSpread(_objectSpread({}, current), {}, { text: `${current.text}${next.text}` }); } else { mergedChildren.push(current); isMerging = false; } } if (isMerging) { mergedChildren.push(current); } else { mergedChildren.push(children[children.length - 1]); } return mergedChildren; } /** * A Remark plugin for converting an MDAST to Slate Raw AST. Remark plugins * return a `transformNode` function that receives the MDAST as it's first argument. */ function remarkToSlate({ voidCodeBlock } = {}) { return transformNode; function transformNode(node) { /** * Call `transformNode` 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. */ let children = !['strong', 'emphasis', 'delete'].includes(node.type) && !(0, _isEmpty2.default)(node.children) && (0, _flatMap2.default)(node.children, transformNode).filter(val => val); if (Array.isArray(children)) { // Merge adjacent text nodes with the same marks to conform to slate schema children = mergeAdjacentTexts(children); } /** * Run individual nodes through the conversion factory. */ const output = convertNode(node, children || undefined); return output; } /** * Add nodes to a parent node only if `nodes` is truthy. */ function addNodes(parent, nodes) { return nodes ? _objectSpread(_objectSpread({}, parent), {}, { children: nodes }) : parent; } /** * Create a Slate Block node. */ function createBlock(type, nodes, props = {}) { if (!(0, _isArray2.default)(nodes)) { props = nodes; nodes = undefined; } // Ensure block nodes have at least one text child to conform to slate schema const children = (0, _isEmpty2.default)(nodes) ? [createText('')] : nodes; const node = _objectSpread({ type }, props); return addNodes(node, children); } /** * Create a Slate Inline node. */ function createInline(type, props = {}, nodes) { const node = _objectSpread({ type }, props); // Ensure inline nodes have at least one text child to conform to slate schema const children = (0, _isEmpty2.default)(nodes) ? [createText('')] : nodes; return addNodes(node, children); } /** * Create a Slate Raw text node. */ function createText(node) { const newNode = {}; if (typeof node === 'string') { return _objectSpread(_objectSpread({}, newNode), {}, { text: node }); } const { text, marks } = node; return normalizeMarks(_objectSpread(_objectSpread({}, newNode), {}, { text, marks })); } function processMarkChild(childNode, marks) { switch (childNode.type) { /** * If a text node is a direct child of the current node, it should be * set aside as a text, and all marks that have been collected in the * `marks` array should apply to that specific text. */ case 'html': case 'text': return _objectSpread(_objectSpread({}, convertNode(childNode)), {}, { marks }); /** * MDAST inline code nodes don't have children, just a text value, similar * to a text node, so it receives the same treatment as a text node, but we * first add the inline code mark to the marks array. */ case 'inlineCode': { return _objectSpread(_objectSpread({}, convertNode(childNode)), {}, { marks: [...marks, { type: 'code' }] }); } /** * Process nested style nodes. The recursive results should be pushed into * the texts array. This way, every MDAST nested text structure becomes a * flat array of texts that can serve as the value of a single Slate Raw * text node. */ case 'strong': case 'emphasis': case 'delete': return processMarkNode(childNode, marks); case 'link': { const nodes = (0, _map2.default)(childNode.children, child => normalizeMarks(processMarkChild(child, marks))); const result = convertNode(childNode, (0, _flatten2.default)(nodes)); return result; } /** * Remaining nodes simply need mark data added to them, and to then be * added into the cumulative children array. */ default: return transformNode(_objectSpread(_objectSpread({}, childNode), {}, { data: _objectSpread(_objectSpread({}, childNode.data), {}, { marks }) })); } } function processMarkNode(node, parentMarks = []) { /** * Add the current node's mark type to the marks collected from parent * mark nodes, if any. */ const markType = markMap[node.type]; const marks = markType ? [...parentMarks.filter(({ type }) => type !== markType), { type: markType }] : parentMarks; const children = (0, _flatMap2.default)(node.children, child => normalizeMarks(processMarkChild(child, marks))); return children; } function normalizeMarks(node) { if (node.marks) { node.marks.forEach(mark => { node[mark.type] = true; }); } return node; } /** * 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) { switch (node.type) { /** * General * * Convert simple cases that only require a type and children, with no * additional properties. */ case 'paragraph': case 'blockquote': case 'tableRow': case 'tableCell': { return createBlock(typeMap[node.type], nodes); } /** * Root element * If the root node is empty, we need to add a paragraph node to it. */ case 'root': { const children = (0, _isEmpty2.default)(nodes) ? [createBlock('paragraph')] : nodes; return createBlock(typeMap[node.type], children); } /** * List Items * * Markdown list items can be empty, but a list item in the Slate schema * should at least have an empty paragraph node. */ case 'listItem': { const children = (0, _isEmpty2.default)(nodes) ? [createBlock('paragraph')] : nodes; return createBlock(typeMap[node.type], children); } /** * 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 nodes = [createText('')]; const data = _objectSpread(_objectSpread({}, node.data), {}, { id: node.data.shortcode, shortcodeNew: true }); return createBlock(typeMap[node.type], nodes, { data }); } case 'text': { const text = node.value; return createText(text); } /** * HTML * * HTML nodes contain plain text like text nodes, except they only contain * HTML. Our serialization results in non-HTML being placed in HTML nodes * sometimes to ensure that we're never escaping HTML from the rich text * editor. We do not replace line feeds in HTML because the HTML is raw * in the rich text editor, so the writer knows they're writing HTML, and * should expect soft breaks to be visually absent in the rendered HTML. */ case 'html': { return createText(node.value); } /** * 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 text 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': { return createText({ text: node.value, code: true, marks: [{ type: 'code' }] }); } /** * 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 processMarkNode(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. If a void * node is required due to a custom code block handler, the value is * stored in the "code" data property instead. We also carry over the "lang" * data property if it's defined. */ case 'code': { const data = _objectSpread(_objectSpread({ lang: node.lang }, voidCodeBlock ? { code: node.value } : {}), {}, { shortcode: 'code-block', shortcodeData: { code: node.value, lang: node.lang } }); const text = createText(voidCodeBlock ? '' : node.value); const nodes = [text]; const block = createBlock('shortcode', nodes, { data }); return block; } /** * 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 }); } /** * Breaks * * MDAST soft break nodes represent a trailing double space or trailing * slash from a Markdown document. In Slate, these are simply transformed to * line breaks within a text node. */ case 'break': { const { data } = node; return createInline('break', { data }); } /** * Thematic Breaks * * Thematic breaks are void nodes in the Slate schema. */ case 'thematicBreak': { return createBlock(typeMap[node.type]); } /** * 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, data } = node; const newData = _objectSpread(_objectSpread({}, data), {}, { title, url }); return createInline(typeMap[node.type], { data: newData }, nodes); } /** * Images * * Identical to link nodes except for the lack of child nodes and addition * of alt attribute data MDAST stores the link attributes directly on the * node, while our Slate schema references them in the data object. */ case 'image': { const { title, url, alt, data } = node; const newData = _objectSpread(_objectSpread({}, data), {}, { title, alt, url }); return createInline(typeMap[node.type], { data: newData }); } /** * 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[node.type], nodes, { data }); } } } }