fulan-editor
Version:
An open source react editor based on draft-Js and ant design, good support HTML, markdown and Draft Raw format.
268 lines (249 loc) • 7.95 kB
JavaScript
/* @flow */
import {
getEntityRanges,
BLOCK_TYPE,
ENTITY_TYPE,
INLINE_STYLE,
} from '../stateUtils/main';
import {Entity} from 'draft-js';
import type {ContentState, ContentBlock} from 'draft-js';
const {
BOLD,
CODE,
ITALIC,
STRIKETHROUGH,
UNDERLINE,
} = INLINE_STYLE;
const CODE_INDENT = ' ';
class MarkupGenerator {
blocks: Array<ContentBlock>;
contentState: ContentState;
currentBlock: number;
output: Array<string>;
totalBlocks: number;
listItemCounts: Object;
constructor(contentState: ContentState) {
this.contentState = contentState;
}
generate(): string {
this.output = [];
this.blocks = this.contentState.getBlockMap().toArray();
this.totalBlocks = this.blocks.length;
this.currentBlock = 0;
this.listItemCounts = {};
while (this.currentBlock < this.totalBlocks) {
this.processBlock();
}
return this.output.join('');
}
processBlock() {
let block = this.blocks[this.currentBlock];
let blockType = block.getType();
switch (blockType) {
case BLOCK_TYPE.HEADER_ONE: {
this.insertLineBreaks(1);
this.output.push('# ' + this.renderBlockContent(block) + '\n');
break;
}
case BLOCK_TYPE.HEADER_TWO: {
this.insertLineBreaks(1);
this.output.push('## ' + this.renderBlockContent(block) + '\n');
break;
}
case BLOCK_TYPE.HEADER_THREE: {
this.insertLineBreaks(1);
this.output.push('### ' + this.renderBlockContent(block) + '\n');
break;
}
case BLOCK_TYPE.HEADER_FOUR: {
this.insertLineBreaks(1);
this.output.push('#### ' + this.renderBlockContent(block) + '\n');
break;
}
case BLOCK_TYPE.HEADER_FIVE: {
this.insertLineBreaks(1);
this.output.push('##### ' + this.renderBlockContent(block) + '\n');
break;
}
case BLOCK_TYPE.HEADER_SIX: {
this.insertLineBreaks(1);
this.output.push('###### ' + this.renderBlockContent(block) + '\n');
break;
}
case BLOCK_TYPE.UNORDERED_LIST_ITEM: {
let blockDepth = block.getDepth();
let lastBlock = this.getLastBlock();
let lastBlockType = lastBlock ? lastBlock.getType() : null;
let lastBlockDepth = lastBlock && canHaveDepth(lastBlockType) ?
lastBlock.getDepth() :
null;
if (
lastBlockType !== blockType &&
lastBlockDepth !== blockDepth - 1
) {
this.insertLineBreaks(1);
// Insert an additional line break if following opposite list type.
if (lastBlockType === BLOCK_TYPE.ORDERED_LIST_ITEM) {
this.insertLineBreaks(1);
}
}
let indent = ' '.repeat(block.depth * 4);
this.output.push(
indent + '- ' + this.renderBlockContent(block) + '\n'
);
break;
}
case BLOCK_TYPE.ORDERED_LIST_ITEM: {
let blockDepth = block.getDepth();
let lastBlock = this.getLastBlock();
let lastBlockType = lastBlock ? lastBlock.getType() : null;
let lastBlockDepth = lastBlock && canHaveDepth(lastBlockType) ?
lastBlock.getDepth() :
null;
if (lastBlockType !== blockType && lastBlockDepth !== blockDepth - 1) {
this.insertLineBreaks(1);
// Insert an additional line break if following opposite list type.
if (lastBlockType === BLOCK_TYPE.UNORDERED_LIST_ITEM) {
this.insertLineBreaks(1);
}
}
let indent = ' '.repeat(blockDepth * 4);
// TODO: figure out what to do with two-digit numbers
let count = this.getListItemCount(block) % 10;
this.output.push(
indent + `${count}. ` + this.renderBlockContent(block) + '\n'
);
break;
}
case BLOCK_TYPE.BLOCKQUOTE: {
this.insertLineBreaks(1);
this.output.push(' > ' + this.renderBlockContent(block) + '\n');
break;
}
case BLOCK_TYPE.CODE: {
this.insertLineBreaks(1);
this.output.push(CODE_INDENT + this.renderBlockContent(block) + '\n');
break;
}
default: {
this.insertLineBreaks(1);
this.output.push(this.renderBlockContent(block) + '\n');
break;
}
}
this.currentBlock += 1;
}
getLastBlock(): ContentBlock {
return this.blocks[this.currentBlock - 1];
}
getNextBlock(): ContentBlock {
return this.blocks[this.currentBlock + 1];
}
getListItemCount(block: ContentBlock): number {
let blockType = block.getType();
let blockDepth = block.getDepth();
// To decide if we need to start over we need to backtrack (skipping list
// items that are of greater depth)
let index = this.currentBlock - 1;
let prevBlock = this.blocks[index];
while (
prevBlock &&
canHaveDepth(prevBlock.getType()) &&
prevBlock.getDepth() > blockDepth
) {
index -= 1;
prevBlock = this.blocks[index];
}
if (
!prevBlock ||
prevBlock.getType() !== blockType ||
prevBlock.getDepth() !== blockDepth
) {
this.listItemCounts[blockDepth] = 0;
}
return (
this.listItemCounts[blockDepth] = this.listItemCounts[blockDepth] + 1
);
}
insertLineBreaks() {
if (this.currentBlock > 0) {
this.output.push('\n');
}
}
renderBlockContent(block: ContentBlock): string {
let blockType = block.getType();
let text = block.getText();
if (text === '') {
// Prevent element collapse if completely empty.
// TODO: Replace with constant.
return '\u200B';
}
let charMetaList = block.getCharacterList();
let entityPieces = getEntityRanges(text, charMetaList);
return entityPieces.map(([entityKey, stylePieces]) => {
let content = stylePieces.map(([text, style]) => {
// Don't allow empty inline elements.
if (!text) {
return '';
}
let content = encodeContent(text);
if (style.has(BOLD)) {
content = `**${content}**`;
}
if (style.has(UNDERLINE)) {
// TODO: encode `+`?
content = `++${content}++`;
}
if (style.has(ITALIC)) {
content = `_${content}_`;
}
if (style.has(STRIKETHROUGH)) {
// TODO: encode `~`?
content = `~~${content}~~`;
}
if (style.has(CODE)) {
content = (blockType === BLOCK_TYPE.CODE) ? content : '`' + content + '`';
}
return content;
}).join('');
let entity = entityKey ? Entity.get(entityKey) : null;
if (entity != null && entity.getType() === ENTITY_TYPE.LINK) {
let data = entity.getData();
let url = data.url || '';
let title = data.title ? ` "${escapeTitle(data.title)}"` : '';
return `[${content}](${encodeURL(url)}${title})`;
} else if (entity != null && entity.getType() === ENTITY_TYPE.IMAGE) {
let data = entity.getData();
let src = data.src || '';
let alt = data.alt ? ` "${escapeTitle(data.alt)}"` : '';
return `})`;
} else {
return content;
}
}).join('');
}
}
function canHaveDepth(blockType: any): boolean {
switch (blockType) {
case BLOCK_TYPE.UNORDERED_LIST_ITEM:
case BLOCK_TYPE.ORDERED_LIST_ITEM:
return true;
default:
return false;
}
}
function encodeContent(text) {
return text.replace(/[*_`]/g, '\\$&');
}
// Encode chars that would normally be allowed in a URL but would conflict with
// our markdown syntax: `[foo](http://foo/)`
function encodeURL(url) {
return url.replace(/\)/g, '%29');
}
// Escape quotes using backslash.
function escapeTitle(text) {
return text.replace(/"/g, '\\"');
}
export default function stateToMarkdown(content: ContentState): string {
return new MarkupGenerator(content).generate();
}