draftjs-md-converter-support-video
Version:
Converter for converting Draft.js state into Markdown and vice versa
201 lines (166 loc) • 6.57 kB
JavaScript
;
const utils = require('../utils/utils');
const defaultMarkdownDict = {
BOLD: '__',
ITALIC: '*'
};
const blockStyleDict = {
'unordered-list-item': '- ',
'header-one': '# ',
'header-two': '## ',
'header-three': '### ',
'header-four': '#### ',
'header-five': '##### ',
'header-six': '###### ',
blockquote: '> '
};
const wrappingBlockStyleDict = {
'code-block': '```'
};
const getBlockStyle = (currentStyle, appliedBlockStyles) => {
if (currentStyle === 'ordered-list-item') {
const counter = appliedBlockStyles.reduce((prev, style) => {
if (style === 'ordered-list-item') {
return prev + 1;
}
return prev;
}, 1);
return `${counter}. `;
}
return blockStyleDict[currentStyle] || '';
};
const applyWrappingBlockStyle = (currentStyle, content) => {
if (currentStyle in wrappingBlockStyleDict) {
const wrappingSymbol = wrappingBlockStyleDict[currentStyle];
return `${wrappingSymbol}\n${content}\n${wrappingSymbol}`;
}
return content;
};
const applyAtomicStyle = (block, entityMap, content) => {
if (block.type !== 'atomic') return content;
// strip the test that was added in the media block
const strippedContent = content.substring(0, content.length - block.text.length);
const key = block.entityRanges[0].key;
const type = entityMap[key].type;
const data = entityMap[key].data;
if (type === 'EMBEDDED_LINK') {
return `${strippedContent}[[ embed url=${utils.buildEmbeddedUrl(data.url || data.src)} ]]`;
}
return `${strippedContent}`;
};
const getEntityStart = entity => {
switch (entity.type) {
case 'LINK':
return '[';
default:
return '';
}
};
const getEntityEnd = entity => {
switch (entity.type) {
case 'LINK':
return `](${entity.data.url})`;
default:
return '';
}
};
function fixWhitespacesInsideStyle(text, style) {
const { symbol } = style;
// Text before style-opening marker (including the marker)
const pre = text.slice(0, style.range.start);
// Text between opening and closing markers
const body = text.slice(style.range.start, style.range.end);
// Trimmed text between markers
const bodyTrimmed = body.trim();
// Text after closing marker
const post = text.slice(style.range.end);
const bodyTrimmedStart = style.range.start + body.indexOf(bodyTrimmed);
// Text between opening marker and trimmed content (leading spaces)
const prefix = text.slice(style.range.start, bodyTrimmedStart);
// Text between the end of trimmed content and closing marker (trailing spaces)
const postfix = text.slice(bodyTrimmedStart + bodyTrimmed.length, style.range.end);
// Temporary text that contains trimmed content wrapped into original pre- and post-texts
const newText = `${pre}${bodyTrimmed}${post}`;
// Insert leading and trailing spaces between pre-/post- contents and their respective markers
return newText.replace(
`${symbol}${bodyTrimmed}${symbol}`,
`${prefix}${symbol}${bodyTrimmed}${symbol}${postfix}`
);
}
function getInlineStyleRangesByLength(inlineStyleRanges) {
return [...inlineStyleRanges].sort((a, b) => b.length - a.length);
// const concatArray = [[].concat.apply([], inlineStyleRanges)];
// const response = concatArray.sort((a, b) => b.length - a.length);
// return response;
}
// function mergeObj(obj1, obj2) {
// var obj3 = {};
// for (var attrname in obj1) { obj3[attrname] = obj1[attrname]; }
// for (var attrname in obj2) { obj3[attrname] = obj2[attrname]; }
// return obj3;
// }
function draftjsToMd(raw, extraMarkdownDict) {
const markdownDict = { ...defaultMarkdownDict, ...extraMarkdownDict };
let returnString = '';
const appliedBlockStyles = [];
// totalOffset is a difference of index position between raw string and enhanced ones
let totalOffset = 0;
raw.blocks.forEach((block, blockIndex) => {
if (blockIndex !== 0) {
returnString += '\n';
totalOffset = 0;
}
// add block style
returnString += getBlockStyle(block.type, appliedBlockStyles);
appliedBlockStyles.push(block.type);
const appliedStyles = [];
returnString += block.text.split('').reduce((text, currentChar, index) => {
let newText = text;
const sortedInlineStyleRanges = getInlineStyleRangesByLength(block.inlineStyleRanges);
// find all styled at this character
const stylesStartAtChar = sortedInlineStyleRanges
.filter(range => range.offset === index)
.filter(range => markdownDict[range.style]); // disregard styles not defined in the md dict
// add the symbol to the md string and push the style in the applied styles stack
stylesStartAtChar.forEach(currentStyle => {
const symbolLength = markdownDict[currentStyle.style].length;
newText += markdownDict[currentStyle.style];
totalOffset += symbolLength;
appliedStyles.push({
symbol: markdownDict[currentStyle.style],
range: {
start: currentStyle.offset + totalOffset,
end: currentStyle.offset + currentStyle.length + totalOffset
},
end: currentStyle.offset + (currentStyle.length - 1)
});
});
// check for entityRanges starting and add if existing
const entitiesStartAtChar = block.entityRanges.filter(range => range.offset === index);
entitiesStartAtChar.forEach(entity => {
newText += getEntityStart(raw.entityMap[entity.key]);
});
// add the current character to the md string
newText += currentChar;
// check for entityRanges ending and add if existing
const entitiesEndAtChar = block.entityRanges.filter(
range => range.offset + range.length - 1 === index
);
entitiesEndAtChar.forEach(entity => {
newText += getEntityEnd(raw.entityMap[entity.key]);
});
// apply the 'ending' tags for any styles that end in the current position in order (stack)
while (appliedStyles.length !== 0 && appliedStyles[appliedStyles.length - 1].end === index) {
const endingStyle = appliedStyles.pop();
newText += endingStyle.symbol;
newText = fixWhitespacesInsideStyle(newText, endingStyle);
totalOffset += endingStyle.symbol.length;
}
return newText;
}, '');
returnString = applyWrappingBlockStyle(block.type, returnString);
returnString = applyAtomicStyle(block, raw.entityMap, returnString);
});
return returnString;
}
module.exports.draftjsToMd = draftjsToMd;