use mdast instead of html for rte local model
markdown is currently serialized to html at load time, which makes it near impossible to support arbitrary html in the markdown. This also means we're stringifying to html on every change. This commit moves to Remark's MDAST for local serialization, including parsing from MDAST to Slates's Raw AST. It brings much more control over the editing experience and full support for processing unescaped HTML.
This commit is contained in:
@ -1,6 +1,6 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { Editor as SlateEditor, Plain as SlatePlain } from 'slate';
|
||||
import { markdownToHtml, htmlToMarkdown } from '../../unified';
|
||||
import { markdownToRemark, remarkToMarkdown } from '../../unified';
|
||||
import Toolbar from '../Toolbar/Toolbar';
|
||||
import { Sticky } from '../../../../UI/Sticky/Sticky';
|
||||
import styles from './index.css';
|
||||
@ -8,7 +8,7 @@ import styles from './index.css';
|
||||
export default class RawEditor extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const value = htmlToMarkdown(this.props.value);
|
||||
const value = remarkToMarkdown(this.props.value);
|
||||
this.state = {
|
||||
editorState: SlatePlain.deserialize(value || ''),
|
||||
};
|
||||
@ -20,7 +20,7 @@ export default class RawEditor extends React.Component {
|
||||
|
||||
handleDocumentChange = (doc, editorState) => {
|
||||
const value = SlatePlain.serialize(editorState);
|
||||
const html = markdownToHtml(value);
|
||||
const html = markdownToRemark(value);
|
||||
this.props.onChange(html);
|
||||
};
|
||||
|
||||
@ -60,5 +60,5 @@ export default class RawEditor extends React.Component {
|
||||
RawEditor.propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onMode: PropTypes.func.isRequired,
|
||||
value: PropTypes.node,
|
||||
value: PropTypes.object,
|
||||
};
|
||||
|
@ -99,6 +99,17 @@
|
||||
border-left: 3px solid #eee;
|
||||
margin-left: 0; margin-right: 0;
|
||||
}
|
||||
|
||||
& table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
& td,
|
||||
& th {
|
||||
border: 2px solid black;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.shortcode {
|
||||
|
@ -1,27 +1,18 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import ReactDOMServer from 'react-dom/server';
|
||||
import { Map, List, fromJS } from 'immutable';
|
||||
import { reduce, mapValues } from 'lodash';
|
||||
import { get, reduce, mapValues } from 'lodash';
|
||||
import cn from 'classnames';
|
||||
import { Editor as SlateEditor, Html as SlateHtml, Raw as SlateRaw, Text as SlateText, Block as SlateBlock, Selection as SlateSelection} from 'slate';
|
||||
import EditList from 'slate-edit-list';
|
||||
import { markdownToHtml, htmlToMarkdown } from '../../unified';
|
||||
import EditTable from 'slate-edit-table';
|
||||
import { markdownToRemark, remarkToMarkdown, slateToRemark, remarkToSlate, markdownToHtml, htmlToMarkdown } from '../../unified';
|
||||
import registry from '../../../../../lib/registry';
|
||||
import { createAssetProxy } from '../../../../../valueObjects/AssetProxy';
|
||||
import Toolbar from '../Toolbar/Toolbar';
|
||||
import { Sticky } from '../../../../UI/Sticky/Sticky';
|
||||
import styles from './index.css';
|
||||
|
||||
/**
|
||||
* Slate can serialize to html, but we persist the value as markdown. Serializing
|
||||
* the html to markdown on every keystroke is a big perf hit, so we'll register
|
||||
* functions to perform those actions only when necessary, such as after loading
|
||||
* and before persisting.
|
||||
*/
|
||||
registry.registerWidgetValueSerializer('markdown', {
|
||||
serialize: htmlToMarkdown,
|
||||
deserialize: markdownToHtml,
|
||||
});
|
||||
|
||||
function processUrl(url) {
|
||||
if (url.match(/^(https?:\/\/|mailto:|\/)/)) {
|
||||
@ -102,10 +93,14 @@ const BLOCK_COMPONENTS = {
|
||||
'container': props => <div {...props.attributes}>{props.children}</div>,
|
||||
'paragraph': props => <p {...props.attributes}>{props.children}</p>,
|
||||
'list-item': props => <li {...props.attributes}>{props.children}</li>,
|
||||
'numbered-list': props => {
|
||||
const { data } = props.node;
|
||||
const start = data.get('start') || 1;
|
||||
return <ol {...props.attributes} start={start}>{props.children}</ol>;
|
||||
},
|
||||
'bulleted-list': props => <ul {...props.attributes}>{props.children}</ul>,
|
||||
'numbered-list': props => <ol {...props.attributes}>{props.children}</ol>,
|
||||
'quote': props => <blockquote {...props.attributes}>{props.children}</blockquote>,
|
||||
'code': props => <pre {...props.attributes}><code>{props.children}</code></pre>,
|
||||
'code': props => <pre><code {...props.attributes}>{props.children}</code></pre>,
|
||||
'heading-one': props => <h1 {...props.attributes}>{props.children}</h1>,
|
||||
'heading-two': props => <h2 {...props.attributes}>{props.children}</h2>,
|
||||
'heading-three': props => <h3 {...props.attributes}>{props.children}</h3>,
|
||||
@ -116,8 +111,13 @@ const BLOCK_COMPONENTS = {
|
||||
const data = props.node && props.node.get('data');
|
||||
const src = data && data.get('src') || props.src;
|
||||
const alt = data && data.get('alt') || props.alt;
|
||||
return <img src={src} alt={alt} {...props.attributes}/>;
|
||||
const title = data && data.get('title') || props.title;
|
||||
return <div><img src={src} alt={alt} title={title}{...props.attributes}/></div>;
|
||||
},
|
||||
'table': props => <table><tbody {...props.attributes}>{props.children}</tbody></table>,
|
||||
'table-row': props => <tr {...props.attributes}>{props.children}</tr>,
|
||||
'table-cell': props => <td {...props.attributes}>{props.children}</td>,
|
||||
'thematic-break': props => <hr {...props.attributes}/>,
|
||||
};
|
||||
const getShortcodeId = props => {
|
||||
if (props.node) {
|
||||
@ -132,8 +132,10 @@ const shortcodeStyles = {border: '2px solid black', padding: '8px', margin: '2px
|
||||
const NODE_COMPONENTS = {
|
||||
...BLOCK_COMPONENTS,
|
||||
'link': props => {
|
||||
const href = props.node && props.node.getIn(['data', 'href']) || props.href;
|
||||
return <a href={href} {...props.attributes}>{props.children}</a>;
|
||||
const data = props.node.get('data');
|
||||
const href = data && data.get('url') || props.href;
|
||||
const title = data && data.get('title') || props.title;
|
||||
return <a href={href} title={title} {...props.attributes}>{props.children}</a>;
|
||||
},
|
||||
'shortcode': props => {
|
||||
const { attributes, node, state: editorState } = props;
|
||||
@ -153,7 +155,6 @@ const NODE_COMPONENTS = {
|
||||
const MARK_COMPONENTS = {
|
||||
bold: props => <strong>{props.children}</strong>,
|
||||
italic: props => <em>{props.children}</em>,
|
||||
underlined: props => <u>{props.children}</u>,
|
||||
strikethrough: props => <s>{props.children}</s>,
|
||||
code: props => <code>{props.children}</code>,
|
||||
};
|
||||
@ -217,9 +218,6 @@ const RULES = [
|
||||
if (['bulleted-list', 'numbered-list'].includes(entity.type)) {
|
||||
return;
|
||||
}
|
||||
if (entity.kind !== 'block') {
|
||||
return;
|
||||
}
|
||||
const component = BLOCK_COMPONENTS[entity.type]
|
||||
if (!component) {
|
||||
return;
|
||||
@ -242,9 +240,6 @@ const RULES = [
|
||||
return;
|
||||
}
|
||||
const component = MARK_COMPONENTS[entity.type]
|
||||
if (!component) {
|
||||
return;
|
||||
}
|
||||
return component({ children });
|
||||
}
|
||||
},
|
||||
@ -268,13 +263,14 @@ const RULES = [
|
||||
deserialize(el, next) {
|
||||
if (el.tagName != 'img') return
|
||||
return {
|
||||
kind: 'inline',
|
||||
kind: 'block',
|
||||
type: 'image',
|
||||
isVoid: true,
|
||||
nodes: [],
|
||||
data: {
|
||||
src: el.attribs.src,
|
||||
alt: el.attribs.alt,
|
||||
title: el.attribs.title,
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -286,6 +282,7 @@ const RULES = [
|
||||
const props = {
|
||||
src: data.get('src'),
|
||||
alt: data.get('alt'),
|
||||
title: data.get('title'),
|
||||
};
|
||||
const result = NODE_COMPONENTS.image(props);
|
||||
return result;
|
||||
@ -300,7 +297,8 @@ const RULES = [
|
||||
type: 'link',
|
||||
nodes: next(el.children),
|
||||
data: {
|
||||
href: el.attribs.href
|
||||
href: el.attribs.href,
|
||||
title: el.attribs.title,
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -311,6 +309,7 @@ const RULES = [
|
||||
const data = entity.get('data');
|
||||
const props = {
|
||||
href: data.get('href'),
|
||||
title: data.get('title'),
|
||||
attributes: data.get('attributes'),
|
||||
children,
|
||||
};
|
||||
@ -328,7 +327,7 @@ const RULES = [
|
||||
|
||||
]
|
||||
|
||||
const serializer = new SlateHtml({ rules: RULES });
|
||||
const htmlSerializer = new SlateHtml({ rules: RULES });
|
||||
|
||||
const SoftBreak = (options = {}) => ({
|
||||
onKeyDown(e, data, state) {
|
||||
@ -374,53 +373,29 @@ const BackspaceCloseBlock = (options = {}) => ({
|
||||
});
|
||||
|
||||
const slatePlugins = [
|
||||
SoftBreak({ ignoreIn: ['paragraph', 'list-item', 'numbered-list', 'bulleted-list'], closeAfter: 1 }),
|
||||
BackspaceCloseBlock({ ignoreIn: ['paragraph', 'list-item', 'bulleted-list', 'numbered-list'] }),
|
||||
SoftBreak({ ignoreIn: ['list-item', 'numbered-list', 'bulleted-list', 'table', 'table-row', 'table-cell'], closeAfter: 1 }),
|
||||
BackspaceCloseBlock({ ignoreIn: ['paragraph', 'list-item', 'bulleted-list', 'numbered-list', 'table', 'table-row', 'table-cell'] }),
|
||||
EditList({ types: ['bulleted-list', 'numbered-list'], typeItem: 'list-item' }),
|
||||
EditTable({ typeTable: 'table', typeRow: 'table-row', typeCell: 'table-cell' }),
|
||||
];
|
||||
|
||||
export default class Editor extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const plugins = registry.getEditorComponents();
|
||||
// Wrap value in div to ensure against trailing text outside of top level html element
|
||||
const initialValue = this.props.value ? `<div>${this.props.value}</div>` : '<p></p>';
|
||||
const emptyRaw = {
|
||||
nodes: [{ kind: 'block', type: 'paragraph', nodes: [
|
||||
{ kind: 'text', ranges: [{ text: '' }] }
|
||||
]}],
|
||||
};
|
||||
const remark = this.props.value && remarkToSlate(this.props.value);
|
||||
const initialValue = get(remark, ['nodes', 'length']) ? remark : emptyRaw;
|
||||
const editorState = SlateRaw.deserialize(initialValue, { terse: true });
|
||||
this.state = {
|
||||
editorState: serializer.deserialize(initialValue),
|
||||
editorState,
|
||||
schema: {
|
||||
nodes: NODE_COMPONENTS,
|
||||
marks: MARK_COMPONENTS,
|
||||
rules: [
|
||||
{
|
||||
match: object => object.kind === 'document',
|
||||
validate: doc => {
|
||||
const blocks = doc.getBlocks();
|
||||
const firstBlock = blocks.first();
|
||||
const lastBlock = blocks.last();
|
||||
const firstBlockIsVoid = firstBlock.isVoid;
|
||||
const lastBlockIsVoid = lastBlock.isVoid;
|
||||
|
||||
if (firstBlockIsVoid || lastBlockIsVoid) {
|
||||
return { blocks, firstBlock, lastBlock, firstBlockIsVoid, lastBlockIsVoid };
|
||||
}
|
||||
},
|
||||
normalize: (transform, doc, { blocks, firstBlock, lastBlock, firstBlockIsVoid, lastBlockIsVoid }) => {
|
||||
const block = SlateBlock.create({
|
||||
type: 'paragraph',
|
||||
nodes: [SlateText.createFromString('')],
|
||||
});
|
||||
if (firstBlockIsVoid) {
|
||||
const { key } = transform.state.document;
|
||||
transform.insertNodeByKey(key, 0, block);
|
||||
}
|
||||
if (lastBlockIsVoid) {
|
||||
const { key, nodes } = transform.state.document;
|
||||
transform.insertNodeByKey(key, nodes.size, block);
|
||||
}
|
||||
return transform;
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
plugins,
|
||||
};
|
||||
@ -437,8 +412,9 @@ export default class Editor extends Component {
|
||||
}
|
||||
|
||||
handleDocumentChange = (doc, editorState) => {
|
||||
const html = serializer.serialize(editorState);
|
||||
this.props.onChange(html);
|
||||
const raw = SlateRaw.serialize(editorState, { terse: true });
|
||||
const mdast = slateToRemark(raw);
|
||||
this.props.onChange(mdast);
|
||||
};
|
||||
|
||||
hasMark = type => this.state.editorState.marks.some(mark => mark.type === type);
|
||||
@ -602,5 +578,5 @@ Editor.propTypes = {
|
||||
getAsset: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onMode: PropTypes.func.isRequired,
|
||||
value: PropTypes.node,
|
||||
value: PropTypes.object,
|
||||
};
|
||||
|
@ -1,18 +1,30 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import registry from '../../../../lib/registry';
|
||||
import { markdownToRemark, remarkToMarkdown } from '../unified';
|
||||
import RawEditor from './RawEditor';
|
||||
import VisualEditor from './VisualEditor';
|
||||
import { StickyContainer } from '../../../UI/Sticky/Sticky';
|
||||
|
||||
const MODE_STORAGE_KEY = 'cms.md-mode';
|
||||
|
||||
/**
|
||||
* Slate can serialize to html, but we persist the value as markdown. Serializing
|
||||
* the html to markdown on every keystroke is a big perf hit, so we'll register
|
||||
* functions to perform those actions only when necessary, such as after loading
|
||||
* and before persisting.
|
||||
*/
|
||||
registry.registerWidgetValueSerializer('markdown', {
|
||||
serialize: remarkToMarkdown,
|
||||
deserialize: markdownToRemark,
|
||||
});
|
||||
|
||||
export default class MarkdownControl extends React.Component {
|
||||
static propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onAddAsset: PropTypes.func.isRequired,
|
||||
onRemoveAsset: PropTypes.func.isRequired,
|
||||
getAsset: PropTypes.func.isRequired,
|
||||
value: PropTypes.node,
|
||||
value: PropTypes.object,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
|
@ -1,13 +1,18 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { remarkToHtml } from '../unified';
|
||||
import previewStyle from '../../defaultPreviewStyle';
|
||||
|
||||
const MarkdownPreview = ({ value, getAsset }) => {
|
||||
return value === null ? null : <div style={previewStyle} dangerouslySetInnerHTML={{__html: value}}></div>;
|
||||
if (value === null) {
|
||||
return null;
|
||||
}
|
||||
const html = remarkToHtml(value);
|
||||
return <div style={previewStyle} dangerouslySetInnerHTML={{__html: html}}></div>;
|
||||
};
|
||||
|
||||
MarkdownPreview.propTypes = {
|
||||
getAsset: PropTypes.func.isRequired,
|
||||
value: PropTypes.string,
|
||||
value: PropTypes.object,
|
||||
};
|
||||
|
||||
export default MarkdownPreview;
|
||||
|
@ -1,29 +1,34 @@
|
||||
import find from 'lodash/find';
|
||||
import { get, find, isEmpty } from 'lodash';
|
||||
import unified from 'unified';
|
||||
import markdownToRemark from 'remark-parse';
|
||||
import u from 'unist-builder';
|
||||
import markdownToRemarkPlugin from 'remark-parse';
|
||||
import remarkToMarkdownPlugin from 'remark-stringify';
|
||||
import mdastDefinitions from 'mdast-util-definitions';
|
||||
import modifyChildren from 'unist-util-modify-children';
|
||||
import remarkToRehype from 'remark-rehype';
|
||||
import rehypeToHtml from 'rehype-stringify';
|
||||
import htmlToRehype from 'rehype-parse';
|
||||
import rehypeToRemark from 'rehype-remark';
|
||||
import remarkToMarkdown from 'remark-stringify';
|
||||
import rehypeSanitize from 'rehype-sanitize';
|
||||
import rehypeReparse from 'rehype-raw';
|
||||
import rehypeMinifyWhitespace from 'rehype-minify-whitespace';
|
||||
import ReactDOMServer from 'react-dom/server';
|
||||
import registry from '../../../lib/registry';
|
||||
import merge from 'deepmerge';
|
||||
import rehypeSanitizeSchemaDefault from 'hast-util-sanitize/lib/github';
|
||||
import hastFromString from 'hast-util-from-string';
|
||||
import hastToMdastHandlerAll from 'hast-util-to-mdast/all';
|
||||
import { reduce, capitalize } from 'lodash';
|
||||
|
||||
// Remove the yaml tokenizer, as the rich text editor doesn't support frontmatter
|
||||
delete markdownToRemarkPlugin.Parser.prototype.blockTokenizers.yamlFrontMatter;
|
||||
console.log(markdownToRemarkPlugin.Parser.prototype.blockTokenizers);
|
||||
|
||||
const shortcodeAttributePrefix = 'ncp';
|
||||
|
||||
/**
|
||||
* Remove empty nodes, including the top level parents of deeply nested empty nodes.
|
||||
*/
|
||||
const rehypeRemoveEmpty = () => {
|
||||
const isVoidElement = node => ['img', 'hr'].includes(node.tagName);
|
||||
const isVoidElement = node => ['img', 'hr', 'br'].includes(node.tagName);
|
||||
const isNonEmptyLeaf = node => ['text', 'raw'].includes(node.type) && node.value;
|
||||
const isShortcode = node => node.properties && node.properties[`data${capitalize(shortcodeAttributePrefix)}`];
|
||||
const isNonEmptyNode = node => {
|
||||
@ -135,28 +140,15 @@ const rehypeShortcodes = () => {
|
||||
}
|
||||
|
||||
/**
|
||||
* we can't escape the less than symbol
|
||||
* which means how do we know {{<thing attr>}} from <tag attr> ?
|
||||
* maybe we escape nothing
|
||||
* then we can check for shortcodes in a unified plugin
|
||||
* and only check against text nodes
|
||||
* and maybe narrow the target text nodes even further somehow
|
||||
* and make shortcode parsing faster
|
||||
* Rewrite the remark-stringify text visitor to simply return the text value,
|
||||
* without encoding or escaping any characters. This means we're completely
|
||||
* trusting the markdown that we receive.
|
||||
*/
|
||||
function remarkPrecompileShortcodes() {
|
||||
const Compiler = this.Compiler;
|
||||
const visitors = Compiler.prototype.visitors;
|
||||
const textVisitor = visitors.text;
|
||||
|
||||
visitors.text = newTextVisitor;
|
||||
|
||||
function newTextVisitor(node, parent) {
|
||||
if (parent.data && parent.data[shortcodeAttributePrefix]) {
|
||||
return node.value;
|
||||
}
|
||||
return textVisitor.call(this, node, parent);
|
||||
}
|
||||
}
|
||||
visitors.text = node => node.value;
|
||||
};
|
||||
|
||||
const parseShortcodesFromMarkdown = markdown => {
|
||||
const plugins = registry.getEditorComponents();
|
||||
@ -180,7 +172,302 @@ const parseShortcodesFromMarkdown = markdown => {
|
||||
return markdownLinesParsed.join('\n');
|
||||
};
|
||||
|
||||
const rehypeSanitizeSchema = merge(rehypeSanitizeSchemaDefault, { attributes: { '*': [ 'data*' ] } });
|
||||
const remarkToSlatePlugin = () => {
|
||||
const typeMap = {
|
||||
paragraph: 'paragraph',
|
||||
blockquote: 'quote',
|
||||
code: 'code',
|
||||
listItem: 'list-item',
|
||||
table: 'table',
|
||||
tableRow: 'table-row',
|
||||
tableCell: 'table-cell',
|
||||
thematicBreak: 'thematic-break',
|
||||
link: 'link',
|
||||
image: 'image',
|
||||
};
|
||||
const markMap = {
|
||||
strong: 'bold',
|
||||
emphasis: 'italic',
|
||||
delete: 'strikethrough',
|
||||
inlineCode: 'code',
|
||||
};
|
||||
const toTextNode = text => ({ kind: 'text', text });
|
||||
const wrapText = (node, index, parent) => {
|
||||
if (['text', 'html'].includes(node.type)) {
|
||||
parent.children.splice(index, 1, u('paragraph', [node]));
|
||||
}
|
||||
};
|
||||
|
||||
let getDefinition;
|
||||
const transform = node => {
|
||||
let nodes;
|
||||
|
||||
if (node.type === 'root') {
|
||||
getDefinition = mdastDefinitions(node);
|
||||
modifyChildren(wrapText)(node);
|
||||
}
|
||||
|
||||
if (isEmpty(node.children)) {
|
||||
nodes = node.children;
|
||||
} else {
|
||||
// If a node returns a falsey value, exclude it. Some nodes do not
|
||||
// translate from MDAST to Slate, such as definitions for link/image
|
||||
// references or footnotes.
|
||||
nodes = node.children.reduce((acc, childNode) => {
|
||||
const transformed = transform(childNode);
|
||||
if (transformed) {
|
||||
acc.push(transformed);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
if (node.type === 'root') {
|
||||
return { nodes };
|
||||
}
|
||||
|
||||
// Process raw html as text, since it's valid markdown
|
||||
if (['text', 'html'].includes(node.type)) {
|
||||
return toTextNode(node.value);
|
||||
}
|
||||
|
||||
if (node.type === 'inlineCode') {
|
||||
return { kind: 'text', ranges: [{ text: node.value, marks: [{ type: 'code' }] }] };
|
||||
}
|
||||
|
||||
if (['strong', 'emphasis', 'delete'].includes(node.type)) {
|
||||
const remarkToSlateMarks = (markNode, parentMarks = []) => {
|
||||
const marks = [...parentMarks, { type: markMap[markNode.type] }];
|
||||
const ranges = [];
|
||||
markNode.children.forEach(childNode => {
|
||||
if (['html', 'text'].includes(childNode.type)) {
|
||||
ranges.push({ text: childNode.value, marks });
|
||||
return;
|
||||
}
|
||||
const nestedRanges = remarkToSlateMarks(childNode, marks);
|
||||
ranges.push(...nestedRanges);
|
||||
});
|
||||
return ranges;
|
||||
};
|
||||
|
||||
return { kind: 'text', ranges: remarkToSlateMarks(node) };
|
||||
}
|
||||
|
||||
if (node.type === 'heading') {
|
||||
const depths = { 1: 'one', 2: 'two', 3: 'three', 4: 'four', 5: 'five', 6: 'six' };
|
||||
return { kind: 'block', type: `heading-${depths[node.depth]}`, nodes };
|
||||
}
|
||||
|
||||
if (['paragraph', 'blockquote', 'tableRow', 'tableCell'].includes(node.type)) {
|
||||
return { kind: 'block', type: typeMap[node.type], nodes };
|
||||
}
|
||||
|
||||
if (node.type === 'code') {
|
||||
const data = { lang: node.lang };
|
||||
const text = toTextNode(node.value);
|
||||
const nodes = [text];
|
||||
return { kind: 'block', type: typeMap[node.type], data, nodes };
|
||||
}
|
||||
|
||||
if (node.type === 'list') {
|
||||
const slateType = node.ordered ? 'numbered-list' : 'bulleted-list';
|
||||
const data = { start: node.start };
|
||||
return { kind: 'block', type: slateType, data, nodes };
|
||||
}
|
||||
|
||||
if (node.type === 'listItem') {
|
||||
const data = { checked: node.checked };
|
||||
return { kind: 'block', type: typeMap[node.type], data, nodes };
|
||||
}
|
||||
|
||||
if (node.type === 'table') {
|
||||
const data = { align: node.align };
|
||||
return { kind: 'block', type: typeMap[node.type], data, nodes };
|
||||
}
|
||||
|
||||
if (node.type === 'thematicBreak') {
|
||||
return { kind: 'block', type: typeMap[node.type], isVoid: true };
|
||||
}
|
||||
|
||||
if (node.type === 'link') {
|
||||
const { title, url } = node;
|
||||
const data = { title, url };
|
||||
return { kind: 'inline', type: typeMap[node.type], data, nodes };
|
||||
}
|
||||
|
||||
if (node.type === 'linkReference') {
|
||||
const definition = getDefinition(node.identifier);
|
||||
const { title, url } = definition;
|
||||
const data = { title, url };
|
||||
return { kind: 'inline', type: typeMap['link'], data, nodes };
|
||||
}
|
||||
|
||||
if (node.type === 'image') {
|
||||
const { title, url, alt } = node;
|
||||
const data = { title, url, alt };
|
||||
return { kind: 'block', type: typeMap[node.type], data };
|
||||
}
|
||||
|
||||
if (node.type === 'imageReference') {
|
||||
const definition = getDefinition(node.identifier);
|
||||
const { title, url } = definition;
|
||||
const data = { title, url };
|
||||
return { kind: 'block', type: typeMap['image'], data };
|
||||
}
|
||||
};
|
||||
return transform;
|
||||
};
|
||||
|
||||
const slateToRemarkPlugin = () => {
|
||||
const transform = node => {
|
||||
console.log(node);
|
||||
return node;
|
||||
};
|
||||
return transform;
|
||||
};
|
||||
|
||||
export const markdownToRemark = markdown => {
|
||||
const result = unified()
|
||||
.use(markdownToRemarkPlugin, { fences: true, pedantic: true, footnotes: true, commonmark: true })
|
||||
.parse(markdown);
|
||||
return result;
|
||||
};
|
||||
|
||||
export const remarkToMarkdown = obj => {
|
||||
const mdast = obj || u('root', [u('paragraph', [u('text', '')])]);
|
||||
|
||||
const result = unified()
|
||||
.use(remarkToMarkdownPlugin, { listItemIndent: '1', fences: true, pedantic: true, commonmark: true })
|
||||
.use(remarkPrecompileShortcodes)
|
||||
.stringify(mdast);
|
||||
return result;
|
||||
};
|
||||
|
||||
export const remarkToSlate = mdast => {
|
||||
const result = unified()
|
||||
.use(remarkToSlatePlugin)
|
||||
.runSync(mdast);
|
||||
return result;
|
||||
};
|
||||
|
||||
export const slateToRemark = raw => {
|
||||
const typeMap = {
|
||||
'paragraph': 'paragraph',
|
||||
'heading-one': 'heading',
|
||||
'heading-two': 'heading',
|
||||
'heading-three': 'heading',
|
||||
'heading-four': 'heading',
|
||||
'heading-five': 'heading',
|
||||
'heading-six': 'heading',
|
||||
'quote': 'blockquote',
|
||||
'code': 'code',
|
||||
'numbered-list': 'list',
|
||||
'bulleted-list': 'list',
|
||||
'list-item': 'listItem',
|
||||
'table': 'table',
|
||||
'table-row': 'tableRow',
|
||||
'table-cell': 'tableCell',
|
||||
'thematic-break': 'thematicBreak',
|
||||
'link': 'link',
|
||||
'image': 'image',
|
||||
};
|
||||
const markMap = {
|
||||
bold: 'strong',
|
||||
italic: 'emphasis',
|
||||
strikethrough: 'delete',
|
||||
code: 'inlineCode',
|
||||
};
|
||||
const transform = node => {
|
||||
const children = isEmpty(node.nodes) ? node.nodes : node.nodes.reduce((acc, childNode) => {
|
||||
if (childNode.kind !== 'text') {
|
||||
acc.push(transform(childNode));
|
||||
return acc;
|
||||
}
|
||||
if (childNode.ranges) {
|
||||
childNode.ranges.forEach(range => {
|
||||
const { marks = [], text } = range;
|
||||
const markTypes = marks.map(mark => markMap[mark.type]);
|
||||
if (markTypes.includes('inlineCode')) {
|
||||
acc.push(u('inlineCode', text));
|
||||
} else {
|
||||
const textNode = u('html', text);
|
||||
const nestedText = !markTypes.length ? textNode : markTypes.reduce((acc, markType) => {
|
||||
const nested = u(markType, [acc]);
|
||||
return nested;
|
||||
}, textNode);
|
||||
acc.push(nestedText);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
acc.push(u('html', childNode.text));
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
if (node.type === 'root') {
|
||||
return u('root', children);
|
||||
}
|
||||
|
||||
if (node.type.startsWith('heading')) {
|
||||
const depths = { one: 1, two: 2, three: 3, four: 4, five: 5, six: 6 };
|
||||
const depth = node.type.split('-')[1];
|
||||
const props = { depth: depths[depth] };
|
||||
return u(typeMap[node.type], props, children);
|
||||
}
|
||||
|
||||
if (['paragraph', 'quote', 'list-item', 'table', 'table-row', 'table-cell'].includes(node.type)) {
|
||||
return u(typeMap[node.type], children);
|
||||
}
|
||||
|
||||
if (node.type === 'code') {
|
||||
const value = get(node.nodes, [0, 'text']);
|
||||
const props = { lang: get(node.data, 'lang') };
|
||||
return u(typeMap[node.type], props, value);
|
||||
}
|
||||
|
||||
if (['numbered-list', 'bulleted-list'].includes(node.type)) {
|
||||
const ordered = node.type === 'numbered-list';
|
||||
const props = { ordered, start: get(node.data, 'start') || 1 };
|
||||
return u(typeMap[node.type], props, children);
|
||||
}
|
||||
|
||||
if (node.type === 'thematic-break') {
|
||||
return u(typeMap[node.type]);
|
||||
}
|
||||
|
||||
if (node.type === 'link') {
|
||||
const data = get(node, 'data', {});
|
||||
const { url, title } = data;
|
||||
return u(typeMap[node.type], data, children);
|
||||
}
|
||||
|
||||
if (node.type === 'image') {
|
||||
const data = get(node, 'data', {});
|
||||
const { url, title, alt } = data;
|
||||
return u(typeMap[node.type], data);
|
||||
}
|
||||
}
|
||||
raw.type = 'root';
|
||||
const result = transform(raw);
|
||||
return result;
|
||||
};
|
||||
|
||||
export const remarkToHtml = mdast => {
|
||||
const result = unified()
|
||||
.use(remarkToRehype, { allowDangerousHTML: true })
|
||||
.use(rehypeReparse)
|
||||
.use(rehypeRemoveEmpty)
|
||||
.use(rehypeMinifyWhitespace)
|
||||
.use(() => node => {
|
||||
return node;
|
||||
})
|
||||
.runSync(mdast);
|
||||
|
||||
const output = unified()
|
||||
.use(rehypeToHtml, { allowDangerousHTML: true, allowDangerousCharacters: true, entities: { subset: [] } })
|
||||
.stringify(result);
|
||||
return output
|
||||
}
|
||||
|
||||
export const markdownToHtml = markdown => {
|
||||
// Parse shortcodes from the raw markdown rather than via Unified plugin.
|
||||
@ -188,11 +475,9 @@ export const markdownToHtml = markdown => {
|
||||
// parsing rules.
|
||||
const markdownWithParsedShortcodes = parseShortcodesFromMarkdown(markdown);
|
||||
const result = unified()
|
||||
.use(markdownToRemark, { fences: true })
|
||||
.use(markdownToRemarkPlugin, { fences: true, pedantic: true, footnotes: true, commonmark: true })
|
||||
.use(remarkToRehype, { allowDangerousHTML: true })
|
||||
.use(rehypeReparse)
|
||||
.use(rehypeRemoveEmpty)
|
||||
.use(rehypeSanitize, rehypeSanitizeSchema)
|
||||
.use(rehypeMinifyWhitespace)
|
||||
.use(rehypeToHtml, { allowDangerousHTML: true })
|
||||
.processSync(markdownWithParsedShortcodes)
|
||||
@ -203,7 +488,6 @@ export const markdownToHtml = markdown => {
|
||||
export const htmlToMarkdown = html => {
|
||||
const result = unified()
|
||||
.use(htmlToRehype, { fragment: true })
|
||||
.use(rehypeSanitize, rehypeSanitizeSchema)
|
||||
.use(rehypeRemoveEmpty)
|
||||
.use(rehypeMinifyWhitespace)
|
||||
.use(rehypePaperEmoji)
|
||||
@ -222,7 +506,7 @@ export const htmlToMarkdown = html => {
|
||||
return node;
|
||||
})
|
||||
.use(remarkNestedList)
|
||||
.use(remarkToMarkdown, { listItemIndent: '1', fences: true })
|
||||
.use(remarkToMarkdownPlugin, { listItemIndent: '1', fences: true, pedantic: true, commonmark: true })
|
||||
.use(remarkPrecompileShortcodes)
|
||||
/*
|
||||
.use(() => node => {
|
||||
|
Reference in New Issue
Block a user