devexpress-richedit
Version:
DevExpress Rich Text Editor is an advanced word-processing tool designed for working with rich text documents.
399 lines (398 loc) • 20.5 kB
JavaScript
import { MapCreator } from '../../../utils/map-creator';
import { ColumnCalculator } from '../../../layout-formatter/formatter/utils/columns-calculator';
import { FontInfoCache } from '../../../model/caches/hashed-caches/font-info-cache';
import { RunType } from '../../../model/runs/run-type';
import { SubDocumentPosition } from '../../../model/sub-document';
import { Log } from '../../../rich-utils/debug/logger/base-logger/log';
import { LogSource } from '../../../rich-utils/debug/logger/base-logger/log-source';
import { UnitConverter } from '@devexpress/utils/lib/class/unit-converter';
import { FixedInterval } from '@devexpress/utils/lib/intervals/fixed';
import { DomUtils } from '@devexpress/utils/lib/utils/dom';
import { ListUtils } from '@devexpress/utils/lib/utils/list';
import { ImportedTextRunInfo, ImportedParagraphRunInfo, ImportedInlinePictureRunInfo, } from './containers/runs';
import { HtmlModelInserter } from './html-model-inserter';
import { HtmlATagImporter } from './importers/a';
import { HtmlBTagImporter } from './importers/b';
import { HtmlBrTagImporter } from './importers/br';
import { HtmlCenterTagImporter } from './importers/center';
import { HtmlCiteTagImporter } from './importers/cite';
import { HtmlDivTagImporter } from './importers/div';
import { HtmlEmTagImporter } from './importers/em';
import { HtmlITagImporter } from './importers/i';
import { HtmlImgTagImporter } from './importers/img';
import { HtmlLiTagImporter } from './importers/li';
import { HtmlOlTagImporter } from './importers/ol';
import { HtmlH1TagImporter, HtmlH2TagImporter, HtmlH3TagImporter, HtmlH4TagImporter, HtmlH5TagImporter, HtmlH6TagImporter, HtmlPTagImporter, } from './importers/p';
import { HtmlPreTagImporter } from './importers/pre';
import { HtmlSpanTagImporter } from './importers/span';
import { HtmlTableTagImporter } from './importers/table';
import { HtmlTbodyTagImporter } from './importers/tbody';
import { HtmlTdTagImporter, HtmlThTagImporter } from './importers/td';
import { HtmlTextNodeImporter } from './importers/text-node';
import { HtmlTrTagImporter } from './importers/tr';
import { HtmlTtTagImporter } from './importers/tt';
import { HtmlUlTagImporter } from './importers/ul';
import { HtmlUndefinedTagImporter } from './importers/undefined';
import { HtmlImporterMaskedCharacterProperties } from './utils/character-properties-utils';
import { ParagraphListPropertiesUtils } from './utils/paragraph-list-properties-utils';
import { RichUtils } from '../../../model/rich-utils';
import { HtmlImporterMaskedParagraphProperties } from './utils/paragraph-properties-utils';
import { HtmlImporterTabStops } from './utils/tab-stops-utils';
import { FormatImagesImporterData } from '../../utils/images-import';
import { InlinePictureRun } from '../../../../common/model/runs/inline-picture-run';
import { ImageLoadingOptions } from '../../../../common/model/manipulators/picture-manipulator/loader/image-loading-options';
export class LevelInfo {
constructor(element, childElements, allowInsertRuns) {
this.element = element;
this.childElements = childElements;
this.allowInsertRuns = allowInsertRuns;
}
initTagImporter(importer) {
if (DomUtils.isTextNode(this.element))
this.tagImporter = new HtmlTextNodeImporter(importer);
else {
const constr = importer.tagImporters[LevelInfo.getElementTag(this.element)];
this.tagImporter = new (constr ? constr : HtmlUndefinedTagImporter)(importer);
}
return this;
}
static getElementTag(elem) {
const tag = DomUtils.isHTMLElementNode(elem) && elem.tagName;
return tag ? tag.toUpperCase() : '';
}
}
export class HtmlImportData {
constructor(runsInfo, tablesInfo) {
this.runsInfo = runsInfo;
this.tablesInfo = tablesInfo;
}
}
export class HtmlImporter {
get currElement() { return ListUtils.last(this.levelInfo).element; }
;
get currElementChildren() { return ListUtils.last(this.levelInfo).childElements; }
;
get prevLevelInfo() { return this.levelInfo[this.levelInfo.length - 2]; }
get currLevelInfo() { return ListUtils.last(this.levelInfo); }
get currListItemLevelInfo() { return ListUtils.reverseElementBy(this.levelInfo, (levelInfo) => { var _a; return ((_a = levelInfo.tagImporter) === null || _a === void 0 ? void 0 : _a.elementTag()) === 'LI'; }); }
get currListInfo() {
const currListItemLevelInfo = this.currListItemLevelInfo;
return currListItemLevelInfo && !currListItemLevelInfo.tagImporter.paragraphWasAddedBefore ? ListUtils.last(this.listInfos) : null;
}
get subDocument() { return this.subDocPosition.subDocument; }
constructor(modelManager, measurer, subDocPosition, initElements, charPropsBundle, formatImagesImporter) {
this.fieldsId = 0;
this.listIndex = 0;
this.listInfos = [];
if (!HtmlImporter.importers) {
HtmlImporter.importers = [
HtmlATagImporter,
HtmlBTagImporter,
HtmlBrTagImporter,
HtmlCenterTagImporter,
HtmlCiteTagImporter,
HtmlDivTagImporter,
HtmlEmTagImporter,
HtmlITagImporter,
HtmlImgTagImporter,
HtmlLiTagImporter,
HtmlOlTagImporter,
HtmlPTagImporter,
HtmlTableTagImporter,
HtmlTbodyTagImporter,
HtmlH1TagImporter,
HtmlH2TagImporter,
HtmlH3TagImporter,
HtmlH4TagImporter,
HtmlH5TagImporter,
HtmlH6TagImporter,
HtmlPreTagImporter,
HtmlSpanTagImporter,
HtmlTrTagImporter,
HtmlTtTagImporter,
HtmlTdTagImporter,
HtmlThTagImporter,
HtmlUlTagImporter,
];
}
this.charPropsBundle = charPropsBundle;
this.subDocPosition = subDocPosition;
this.modelManager = modelManager;
this.measurer = measurer;
this.currPosition = this.subDocPosition.position;
this.levelInfo = [new LevelInfo(null, initElements, true)];
this.formatImagesImporter = formatImagesImporter;
this.loadFontInfos = [];
this.tempFontInfoCache = new FontInfoCache(this.modelManager.model.cache.fontInfoCache.fontMeasurer);
this.htmlImporterMaskedCharacterProperties =
new HtmlImporterMaskedCharacterProperties(this, this.loadFontInfos, this.tempFontInfoCache, !modelManager.richOptions.fonts.limitedFonts);
this.paragraphListPropertiesUtils = new ParagraphListPropertiesUtils(this, this.htmlImporterMaskedCharacterProperties);
this.tagImporters = {};
for (let importerConst of HtmlImporter.importers)
this.tagImporters[new importerConst(this).elementTag()] = importerConst;
}
import() {
this.importStarted = false;
this.importedRunsInfo = [];
this.importedTablesInfo = [];
ListUtils.clear(this.loadFontInfos);
this.tempFontInfoCache.clear();
let insertedInterval;
this.modelManager.history.addTransaction(() => {
const pos = this.subDocPosition.position;
this.prevRunIsParagraph = pos == 0 ||
(this.subDocument.getRunByPosition(pos - 1).isParagraphOrSectionRun() &&
ListUtils.allOf(this.subDocument.tables, (tbl) => tbl.getEndPosition() != pos));
this.convertChildElements();
if (this.importedRunsInfo.length)
insertedInterval = new HtmlModelInserter(this.modelManager, this.subDocPosition, new HtmlImportData(this.importedRunsInfo, this.getSortedTables()), this.charPropsBundle).insert();
else
insertedInterval = new FixedInterval(this.subDocPosition.position, 0);
for (let info of this.loadFontInfos)
this.modelManager.modelManipulator.font.loadFontInfo(info.fontInfo, info.subDocument, [info.applyNewFontOnIntervalsAfterLoad], this.measurer);
if (this.formatImagesImporter)
this.registerImageRuns();
});
return insertedInterval;
}
convertChildElements(preserveLineBreaks = false) {
for (let element of this.currElementChildren)
this.convertElement(element, preserveLineBreaks);
}
getSortedTables() {
return this.importedTablesInfo.sort((a, b) => {
const aInt = a.interval;
const bInt = b.interval;
const posDiff = aInt.start - bInt.start;
if (posDiff)
return posDiff;
return aInt.containsInterval(bInt) ? -1 : 1;
});
}
convertElement(element, preserveLineBreaks) {
const currLevelInfo = new LevelInfo(element, element.childNodes, ListUtils.last(this.levelInfo).allowInsertRuns)
.initTagImporter(this);
this.levelInfo.push(currLevelInfo);
const importer = currLevelInfo.tagImporter;
this.putDownParentPropertiesToChild();
importer.enablePreserveLineBreaks = preserveLineBreaks;
if (importer.isAllowed())
importer.importBefore();
if (importer.isImportChildren())
this.convertChildElements(preserveLineBreaks || importer.shouldPreserveLineBreaksOnChilds());
if (importer.isAllowed())
importer.importAfter();
this.levelInfo.pop();
}
putDownParentPropertiesToChild() {
if (!this.currElementChildren)
return;
const element = this.currElement;
const missTag = HtmlImporter.MapMissTablePropertiesByTagNames[ListUtils.last(this.levelInfo).tagImporter.elementTag()];
ListUtils.forEach(this.currElementChildren, (childElement) => {
const childElemStyle = this.getStyles(childElement);
if (childElement.nodeType !== Node.ELEMENT_NODE)
return;
for (var prop in this.getStyles(element)) {
if (missTag && /^(border|background|marginLeft)/gi.test(prop))
continue;
if ((childElemStyle[prop] === "" || childElemStyle[prop] === undefined) && element.style[prop] !== "" && !(HtmlImporter.MapShorthandProperty[prop]))
childElemStyle[prop] = element.style[prop];
}
childElement.setAttribute('style', Object.keys(childElemStyle).map(k => `${k}:${childElemStyle[k]}`).join(';'));
});
}
getStyles(element) {
var _a;
const styleStr = (_a = element.getAttribute) === null || _a === void 0 ? void 0 : _a.call(element, 'style');
if (!styleStr)
return {};
const urlRegExp = new RegExp('url\\(.*?\\)', 'gi');
const urlKey = '$URL';
const urlValues = styleStr.match(urlRegExp);
return styleStr
.replace(urlRegExp, urlKey)
.split(';')
.reduceRight((res, style) => {
if (!style)
return res;
const colonIndex = style.indexOf(':');
const key = style.substring(0, colonIndex).trim();
const value = style.substring(colonIndex + 1).trim();
res[key] = value === urlKey ? urlValues.pop() : value;
return res;
}, {});
}
addRun(run, forceAdd = false) {
if (forceAdd || ListUtils.last(this.levelInfo).allowInsertRuns) {
const isParagraph = run.runType == RunType.ParagraphRun || run.runType == RunType.SectionRun;
this.importedRunsInfo.push(run);
this.currPosition += run.runLength;
this.prevRunIsParagraph = isParagraph;
this.importStarted = true;
if (isParagraph && this.currListItemLevelInfo)
this.currListItemLevelInfo.tagImporter.paragraphWasAddedBefore = true;
}
}
addParagraphRun(element, listInfo = null, isTableCellTag = false) {
const htmlProperties = new HtmlImporterMaskedParagraphProperties();
const properties = htmlProperties.import(this.modelManager.model.colorProvider, element, isTableCellTag);
const tabs = HtmlImporterTabStops.import(element);
this.removeAllTrailingLineBreaks();
this.addRun(new ImportedParagraphRunInfo(listInfo, this.charPropsBundle, properties, tabs));
}
addCurrLevelParagraphRunIfNeeded() {
if (this.currLevelInfo.element.previousSibling && !this.prevRunIsParagraph)
this.addParagraphRun(this.currLevelInfo.element, this.currListInfo);
}
removeAllTrailingLineBreaks() {
const last = this.importedRunsInfo.length - 1;
for (let i = last; i >= last - 1; i--) {
let runInfo = this.importedRunsInfo[i];
if (!(runInfo instanceof ImportedTextRunInfo))
return;
if (runInfo.text !== RichUtils.specialCharacters.LineBreak)
return;
this.importedRunsInfo.pop();
this.currPosition -= runInfo.runLength;
}
}
getLastImportedRun() {
return ListUtils.last(this.importedRunsInfo);
}
columnSize() {
const section = this.modelManager.model.getSectionByPosition(this.subDocPosition.position);
return ColumnCalculator.findMinimalColumnSize(section.sectionProperties)
.applyConverter(UnitConverter.pixelsToTwips);
}
registerImageRuns() {
let importedRunsInfoIndex = -1;
this.subDocument.chunks.forEach((chunk) => {
chunk.textRuns.forEach((run) => {
if (run instanceof InlinePictureRun) {
importedRunsInfoIndex = this.findIndexImportedInlinePictureRunInfo(run.info.publicAPIID, ++importedRunsInfoIndex);
const subDocPos = new SubDocumentPosition(this.subDocument, run.startOffset);
const importedRunsInfo = this.importedRunsInfo[importedRunsInfoIndex];
const options = ImageLoadingOptions.initByActualSize(importedRunsInfo.actualSize);
const importerData = new FormatImagesImporterData(subDocPos, options, run);
this.formatImagesImporter.registerImageRun(importerData);
}
});
});
}
findIndexImportedInlinePictureRunInfo(publicAPIID, startIndex = 0) {
for (let i = startIndex, runInfo; runInfo = this.importedRunsInfo[i]; i++) {
if (runInfo instanceof ImportedInlinePictureRunInfo && runInfo.picInfo.publicAPIID === publicAPIID)
return i;
}
return -1;
}
static convertHtml(html) {
Log.print(LogSource.HtmlImporter, "convertHtml", () => html);
html = html.replace(/<(\w[^>]*) lang=([^ |>]*)([^>]*)/gi, "<$1$3");
html = html.replace(/\s*mso-bidi-font-family/gi, "font-family");
html = html.replace(/\s*MARGIN: 0cm 0cm 0pt\s*;/gi, '');
html = html.replace(/\s*MARGIN: 0cm 0cm 0pt\s*"/gi, "\"");
html = html.replace(/\s*TEXT-INDENT: 0cm\s*;/gi, '');
html = html.replace(/\s*TEXT-INDENT: 0cm\s*"/gi, "\"");
html = html.replace(/\s*FONT-VARIANT: [^\s;]+;?"/gi, "\"");
html = html.replace(/<\w+:imagedata/gi, '<img');
html = html.replace(/<p([^>]*)><o:[pP][^>]*>\s*<\/o:[pP]><\/p>(?=\s*<\/td>)/gi, '<p$1> <\/p>');
html = html.replace(/<\/?\w+:[^>]*>/gi, '');
html = html.replace(/<STYLE[^>]*>[\s\S]*?<\/STYLE[^>]*>/gi, '');
html = html.replace(/<(?:META|LINK)[^>]*>\s*/gi, '');
html = html.replace(/<\\?\?xml[^>]*>/gi, '');
html = html.replace(/<o:[pP][^>]*>\s*<\/o:[pP]>/gi, '');
html = html.replace(/<o:[pP][^>]*>.*?<\/o:[pP]>/gi, ' ');
html = html.replace(/<st1:.*?>/gi, '');
html = html.replace(/<\!--[\s\S]*?-->/g, '');
html = html.replace(/\s*style="\s*"/gi, '');
html = html.replace(/style=""/ig, "");
html = html.replace(/style=''/ig, "");
var stRegExp = new RegExp('(?:style=\\")([^\\"]*)(?:\\")', 'gi');
html = html.replace(stRegExp, (str) => {
str = str.replace(/"/gi, "'");
str = str.replace(/
/gi, " ");
return str;
});
html = html.replace(/^\s|\s$/gi, '');
html = html.replace(/<font[^>]*>([^<>]+)<\/font>/gi, '$1');
html = html.replace(/<span\s*><span\s*>([^<>]+)<\/span><\/span>/ig, '$1');
html = html.replace(/<span>([^<>]+)<\/span>/gi, '$1');
html = html.replace(/<caption([^>]*)>[\s\S]*?<\/caption>/gi, '');
var array = html.match(/<[^>]*style\s*=\s*[^>]*>/gi);
if (array && array.length > 0) {
for (var i = 0, elementHtml; elementHtml = array[i]; i++) {
var fontFamilyArray = elementHtml.match(/\s*font-family\s*:\s*(([^;]*)([\"';\s)](?!>))|([^;"']*))/gi);
if (fontFamilyArray && fontFamilyArray.length > 1) {
var commonValue = fontFamilyArray[0].replace(/font-family\s*:\s*([^;]*)[\"'; ]/gi, "$1");
var resultElementHtml = elementHtml;
for (var j = 0, fontFamily; fontFamily = fontFamilyArray[j]; j++)
resultElementHtml = resultElementHtml.replace(fontFamily, "font-family: " + commonValue + ";");
html = html.replace(elementHtml, resultElementHtml);
}
}
}
html = html.replace(/^\n|\n$/gi, '');
html = html.replace(/(\n+(<br>)|(<\/p>|<br>)\n+)/gi, '$2$3');
var preTags = html.match(/<pre([\s\S]*)<\/pre>/g);
if (preTags) {
ListUtils.forEach(preTags, (val) => {
html = html.replace(val, val.replace(/\n/gi, "<p/>"));
});
}
html = html.replace(/(\r*\n+\s+)|(\s+\r*\n+)/gi, ' ');
html = html.replace(/\n+/gi, ' ');
html = html.replace(/\n/gi, RichUtils.specialCharacters.LineBreak);
html = html.replace(/(<\/(?!(p)+)(\s*[^>]*)?>)<\/td>/gi, '$1<p> </p></td>');
html = html.replace(/(<\/(?!(p)+)(\s*[^>]*)?>)<\/th>/gi, '$1<p> </p></th>');
html = html.replace(/<script(\s[^>]*)?>[\s\S]*?<\/script>/gi, '');
html = html.replace(/<u>([\s\S]*?)<\/u>/gi, '<span style="text-decoration: underline">$1</span>');
html = html.replace(/<s>([\s\S]*?)<\/s>/gi, '<span style="text-decoration: line-through">$1</span>');
html = html.replace(/<\/([^\s>]+)(\s[^>]*)?><br><\/([^\s>]+)(\s[^>]*)?>/gi, '');
html = html.replace(/\s*(<([ph]\d?|ol|ul|li))/gi, '$1');
html = html.replace(/(<\/([ph]\d?|ol|ul|li)>)\s*/gi, '$1');
html = this.extractBodyContent(html);
Log.print(LogSource.HtmlImporter, "convertHtml", () => html);
return html;
}
static extractBodyContent(html) {
const startTagMatch = /<body[^>]*>\s*/i.exec(html);
let startIndex = 0;
if (startTagMatch)
startIndex = startTagMatch.index + startTagMatch[0].length;
const endTagMatch = /\s*<\/body[^>]*>\s*/i.exec(html);
let endIndex = html.length;
if (endTagMatch)
endIndex = endTagMatch.index;
return html.substring(startIndex, endIndex);
}
}
HtmlImporter.importers = null;
HtmlImporter.MapMissTablePropertiesByTagNames = new MapCreator()
.add("TABLE", true)
.add("TD", true)
.add("TH", true)
.get();
HtmlImporter.MapShorthandProperty = new MapCreator()
.add("background", false)
.add("border", true)
.add("borderImage", true)
.add("borderTop", true)
.add("borderRight", true)
.add("borderBottom", true)
.add("borderLeft", true)
.add("borderWidth", true)
.add("borderColor", true)
.add("borderStyle", true)
.add("borderRadius", true)
.add("font", true)
.add("fontVariant", true)
.add("listStyle", true)
.add("margin", true)
.add("padding", true)
.add("transition", true)
.add("transform", true)
.add("listStyleType", true)
.add("cssText", true)
.get();