From 1d4d7fabec5c16cc84972025025821470dc44d9f Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Wed, 24 Mar 2021 21:04:12 +0100 Subject: [PATCH] Move most of tags allowed to its own file --- ts/editor/helpers.ts | 4 ++ ts/editor/htmlFilter.ts | 108 ++--------------------------- ts/editor/htmlFilterSpan.ts | 44 ++++++++++++ ts/editor/htmlFilterTagsAllowed.ts | 72 +++++++++++++++++++ 4 files changed, 126 insertions(+), 102 deletions(-) create mode 100644 ts/editor/htmlFilterSpan.ts create mode 100644 ts/editor/htmlFilterTagsAllowed.ts diff --git a/ts/editor/helpers.ts b/ts/editor/helpers.ts index 9281af818..5ff18f3a6 100644 --- a/ts/editor/helpers.ts +++ b/ts/editor/helpers.ts @@ -77,3 +77,7 @@ export function caretToEnd(currentField: EditingArea): void { selection.removeAllRanges(); selection.addRange(range); } + +export function isNightMode(): boolean { + return document.body.classList.contains("nightMode"); +} diff --git a/ts/editor/htmlFilter.ts b/ts/editor/htmlFilter.ts index b59846f3a..3eaeee159 100644 --- a/ts/editor/htmlFilter.ts +++ b/ts/editor/htmlFilter.ts @@ -2,92 +2,9 @@ * License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */ import { nodeIsElement } from "./helpers"; +import { tagsAllowedBasic, tagsAllowedExtended } from "./htmlFilterTagsAllowed"; -const allowedTagsBasic = {}; -const allowedTagsExtended = {}; - -let TAGS_WITHOUT_ATTRS = ["P", "DIV", "BR", "SUB", "SUP"]; -for (const tag of TAGS_WITHOUT_ATTRS) { - allowedTagsBasic[tag] = { attrs: [] }; -} - -TAGS_WITHOUT_ATTRS = [ - "B", - "BLOCKQUOTE", - "CODE", - "DD", - "DL", - "DT", - "EM", - "H1", - "H2", - "H3", - "I", - "LI", - "OL", - "PRE", - "RP", - "RT", - "RUBY", - "STRONG", - "TABLE", - "U", - "UL", -]; -for (const tag of TAGS_WITHOUT_ATTRS) { - allowedTagsExtended[tag] = { attrs: [] }; -} - -allowedTagsBasic["IMG"] = { attrs: ["SRC"] }; - -allowedTagsExtended["A"] = { attrs: ["HREF"] }; -allowedTagsExtended["TR"] = { attrs: ["ROWSPAN"] }; -allowedTagsExtended["TD"] = { attrs: ["COLSPAN", "ROWSPAN"] }; -allowedTagsExtended["TH"] = { attrs: ["COLSPAN", "ROWSPAN"] }; -allowedTagsExtended["FONT"] = { attrs: ["COLOR"] }; - -const allowedStyling = { - color: true, - "background-color": true, - "font-weight": true, - "font-style": true, - "text-decoration-line": true, -}; - -function isNightMode(): boolean { - return document.body.classList.contains("nightMode"); -} - -function filterExternalSpan(elem: HTMLElement): void { - // filter out attributes - for (const attr of [...elem.attributes]) { - const attrName = attr.name.toUpperCase(); - - if (attrName !== "STYLE") { - elem.removeAttributeNode(attr); - } - } - - // filter styling - for (const name of [...elem.style]) { - const value = elem.style.getPropertyValue(name); - - if ( - !allowedStyling.hasOwnProperty(name) || - // google docs adds this unnecessarily - (name === "background-color" && value === "transparent") || - // ignore coloured text in night mode for now - (isNightMode() && (name === "background-color" || name === "color")) - ) { - elem.style.removeProperty(name); - } - } -} - -allowedTagsExtended["SPAN"] = filterExternalSpan; - -// add basic tags to extended -Object.assign(allowedTagsExtended, allowedTagsBasic); +////////////////////// //////////////////// //////////////////// function isHTMLElement(elem: Element): elem is HTMLElement { return elem instanceof HTMLElement; @@ -127,29 +44,16 @@ function filterNode(node: Node, extendedMode: boolean): void { return; } - const tag = extendedMode - ? allowedTagsExtended[node.tagName] - : allowedTagsBasic[node.tagName]; + const tagsAllowed = extendedMode ? tagsAllowedExtended : tagsAllowedBasic; - if (!tag) { + if (tagsAllowed.hasOwnProperty(node.tagName)) { + tagsAllowed[node.tagName](node); + } else { if (!node.innerHTML || node.tagName === "TITLE") { node.parentNode.removeChild(node); } else { node.outerHTML = node.innerHTML; } - } else { - if (typeof tag === "function") { - // filtering function provided - tag(node); - } else { - // allowed, filter out attributes - for (const attr of [...node.attributes]) { - const attrName = attr.name.toUpperCase(); - if (tag.attrs.indexOf(attrName) === -1) { - node.removeAttributeNode(attr); - } - } - } } } diff --git a/ts/editor/htmlFilterSpan.ts b/ts/editor/htmlFilterSpan.ts new file mode 100644 index 000000000..b02e76281 --- /dev/null +++ b/ts/editor/htmlFilterSpan.ts @@ -0,0 +1,44 @@ +import { isNightMode } from "./helpers"; + +/* keys are allowed properties, values are blocked values */ +const stylingAllowListNightMode = { + "font-weight": [], + "font-style": [], + "text-decoration-line": [], +}; + +const stylingAllowList = { + color: [], + "background-color": ["transparent"], + ...stylingAllowListNightMode, +}; + +function isStylingAllowed(property: string, value: string): boolean { + const allowList = isNightMode() ? stylingAllowListNightMode : stylingAllowList; + + return allowList.hasOwnProperty(property) && !allowList[property].includes(value); +} + +const allowedAttrs = ["STYLE"]; + +export function filterSpan(element: Element): void { + // filter out attributes + for (const attr of [...element.attributes]) { + const attrName = attr.name.toUpperCase(); + + if (!allowedAttrs.includes(attrName)) { + element.removeAttributeNode(attr); + } + } + + // filter styling + const elementStyle = (element as HTMLSpanElement).style; + + for (const property of [...elementStyle]) { + const value = elementStyle.getPropertyValue(name); + + if (!isStylingAllowed(property, value)) { + elementStyle.removeProperty(property); + } + } +} diff --git a/ts/editor/htmlFilterTagsAllowed.ts b/ts/editor/htmlFilterTagsAllowed.ts new file mode 100644 index 000000000..6293896f5 --- /dev/null +++ b/ts/editor/htmlFilterTagsAllowed.ts @@ -0,0 +1,72 @@ +import { filterSpan } from "./htmlFilterSpan"; + +type FilterMethod = (element: Element) => void; + +interface TagsAllowed { + [key: string]: FilterMethod; +} + +function filterOutAttributes( + attributePredicate: (attributeName: string) => boolean, + element: Element +): void { + for (const attr of [...element.attributes]) { + const attrName = attr.name.toUpperCase(); + + if (attributePredicate(attrName)) { + element.removeAttributeNode(attr); + } + } +} + +function blockExcept(attrs: string[]): FilterMethod { + return (element: Element) => + filterOutAttributes( + (attributeName: string) => !attrs.includes(attributeName), + element + ); +} + +function blockAll(element: Element): void { + filterOutAttributes(() => true, element); +} + +export const tagsAllowedBasic: TagsAllowed = { + BR: blockAll, + IMG: blockExcept(["SRC"]), + DIV: blockAll, + P: blockAll, + SUB: blockAll, + SUP: blockAll, +}; + +export const tagsAllowedExtended: TagsAllowed = { + ...tagsAllowedBasic, + A: blockExcept(["HREF"]), + B: blockAll, + BLOCKQUOTE: blockAll, + CODE: blockAll, + DD: blockAll, + DL: blockAll, + DT: blockAll, + EM: blockAll, + FONT: blockExcept(["COLOR"]), + H1: blockAll, + H2: blockAll, + H3: blockAll, + I: blockAll, + LI: blockAll, + OL: blockAll, + PRE: blockAll, + RP: blockAll, + RT: blockAll, + RUBY: blockAll, + SPAN: filterSpan, + STRONG: blockAll, + TABLE: blockAll, + TD: blockExcept(["COLSPAN", "ROWSPAN"]), + TH: blockExcept(["COLSPAN", "ROWSPAN"]), + TR: blockExcept(["ROWSPAN"]), + U: blockAll, + UL: blockAll, +};