mirror of
https://github.com/ankitects/anki.git
synced 2025-09-24 08:46:37 -04: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;
|
||||
}
|
||||
|
||||
export function isHTMLElement(elem: Element): elem is HTMLElement {
|
||||
return elem instanceof HTMLElement;
|
||||
}
|
||||
|
||||
const INLINE_TAGS = [
|
||||
"A",
|
||||
"ABBR",
|
||||
|
|
|
@ -1,68 +1,56 @@
|
|||
/* Copyright: Ankitects Pty Ltd and contributors
|
||||
* License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */
|
||||
|
||||
import { nodeIsElement } from "./helpers";
|
||||
import { filterElement } from "./htmlFilterElement";
|
||||
import {
|
||||
filterElementBasic,
|
||||
filterElementExtended,
|
||||
filterElementInternal,
|
||||
} from "./htmlFilterElement";
|
||||
import { filterNode } from "./htmlFilterNode";
|
||||
|
||||
////////////////////// //////////////////// ////////////////////
|
||||
|
||||
function isHTMLElement(elem: Element): elem is HTMLElement {
|
||||
return elem instanceof HTMLElement;
|
||||
enum FilterMode {
|
||||
Basic,
|
||||
Extended,
|
||||
Internal,
|
||||
}
|
||||
|
||||
// filtering from another field
|
||||
function filterInternalNode(elem: Element): void {
|
||||
if (isHTMLElement(elem)) {
|
||||
elem.style.removeProperty("background-color");
|
||||
elem.style.removeProperty("font-size");
|
||||
elem.style.removeProperty("font-family");
|
||||
}
|
||||
// recurse
|
||||
for (let i = 0; i < elem.children.length; i++) {
|
||||
const child = elem.children[i];
|
||||
filterInternalNode(child);
|
||||
}
|
||||
const filters: Record<FilterMode, (element: Element) => void> = {
|
||||
[FilterMode.Basic]: filterElementBasic,
|
||||
[FilterMode.Extended]: filterElementExtended,
|
||||
[FilterMode.Internal]: filterElementInternal,
|
||||
};
|
||||
|
||||
const whitespace = /[\n\t ]+/g;
|
||||
|
||||
function collapseWhitespace(value: string): string {
|
||||
return value.replace(whitespace, " ");
|
||||
}
|
||||
|
||||
// filtering from external sources
|
||||
function filterNode(node: Node, extendedMode: boolean): void {
|
||||
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);
|
||||
function trim(value: string): string {
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
const outputHTMLProcessors: Record<FilterMode, (outputHTML: string) => string> = {
|
||||
[FilterMode.Basic]: (outputHTML: string): string =>
|
||||
trim(collapseWhitespace(outputHTML)),
|
||||
[FilterMode.Extended]: trim,
|
||||
[FilterMode.Internal]: trim,
|
||||
};
|
||||
|
||||
export function filterHTML(
|
||||
html: string,
|
||||
internal: boolean,
|
||||
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;
|
||||
export function filterHTML(html: string, internal: boolean, extended: boolean): string {
|
||||
const template = document.createElement("template");
|
||||
template.innerHTML = html;
|
||||
|
||||
if (internal) {
|
||||
filterInternalNode(top);
|
||||
} else {
|
||||
filterNode(top, extendedMode);
|
||||
}
|
||||
let outHtml = top.innerHTML;
|
||||
if (!extendedMode && !internal) {
|
||||
// collapse whitespace
|
||||
outHtml = outHtml.replace(/[\n\t ]+/g, " ");
|
||||
}
|
||||
outHtml = outHtml.trim();
|
||||
return outHtml;
|
||||
const mode = internal
|
||||
? FilterMode.Internal
|
||||
: extended
|
||||
? FilterMode.Extended
|
||||
: FilterMode.Basic;
|
||||
|
||||
const content = template.content;
|
||||
const filter = filterNode(filters[mode]);
|
||||
|
||||
filter(content);
|
||||
|
||||
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 {
|
||||
[tagName: string]: FilterMethod;
|
||||
|
@ -6,8 +12,6 @@ interface TagsAllowed {
|
|||
|
||||
type FilterMethod = (element: Element) => void;
|
||||
|
||||
function doNothing() {}
|
||||
|
||||
function filterOutAttributes(
|
||||
attributePredicate: (attributeName: string) => boolean,
|
||||
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 {
|
||||
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 = {
|
||||
ANKITOP: doNothing,
|
||||
BR: blockAll,
|
||||
IMG: blockExcept(["SRC"]),
|
||||
DIV: blockAll,
|
||||
|
@ -84,17 +90,25 @@ const tagsAllowedExtended: TagsAllowed = {
|
|||
UL: blockAll,
|
||||
};
|
||||
|
||||
export function filterElement(element: Element, extendedMode: boolean): void {
|
||||
const filterElementTagsAllowed = (tagsAllowed: TagsAllowed) => (
|
||||
element: Element
|
||||
): void => {
|
||||
const tagName = element.tagName;
|
||||
const tagsAllowed = extendedMode ? tagsAllowedExtended : tagsAllowedBasic;
|
||||
|
||||
if (tagsAllowed.hasOwnProperty(tagName)) {
|
||||
tagsAllowed[tagName](element);
|
||||
}
|
||||
else if (element.innerHTML) {
|
||||
} else if (element.innerHTML) {
|
||||
removeElement(element);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
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