Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,41 @@
import { Plugin } from "@ckeditor/ckeditor5-core";
import {
ClipboardContentInsertionEvent,
ClipboardInputTransformationEvent,
ClipboardObserver,
ClipboardPipeline,
} from "@ckeditor/ckeditor5-clipboard";
import { Model, ModelDocumentFragment } from "@ckeditor/ckeditor5-engine";
import {
Model,
ModelDocumentFragment,
ViewDocumentFragment,
ViewElement,
ViewUpcastWriter,
} from "@ckeditor/ckeditor5-engine";

const INHERITED_TEXT_COLOR_VARIABLE = "--wcfContentText";
const COLOR_STYLE = "color";

export class WoltlabPasteFromOffice extends Plugin {
#colorNormalizer?: CanvasRenderingContext2D | null;

static get pluginName() {
return "WoltlabPasteFromOffice";
}

init() {
this.editor.editing.view.addObserver(ClipboardObserver);

this.editor.plugins
.get(ClipboardPipeline)
.on<ClipboardInputTransformationEvent>(
"inputTransformation",
(event, data) => {
this.#removeInheritedTextColor(data.content);
},
{ priority: "high" },
);

this.editor.plugins
.get(ClipboardPipeline)
.on<ClipboardContentInsertionEvent>("contentInsertion", (event, data) => {
Expand All @@ -36,7 +58,7 @@ export class WoltlabPasteFromOffice extends Plugin {
* Pasting from the web version of Excel can sometimes prepend a block of CSS
* in front of the table. This block is added as a plain text paragraph that
* needs to be removed.
*
*
* This can be easily detected by checking if a paragraph followed by a table
* is pasted and the paragraph contains certain strings that are unique to
* MS office.
Expand All @@ -45,7 +67,7 @@ export class WoltlabPasteFromOffice extends Plugin {
documentFragment: ModelDocumentFragment,
model: Model,
): boolean {
let range = model.createRangeIn(documentFragment);
const range = model.createRangeIn(documentFragment);
if (documentFragment.childCount !== 2) {
return false;
}
Expand Down Expand Up @@ -91,6 +113,140 @@ export class WoltlabPasteFromOffice extends Plugin {
writer.remove(items[0]);
});
}

#removeInheritedTextColor(documentFragment: ViewDocumentFragment): void {
const colors = this.#getInheritedTextColors();
if (colors.size === 0) {
return;
}

const writer = new ViewUpcastWriter(documentFragment.document);
const range = writer.createRangeIn(documentFragment);
const elements = Array.from(range.getItems()).filter(
(item): item is ViewElement => item.is("element"),
);

for (const element of elements) {
const color = element.getStyle(COLOR_STYLE);
if (color === undefined) {
continue;
}

const normalizedColor = this.#normalizeColor(color);
if (normalizedColor !== undefined && colors.has(normalizedColor)) {
writer.removeStyle(COLOR_STYLE, element);
}
}
}

#getInheritedTextColors(): Set<string> {
const colors = new Set<string>();
const editableElement = this.editor.ui.getEditableElement();

if (editableElement !== null) {
this.#addComputedTextColors(colors, editableElement);
}

if (document.body !== null) {
this.#addComputedTextColors(colors, document.body);
}

this.#addComputedTextColors(colors, document.documentElement);
this.#addStyleSheetTextColors(colors);

return colors;
}

#addComputedTextColors(colors: Set<string>, element: HTMLElement): void {
const style = getComputedStyle(element);

this.#addColor(colors, style.color);
this.#addColor(
colors,
style.getPropertyValue(INHERITED_TEXT_COLOR_VARIABLE),
);
}

#addStyleSheetTextColors(colors: Set<string>): void {
for (const styleSheet of Array.from(document.styleSheets)) {
let cssRules: CSSRuleList;

try {
cssRules = styleSheet.cssRules;
} catch {
continue;
}

this.#addStyleSheetRuleTextColors(colors, cssRules);
}
}

#addStyleSheetRuleTextColors(
colors: Set<string>,
cssRules: CSSRuleList,
): void {
for (const rule of Array.from(cssRules)) {
if ("style" in rule) {
this.#addColor(
colors,
(
rule as CSSRule & { style: CSSStyleDeclaration }
).style.getPropertyValue(INHERITED_TEXT_COLOR_VARIABLE),
);
}

if ("cssRules" in rule) {
this.#addStyleSheetRuleTextColors(
colors,
(rule as CSSRule & { cssRules: CSSRuleList }).cssRules,
);
}
}
}

#addColor(colors: Set<string>, color: string): void {
const normalizedColor = this.#normalizeColor(color);

if (normalizedColor !== undefined) {
colors.add(normalizedColor);
}
}

#normalizeColor(color: string): string | undefined {
const trimmedColor = color.trim();
if (trimmedColor === "" || trimmedColor.includes("var(")) {
return undefined;
}

const context = this.#getColorNormalizer();
if (context !== null) {
context.fillStyle = "#000001";
context.fillStyle = trimmedColor;

const normalizedColor = context.fillStyle.toLowerCase();
if (
normalizedColor !== "#000001" ||
trimmedColor.replace(/\s/g, "").toLowerCase() === "rgb(0,0,1)"
) {
return normalizedColor;
}
}

return trimmedColor
.replace(/\s/g, "")
.replace(/^rgba\((\d+),(\d+),(\d+),(?:1|1\.0)\)$/i, "rgb($1,$2,$3)")
.toLowerCase();
}

#getColorNormalizer(): CanvasRenderingContext2D | null {
if (this.#colorNormalizer !== undefined) {
return this.#colorNormalizer;
}

this.#colorNormalizer = document.createElement("canvas").getContext("2d");

return this.#colorNormalizer;
}
}

export default WoltlabPasteFromOffice;