@20minutes/draft-convert
Version:
Extensibly serialize & deserialize Draft.js ContentState
76 lines (75 loc) • 4.31 kB
JavaScript
import invariant from 'tiny-invariant';
import defaultInlineHTML from './default/defaultInlineHTML.mjs';
import accumulateFunction from './util/accumulateFunction.mjs';
import getElementHTML from './util/getElementHTML.mjs';
import rangeSort from './util/rangeSort.mjs';
import styleObjectFunction from './util/styleObjectFunction.mjs';
const subtractStyles = (original, toRemove)=>original.filter((el)=>!toRemove.some((elToRemove)=>elToRemove.style === el.style));
const popEndingStyles = (styleStack, endingStyles)=>endingStyles.reduceRight((stack, style)=>{
const styleToRemove = stack[stack.length - 1];
invariant(styleToRemove.style === style.style, `Style ${styleToRemove.style} to be removed doesn't match expected ${style.style}`);
return stack.slice(0, -1);
}, styleStack);
const characterStyles = (offset, ranges)=>ranges.filter((range)=>offset >= range.offset && offset < range.offset + range.length);
const rangeIsSubset = (firstRange, secondRange)=>{
// returns true if the second range is a subset of the first
const secondStartWithinFirst = firstRange.offset <= secondRange.offset;
const secondEndWithinFirst = firstRange.offset + firstRange.length >= secondRange.offset + secondRange.length;
return secondStartWithinFirst && secondEndWithinFirst;
};
const latestStyleLast = (s1, s2)=>{
// make sure longer-lasting styles are added first
const s2endIndex = s2.offset + s2.length;
const s1endIndex = s1.offset + s1.length;
return s2endIndex - s1endIndex;
};
const getStylesToReset = (remainingStyles, newStyles)=>{
let i = 0;
while(i < remainingStyles.length){
if (newStyles.every(rangeIsSubset.bind(null, remainingStyles[i]))) {
i += 1;
} else {
return remainingStyles.slice(i);
}
}
return [];
};
const appendStartMarkup = (inlineHTML, string, styleRange)=>string + getElementHTML(inlineHTML(styleRange.style)).start;
const prependEndMarkup = (inlineHTML, string, styleRange)=>getElementHTML(inlineHTML(styleRange.style)).end + string;
const defaultCustomInlineHTML = (next)=>(style)=>next(style);
defaultCustomInlineHTML.__isMiddleware = true;
export default ((rawBlock, customInlineHTML = defaultCustomInlineHTML)=>{
invariant(rawBlock !== null && rawBlock !== undefined, 'Expected raw block to be non-null');
let inlineHTML;
if (customInlineHTML.__isMiddleware === true) {
inlineHTML = customInlineHTML(defaultInlineHTML);
} else {
inlineHTML = accumulateFunction(styleObjectFunction(customInlineHTML), styleObjectFunction(defaultInlineHTML));
}
let result = '';
let styleStack = [];
const sortedRanges = rawBlock.inlineStyleRanges.sort(rangeSort);
const originalTextArray = [
...rawBlock.text
];
for(let i = 0; i < originalTextArray.length; i += 1){
const styles = characterStyles(i, sortedRanges);
const endingStyles = subtractStyles(styleStack, styles);
const newStyles = subtractStyles(styles, styleStack);
const remainingStyles = subtractStyles(styleStack, endingStyles);
// reset styles: look for any already existing styles that will need to
// end before styles that are being added on this character. to solve this
// close out those current tags and all nested children,
// then open new ones nested within the new styles.
const resetStyles = getStylesToReset(remainingStyles, newStyles);
const openingStyles = resetStyles.concat(newStyles).sort(latestStyleLast);
const openingStyleTags = openingStyles.reduce(appendStartMarkup.bind(null, inlineHTML), '');
const endingStyleTags = endingStyles.concat(resetStyles).reduce(prependEndMarkup.bind(null, inlineHTML), '');
result += endingStyleTags + openingStyleTags + originalTextArray[i];
styleStack = popEndingStyles(styleStack, resetStyles.concat(endingStyles));
styleStack = styleStack.concat(openingStyles);
invariant(styleStack.length === styles.length, `Character ${i}: ${styleStack.length - styles.length} styles left on stack that should no longer be there`);
}
result = styleStack.reduceRight((res, openStyle)=>res + getElementHTML(inlineHTML(openStyle.style)).end, result);
return result;
});