mirror of
https://github.com/ankitects/anki.git
synced 2025-12-10 21:36:55 -05:00
Create htmlFilter{Node,Styling} for better separation of concerns
This commit is contained in:
parent
d3d3720b39
commit
39aa549ac9
6 changed files with 162 additions and 114 deletions
|
|
@ -7,6 +7,10 @@ export function nodeIsElement(node: Node): node is Element {
|
||||||
return node.nodeType === Node.ELEMENT_NODE;
|
return node.nodeType === Node.ELEMENT_NODE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isHTMLElement(elem: Element): elem is HTMLElement {
|
||||||
|
return elem instanceof HTMLElement;
|
||||||
|
}
|
||||||
|
|
||||||
const INLINE_TAGS = [
|
const INLINE_TAGS = [
|
||||||
"A",
|
"A",
|
||||||
"ABBR",
|
"ABBR",
|
||||||
|
|
|
||||||
|
|
@ -1,68 +1,56 @@
|
||||||
/* Copyright: Ankitects Pty Ltd and contributors
|
/* Copyright: Ankitects Pty Ltd and contributors
|
||||||
* License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */
|
* License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */
|
||||||
|
|
||||||
import { nodeIsElement } from "./helpers";
|
import {
|
||||||
import { filterElement } from "./htmlFilterElement";
|
filterElementBasic,
|
||||||
|
filterElementExtended,
|
||||||
|
filterElementInternal,
|
||||||
|
} from "./htmlFilterElement";
|
||||||
|
import { filterNode } from "./htmlFilterNode";
|
||||||
|
|
||||||
////////////////////// //////////////////// ////////////////////
|
enum FilterMode {
|
||||||
|
Basic,
|
||||||
function isHTMLElement(elem: Element): elem is HTMLElement {
|
Extended,
|
||||||
return elem instanceof HTMLElement;
|
Internal,
|
||||||
}
|
}
|
||||||
|
|
||||||
// filtering from another field
|
const filters: Record<FilterMode, (element: Element) => void> = {
|
||||||
function filterInternalNode(elem: Element): void {
|
[FilterMode.Basic]: filterElementBasic,
|
||||||
if (isHTMLElement(elem)) {
|
[FilterMode.Extended]: filterElementExtended,
|
||||||
elem.style.removeProperty("background-color");
|
[FilterMode.Internal]: filterElementInternal,
|
||||||
elem.style.removeProperty("font-size");
|
};
|
||||||
elem.style.removeProperty("font-family");
|
|
||||||
}
|
const whitespace = /[\n\t ]+/g;
|
||||||
// recurse
|
|
||||||
for (let i = 0; i < elem.children.length; i++) {
|
function collapseWhitespace(value: string): string {
|
||||||
const child = elem.children[i];
|
return value.replace(whitespace, " ");
|
||||||
filterInternalNode(child);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// filtering from external sources
|
function trim(value: string): string {
|
||||||
function filterNode(node: Node, extendedMode: boolean): void {
|
return value.trim();
|
||||||
if (node.nodeType === Node.COMMENT_NODE) {
|
|
||||||
node.parentNode.removeChild(node);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!nodeIsElement(node)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// descend first, and take a copy of the child nodes as the loop will skip
|
|
||||||
// elements due to node modifications otherwise
|
|
||||||
for (const child of [...node.childNodes]) {
|
|
||||||
filterNode(child, extendedMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
filterElement(node, extendedMode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const outputHTMLProcessors: Record<FilterMode, (outputHTML: string) => string> = {
|
||||||
|
[FilterMode.Basic]: (outputHTML: string): string =>
|
||||||
|
trim(collapseWhitespace(outputHTML)),
|
||||||
|
[FilterMode.Extended]: trim,
|
||||||
|
[FilterMode.Internal]: trim,
|
||||||
|
};
|
||||||
|
|
||||||
export function filterHTML(
|
export function filterHTML(html: string, internal: boolean, extended: boolean): string {
|
||||||
html: string,
|
const template = document.createElement("template");
|
||||||
internal: boolean,
|
template.innerHTML = html;
|
||||||
extendedMode: boolean
|
|
||||||
): string {
|
|
||||||
// wrap it in <top> as we aren't allowed to change top level elements
|
|
||||||
const top = document.createElement("ankitop");
|
|
||||||
top.innerHTML = html;
|
|
||||||
|
|
||||||
if (internal) {
|
const mode = internal
|
||||||
filterInternalNode(top);
|
? FilterMode.Internal
|
||||||
} else {
|
: extended
|
||||||
filterNode(top, extendedMode);
|
? FilterMode.Extended
|
||||||
}
|
: FilterMode.Basic;
|
||||||
let outHtml = top.innerHTML;
|
|
||||||
if (!extendedMode && !internal) {
|
const content = template.content;
|
||||||
// collapse whitespace
|
const filter = filterNode(filters[mode]);
|
||||||
outHtml = outHtml.replace(/[\n\t ]+/g, " ");
|
|
||||||
}
|
filter(content);
|
||||||
outHtml = outHtml.trim();
|
|
||||||
return outHtml;
|
return outputHTMLProcessors[mode](template.innerHTML);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,10 @@
|
||||||
import { filterSpan } from "./htmlFilterSpan";
|
import { isNightMode, isHTMLElement } from "./helpers";
|
||||||
|
import { removeNode as removeElement } from "./htmlFilterNode";
|
||||||
|
import {
|
||||||
|
filterStylingNightMode,
|
||||||
|
filterStylingLightMode,
|
||||||
|
filterStylingInternal,
|
||||||
|
} from "./htmlFilterStyling";
|
||||||
|
|
||||||
interface TagsAllowed {
|
interface TagsAllowed {
|
||||||
[tagName: string]: FilterMethod;
|
[tagName: string]: FilterMethod;
|
||||||
|
|
@ -6,8 +12,6 @@ interface TagsAllowed {
|
||||||
|
|
||||||
type FilterMethod = (element: Element) => void;
|
type FilterMethod = (element: Element) => void;
|
||||||
|
|
||||||
function doNothing() {}
|
|
||||||
|
|
||||||
function filterOutAttributes(
|
function filterOutAttributes(
|
||||||
attributePredicate: (attributeName: string) => boolean,
|
attributePredicate: (attributeName: string) => boolean,
|
||||||
element: Element
|
element: Element
|
||||||
|
|
@ -33,17 +37,19 @@ function blockExcept(attrs: string[]): FilterMethod {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function removeElement(element: Element): void {
|
|
||||||
element.parentNode?.removeChild(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
function unwrapElement(element: Element): void {
|
function unwrapElement(element: Element): void {
|
||||||
element.outerHTML = element.innerHTML;
|
element.outerHTML = element.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function filterSpan(element: Element): void {
|
||||||
|
const filterAttrs = blockExcept(["STYLE"]);
|
||||||
|
filterAttrs(element);
|
||||||
|
|
||||||
|
const filterStyle = isNightMode() ? filterStylingNightMode : filterStylingLightMode;
|
||||||
|
filterStyle(element as HTMLSpanElement);
|
||||||
|
}
|
||||||
|
|
||||||
const tagsAllowedBasic: TagsAllowed = {
|
const tagsAllowedBasic: TagsAllowed = {
|
||||||
ANKITOP: doNothing,
|
|
||||||
BR: blockAll,
|
BR: blockAll,
|
||||||
IMG: blockExcept(["SRC"]),
|
IMG: blockExcept(["SRC"]),
|
||||||
DIV: blockAll,
|
DIV: blockAll,
|
||||||
|
|
@ -84,17 +90,25 @@ const tagsAllowedExtended: TagsAllowed = {
|
||||||
UL: blockAll,
|
UL: blockAll,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function filterElement(element: Element, extendedMode: boolean): void {
|
const filterElementTagsAllowed = (tagsAllowed: TagsAllowed) => (
|
||||||
|
element: Element
|
||||||
|
): void => {
|
||||||
const tagName = element.tagName;
|
const tagName = element.tagName;
|
||||||
const tagsAllowed = extendedMode ? tagsAllowedExtended : tagsAllowedBasic;
|
|
||||||
|
|
||||||
if (tagsAllowed.hasOwnProperty(tagName)) {
|
if (tagsAllowed.hasOwnProperty(tagName)) {
|
||||||
tagsAllowed[tagName](element);
|
tagsAllowed[tagName](element);
|
||||||
}
|
} else if (element.innerHTML) {
|
||||||
else if (element.innerHTML) {
|
|
||||||
removeElement(element);
|
removeElement(element);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
unwrapElement(element);
|
unwrapElement(element);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const filterElementBasic = filterElementTagsAllowed(tagsAllowedBasic);
|
||||||
|
export const filterElementExtended = filterElementTagsAllowed(tagsAllowedExtended);
|
||||||
|
|
||||||
|
export function filterElementInternal(element: Element): void {
|
||||||
|
if (isHTMLElement(element)) {
|
||||||
|
filterStylingInternal(element);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
32
ts/editor/htmlFilterNode.ts
Normal file
32
ts/editor/htmlFilterNode.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
export function removeNode(element: Node): void {
|
||||||
|
element.parentNode?.removeChild(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
function iterateElement(
|
||||||
|
filter: (node: Node) => void,
|
||||||
|
fragment: DocumentFragment | Element
|
||||||
|
): void {
|
||||||
|
for (const child of [...fragment.childNodes]) {
|
||||||
|
filter(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const filterNode = (elementFilter: (element: Element) => void) => (
|
||||||
|
node: Node
|
||||||
|
): void => {
|
||||||
|
switch (node.nodeType) {
|
||||||
|
case Node.COMMENT_NODE:
|
||||||
|
removeNode(node);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Node.DOCUMENT_FRAGMENT_NODE:
|
||||||
|
iterateElement(filterNode(elementFilter), node as DocumentFragment);
|
||||||
|
|
||||||
|
case Node.ELEMENT_NODE:
|
||||||
|
iterateElement(filterNode(elementFilter), node as Element);
|
||||||
|
elementFilter(node as Element);
|
||||||
|
|
||||||
|
default:
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
54
ts/editor/htmlFilterStyling.ts
Normal file
54
ts/editor/htmlFilterStyling.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
interface AllowPropertiesBlockValues {
|
||||||
|
[property: string]: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type BlockProperties = string[];
|
||||||
|
|
||||||
|
type StylingPredicate = (property: string, value: string) => boolean;
|
||||||
|
|
||||||
|
const stylingNightMode: AllowPropertiesBlockValues = {
|
||||||
|
"font-weight": [],
|
||||||
|
"font-style": [],
|
||||||
|
"text-decoration-line": [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const stylingLightMode: AllowPropertiesBlockValues = {
|
||||||
|
color: [],
|
||||||
|
"background-color": ["transparent"],
|
||||||
|
...stylingNightMode,
|
||||||
|
};
|
||||||
|
|
||||||
|
const stylingInternal: BlockProperties = [
|
||||||
|
"background-color",
|
||||||
|
"font-size",
|
||||||
|
"font-family",
|
||||||
|
];
|
||||||
|
|
||||||
|
const allowPropertiesBlockValues = (
|
||||||
|
allowBlock: AllowPropertiesBlockValues
|
||||||
|
): StylingPredicate => (property: string, value: string): boolean =>
|
||||||
|
allowBlock.hasOwnProperty(property) && !allowBlock[property].includes(value);
|
||||||
|
|
||||||
|
const blockProperties = (block: BlockProperties): StylingPredicate => (
|
||||||
|
property: string
|
||||||
|
): boolean => !block.includes(property);
|
||||||
|
|
||||||
|
const filterStyling = (predicate: (property: string, value: string) => boolean) => (
|
||||||
|
element: HTMLElement
|
||||||
|
): void => {
|
||||||
|
for (const property of [...element.style]) {
|
||||||
|
const value = element.style.getPropertyValue(name);
|
||||||
|
|
||||||
|
if (!predicate(property, value)) {
|
||||||
|
element.style.removeProperty(property);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const filterStylingNightMode = filterStyling(
|
||||||
|
allowPropertiesBlockValues(stylingNightMode)
|
||||||
|
);
|
||||||
|
export const filterStylingLightMode = filterStyling(
|
||||||
|
allowPropertiesBlockValues(stylingLightMode)
|
||||||
|
);
|
||||||
|
export const filterStylingInternal = filterStyling(blockProperties(stylingInternal));
|
||||||
Loading…
Reference in a new issue