From 76ea4cfa9a5a454a3db055ad251501e9538096b2 Mon Sep 17 00:00:00 2001 From: pkliesch Date: Tue, 8 Aug 2023 12:46:31 +0200 Subject: [PATCH 001/403] feature(bbcode-dataprocessor): Add another editor to example app --- app/sample/index.html | 50 +---- app/src/ckeditor.ts | 427 ------------------------------------- app/src/editors/bbCode.ts | 66 ++++++ app/src/editors/default.ts | 406 +++++++++++++++++++++++++++++++++++ app/src/index.ts | 43 ++++ app/webpack.config.js | 2 +- 6 files changed, 525 insertions(+), 469 deletions(-) delete mode 100644 app/src/ckeditor.ts create mode 100644 app/src/editors/bbCode.ts create mode 100644 app/src/editors/default.ts create mode 100644 app/src/index.ts diff --git a/app/sample/index.html b/app/sample/index.html index 60293c6e2f..d1468e2bc1 100644 --- a/app/sample/index.html +++ b/app/sample/index.html @@ -72,6 +72,13 @@

CKEditor 5: CoreMedia Plugin Showcase

+
+
+ + +
+
+
@@ -80,46 +87,7 @@

CKEditor 5: CoreMedia Plugin Showcase

CoreMedia

- + + diff --git a/app/src/ckeditor.ts b/app/src/ckeditor.ts deleted file mode 100644 index e99481993e..0000000000 --- a/app/src/ckeditor.ts +++ /dev/null @@ -1,427 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import { Alignment } from "@ckeditor/ckeditor5-alignment"; -import { AutoLink, Link, LinkImage } from "@ckeditor/ckeditor5-link"; -import { Autoformat } from "@ckeditor/ckeditor5-autoformat"; -import { Autosave } from "@ckeditor/ckeditor5-autosave"; -import { BlockQuote } from "@ckeditor/ckeditor5-block-quote"; -import { Bold, Code, Italic, Strikethrough, Subscript, Superscript, Underline } from "@ckeditor/ckeditor5-basic-styles"; -import { ClassicEditor } from "@ckeditor/ckeditor5-editor-classic"; -import { CodeBlock } from "@ckeditor/ckeditor5-code-block"; -import { Essentials } from "@ckeditor/ckeditor5-essentials"; -import { FindAndReplace } from "@ckeditor/ckeditor5-find-and-replace"; -import { Heading } from "@ckeditor/ckeditor5-heading"; -// ImageInline: See ckeditor/ckeditor5#12027. -import ImageInline from "@ckeditor/ckeditor5-image/src/imageinline"; -// ImageBlockEditing: See ckeditor/ckeditor5#12027. -import ImageBlockEditing from "@ckeditor/ckeditor5-image/src/image/imageblockediting"; -import { ImageStyle, ImageTextAlternative, ImageToolbar } from "@ckeditor/ckeditor5-image"; -import { Indent } from "@ckeditor/ckeditor5-indent"; -import { DocumentList } from "@ckeditor/ckeditor5-list"; -import { Paragraph } from "@ckeditor/ckeditor5-paragraph"; -import { PasteFromOffice } from "@ckeditor/ckeditor5-paste-from-office"; -import { RemoveFormat } from "@ckeditor/ckeditor5-remove-format"; -import { SourceEditing } from "@ckeditor/ckeditor5-source-editing"; -import { Table, TableToolbar } from "@ckeditor/ckeditor5-table"; -import { Highlight } from "@ckeditor/ckeditor5-highlight"; - -import { LinkTarget, ContentLinks } from "@coremedia/ckeditor5-coremedia-link"; -import { ContentClipboard } from "@coremedia/ckeditor5-coremedia-content-clipboard"; -import { ContentImagePlugin } from "@coremedia/ckeditor5-coremedia-images"; -import { Blocklist } from "@coremedia/ckeditor5-coremedia-blocklist"; -import { FontMapper as CoreMediaFontMapper } from "@coremedia/ckeditor5-font-mapper"; -import MockStudioIntegration from "@coremedia/ckeditor5-coremedia-studio-integration-mock/src/MockStudioIntegration"; - -import { setupPreview, updatePreview } from "./preview"; -import { initReadOnlyMode } from "./readOnlySupport"; -import { initExamples, setExampleData } from "./example-data"; -import { - CoreMediaStudioEssentials, - COREMEDIA_RICHTEXT_CONFIG_KEY, - COREMEDIA_RICHTEXT_SUPPORT_CONFIG_KEY, - Strictness, -} from "@coremedia/ckeditor5-coremedia-studio-essentials"; -import { initInputExampleContent } from "./inputExampleContents"; -import { COREMEDIA_MOCK_CONTENT_PLUGIN } from "@coremedia/ckeditor5-coremedia-studio-integration-mock/src/content/MockContentPlugin"; - -import { Command, Editor, icons, PluginConstructor } from "@ckeditor/ckeditor5-core"; -import { saveData } from "./dataFacade"; -import MockInputExamplePlugin from "@coremedia/ckeditor5-coremedia-studio-integration-mock/src/content/MockInputExamplePlugin"; -import PasteContentPlugin from "@coremedia/ckeditor5-coremedia-content-clipboard/src/paste/PasteContentPlugin"; -import { RuleConfig } from "@coremedia/ckeditor5-dom-converter/src/Rule"; -import { replaceElementByElementAndClass } from "@coremedia/ckeditor5-coremedia-richtext/src/rules/ReplaceElementByElementAndClass"; -import { FilterRuleSetConfiguration } from "@coremedia/ckeditor5-dataprocessor-support/src/Rules"; -import { replaceByElementAndClassBackAndForth } from "@coremedia/ckeditor5-coremedia-richtext/src/compatibility/v10/rules/ReplaceBy"; -import { getHashParam } from "./HashParams"; -import { COREMEDIA_LINK_CONFIG_KEY } from "@coremedia/ckeditor5-coremedia-link/src/contentlink/LinkBalloonConfig"; -import { LinkAttributesConfig } from "@coremedia/ckeditor5-link-common/src/LinkAttributesConfig"; -import { LinkAttributes } from "@coremedia/ckeditor5-link-common/src/LinkAttributes"; -import { Differencing } from "@coremedia/ckeditor5-coremedia-differencing"; - -/** - * Typings for CKEditorInspector, as it does not ship with typings yet. - */ -// See https://github.com/ckeditor/ckeditor5-inspector/issues/173 -// eslint-disable-next-line @typescript-eslint/no-extraneous-class -declare class CKEditorInspector { - static attach(editorOrConfig: Editor | Record, options?: { isCollapsed?: boolean }): string[]; -} - -const { - objectInline: withinTextIcon, - objectLeft: alignLeftIcon, - objectRight: alignRightIcon, - objectSizeFull: pageDefaultIcon, -} = icons; - -const editorLanguage = document?.currentScript?.dataset.lang ?? "en"; - -// setup input example content IFrame -const showHideExampleContentButton = document.querySelector("#inputExampleContentButton"); -const inputExampleContentFrame = document.querySelector("#inputExampleContentDiv") as HTMLDivElement; -if (showHideExampleContentButton && inputExampleContentFrame) { - showHideExampleContentButton.addEventListener("click", () => { - inputExampleContentFrame.hidden = !inputExampleContentFrame.hidden; - showHideExampleContentButton.textContent = `${ - inputExampleContentFrame.hidden ? "Show" : "Hide" - } input example contents`; - }); -} - -setupPreview(); - -const imagePlugins: PluginConstructor[] = [ - ContentImagePlugin, - ImageInline, - ImageBlockEditing, - ImageStyle, - ImageToolbar, - ImageTextAlternative, -]; - -const sourceElement = document.querySelector("#editor") as HTMLElement; -if (!sourceElement) { - throw new Error("No element with class editor defined in html. Nothing to create the editor in."); -} - -/** - * You may switch the compatibility, for example, by providing - * `compatibility=v10`. - */ -const richTextCompatibility = getHashParam("compatibility") || "latest"; - -/** - * Apply custom mapping rules. - */ -const richTextRuleConfigurations: RuleConfig[] = [ - // Highlight plugin support. - replaceElementByElementAndClass({ - viewLocalName: "mark", - dataLocalName: "span", - // "mark" is the default here, derived from `viewLocalName`. Thus, - // we may skip it here. - dataReservedClass: "mark", - }), -]; - -/** - * v10 compatible configuration. - */ -const v10RichTextRuleConfigurations: FilterRuleSetConfiguration = { - elements: { - // Highlight Plugin Support - mark: replaceByElementAndClassBackAndForth("mark", "span", "mark"), - }, -}; - -/** - * Configuration that holds all link-related attributes, that are not - * covered yet by any plugin. - * - * Similar to GHS/GRS, they are just registered as being _valid_ **and** - * (this is important) register them to belong to a link element, which again - * ensures that they are removed on remove-link, that cursor positioning - * handles them correctly, etc. - * - * For demonstration purpose, the link attributes configuration can be disabled - * via hash parameter `skipLinkAttributes`. - */ -const linkAttributesConfig: LinkAttributesConfig = getHashParam("skipLinkAttributes") - ? { attributes: [] } - : { - attributes: [ - { view: "title", model: "linkTitle" }, - { view: "data-xlink-actuate", model: "linkActuate" }, - ], - }; - -ClassicEditor.create(sourceElement, { - placeholder: "Type your text here...", - plugins: [ - ...imagePlugins, - Alignment, - Autoformat, - Autosave, - BlockQuote, - Blocklist, - Bold, - Code, - CodeBlock, - ContentLinks, - ContentClipboard, - Differencing, - Essentials, - FindAndReplace, - Heading, - Highlight, - Indent, - Italic, - AutoLink, - Link, - LinkAttributes, - LinkImage, - LinkTarget, - CoreMediaStudioEssentials, - DocumentList, - Paragraph, - PasteContentPlugin, - PasteFromOffice, - RemoveFormat, - Strikethrough, - SourceEditing, - Subscript, - Superscript, - Table, - TableToolbar, - Underline, - CoreMediaFontMapper, - MockInputExamplePlugin, - MockStudioIntegration, - ], - toolbar: [ - "undo", - "redo", - "|", - "heading", - "|", - "bold", - "italic", - "underline", - { - label: "More formatting", - icon: "threeVerticalDots", - items: ["strikethrough", "subscript", "superscript", "code"], - }, - "highlight", - "removeFormat", - "|", - "link", - "|", - "alignment", - "blockQuote", - "codeBlock", - "|", - "insertTable", - "|", - "numberedList", - "bulletedList", - "outdent", - "indent", - "|", - "pasteContent", - "findAndReplace", - "blocklist", - "|", - "sourceEditing", - ], - alignment: { - // The following alternative to signal alignment was used in CKEditor 4 - // of CoreMedia CMCC 10 and before. - // Note that in contrast to CKEditor 4 approach, these classes are now - // applicable to any block element, while it supported only `

` in the - // past. - options: [ - { - name: "left", - className: "align--left", - }, - { - name: "right", - className: "align--right", - }, - { - name: "center", - className: "align--center", - }, - { - name: "justify", - className: "align--justify", - }, - ], - }, - - heading: { - options: [ - { model: "paragraph", title: "Paragraph", class: "ck-heading_paragraph" }, - { model: "heading1", view: "h1", title: "Heading 1", class: "ck-heading_heading1" }, - { model: "heading2", view: "h2", title: "Heading 2", class: "ck-heading_heading2" }, - { model: "heading3", view: "h3", title: "Heading 3", class: "ck-heading_heading3" }, - { model: "heading4", view: "h4", title: "Heading 4", class: "ck-heading_heading4" }, - { model: "heading5", view: "h5", title: "Heading 5", class: "ck-heading_heading5" }, - { model: "heading6", view: "h6", title: "Heading 6", class: "ck-heading_heading6" }, - ], - }, - link: { - defaultProtocol: "https://", - /*defaultTargets: [ - { - type: "externalLink", - target: "_blank", - }, - ],*/ - ...linkAttributesConfig, - /*decorators: { - hasTitle: { - mode: "manual", - label: "Title", - attributes: { - title: - 'Example how standard-decorators of the link-plugin works. To enable/disable, just rename the decorators section to "disabled_decorators" and back again to "decorators" to activate it and see the results.', - }, - }, - },*/ - }, - image: { - styles: { - // Defining custom styling options for the images. - options: [ - { - name: "float-left", - icon: alignLeftIcon, - title: "Left-aligned", - className: "float--left", - modelElements: ["imageInline"], - }, - { - name: "float-right", - icon: alignRightIcon, - title: "Right-aligned", - className: "float--right", - modelElements: ["imageInline"], - }, - { - name: "float-none", - icon: withinTextIcon, - title: "Within Text", - className: "float--none", - modelElements: ["imageInline"], - }, - { - name: "inline", - title: "Page default", - icon: pageDefaultIcon, - modelElements: ["imageInline"], - }, - ], - }, - toolbar: [ - "imageStyle:float-left", - "imageStyle:float-right", - "imageStyle:float-none", - "|", - "imageStyle:inline", - "|", - "linkImage", - "imageTextAlternative", - "contentImageOpenInTab", - ], - }, - table: { - contentToolbar: ["tableColumn", "tableRow", "mergeTableCells"], - }, - language: { - // Language switch only applies to editor instance. - ui: editorLanguage, - // Won't change the language of content. - content: "en", - }, - autosave: { - waitingTime: 1000, // in ms - save(currentEditor: Editor) { - console.log("Save triggered..."); - const start = performance.now(); - return saveData(currentEditor, "autosave").then(() => { - console.log(`Saved data within ${performance.now() - start} ms.`); - }); - }, - }, - [COREMEDIA_RICHTEXT_CONFIG_KEY]: { - // Defaults to: Loose - strictness: Strictness.STRICT, - // The Latest is the default. Use v10 for first data-processor architecture, - // for example. - // @ts-expect-error - TODO[cke] 37.x Fix Typings - compatibility: richTextCompatibility, - rules: richTextCompatibility === "v10" ? v10RichTextRuleConfigurations : richTextRuleConfigurations, - }, - [COREMEDIA_RICHTEXT_SUPPORT_CONFIG_KEY]: { - aliases: [ - // As we represent `` as ``, we must ensure, - // that the same attributes are kept as is from CMS. For example, the - // dir-attribute, which is valid for `` must not be removed just - // because CKEditor is not configured to handle it. - { name: "mark", inherit: "span" }, - ], - }, - [COREMEDIA_LINK_CONFIG_KEY]: { - linkBalloon: { - keepOpen: { - ids: ["example-to-keep-the-link-balloon-open-on-click", "inputExampleContentButton"], - classes: ["example-class-to-keep-the-link-balloon-open-on-click"], - }, - }, - }, - [COREMEDIA_MOCK_CONTENT_PLUGIN]: { - // Demonstrates, how you may add more contents on the fly. - contents: [{ id: 2, name: "Some Example Document", type: "document" }], - }, -}) - .then((newEditor: ClassicEditor) => { - CKEditorInspector.attach( - { - "main-editor": newEditor, - }, - { - // With hash parameter #expandInspector you may expand the - // inspector by default. - isCollapsed: !getHashParam("expandInspector"), - }, - ); - - (newEditor.plugins.get("Differencing") as Differencing)?.activateDifferencing(); - - initReadOnlyMode(newEditor); - initExamples(newEditor); - initInputExampleContent(newEditor); - - const undoCommand: Command | undefined = newEditor.commands.get("undo"); - - if (undoCommand) { - //@ts-expect-error Editor extension, no typing available. - // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-return - newEditor.resetUndo = () => undoCommand.clearStack(); - console.log("Registered `editor.resetUndo()` to clear undo history."); - } - - // Do it late, so that we also have a clear signal (e.g., to integration - // tests), that the editor is ready. - //@ts-expect-error Unknown, but we set it. - window.editor = newEditor; - console.log("Exposed editor instance as `editor`."); - - setExampleData(newEditor, "Welcome"); - // Initialize Preview - updatePreview(newEditor.getData()); - }) - .catch((error) => { - console.error(error); - }); diff --git a/app/src/editors/bbCode.ts b/app/src/editors/bbCode.ts new file mode 100644 index 0000000000..fe8d255c32 --- /dev/null +++ b/app/src/editors/bbCode.ts @@ -0,0 +1,66 @@ +import { Autosave } from "@ckeditor/ckeditor5-autosave"; +import { Bold, Italic } from "@ckeditor/ckeditor5-basic-styles"; +import { ClassicEditor } from "@ckeditor/ckeditor5-editor-classic"; +import { Essentials } from "@ckeditor/ckeditor5-essentials"; +import { Heading } from "@ckeditor/ckeditor5-heading"; +import { Paragraph } from "@ckeditor/ckeditor5-paragraph"; +import { SourceEditing } from "@ckeditor/ckeditor5-source-editing"; + +import { Editor } from "@ckeditor/ckeditor5-core"; +import { saveData } from "../dataFacade"; +import { getHashParam } from "../HashParams"; + +/** + * Typings for CKEditorInspector, as it does not ship with typings yet. + */ +// See https://github.com/ckeditor/ckeditor5-inspector/issues/173 +// eslint-disable-next-line @typescript-eslint/no-extraneous-class +declare class CKEditorInspector { + static attach(editorOrConfig: Editor | Record, options?: { isCollapsed?: boolean }): string[]; +} + +const editorElementSelector = "#bbcodeEditor"; + +export const createBBCodeEditor = (language = "en") => { + const sourceElement = document.querySelector(editorElementSelector) as HTMLElement; + if (!sourceElement) { + throw new Error(`No element with id ${editorElementSelector} defined in html. Nothing to create the editor in.`); + } + + ClassicEditor.create(document.querySelector(editorElementSelector) as HTMLElement, { + placeholder: "Type your text here...", + plugins: [Autosave, Bold, Essentials, Heading, Italic, Paragraph, SourceEditing], + toolbar: ["undo", "redo", "|", "heading", "|", "bold", "italic", "sourceEditing"], + language: { + // Language switch only applies to editor instance. + ui: language, + // Won't change the language of content. + content: "en", + }, + autosave: { + waitingTime: 1000, // in ms + save(currentEditor: Editor) { + console.log("BBCode Save triggered..."); + const start = performance.now(); + return saveData(currentEditor, "autosave").then(() => { + console.log(`Saved BBCode data within ${performance.now() - start} ms.`); + }); + }, + }, + }) + .then((newEditor: ClassicEditor) => { + CKEditorInspector.attach( + { + "bbcode-editor": newEditor, + }, + { + // With hash parameter #expandInspector you may expand the + // inspector by default. + isCollapsed: !getHashParam("expandInspector"), + } + ); + }) + .catch((error) => { + console.error(error); + }); +}; diff --git a/app/src/editors/default.ts b/app/src/editors/default.ts new file mode 100644 index 0000000000..93fa0f3328 --- /dev/null +++ b/app/src/editors/default.ts @@ -0,0 +1,406 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { Alignment } from "@ckeditor/ckeditor5-alignment"; +import { AutoLink, Link, LinkImage } from "@ckeditor/ckeditor5-link"; +import { Autoformat } from "@ckeditor/ckeditor5-autoformat"; +import { Autosave } from "@ckeditor/ckeditor5-autosave"; +import { BlockQuote } from "@ckeditor/ckeditor5-block-quote"; +import { Bold, Code, Italic, Strikethrough, Subscript, Superscript, Underline } from "@ckeditor/ckeditor5-basic-styles"; +import { ClassicEditor } from "@ckeditor/ckeditor5-editor-classic"; +import { CodeBlock } from "@ckeditor/ckeditor5-code-block"; +import { Essentials } from "@ckeditor/ckeditor5-essentials"; +import { FindAndReplace } from "@ckeditor/ckeditor5-find-and-replace"; +import { Heading } from "@ckeditor/ckeditor5-heading"; +// ImageInline: See ckeditor/ckeditor5#12027. +import ImageInline from "@ckeditor/ckeditor5-image/src/imageinline"; +// ImageBlockEditing: See ckeditor/ckeditor5#12027. +import ImageBlockEditing from "@ckeditor/ckeditor5-image/src/image/imageblockediting"; +import { ImageStyle, ImageTextAlternative, ImageToolbar } from "@ckeditor/ckeditor5-image"; +import { Indent } from "@ckeditor/ckeditor5-indent"; +import { DocumentList } from "@ckeditor/ckeditor5-list"; +import { Paragraph } from "@ckeditor/ckeditor5-paragraph"; +import { PasteFromOffice } from "@ckeditor/ckeditor5-paste-from-office"; +import { RemoveFormat } from "@ckeditor/ckeditor5-remove-format"; +import { SourceEditing } from "@ckeditor/ckeditor5-source-editing"; +import { Table, TableToolbar } from "@ckeditor/ckeditor5-table"; +import { Highlight } from "@ckeditor/ckeditor5-highlight"; + +import { LinkTarget, ContentLinks } from "@coremedia/ckeditor5-coremedia-link"; +import { ContentClipboard } from "@coremedia/ckeditor5-coremedia-content-clipboard"; +import { ContentImagePlugin } from "@coremedia/ckeditor5-coremedia-images"; +import { FontMapper as CoreMediaFontMapper } from "@coremedia/ckeditor5-font-mapper"; +import MockStudioIntegration from "@coremedia/ckeditor5-coremedia-studio-integration-mock/src/MockStudioIntegration"; + +import { updatePreview } from "../preview"; +import { initReadOnlyMode } from "../readOnlySupport"; +import { initExamples, setExampleData } from "../example-data"; +import { + CoreMediaStudioEssentials, + COREMEDIA_RICHTEXT_CONFIG_KEY, + COREMEDIA_RICHTEXT_SUPPORT_CONFIG_KEY, + Strictness, +} from "@coremedia/ckeditor5-coremedia-studio-essentials"; +import { initInputExampleContent } from "../inputExampleContents"; +import { COREMEDIA_MOCK_CONTENT_PLUGIN } from "@coremedia/ckeditor5-coremedia-studio-integration-mock/src/content/MockContentPlugin"; + +import { Command, Editor, icons, PluginConstructor } from "@ckeditor/ckeditor5-core"; +import { saveData } from "../dataFacade"; +import MockInputExamplePlugin from "@coremedia/ckeditor5-coremedia-studio-integration-mock/src/content/MockInputExamplePlugin"; +import PasteContentPlugin from "@coremedia/ckeditor5-coremedia-content-clipboard/src/paste/PasteContentPlugin"; +import { RuleConfig } from "@coremedia/ckeditor5-dom-converter/src/Rule"; +import { replaceElementByElementAndClass } from "@coremedia/ckeditor5-coremedia-richtext/src/rules/ReplaceElementByElementAndClass"; +import { FilterRuleSetConfiguration } from "@coremedia/ckeditor5-dataprocessor-support/src/Rules"; +import { replaceByElementAndClassBackAndForth } from "@coremedia/ckeditor5-coremedia-richtext/src/compatibility/v10/rules/ReplaceBy"; +import { getHashParam } from "../HashParams"; +import { COREMEDIA_LINK_CONFIG_KEY } from "@coremedia/ckeditor5-coremedia-link/src/contentlink/LinkBalloonConfig"; +import { LinkAttributesConfig } from "@coremedia/ckeditor5-link-common/src/LinkAttributesConfig"; +import { LinkAttributes } from "@coremedia/ckeditor5-link-common/src/LinkAttributes"; +import { Differencing } from "@coremedia/ckeditor5-coremedia-differencing"; +/** + * Typings for CKEditorInspector, as it does not ship with typings yet. + */ +// See https://github.com/ckeditor/ckeditor5-inspector/issues/173 +// eslint-disable-next-line @typescript-eslint/no-extraneous-class +declare class CKEditorInspector { + static attach(editorOrConfig: Editor | Record, options?: { isCollapsed?: boolean }): string[]; +} + +const { + objectInline: withinTextIcon, + objectLeft: alignLeftIcon, + objectRight: alignRightIcon, + objectSizeFull: pageDefaultIcon, +} = icons; + +const imagePlugins: PluginConstructor[] = [ + ContentImagePlugin, + ImageInline, + ImageBlockEditing, + ImageStyle, + ImageToolbar, + ImageTextAlternative, +]; + +/** + * You may switch the compatibility, for example, by providing + * `compatibility=v10`. + */ +const richTextCompatibility = getHashParam("compatibility") || "latest"; + +/** + * Apply custom mapping rules. + */ +const richTextRuleConfigurations: RuleConfig[] = [ + // Highlight plugin support. + replaceElementByElementAndClass({ + viewLocalName: "mark", + dataLocalName: "span", + // "mark" is the default here, derived from `viewLocalName`. Thus, + // we may skip it here. + dataReservedClass: "mark", + }), +]; + +/** + * v10 compatible configuration. + */ +const v10RichTextRuleConfigurations: FilterRuleSetConfiguration = { + elements: { + // Highlight Plugin Support + mark: replaceByElementAndClassBackAndForth("mark", "span", "mark"), + }, +}; + +/** + * Configuration that holds all link-related attributes, that are not + * covered yet by any plugin. + * + * Similar to GHS/GRS, they are just registered as being _valid_ **and** + * (this is important) register them to belong to a link element, which again + * ensures that they are removed on remove-link, that cursor positioning + * handles them correctly, etc. + * + * For demonstration purpose, the link attributes configuration can be disabled + * via hash parameter `skipLinkAttributes`. + */ +const linkAttributesConfig: LinkAttributesConfig = getHashParam("skipLinkAttributes") + ? { attributes: [] } + : { + attributes: [ + { view: "title", model: "linkTitle" }, + { view: "data-xlink-actuate", model: "linkActuate" }, + ], + }; + +const editorElementSelector = "#editor"; + +export const createDefaultEditor = (language = "en") => { + const sourceElement = document.querySelector(editorElementSelector) as HTMLElement; + if (!sourceElement) { + throw new Error(`No element with id ${editorElementSelector} defined in html. Nothing to create the editor in.`); + } + + ClassicEditor.create(sourceElement, { + placeholder: "Type your text here...", + plugins: [ + ...imagePlugins, + Alignment, + Autoformat, + Autosave, + BlockQuote, + Bold, + Code, + CodeBlock, + ContentLinks, + ContentClipboard, + Differencing, + Essentials, + FindAndReplace, + Heading, + Highlight, + Indent, + Italic, + AutoLink, + Link, + LinkAttributes, + LinkImage, + LinkTarget, + CoreMediaStudioEssentials, + DocumentList, + Paragraph, + PasteContentPlugin, + PasteFromOffice, + RemoveFormat, + Strikethrough, + SourceEditing, + Subscript, + Superscript, + Table, + TableToolbar, + Underline, + CoreMediaFontMapper, + MockInputExamplePlugin, + MockStudioIntegration, + ], + toolbar: [ + "undo", + "redo", + "|", + "heading", + "|", + "bold", + "italic", + "underline", + { + label: "More formatting", + icon: "threeVerticalDots", + items: ["strikethrough", "subscript", "superscript", "code"], + }, + "highlight", + "removeFormat", + "|", + "link", + "|", + "alignment", + "blockQuote", + "codeBlock", + "|", + "insertTable", + "|", + "numberedList", + "bulletedList", + "outdent", + "indent", + "|", + "pasteContent", + "findAndReplace", + "|", + "sourceEditing", + ], + alignment: { + // The following alternative to signal alignment was used in CKEditor 4 + // of CoreMedia CMCC 10 and before. + // Note that in contrast to CKEditor 4 approach, these classes are now + // applicable to any block element, while it supported only `

` in the + // past. + options: [ + { + name: "left", + className: "align--left", + }, + { + name: "right", + className: "align--right", + }, + { + name: "center", + className: "align--center", + }, + { + name: "justify", + className: "align--justify", + }, + ], + }, + + heading: { + options: [ + { model: "paragraph", title: "Paragraph", class: "ck-heading_paragraph" }, + { model: "heading1", view: "h1", title: "Heading 1", class: "ck-heading_heading1" }, + { model: "heading2", view: "h2", title: "Heading 2", class: "ck-heading_heading2" }, + { model: "heading3", view: "h3", title: "Heading 3", class: "ck-heading_heading3" }, + { model: "heading4", view: "h4", title: "Heading 4", class: "ck-heading_heading4" }, + { model: "heading5", view: "h5", title: "Heading 5", class: "ck-heading_heading5" }, + { model: "heading6", view: "h6", title: "Heading 6", class: "ck-heading_heading6" }, + ], + }, + link: { + defaultProtocol: "https://", + ...linkAttributesConfig, + /*decorators: { + hasTitle: { + mode: "manual", + label: "Title", + attributes: { + title: + 'Example how standard-decorators of the link-plugin works. To enable/disable, just rename the decorators section to "disabled_decorators" and back again to "decorators" to activate it and see the results.', + }, + }, + },*/ + }, + image: { + styles: { + // Defining custom styling options for the images. + options: [ + { + name: "float-left", + icon: alignLeftIcon, + title: "Left-aligned", + className: "float--left", + modelElements: ["imageInline"], + }, + { + name: "float-right", + icon: alignRightIcon, + title: "Right-aligned", + className: "float--right", + modelElements: ["imageInline"], + }, + { + name: "float-none", + icon: withinTextIcon, + title: "Within Text", + className: "float--none", + modelElements: ["imageInline"], + }, + { + name: "inline", + title: "Page default", + icon: pageDefaultIcon, + modelElements: ["imageInline"], + }, + ], + }, + toolbar: [ + "imageStyle:float-left", + "imageStyle:float-right", + "imageStyle:float-none", + "|", + "imageStyle:inline", + "|", + "linkImage", + "imageTextAlternative", + "contentImageOpenInTab", + ], + }, + table: { + contentToolbar: ["tableColumn", "tableRow", "mergeTableCells"], + }, + language: { + // Language switch only applies to editor instance. + ui: language, + // Won't change the language of content. + content: "en", + }, + autosave: { + waitingTime: 1000, // in ms + save(currentEditor: Editor) { + console.log("Save triggered..."); + const start = performance.now(); + return saveData(currentEditor, "autosave").then(() => { + console.log(`Saved data within ${performance.now() - start} ms.`); + }); + }, + }, + [COREMEDIA_RICHTEXT_CONFIG_KEY]: { + // Defaults to: Loose + strictness: Strictness.STRICT, + // The Latest is the default. Use v10 for first data-processor architecture, + // for example. + // @ts-expect-error - TODO[cke] 37.x Fix Typings + compatibility: richTextCompatibility, + //@ts-expect-error the types do not match here rules may not be RuleConfig[] TODO + rules: richTextCompatibility === "v10" ? v10RichTextRuleConfigurations : richTextRuleConfigurations, + }, + [COREMEDIA_RICHTEXT_SUPPORT_CONFIG_KEY]: { + aliases: [ + // As we represent `` as ``, we must ensure, + // that the same attributes are kept as is from CMS. For example, the + // dir-attribute, which is valid for `` must not be removed just + // because CKEditor is not configured to handle it. + { name: "mark", inherit: "span" }, + ], + }, + [COREMEDIA_LINK_CONFIG_KEY]: { + linkBalloon: { + keepOpen: { + ids: ["example-to-keep-the-link-balloon-open-on-click", "inputExampleContentButton"], + classes: ["example-class-to-keep-the-link-balloon-open-on-click"], + }, + }, + }, + [COREMEDIA_MOCK_CONTENT_PLUGIN]: { + // Demonstrates, how you may add more contents on the fly. + contents: [{ id: 2, name: "Some Example Document", type: "document" }], + }, + }) + .then((newEditor: ClassicEditor) => { + CKEditorInspector.attach( + { + "main-editor": newEditor, + }, + { + // With hash parameter #expandInspector you may expand the + // inspector by default. + isCollapsed: !getHashParam("expandInspector"), + } + ); + + (newEditor.plugins.get("Differencing") as Differencing)?.activateDifferencing(); + + initReadOnlyMode(newEditor); + initExamples(newEditor); + initInputExampleContent(newEditor); + + const undoCommand: Command | undefined = newEditor.commands.get("undo"); + + if (undoCommand) { + //@ts-expect-error Editor extension, no typing available. + // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-return + newEditor.resetUndo = () => undoCommand.clearStack(); + console.log("Registered `editor.resetUndo()` to clear undo history."); + } + + // Do it late, so that we also have a clear signal (e.g., to integration + // tests), that the editor is ready. + //@ts-expect-error Unknown, but we set it. + window.editor = newEditor; + console.log("Exposed editor instance as `editor`."); + + setExampleData(newEditor, "Welcome"); + // Initialize Preview + updatePreview(newEditor.getData()); + }) + .catch((error) => { + console.error(error); + }); +}; diff --git a/app/src/index.ts b/app/src/index.ts new file mode 100644 index 0000000000..7049cfdfb2 --- /dev/null +++ b/app/src/index.ts @@ -0,0 +1,43 @@ +import { setupPreview } from "./preview"; +import { createDefaultEditor } from "./editors/default"; +import { createBBCodeEditor } from "./editors/bbCode"; + +// setup input example content IFrame +const showHideExampleContentButton = document.querySelector("#inputExampleContentButton"); +const inputExampleContentFrame = document.querySelector("#inputExampleContentDiv") as HTMLDivElement; +if (showHideExampleContentButton && inputExampleContentFrame) { + showHideExampleContentButton.addEventListener("click", () => { + inputExampleContentFrame.hidden = !inputExampleContentFrame.hidden; + showHideExampleContentButton.textContent = `${ + inputExampleContentFrame.hidden ? "Show" : "Hide" + } input example contents`; + }); +} + +const initLanguage = () => { + const queryString = window.location.search; + const urlParams = new URLSearchParams(queryString); + const languageFlag = "lang"; + const language = urlParams.get(languageFlag)?.toLowerCase() ?? "en"; + const languageToggle = document.getElementById(languageFlag); + if (!languageToggle) { + throw Error("No language toggle element found."); + } + let label, hrefLang; + if (language === "de") { + label = "EN | DE"; + hrefLang = "en"; + } else { + label = "EN | DE"; + hrefLang = "de"; + } + languageToggle.setAttribute("href", `.?lang=${hrefLang}`); + languageToggle.innerHTML = label; + return language; +}; + +const lang = initLanguage(); + +setupPreview(); +createDefaultEditor(lang); +createBBCodeEditor(lang); diff --git a/app/webpack.config.js b/app/webpack.config.js index cfff25de4d..214382ed45 100644 --- a/app/webpack.config.js +++ b/app/webpack.config.js @@ -13,7 +13,7 @@ module.exports = { devtool: "source-map", performance: { hints: false }, - entry: path.resolve(__dirname, "src", "ckeditor.ts"), + entry: path.resolve(__dirname, "src", "index.ts"), output: { // The name under which the editor will be exported. From fdf42460f3ab740c06f86684d4be553cf7de159a Mon Sep 17 00:00:00 2001 From: pkliesch Date: Tue, 8 Aug 2023 16:03:21 +0200 Subject: [PATCH 002/403] feature(bbcode-dataprocessor): Add tabs to switch between editors --- app/sample/index.html | 10 ++++++++-- app/sample/styles.css | 25 +++++++++++++++++++++++++ app/src/index.ts | 41 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/app/sample/index.html b/app/sample/index.html index d1468e2bc1..8eb020c50c 100644 --- a/app/sample/index.html +++ b/app/sample/index.html @@ -61,10 +61,16 @@

CKEditor 5: CoreMedia Plugin Showcase

+ -
+ +
+ + +
+
@@ -72,7 +78,7 @@

CKEditor 5: CoreMedia Plugin Showcase

-
+
diff --git a/app/sample/styles.css b/app/sample/styles.css index 92cb4ecd09..bc192100b1 100644 --- a/app/sample/styles.css +++ b/app/sample/styles.css @@ -360,3 +360,28 @@ span.image-inline[xdiff\:changetype="diff-conflict-image"] > img, span.html-obje font-size: 1.5em; } } + + +.editor-tab { + padding: 10px 20px; + background-color: white; + color: #333; + border: 1px solid var(--ck-color-base-border); + margin-top: 16px; + margin-bottom: 4px; + opacity: 0.5; +} + +.editor-tab:hover { + opacity: 0.3; +} + +.editor-tab.active { + opacity: 1; +} + +.editor-tab.active:hover { + opacity: 0.8; +} + + diff --git a/app/src/index.ts b/app/src/index.ts index 7049cfdfb2..e5443bb839 100644 --- a/app/src/index.ts +++ b/app/src/index.ts @@ -36,6 +36,47 @@ const initLanguage = () => { return language; }; +const initToggleEditorTabs = () => { + const hideAllEditorRows = () => { + const rows = document.getElementsByClassName("editor-row"); + Array.from(rows).forEach((rowEl) => { + const divElement = rowEl as HTMLDivElement; + if (divElement) { + divElement.style.display = "none"; + } + }); + + const tabs = document.getElementsByClassName("editor-tab"); + Array.from(tabs).forEach((tabsEl) => { + const divElement = tabsEl as HTMLDivElement; + if (divElement) { + divElement.classList.remove("active"); + } + }); + }; + + const initToggleEditorTab = (buttonSelector: string, editorRowSelector: string) => { + const editorTab = document.querySelector(buttonSelector) as HTMLButtonElement; + editorTab.addEventListener("click", () => { + const editorRow = document.querySelector(editorRowSelector) as HTMLDivElement; + hideAllEditorRows(); + editorTab.classList.add("active"); + editorRow.style.display = "block"; + }); + }; + + initToggleEditorTab("#defaultEditorTab", "#defaultEditorRow"); + initToggleEditorTab("#bbcodeEditorTab", "#bbcodeEditorRow"); + + hideAllEditorRows(); + const defaultEditorRow = document.querySelector("#defaultEditorRow") as HTMLDivElement; + const defaultEditorTab = document.querySelector("#defaultEditorTab") as HTMLButtonElement; + defaultEditorTab.classList.add("active"); + defaultEditorRow.style.display = "block"; +}; + +initToggleEditorTabs(); + const lang = initLanguage(); setupPreview(); From 1c0be4ae966447d77f892c564cb235ffb462977a Mon Sep 17 00:00:00 2001 From: pkliesch Date: Tue, 15 Aug 2023 11:47:16 +0200 Subject: [PATCH 003/403] feature(bbcode-dataprocessor): Introduce coremedia-bbcode plugin Plugin to convert BBCode to HTML for CKEditor data view (and vice versa). This is just the initial commit and still work in progress. --- .../ckeditor5-coremedia-bbcode/.eslintrc.js | 12 +++ .../ckeditor5-coremedia-bbcode/jest.config.js | 3 + .../ckeditor5-coremedia-bbcode/package.json | 50 +++++++++++++ .../ckeditor5-coremedia-bbcode/src/BBCode.ts | 18 +++++ .../src/BBCodeDataProcessor.ts | 74 +++++++++++++++++++ .../src/augmentation.ts | 7 ++ .../src/bbcode2html/bbcode2html.ts | 9 +++ .../src/bbcode2html/index-doc.ts | 8 ++ .../src/html2bbcode/html2bbcode.ts | 6 ++ .../src/html2bbcode/index-doc.ts | 8 ++ .../src/index-doc.ts | 14 ++++ .../ckeditor5-coremedia-bbcode/src/index.ts | 6 ++ .../ckeditor5-coremedia-bbcode/src/tsdoc.json | 4 + .../ckeditor5-coremedia-bbcode/tsconfig.json | 7 ++ .../tsconfig.release.json | 6 ++ .../ckeditor5-coremedia-bbcode/typedoc.json | 5 ++ 16 files changed, 237 insertions(+) create mode 100644 packages/ckeditor5-coremedia-bbcode/.eslintrc.js create mode 100644 packages/ckeditor5-coremedia-bbcode/jest.config.js create mode 100644 packages/ckeditor5-coremedia-bbcode/package.json create mode 100644 packages/ckeditor5-coremedia-bbcode/src/BBCode.ts create mode 100644 packages/ckeditor5-coremedia-bbcode/src/BBCodeDataProcessor.ts create mode 100644 packages/ckeditor5-coremedia-bbcode/src/augmentation.ts create mode 100644 packages/ckeditor5-coremedia-bbcode/src/bbcode2html/bbcode2html.ts create mode 100644 packages/ckeditor5-coremedia-bbcode/src/bbcode2html/index-doc.ts create mode 100644 packages/ckeditor5-coremedia-bbcode/src/html2bbcode/html2bbcode.ts create mode 100644 packages/ckeditor5-coremedia-bbcode/src/html2bbcode/index-doc.ts create mode 100644 packages/ckeditor5-coremedia-bbcode/src/index-doc.ts create mode 100644 packages/ckeditor5-coremedia-bbcode/src/index.ts create mode 100644 packages/ckeditor5-coremedia-bbcode/src/tsdoc.json create mode 100644 packages/ckeditor5-coremedia-bbcode/tsconfig.json create mode 100644 packages/ckeditor5-coremedia-bbcode/tsconfig.release.json create mode 100644 packages/ckeditor5-coremedia-bbcode/typedoc.json diff --git a/packages/ckeditor5-coremedia-bbcode/.eslintrc.js b/packages/ckeditor5-coremedia-bbcode/.eslintrc.js new file mode 100644 index 0000000000..c2deb6f08f --- /dev/null +++ b/packages/ckeditor5-coremedia-bbcode/.eslintrc.js @@ -0,0 +1,12 @@ +module.exports = { + parser: "@typescript-eslint/parser", + overrides: [ + { + files: ["**/*.ts", "**/*.tsx"], + parserOptions: { + tsconfigRootDir: __dirname, + project: "./tsconfig.json", + }, + }, + ], +}; diff --git a/packages/ckeditor5-coremedia-bbcode/jest.config.js b/packages/ckeditor5-coremedia-bbcode/jest.config.js new file mode 100644 index 0000000000..208f377fa0 --- /dev/null +++ b/packages/ckeditor5-coremedia-bbcode/jest.config.js @@ -0,0 +1,3 @@ +const jestConfig = require("@coremedia-internal/ckeditor5-jest-test-helpers/shared-jest.config.js"); + +module.exports = { ...jestConfig }; diff --git a/packages/ckeditor5-coremedia-bbcode/package.json b/packages/ckeditor5-coremedia-bbcode/package.json new file mode 100644 index 0000000000..310ce7f769 --- /dev/null +++ b/packages/ckeditor5-coremedia-bbcode/package.json @@ -0,0 +1,50 @@ +{ + "name": "@coremedia/ckeditor5-coremedia-bbcode", + "version": "15.0.1", + "description": "BBCode Data-Processor", + "keywords": [ + "coremedia", + "ckeditor", + "ckeditor5", + "bbcode", + "dataprocessor" + ], + "engines": { + "node": "18", + "pnpm": "^8.1" + }, + "license": "Apache-2.0", + "scripts": { + "build": "tsc --project ./tsconfig.release.json", + "clean": "pnpm clean:src && pnpm clean:dist", + "clean:src": "rimraf --glob \"src/**/*.@(js|js.map|d.ts|d.ts.map)\"", + "clean:dist": "rimraf ./dist", + "jest": "jest", + "jest:coverage": "jest --collect-coverage", + "npm-check-updates": "npm-check-updates --upgrade" + }, + "devDependencies": { + "@ckeditor/ckeditor5-core": "^37.1.0", + "@ckeditor/ckeditor5-engine": "^37.1.0", + "@types/jest": "^29.5.1", + "jest": "^29.5.0", + "jest-each": "^29.5.0", + "jest-xml-matcher": "^1.2.0", + "rimraf": "^5.0.0", + "typescript": "^4.9.5" + }, + "main": "./src/index.ts", + "publishConfig": { + "main": "./src/index.js", + "types": "./src/index.d.ts" + }, + "peerDependencies": { + "@ckeditor/ckeditor5-core": "^37.0.1", + "@ckeditor/ckeditor5-engine": "^37.0.1" + }, + "dependencies": { + "@bbob/html": "^3.0.0", + "@bbob/preset-html5": "^3.0.0", + "@coremedia/ckeditor5-core-common": "^15.0.1" + } +} diff --git a/packages/ckeditor5-coremedia-bbcode/src/BBCode.ts b/packages/ckeditor5-coremedia-bbcode/src/BBCode.ts new file mode 100644 index 0000000000..528a7e0824 --- /dev/null +++ b/packages/ckeditor5-coremedia-bbcode/src/BBCode.ts @@ -0,0 +1,18 @@ +import { Plugin } from "@ckeditor/ckeditor5-core"; +import { reportInitEnd, reportInitStart } from "@coremedia/ckeditor5-core-common"; +import BBCodeDataProcessor from "./BBCodeDataProcessor"; + +/** + * Applies a data-processor for BBCode. + */ +export default class BBCode extends Plugin { + public static readonly pluginName = "BBCodeDataProcessor"; + + init(): void { + const initInformation = reportInitStart(this); + + this.editor.data.processor = new BBCodeDataProcessor(this.editor.data.viewDocument); + + reportInitEnd(initInformation); + } +} diff --git a/packages/ckeditor5-coremedia-bbcode/src/BBCodeDataProcessor.ts b/packages/ckeditor5-coremedia-bbcode/src/BBCodeDataProcessor.ts new file mode 100644 index 0000000000..c8eef69683 --- /dev/null +++ b/packages/ckeditor5-coremedia-bbcode/src/BBCodeDataProcessor.ts @@ -0,0 +1,74 @@ +import { + DataProcessor, + HtmlDataProcessor, + MatcherPattern, + ViewDocument, + ViewDocumentFragment, +} from "@ckeditor/ckeditor5-engine"; + +import { bbcode2html } from "./bbcode2html/bbcode2html"; +import { html2bbcode } from "./html2bbcode/html2bbcode"; + +/** + * Data processor for BBCode. + * This data processor converts BBCode to HTML and uses the HtmlDataProcessor + * to generate the resulting view tree. + */ +export default class BBCodeDataProcessor implements DataProcessor { + /** + * HTML data processor used to process HTML produced by the third-party @bbob/html converter. + */ + readonly #htmlDataProcessor: HtmlDataProcessor; + + /** + * Creates a new instance of the BBCode data processor class. + */ + constructor(document: ViewDocument) { + this.#htmlDataProcessor = new HtmlDataProcessor(document); + } + + /** + * Converts the provided BBCode string to a data view tree. + * + * @param data - The BBCode string. + * @returns The converted view element. + */ + public toView(data: string): ViewDocumentFragment { + const html = bbcode2html(data); + return this.#htmlDataProcessor.toView(html); + } + + /** + * Converts the provided {@link module:engine/view/documentfragment~DocumentFragment} to data format — in this + * case to a BBCode string. + * + * @param viewFragment - The viewFragment. + * @returns BBCode string. + */ + public toData(viewFragment: ViewDocumentFragment): string { + const html = this.#htmlDataProcessor.toData(viewFragment); + return html2bbcode(html); + } + + /** + * Registers a {@link module:engine/view/matcher~MatcherPattern} for view elements whose content should be treated as raw data + * and not processed during the conversion from Markdown to view elements. + * + * The raw data can be later accessed by a + * {@link module:engine/view/element~Element#getCustomProperty custom property of a view element} called `"$rawContent"`. + * + * @param pattern - The pattern matching all view elements whose content should + * be treated as raw data. + */ + public registerRawContentMatcher(pattern: MatcherPattern): void { + this.#htmlDataProcessor.registerRawContentMatcher(pattern); + } + + /** + * This method does not have any effect on the data processor result. It exists for compatibility with the + * {@link module:engine/dataprocessor/dataprocessor~DataProcessor `DataProcessor` interface}. + */ + public useFillerType(): void { + this.#htmlDataProcessor.useFillerType("default"); + } +} diff --git a/packages/ckeditor5-coremedia-bbcode/src/augmentation.ts b/packages/ckeditor5-coremedia-bbcode/src/augmentation.ts new file mode 100644 index 0000000000..0761149a77 --- /dev/null +++ b/packages/ckeditor5-coremedia-bbcode/src/augmentation.ts @@ -0,0 +1,7 @@ +import { BBCode } from "./index"; + +declare module "@ckeditor/ckeditor5-core" { + interface PluginsMap { + [BBCode.pluginName]: BBCode; + } +} diff --git a/packages/ckeditor5-coremedia-bbcode/src/bbcode2html/bbcode2html.ts b/packages/ckeditor5-coremedia-bbcode/src/bbcode2html/bbcode2html.ts new file mode 100644 index 0000000000..db89db94cc --- /dev/null +++ b/packages/ckeditor5-coremedia-bbcode/src/bbcode2html/bbcode2html.ts @@ -0,0 +1,9 @@ +import bbobHTML from "@bbob/html/es"; +import presetHTML5 from "@bbob/preset-html5"; + +/** + * Parses BBCode to HTML. + */ +export const bbcode2html = (bbcode: string): string => { + return bbobHTML(bbcode, presetHTML5()); +}; diff --git a/packages/ckeditor5-coremedia-bbcode/src/bbcode2html/index-doc.ts b/packages/ckeditor5-coremedia-bbcode/src/bbcode2html/index-doc.ts new file mode 100644 index 0000000000..ee0ac7b12c --- /dev/null +++ b/packages/ckeditor5-coremedia-bbcode/src/bbcode2html/index-doc.ts @@ -0,0 +1,8 @@ +/** + * BBCode to HTML conversion. + * + * @packageDocumentation + * @category Virtual + */ + +export * from "./bbcode2html"; diff --git a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/html2bbcode.ts b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/html2bbcode.ts new file mode 100644 index 0000000000..2a83b95e8b --- /dev/null +++ b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/html2bbcode.ts @@ -0,0 +1,6 @@ +/** + * Parses HTML to BBCode. + */ +export const html2bbcode = (html: string): string => { + return html; // TODO implement +}; diff --git a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/index-doc.ts b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/index-doc.ts new file mode 100644 index 0000000000..efab6f42ea --- /dev/null +++ b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/index-doc.ts @@ -0,0 +1,8 @@ +/** + * HTML to BBCode conversion. + * + * @packageDocumentation + * @category Virtual + */ + +export * from "./html2bbcode"; diff --git a/packages/ckeditor5-coremedia-bbcode/src/index-doc.ts b/packages/ckeditor5-coremedia-bbcode/src/index-doc.ts new file mode 100644 index 0000000000..cf379e1f3b --- /dev/null +++ b/packages/ckeditor5-coremedia-bbcode/src/index-doc.ts @@ -0,0 +1,14 @@ +/** + * This plugin grants conversion of BBCode to the CKEditor data model + * as well as conversion from CKEditor data model to BBCode. + * + * @module ckeditor5-coremedia-bbcode + */ + +export * from "./BBCode"; +export { default as BBCode } from "./BBCode"; + +export { default as BBCodeDataProcessor } from "./BBCodeDataProcessor"; + +export * as bbcode2html from "./bbcode2html/index-doc"; +export * as html2bbcode from "./html2bbcode/index-doc"; diff --git a/packages/ckeditor5-coremedia-bbcode/src/index.ts b/packages/ckeditor5-coremedia-bbcode/src/index.ts new file mode 100644 index 0000000000..25ca36d41b --- /dev/null +++ b/packages/ckeditor5-coremedia-bbcode/src/index.ts @@ -0,0 +1,6 @@ +/** + * @module ckeditor5-coremedia-bbcode + */ + +export { default as BBCode } from "./BBCode"; +import "./augmentation"; diff --git a/packages/ckeditor5-coremedia-bbcode/src/tsdoc.json b/packages/ckeditor5-coremedia-bbcode/src/tsdoc.json new file mode 100644 index 0000000000..c012e96209 --- /dev/null +++ b/packages/ckeditor5-coremedia-bbcode/src/tsdoc.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", + "extends": ["../../../tsdoc-typedoc.json"] +} diff --git a/packages/ckeditor5-coremedia-bbcode/tsconfig.json b/packages/ckeditor5-coremedia-bbcode/tsconfig.json new file mode 100644 index 0000000000..1b6674928a --- /dev/null +++ b/packages/ckeditor5-coremedia-bbcode/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "include": [ + "./__tests__", + "./src", + ] +} diff --git a/packages/ckeditor5-coremedia-bbcode/tsconfig.release.json b/packages/ckeditor5-coremedia-bbcode/tsconfig.release.json new file mode 100644 index 0000000000..5b48a1de22 --- /dev/null +++ b/packages/ckeditor5-coremedia-bbcode/tsconfig.release.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.json", + "exclude": [ + "./__tests__/", + ] +} diff --git a/packages/ckeditor5-coremedia-bbcode/typedoc.json b/packages/ckeditor5-coremedia-bbcode/typedoc.json new file mode 100644 index 0000000000..7813cb8d87 --- /dev/null +++ b/packages/ckeditor5-coremedia-bbcode/typedoc.json @@ -0,0 +1,5 @@ +{ + "extends": ["../../typedoc.base.json"], + "entryPoints": ["./src/index-doc.ts"], + "name": "ckeditor5-coremedia-bbcode" +} From f48a2c6088b042cd1ea70d67d52668faa851d154 Mon Sep 17 00:00:00 2001 From: pkliesch Date: Tue, 15 Aug 2023 11:48:01 +0200 Subject: [PATCH 004/403] feature(bbcode-dataprocessor): Use coremedia-bbcode plugin in example app bbcode editor --- app/package.json | 1 + app/src/editors/bbCode.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/package.json b/app/package.json index bc8e6b9e69..2dc9401cd6 100644 --- a/app/package.json +++ b/app/package.json @@ -37,6 +37,7 @@ "@ckeditor/ckeditor5-ui": "39.0.2", "@ckeditor/ckeditor5-utils": "39.0.2", "@coremedia-internal/ckeditor5-coremedia-example-data": "^1.0.0", + "@coremedia/ckeditor5-coremedia-bbcode": "16.0.1-rc.2", "@coremedia/ckeditor5-coremedia-blocklist": "16.0.1-rc.2", "@coremedia/ckeditor5-coremedia-content-clipboard": "16.0.1-rc.2", "@coremedia/ckeditor5-coremedia-differencing": "16.0.1-rc.2", diff --git a/app/src/editors/bbCode.ts b/app/src/editors/bbCode.ts index fe8d255c32..3877bd2787 100644 --- a/app/src/editors/bbCode.ts +++ b/app/src/editors/bbCode.ts @@ -2,6 +2,7 @@ import { Autosave } from "@ckeditor/ckeditor5-autosave"; import { Bold, Italic } from "@ckeditor/ckeditor5-basic-styles"; import { ClassicEditor } from "@ckeditor/ckeditor5-editor-classic"; import { Essentials } from "@ckeditor/ckeditor5-essentials"; +import { BBCode } from "@coremedia/ckeditor5-coremedia-bbcode"; import { Heading } from "@ckeditor/ckeditor5-heading"; import { Paragraph } from "@ckeditor/ckeditor5-paragraph"; import { SourceEditing } from "@ckeditor/ckeditor5-source-editing"; @@ -29,7 +30,7 @@ export const createBBCodeEditor = (language = "en") => { ClassicEditor.create(document.querySelector(editorElementSelector) as HTMLElement, { placeholder: "Type your text here...", - plugins: [Autosave, Bold, Essentials, Heading, Italic, Paragraph, SourceEditing], + plugins: [Autosave, Bold, Essentials, Heading, Italic, Paragraph, SourceEditing, BBCode], toolbar: ["undo", "redo", "|", "heading", "|", "bold", "italic", "sourceEditing"], language: { // Language switch only applies to editor instance. From 480d3b41b719e642acd1e1890fffcf1eb288bb3e Mon Sep 17 00:00:00 2001 From: pkliesch Date: Wed, 23 Aug 2023 08:01:28 +0200 Subject: [PATCH 005/403] feature(bbcode-dataprocessor): Remove logging + preview from bbcode editor autosave --- app/src/editors/bbCode.ts | 8 +- .../src/BBCodeDataProcessor.ts | 34 +++++++- .../src/html2bbcode/html2bbcode.ts | 77 ++++++++++++++++++- .../src/html2bbcode/rules/Bold.ts | 16 ++++ .../src/html2bbcode/rules/DefaultRules.ts | 10 +++ .../src/html2bbcode/rules/Hyperlink.ts | 16 ++++ .../src/html2bbcode/rules/Italic.ts | 16 ++++ .../src/html2bbcode/rules/Underline.ts | 16 ++++ 8 files changed, 182 insertions(+), 11 deletions(-) create mode 100644 packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Bold.ts create mode 100644 packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/DefaultRules.ts create mode 100644 packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Hyperlink.ts create mode 100644 packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Italic.ts create mode 100644 packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Underline.ts diff --git a/app/src/editors/bbCode.ts b/app/src/editors/bbCode.ts index 3877bd2787..d35f42a3a4 100644 --- a/app/src/editors/bbCode.ts +++ b/app/src/editors/bbCode.ts @@ -8,7 +8,6 @@ import { Paragraph } from "@ckeditor/ckeditor5-paragraph"; import { SourceEditing } from "@ckeditor/ckeditor5-source-editing"; import { Editor } from "@ckeditor/ckeditor5-core"; -import { saveData } from "../dataFacade"; import { getHashParam } from "../HashParams"; /** @@ -40,12 +39,9 @@ export const createBBCodeEditor = (language = "en") => { }, autosave: { waitingTime: 1000, // in ms - save(currentEditor: Editor) { + save() { console.log("BBCode Save triggered..."); - const start = performance.now(); - return saveData(currentEditor, "autosave").then(() => { - console.log(`Saved BBCode data within ${performance.now() - start} ms.`); - }); + return Promise.resolve(); }, }, }) diff --git a/packages/ckeditor5-coremedia-bbcode/src/BBCodeDataProcessor.ts b/packages/ckeditor5-coremedia-bbcode/src/BBCodeDataProcessor.ts index c8eef69683..fe7bee93f9 100644 --- a/packages/ckeditor5-coremedia-bbcode/src/BBCodeDataProcessor.ts +++ b/packages/ckeditor5-coremedia-bbcode/src/BBCodeDataProcessor.ts @@ -1,5 +1,5 @@ import { - DataProcessor, + DataProcessor, DomConverter, HtmlDataProcessor, MatcherPattern, ViewDocument, @@ -8,6 +8,8 @@ import { import { bbcode2html } from "./bbcode2html/bbcode2html"; import { html2bbcode } from "./html2bbcode/html2bbcode"; +import { defaultRules, HTML2BBCodeRule } from "./html2bbcode/rules/DefaultRules"; +import BasicHtmlWriter from "@ckeditor/ckeditor5-engine/src/dataprocessor/basichtmlwriter"; /** * Data processor for BBCode. @@ -20,11 +22,20 @@ export default class BBCodeDataProcessor implements DataProcessor { */ readonly #htmlDataProcessor: HtmlDataProcessor; + readonly #domConverter: DomConverter; + + #toDataRules: HTML2BBCodeRule[]; + /** * Creates a new instance of the BBCode data processor class. */ constructor(document: ViewDocument) { this.#htmlDataProcessor = new HtmlDataProcessor(document); + // Remember and re-use DOM converter. + this.#domConverter = this.#htmlDataProcessor.domConverter; + + // TODO might be extended in the future + this.#toDataRules = defaultRules; } /** @@ -46,10 +57,27 @@ export default class BBCodeDataProcessor implements DataProcessor { * @returns BBCode string. */ public toData(viewFragment: ViewDocumentFragment): string { - const html = this.#htmlDataProcessor.toData(viewFragment); - return html2bbcode(html); + const htmlDomFragment: Node | DocumentFragment = this.#domConverter.viewToDom(viewFragment); + //const htmlWriter = new BasicHtmlWriter(); + + //htmlWriter.getHtml( htmlDomFragment ); + const bbcode2 = html2bbcode(htmlDomFragment, this.#toDataRules); + console.log("BBCO_DP", bbcode2); + const bbcode = this.#htmlDataProcessor.toData(viewFragment); + console.log("HTML_DP", bbcode); + return bbcode2; } + /*public getBBCode( fragment: DocumentFragment ): string { + const doc = document.implementation.createHTMLDocument( "" ); + const container = doc.createElement( "div" ); + + const bbcode2 = html2bbcode(htmlDomFragment, this.#toDataRules); + container.appendChild( fragment ); + + return container.innerHTML; + }*/ + /** * Registers a {@link module:engine/view/matcher~MatcherPattern} for view elements whose content should be treated as raw data * and not processed during the conversion from Markdown to view elements. diff --git a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/html2bbcode.ts b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/html2bbcode.ts index 2a83b95e8b..ba116d7be5 100644 --- a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/html2bbcode.ts +++ b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/html2bbcode.ts @@ -1,6 +1,79 @@ /** * Parses HTML to BBCode. */ -export const html2bbcode = (html: string): string => { - return html; // TODO implement +import { HTML2BBCodeRule } from "./rules/DefaultRules"; + +export const html2bbcode = (domFragment: Node, rules: HTML2BBCodeRule[]): string => { + const result = convertWithChildren(domFragment, rules); + return result; }; + +const convertWithChildren = (domFragment: Node, rules: HTML2BBCodeRule[]): string => { + console.log("NODE", domFragment.nodeName) + let result = ""; + const children = Array.from(domFragment.childNodes); + if (children.length > 0) { + const results: string[] = []; + children.forEach((child) => { + console.log("GOTO CHILD"); + results.push(convertWithChildren(child, rules)); + }); + result = results.join(""); + console.log("ALL CHILDREN STRING:", result, "(",domFragment.nodeName,")"); + } + + for (const rule of rules) { + const ruleResult = rule.toData(domFragment); + if (typeof ruleResult === "string") { + console.log("RULE APPLIED RETURN ", ruleResult, "(",domFragment.nodeName,")"); + return ruleResult; + } + } + + if (domFragment.nodeName === "#text") { + console.log("RETURN TEXTCONTENT", domFragment.textContent ?? ""); + return domFragment.textContent ?? ""; + } + + console.log("RETURN CHILDREN STRING", "(",domFragment.nodeName,")"); + return result; +}; + +/* +const checkDocumentFragment = (fragment: Node | DocumentFragment): Node | DocumentFragment => { + if (fragment.is("element")) { + const element = fragment as Element; + element. + } +}*/ + +/*const convertNode = (node: Node) => { + let result: Node | Skip = importedNode; + for (const rule of this.#rules) { + if (result === skip) { + return skip; + } + result = rule.imported?.(result, context) ?? result; + } + return result; +}*/ + +/* +export class HTML2BBCodeConverter { + readonly rules: HTML2BBCodeRule[]; + + constructor(rules: HTML2BBCodeRule[]) { + this.rules = rules; + } + + convertNode: (node: Node) => { + let result: Node | Skip = importedNode; + for (const rule of this.#rules) { + if (result === skip) { + return skip; + } + result = rule.imported?.(result, context) ?? result; + } + return result; + } +}*/ diff --git a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Bold.ts b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Bold.ts new file mode 100644 index 0000000000..5f4067170c --- /dev/null +++ b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Bold.ts @@ -0,0 +1,16 @@ +import { HTML2BBCodeRule } from "./DefaultRules"; + +export const boldRule: HTML2BBCodeRule = { + id: "Bold", + toData: (node) => { + if (!isBold(node)) { + return node; + } + return `[b]${node.textContent}[/b]`; + }, +}; + +const isBold = (node: Node): boolean => { + const nodeName = node.nodeName; + return nodeName === "B" || nodeName === "STRONG"; +}; diff --git a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/DefaultRules.ts b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/DefaultRules.ts new file mode 100644 index 0000000000..c54adf0819 --- /dev/null +++ b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/DefaultRules.ts @@ -0,0 +1,10 @@ +import { boldRule } from "./Bold"; +import { italicRule } from "./Italic"; +import { underlineRule } from "./Underline"; + +export interface HTML2BBCodeRule { + id: string; + toData: (node: Node) => string | Node; +} + +export const defaultRules: HTML2BBCodeRule[] = [boldRule, italicRule, underlineRule]; diff --git a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Hyperlink.ts b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Hyperlink.ts new file mode 100644 index 0000000000..08457cbe2d --- /dev/null +++ b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Hyperlink.ts @@ -0,0 +1,16 @@ +import { HTML2BBCodeRule } from "./DefaultRules"; + +export const hyperlinkRule: HTML2BBCodeRule = { + id: "Hyperlink", + toData: (node) => { + if (!isHyperlink(node)) { + return node; + } + return `[url]${node.textContent}[/url]`; + }, +}; + +const isHyperlink = (node: Node): boolean => { + const nodeName = node.nodeName; + return nodeName === "A"; +}; diff --git a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Italic.ts b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Italic.ts new file mode 100644 index 0000000000..a7d41f8fe1 --- /dev/null +++ b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Italic.ts @@ -0,0 +1,16 @@ +import { HTML2BBCodeRule } from "./DefaultRules"; + +export const italicRule: HTML2BBCodeRule = { + id: "Italic", + toData: (node) => { + if (!isItalic(node)) { + return node; + } + return `[i]${node.textContent}[/i]`; + }, +}; + +const isItalic = (node: Node): boolean => { + const nodeName = node.nodeName; + return nodeName === "I" || nodeName === "EM"; +}; diff --git a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Underline.ts b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Underline.ts new file mode 100644 index 0000000000..81b213003d --- /dev/null +++ b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Underline.ts @@ -0,0 +1,16 @@ +import { HTML2BBCodeRule } from "./DefaultRules"; + +export const underlineRule: HTML2BBCodeRule = { + id: "Underline", + toData: (node) => { + if (!isItalic(node)) { + return node; + } + return `[u]${node.textContent}[/u]`; + }, +}; + +const isItalic = (node: Node): boolean => { + const nodeName = node.nodeName; + return nodeName === "U"; +}; From bb4a0c7182ae539a72dd9ee50cf79c81d8d9863e Mon Sep 17 00:00:00 2001 From: pkliesch Date: Wed, 23 Aug 2023 17:26:21 +0200 Subject: [PATCH 006/403] feature(bbcode-dataprocessor): Take child contents into account in HTML2BBCodeRule Instead of returning bbcode with the node's textContent, we should return the child contents instead. This is mandatory since a strong/italic etc. node might have children that need to be rendered inside the resulting bbcode tag. Since we are computing the bbcode string recursively, starting with the inner nodes, we can just use the already computed child string. --- .../src/html2bbcode/html2bbcode.ts | 60 +++---------------- .../src/html2bbcode/rules/Bold.ts | 4 +- .../src/html2bbcode/rules/DefaultRules.ts | 2 +- .../src/html2bbcode/rules/Hyperlink.ts | 4 +- .../src/html2bbcode/rules/Italic.ts | 4 +- .../src/html2bbcode/rules/Underline.ts | 4 +- 6 files changed, 16 insertions(+), 62 deletions(-) diff --git a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/html2bbcode.ts b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/html2bbcode.ts index ba116d7be5..0c29e4a3f8 100644 --- a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/html2bbcode.ts +++ b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/html2bbcode.ts @@ -4,76 +4,30 @@ import { HTML2BBCodeRule } from "./rules/DefaultRules"; export const html2bbcode = (domFragment: Node, rules: HTML2BBCodeRule[]): string => { - const result = convertWithChildren(domFragment, rules); - return result; + return convertWithChildren(domFragment, rules); }; const convertWithChildren = (domFragment: Node, rules: HTML2BBCodeRule[]): string => { - console.log("NODE", domFragment.nodeName) let result = ""; + + if (domFragment.nodeName === "#text") { + return domFragment.textContent ?? ""; + } + const children = Array.from(domFragment.childNodes); if (children.length > 0) { const results: string[] = []; children.forEach((child) => { - console.log("GOTO CHILD"); results.push(convertWithChildren(child, rules)); }); result = results.join(""); - console.log("ALL CHILDREN STRING:", result, "(",domFragment.nodeName,")"); } for (const rule of rules) { - const ruleResult = rule.toData(domFragment); + const ruleResult = rule.toData(domFragment, result); if (typeof ruleResult === "string") { - console.log("RULE APPLIED RETURN ", ruleResult, "(",domFragment.nodeName,")"); return ruleResult; } } - - if (domFragment.nodeName === "#text") { - console.log("RETURN TEXTCONTENT", domFragment.textContent ?? ""); - return domFragment.textContent ?? ""; - } - - console.log("RETURN CHILDREN STRING", "(",domFragment.nodeName,")"); return result; }; - -/* -const checkDocumentFragment = (fragment: Node | DocumentFragment): Node | DocumentFragment => { - if (fragment.is("element")) { - const element = fragment as Element; - element. - } -}*/ - -/*const convertNode = (node: Node) => { - let result: Node | Skip = importedNode; - for (const rule of this.#rules) { - if (result === skip) { - return skip; - } - result = rule.imported?.(result, context) ?? result; - } - return result; -}*/ - -/* -export class HTML2BBCodeConverter { - readonly rules: HTML2BBCodeRule[]; - - constructor(rules: HTML2BBCodeRule[]) { - this.rules = rules; - } - - convertNode: (node: Node) => { - let result: Node | Skip = importedNode; - for (const rule of this.#rules) { - if (result === skip) { - return skip; - } - result = rule.imported?.(result, context) ?? result; - } - return result; - } -}*/ diff --git a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Bold.ts b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Bold.ts index 5f4067170c..257affefeb 100644 --- a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Bold.ts +++ b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Bold.ts @@ -2,11 +2,11 @@ import { HTML2BBCodeRule } from "./DefaultRules"; export const boldRule: HTML2BBCodeRule = { id: "Bold", - toData: (node) => { + toData: (node, content: string) => { if (!isBold(node)) { return node; } - return `[b]${node.textContent}[/b]`; + return `[b]${content}[/b]`; }, }; diff --git a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/DefaultRules.ts b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/DefaultRules.ts index c54adf0819..64ae4fac2c 100644 --- a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/DefaultRules.ts +++ b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/DefaultRules.ts @@ -4,7 +4,7 @@ import { underlineRule } from "./Underline"; export interface HTML2BBCodeRule { id: string; - toData: (node: Node) => string | Node; + toData: (node: Node, content: string) => string | Node; } export const defaultRules: HTML2BBCodeRule[] = [boldRule, italicRule, underlineRule]; diff --git a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Hyperlink.ts b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Hyperlink.ts index 08457cbe2d..e922c36421 100644 --- a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Hyperlink.ts +++ b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Hyperlink.ts @@ -2,11 +2,11 @@ import { HTML2BBCodeRule } from "./DefaultRules"; export const hyperlinkRule: HTML2BBCodeRule = { id: "Hyperlink", - toData: (node) => { + toData: (node, content: string) => { if (!isHyperlink(node)) { return node; } - return `[url]${node.textContent}[/url]`; + return `[url]${content}[/url]`; }, }; diff --git a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Italic.ts b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Italic.ts index a7d41f8fe1..e38f5ba330 100644 --- a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Italic.ts +++ b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Italic.ts @@ -2,11 +2,11 @@ import { HTML2BBCodeRule } from "./DefaultRules"; export const italicRule: HTML2BBCodeRule = { id: "Italic", - toData: (node) => { + toData: (node, content: string) => { if (!isItalic(node)) { return node; } - return `[i]${node.textContent}[/i]`; + return `[i]${content}[/i]`; }, }; diff --git a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Underline.ts b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Underline.ts index 81b213003d..89ec4a1c94 100644 --- a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Underline.ts +++ b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Underline.ts @@ -2,11 +2,11 @@ import { HTML2BBCodeRule } from "./DefaultRules"; export const underlineRule: HTML2BBCodeRule = { id: "Underline", - toData: (node) => { + toData: (node, content: string) => { if (!isItalic(node)) { return node; } - return `[u]${node.textContent}[/u]`; + return `[u]${content}[/u]`; }, }; From 87b3bb349e597462b59b87d7dd7baccc6a5a49e4 Mon Sep 17 00:00:00 2001 From: pkliesch Date: Thu, 24 Aug 2023 08:22:56 +0200 Subject: [PATCH 007/403] feature(bbcode-dataprocessor): Return undefined if rule is not applied Also: Inline comments and dom-converter package used to simplify types --- .../ckeditor5-coremedia-bbcode/package.json | 1 + .../src/html2bbcode/html2bbcode.ts | 40 +++++++++++++++---- .../src/html2bbcode/rules/Bold.ts | 2 +- .../src/html2bbcode/rules/DefaultRules.ts | 2 +- .../src/html2bbcode/rules/Hyperlink.ts | 2 +- .../src/html2bbcode/rules/Italic.ts | 2 +- .../src/html2bbcode/rules/Underline.ts | 2 +- 7 files changed, 38 insertions(+), 13 deletions(-) diff --git a/packages/ckeditor5-coremedia-bbcode/package.json b/packages/ckeditor5-coremedia-bbcode/package.json index 310ce7f769..c48463ff74 100644 --- a/packages/ckeditor5-coremedia-bbcode/package.json +++ b/packages/ckeditor5-coremedia-bbcode/package.json @@ -26,6 +26,7 @@ "devDependencies": { "@ckeditor/ckeditor5-core": "^37.1.0", "@ckeditor/ckeditor5-engine": "^37.1.0", + "@coremedia/ckeditor5-dom-support": "^15.0.1", "@types/jest": "^29.5.1", "jest": "^29.5.0", "jest-each": "^29.5.0", diff --git a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/html2bbcode.ts b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/html2bbcode.ts index 0c29e4a3f8..79ca14ebd7 100644 --- a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/html2bbcode.ts +++ b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/html2bbcode.ts @@ -2,30 +2,54 @@ * Parses HTML to BBCode. */ import { HTML2BBCodeRule } from "./rules/DefaultRules"; +import { isText } from "@coremedia/ckeditor5-dom-support"; -export const html2bbcode = (domFragment: Node, rules: HTML2BBCodeRule[]): string => { - return convertWithChildren(domFragment, rules); -}; +export const html2bbcode = (domFragment: Node, rules: HTML2BBCodeRule[]): string => + convertWithChildren(domFragment, rules); +/** + * Recursively traverses all nodes in the given dom fragment and computes a bbcode string + * from it. + * + * @param domFragment - the current node to check + * @param rules - the bbcode rules that might be applied + * @returns a bbcode string that matches the given node and its children + */ const convertWithChildren = (domFragment: Node, rules: HTML2BBCodeRule[]): string => { let result = ""; - if (domFragment.nodeName === "#text") { + /** + * If this is a text node, there will be no children and no + * further rules need to be applied. + */ + if (isText(domFragment)) { return domFragment.textContent ?? ""; } + /** + * This is not a text node and therefore might have child nodes. + * If that's the case, we need to compute the resulting strings of + * the children first, before we can proceed with this node. + * + * This code block converts all children to a joined string. + */ const children = Array.from(domFragment.childNodes); if (children.length > 0) { - const results: string[] = []; + const childResults: string[] = []; children.forEach((child) => { - results.push(convertWithChildren(child, rules)); + childResults.push(convertWithChildren(child, rules)); }); - result = results.join(""); + result = childResults.join(""); } + /** + * Now we can check if any of the given rules apply on the + * given node. If true, the result string will be wrapped by the + * computed bbcode. Otherwise, just the result string will be returned. + */ for (const rule of rules) { const ruleResult = rule.toData(domFragment, result); - if (typeof ruleResult === "string") { + if (ruleResult !== undefined) { return ruleResult; } } diff --git a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Bold.ts b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Bold.ts index 257affefeb..6fe2a82df1 100644 --- a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Bold.ts +++ b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Bold.ts @@ -4,7 +4,7 @@ export const boldRule: HTML2BBCodeRule = { id: "Bold", toData: (node, content: string) => { if (!isBold(node)) { - return node; + return undefined; } return `[b]${content}[/b]`; }, diff --git a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/DefaultRules.ts b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/DefaultRules.ts index 64ae4fac2c..66ff9d869b 100644 --- a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/DefaultRules.ts +++ b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/DefaultRules.ts @@ -4,7 +4,7 @@ import { underlineRule } from "./Underline"; export interface HTML2BBCodeRule { id: string; - toData: (node: Node, content: string) => string | Node; + toData: (node: Node, content: string) => string | undefined; } export const defaultRules: HTML2BBCodeRule[] = [boldRule, italicRule, underlineRule]; diff --git a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Hyperlink.ts b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Hyperlink.ts index e922c36421..d224f4e4a0 100644 --- a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Hyperlink.ts +++ b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Hyperlink.ts @@ -4,7 +4,7 @@ export const hyperlinkRule: HTML2BBCodeRule = { id: "Hyperlink", toData: (node, content: string) => { if (!isHyperlink(node)) { - return node; + return undefined; } return `[url]${content}[/url]`; }, diff --git a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Italic.ts b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Italic.ts index e38f5ba330..b5b0702044 100644 --- a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Italic.ts +++ b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Italic.ts @@ -4,7 +4,7 @@ export const italicRule: HTML2BBCodeRule = { id: "Italic", toData: (node, content: string) => { if (!isItalic(node)) { - return node; + return undefined; } return `[i]${content}[/i]`; }, diff --git a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Underline.ts b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Underline.ts index 89ec4a1c94..66db51361a 100644 --- a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Underline.ts +++ b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Underline.ts @@ -4,7 +4,7 @@ export const underlineRule: HTML2BBCodeRule = { id: "Underline", toData: (node, content: string) => { if (!isItalic(node)) { - return node; + return undefined; } return `[u]${content}[/u]`; }, From 8a7b880affd73ab9f678231d190952e68c401204 Mon Sep 17 00:00:00 2001 From: pkliesch Date: Thu, 24 Aug 2023 09:05:45 +0200 Subject: [PATCH 008/403] feature(bbcode-dataprocessor): Enable underline and link plugins in bbcode editor --- app/src/editors/bbCode.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/editors/bbCode.ts b/app/src/editors/bbCode.ts index d35f42a3a4..09c3d29f80 100644 --- a/app/src/editors/bbCode.ts +++ b/app/src/editors/bbCode.ts @@ -1,5 +1,5 @@ import { Autosave } from "@ckeditor/ckeditor5-autosave"; -import { Bold, Italic } from "@ckeditor/ckeditor5-basic-styles"; +import { Bold, Italic, Underline } from "@ckeditor/ckeditor5-basic-styles"; import { ClassicEditor } from "@ckeditor/ckeditor5-editor-classic"; import { Essentials } from "@ckeditor/ckeditor5-essentials"; import { BBCode } from "@coremedia/ckeditor5-coremedia-bbcode"; @@ -9,6 +9,7 @@ import { SourceEditing } from "@ckeditor/ckeditor5-source-editing"; import { Editor } from "@ckeditor/ckeditor5-core"; import { getHashParam } from "../HashParams"; +import { Link } from "@ckeditor/ckeditor5-link"; /** * Typings for CKEditorInspector, as it does not ship with typings yet. @@ -29,8 +30,8 @@ export const createBBCodeEditor = (language = "en") => { ClassicEditor.create(document.querySelector(editorElementSelector) as HTMLElement, { placeholder: "Type your text here...", - plugins: [Autosave, Bold, Essentials, Heading, Italic, Paragraph, SourceEditing, BBCode], - toolbar: ["undo", "redo", "|", "heading", "|", "bold", "italic", "sourceEditing"], + plugins: [Autosave, Bold, Essentials, Heading, Italic, Underline, Paragraph, SourceEditing, Link, BBCode], + toolbar: ["undo", "redo", "|", "heading", "|", "bold", "italic", "underline", "|", "link", "|", "sourceEditing"], language: { // Language switch only applies to editor instance. ui: language, From 862c2378738c893a0c2af08179b9d579fc83ff9c Mon Sep 17 00:00:00 2001 From: pkliesch Date: Thu, 24 Aug 2023 09:07:26 +0200 Subject: [PATCH 009/403] feature(bbcode-dataprocessor): Cleanup --- .../src/BBCodeDataProcessor.ts | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/packages/ckeditor5-coremedia-bbcode/src/BBCodeDataProcessor.ts b/packages/ckeditor5-coremedia-bbcode/src/BBCodeDataProcessor.ts index fe7bee93f9..18bf5e355d 100644 --- a/packages/ckeditor5-coremedia-bbcode/src/BBCodeDataProcessor.ts +++ b/packages/ckeditor5-coremedia-bbcode/src/BBCodeDataProcessor.ts @@ -58,26 +58,9 @@ export default class BBCodeDataProcessor implements DataProcessor { */ public toData(viewFragment: ViewDocumentFragment): string { const htmlDomFragment: Node | DocumentFragment = this.#domConverter.viewToDom(viewFragment); - //const htmlWriter = new BasicHtmlWriter(); - - //htmlWriter.getHtml( htmlDomFragment ); - const bbcode2 = html2bbcode(htmlDomFragment, this.#toDataRules); - console.log("BBCO_DP", bbcode2); - const bbcode = this.#htmlDataProcessor.toData(viewFragment); - console.log("HTML_DP", bbcode); - return bbcode2; + return html2bbcode(htmlDomFragment, this.#toDataRules); } - /*public getBBCode( fragment: DocumentFragment ): string { - const doc = document.implementation.createHTMLDocument( "" ); - const container = doc.createElement( "div" ); - - const bbcode2 = html2bbcode(htmlDomFragment, this.#toDataRules); - container.appendChild( fragment ); - - return container.innerHTML; - }*/ - /** * Registers a {@link module:engine/view/matcher~MatcherPattern} for view elements whose content should be treated as raw data * and not processed during the conversion from Markdown to view elements. From 3272bf2f573df6273173852d624f06f0364d292a Mon Sep 17 00:00:00 2001 From: pkliesch Date: Thu, 24 Aug 2023 09:08:06 +0200 Subject: [PATCH 010/403] feature(bbcode-dataprocessor): Fix & enable Hyperlink Rule --- .../src/html2bbcode/rules/DefaultRules.ts | 3 ++- .../src/html2bbcode/rules/Hyperlink.ts | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/DefaultRules.ts b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/DefaultRules.ts index 66ff9d869b..703040e3c2 100644 --- a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/DefaultRules.ts +++ b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/DefaultRules.ts @@ -1,10 +1,11 @@ import { boldRule } from "./Bold"; import { italicRule } from "./Italic"; import { underlineRule } from "./Underline"; +import { hyperlinkRule } from "./Hyperlink"; export interface HTML2BBCodeRule { id: string; toData: (node: Node, content: string) => string | undefined; } -export const defaultRules: HTML2BBCodeRule[] = [boldRule, italicRule, underlineRule]; +export const defaultRules: HTML2BBCodeRule[] = [boldRule, italicRule, underlineRule, hyperlinkRule]; diff --git a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Hyperlink.ts b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Hyperlink.ts index d224f4e4a0..cc2828532e 100644 --- a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Hyperlink.ts +++ b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Hyperlink.ts @@ -1,4 +1,5 @@ import { HTML2BBCodeRule } from "./DefaultRules"; +import { isElement } from "@coremedia/ckeditor5-dom-support"; export const hyperlinkRule: HTML2BBCodeRule = { id: "Hyperlink", @@ -6,7 +7,11 @@ export const hyperlinkRule: HTML2BBCodeRule = { if (!isHyperlink(node)) { return undefined; } - return `[url]${content}[/url]`; + if (!isElement(node)) { + return `[url]${content}[/url]`; + } + const href = node.getAttribute("href"); + return `[url=${href}]${content}[/url]`; }, }; From c4a80ef0ca70a582ed235574cf53c0fa2eb82787 Mon Sep 17 00:00:00 2001 From: pkliesch Date: Thu, 24 Aug 2023 10:13:39 +0200 Subject: [PATCH 011/403] feature(bbcode-dataprocessor): Fix falsy EditorConfig types The EditorConfig type extensions in some of our augmentation files were marked as mandatory. This is false. TypeScript would complain about insufficient configs if these plugins were not configured. (Which is totally fine) Therefore, these config extensions are now marked as optional. Also, the configuration of the CoreMedia RichText plugin was now moved into a separate function. TypeScript was not happy with compatibility and rules being set independently because these values have to match in order to represent a certain config type (v10 or latest). Compatibility is now also set explicitly to make sure the value matches the given type. (type expects "v10" or "latest" and the value was of type "string | true") --- app/src/editors/default.ts | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/app/src/editors/default.ts b/app/src/editors/default.ts index 93fa0f3328..abdab30089 100644 --- a/app/src/editors/default.ts +++ b/app/src/editors/default.ts @@ -55,6 +55,10 @@ import { COREMEDIA_LINK_CONFIG_KEY } from "@coremedia/ckeditor5-coremedia-link/s import { LinkAttributesConfig } from "@coremedia/ckeditor5-link-common/src/LinkAttributesConfig"; import { LinkAttributes } from "@coremedia/ckeditor5-link-common/src/LinkAttributes"; import { Differencing } from "@coremedia/ckeditor5-coremedia-differencing"; +import type { + LatestCoreMediaRichTextConfig, + V10CoreMediaRichTextConfig, +} from "@coremedia/ckeditor5-coremedia-richtext"; /** * Typings for CKEditorInspector, as it does not ship with typings yet. */ @@ -133,6 +137,25 @@ const linkAttributesConfig: LinkAttributesConfig = getHashParam("skipLinkAttribu const editorElementSelector = "#editor"; +const getRichTextConfig = ( + richTextCompatibility: string | true +): Partial | V10CoreMediaRichTextConfig => { + // Use v10 for first data-processor architecture, for example. + if (richTextCompatibility === "v10") { + return { + // Defaults to: Loose + strictness: Strictness.STRICT, + compatibility: "v10", + rules: v10RichTextRuleConfigurations, + }; + } + return { + strictness: Strictness.STRICT, + compatibility: "latest", + rules: richTextRuleConfigurations, + }; +}; + export const createDefaultEditor = (language = "en") => { const sourceElement = document.querySelector(editorElementSelector) as HTMLElement; if (!sourceElement) { @@ -331,16 +354,7 @@ export const createDefaultEditor = (language = "en") => { }); }, }, - [COREMEDIA_RICHTEXT_CONFIG_KEY]: { - // Defaults to: Loose - strictness: Strictness.STRICT, - // The Latest is the default. Use v10 for first data-processor architecture, - // for example. - // @ts-expect-error - TODO[cke] 37.x Fix Typings - compatibility: richTextCompatibility, - //@ts-expect-error the types do not match here rules may not be RuleConfig[] TODO - rules: richTextCompatibility === "v10" ? v10RichTextRuleConfigurations : richTextRuleConfigurations, - }, + [COREMEDIA_RICHTEXT_CONFIG_KEY]: getRichTextConfig(richTextCompatibility), [COREMEDIA_RICHTEXT_SUPPORT_CONFIG_KEY]: { aliases: [ // As we represent `` as ``, we must ensure, From 353d8366701cb39b9b85f3437d03a6a78970f13c Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Tue, 12 Sep 2023 15:01:03 +0200 Subject: [PATCH 012/403] chore: Fix Version After Rebase main is now at 15.0.2-rc.2. Updated new BBCode module accordingly. --- packages/ckeditor5-coremedia-bbcode/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-coremedia-bbcode/package.json b/packages/ckeditor5-coremedia-bbcode/package.json index c48463ff74..5e2c3ea262 100644 --- a/packages/ckeditor5-coremedia-bbcode/package.json +++ b/packages/ckeditor5-coremedia-bbcode/package.json @@ -1,6 +1,6 @@ { "name": "@coremedia/ckeditor5-coremedia-bbcode", - "version": "15.0.1", + "version": "15.0.2-rc.2", "description": "BBCode Data-Processor", "keywords": [ "coremedia", @@ -26,7 +26,7 @@ "devDependencies": { "@ckeditor/ckeditor5-core": "^37.1.0", "@ckeditor/ckeditor5-engine": "^37.1.0", - "@coremedia/ckeditor5-dom-support": "^15.0.1", + "@coremedia/ckeditor5-dom-support": "15.0.2-rc.2", "@types/jest": "^29.5.1", "jest": "^29.5.0", "jest-each": "^29.5.0", @@ -46,6 +46,6 @@ "dependencies": { "@bbob/html": "^3.0.0", "@bbob/preset-html5": "^3.0.0", - "@coremedia/ckeditor5-core-common": "^15.0.1" + "@coremedia/ckeditor5-core-common": "15.0.2-rc.2" } } From 0e85dc30df5181472bc0d1b00989cea547907508 Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Tue, 26 Sep 2023 15:47:49 +0200 Subject: [PATCH 013/403] feat: Common Examples UI To share selection support for a given set of examples, moved the corresponding code to `ckeditor5-coremedia-example-data`. This API could not be reused for all our example applications. --- .../package.json | 5 + .../src/InitExamples.ts | 200 ++++++++++++++++++ .../src/index.ts | 5 + 3 files changed, 210 insertions(+) create mode 100644 packages/ckeditor5-coremedia-example-data/src/InitExamples.ts create mode 100644 packages/ckeditor5-coremedia-example-data/src/index.ts diff --git a/packages/ckeditor5-coremedia-example-data/package.json b/packages/ckeditor5-coremedia-example-data/package.json index 6fd856be3b..5e4c0f42d0 100644 --- a/packages/ckeditor5-coremedia-example-data/package.json +++ b/packages/ckeditor5-coremedia-example-data/package.json @@ -14,6 +14,11 @@ "node": "18", "pnpm": "^8.6.9" }, + "main": "./src/index.ts", + "publishConfig": { + "main": "./src/index.js", + "types": "./src/index.d.ts" + }, "license": "Apache-2.0", "devDependencies": { "@coremedia-internal/ckeditor5-jest-test-helpers": "^1.0.0", diff --git a/packages/ckeditor5-coremedia-example-data/src/InitExamples.ts b/packages/ckeditor5-coremedia-example-data/src/InitExamples.ts new file mode 100644 index 0000000000..c033d6d146 --- /dev/null +++ b/packages/ckeditor5-coremedia-example-data/src/InitExamples.ts @@ -0,0 +1,200 @@ +const createLabelFor = (inputElement: HTMLInputElement): HTMLLabelElement => { + const { id: inputId } = inputElement; + const element = document.createElement("label"); + + if (!inputId) { + throw new Error("Input Element must provide an ID."); + } + + element.htmlFor = inputId; + element.textContent = "Example:"; + return element; +}; + +const createInputWithDataFrom = (dataListElement: HTMLDataListElement): HTMLInputElement => { + const { id: dataListId } = dataListElement; + const element = document.createElement("input"); + + if (!dataListId) { + throw new Error("DataList Element must provide an ID."); + } + + element.id = "xmp-input"; + element.placeholder = "Start typing..."; + element.autocomplete = "on"; + element.setAttribute("list", dataListId); + + // Clear input on focus (otherwise, only the matched option is shown) + element.addEventListener("focus", () => { + element.value = ""; + }); + + return element; +}; + +const createDataListElement = (): HTMLDataListElement => { + const element: HTMLDataListElement = document.createElement("datalist"); + element.id = "xmp-data"; + return element; +}; + +const createReloadButton = (): HTMLButtonElement => { + const element = document.createElement("button"); + element.id = "xmp-reload"; + element.title = "Reload"; + const text = document.createTextNode("🔄"); + element.appendChild(text); + return element; +}; + +const createClearButton = (): HTMLButtonElement => { + const element = document.createElement("button"); + element.id = "xmp-clear"; + element.title = "Clear"; + const text = document.createTextNode("🚮"); + element.appendChild(text); + return element; +}; + +/** + * UI Elements relevant for example selection. + */ +interface ExamplesUiElements { + /** + * The input field. You may want a `change` listener, to forward + * changes to the CKEditor instance. + */ + input: HTMLInputElement; + /** + * The data list that is expected to contain the options, users may + * select from. + */ + dataList: HTMLDataListElement; + /** + * Pressed to trigger reload of the current example. + */ + reload: HTMLButtonElement; + /** + * Triggered to clear the contents of the CKEditor. + */ + clear: HTMLButtonElement; +} + +const initExamplesUi = (parent: ParentNode): ExamplesUiElements => { + const dataList = createDataListElement(); + const input = createInputWithDataFrom(dataList); + const label = createLabelFor(input); + const reload = createReloadButton(); + const clear = createClearButton(); + + parent.append(label, input, dataList, reload, clear); + + return { input, dataList, reload, clear }; +}; + +const addExampleOptions = ( + dataList: HTMLDataListElement, + defaultKey: string | undefined, + exampleKeys: string[] +): void => { + // Now add all examples + for (const exampleKey of exampleKeys.sort()) { + const option = document.createElement("option"); + // noinspection InnerHTMLJS + option.innerHTML = exampleKey; + option.value = exampleKey; + option.defaultSelected = exampleKey === defaultKey; + dataList.appendChild(option); + } +}; + +/** + * Initializes the examples. + * + * @param config - configuration for examples + */ +export const initExamples = (config: ExamplesConfig): void => { + const { id = "examples", default: defaultExampleKey, examples, onChange } = config; + const examplesContainer = document.getElementById(id); + + if (!examplesContainer) { + throw new Error(`Cannot locate examples container with ID "${id}".`); + } + + const { input, dataList, reload, clear } = initExamplesUi(examplesContainer); + + // On change, set the data – or show an error if data are unknown. + input.addEventListener("change", () => { + const newValue = input.value; + if (examples.hasOwnProperty(newValue)) { + input.classList.remove("error"); + const data = examples[newValue]; + console.log("Setting example data.", { [newValue]: data }); + onChange(data); + input.blur(); + } else { + input.classList.add("error"); + input.select(); + } + }); + + // Init the reload-button, to also listen to the value of example input field. + reload.addEventListener("click", () => { + const newValue = input.value; + if (examples.hasOwnProperty(newValue)) { + input.classList.remove("error"); + const data = examples[newValue]; + console.log("Resetting example data.", { [newValue]: data }); + onChange(data); + input.blur(); + } + }); + + clear.addEventListener("click", () => { + input.blur(); + console.log("Clearing data."); + onChange(""); + }); + + addExampleOptions(dataList, defaultExampleKey, Object.keys(examples)); + + if (defaultExampleKey) { + input.value = defaultExampleKey; + if (examples.hasOwnProperty(defaultExampleKey)) { + input.classList.remove("error"); + const data = examples[defaultExampleKey]; + console.log("Setting default example data.", { [defaultExampleKey]: data }); + onChange(data); + } else { + input.classList.add("error"); + console.error(`Invalid default example key given: "${defaultExampleKey}".`); + } + } +}; + +/** + * Examples configuration. + */ +export interface ExamplesConfig { + /** + * ID of the DOM element to insert the example UI to. + * Defaults to `examples`. + */ + id?: string; + /** + * If set, triggers selection of a default entry when initialized. + */ + default?: string; + /** + * Set of examples to initialize. The key is the title to render within + * the option selection; the value represents the data to set. + */ + examples: Record; + /** + * Listener, that will be informed on any valid selection about the data + * to set at the editor. + * + * @param data - data to set at editor + */ + onChange: (data: string) => void; +} diff --git a/packages/ckeditor5-coremedia-example-data/src/index.ts b/packages/ckeditor5-coremedia-example-data/src/index.ts new file mode 100644 index 0000000000..fbe96d6d6b --- /dev/null +++ b/packages/ckeditor5-coremedia-example-data/src/index.ts @@ -0,0 +1,5 @@ +/** + * @module ckeditor5-coremedia-example-data + */ + +export { initExamples, type ExamplesConfig } from "./InitExamples"; From c59b5baa2a4a0c2b5e8a6cbce4299346613ec265 Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Tue, 26 Sep 2023 15:48:18 +0200 Subject: [PATCH 014/403] feat: Refactor to new Examples UI Refactoring the Rich Text Editor to the new examples UI. --- app/sample/index.html | 16 +----- app/src/editors/default.ts | 5 +- app/src/example-data.ts | 105 ++++++++++++++----------------------- 3 files changed, 42 insertions(+), 84 deletions(-) diff --git a/app/sample/index.html b/app/sample/index.html index 8eb020c50c..89dd58a106 100644 --- a/app/sample/index.html +++ b/app/sample/index.html @@ -32,21 +32,7 @@

- - - - - - - -

+
diff --git a/app/src/editors/default.ts b/app/src/editors/default.ts index abdab30089..8fe8ad4232 100644 --- a/app/src/editors/default.ts +++ b/app/src/editors/default.ts @@ -32,7 +32,7 @@ import MockStudioIntegration from "@coremedia/ckeditor5-coremedia-studio-integra import { updatePreview } from "../preview"; import { initReadOnlyMode } from "../readOnlySupport"; -import { initExamples, setExampleData } from "../example-data"; +import { initExamplesAndBindTo } from "../example-data"; import { CoreMediaStudioEssentials, COREMEDIA_RICHTEXT_CONFIG_KEY, @@ -392,7 +392,7 @@ export const createDefaultEditor = (language = "en") => { (newEditor.plugins.get("Differencing") as Differencing)?.activateDifferencing(); initReadOnlyMode(newEditor); - initExamples(newEditor); + initExamplesAndBindTo(newEditor); initInputExampleContent(newEditor); const undoCommand: Command | undefined = newEditor.commands.get("undo"); @@ -410,7 +410,6 @@ export const createDefaultEditor = (language = "en") => { window.editor = newEditor; console.log("Exposed editor instance as `editor`."); - setExampleData(newEditor, "Welcome"); // Initialize Preview updatePreview(newEditor.getData()); }) diff --git a/app/src/example-data.ts b/app/src/example-data.ts index 79087152b0..53507e8dc6 100644 --- a/app/src/example-data.ts +++ b/app/src/example-data.ts @@ -12,8 +12,9 @@ import { linkTargetData } from "@coremedia-internal/ckeditor5-coremedia-example- import { h1, richtext } from "@coremedia-internal/ckeditor5-coremedia-example-data/src/RichText"; import { richTextDocument } from "@coremedia-internal/ckeditor5-coremedia-example-data/src/RichTextDOM"; import { entitiesData } from "@coremedia-internal/ckeditor5-coremedia-example-data/src/data/EntitiesData"; -import { ClassicEditor } from "@ckeditor/ckeditor5-editor-classic"; import { View } from "@ckeditor/ckeditor5-engine"; +import { initExamples } from "@coremedia-internal/ckeditor5-coremedia-example-data"; +import { Editor } from "@ckeditor/ckeditor5-core"; const CM_RICHTEXT = "http://www.coremedia.com/2003/richtext-1.0"; const XLINK = "http://www.w3.org/1999/xlink"; @@ -162,26 +163,22 @@ const exampleData: Record = { ).replace("LINK", `Link`), }; -export const setExampleData = (editor: ClassicEditor, exampleKey: string) => { +const dumpEditingViewOnRender = (editor: Editor): void => { const { editing: { view }, } = editor; - try { - // noinspection InnerHTMLJS - view.once( - "render", - (event) => { - const { source } = event; - if (source instanceof View) { - console.log("CKEditor's Editing-Controller rendered data.", { - source, - innerHtml: source.getDomRoot()?.innerHTML, - }); - } - }, - { - priority: "lowest", }, + + // noinspection InnerHTMLJS + view.once( + "render", + (event) => { + const { source } = event; + if (source instanceof View) { + console.log("CKEditor's Editing-Controller rendered data.", { + source, + innerHtml: source.getDomRoot()?.innerHTML, + }); ); editor.data.once( "set", @@ -204,58 +201,34 @@ export const setExampleData = (editor: ClassicEditor, exampleKey: string) => { if (xmpInput) { xmpInput.value = exampleKey; } - } catch (e) { - console.error(`Failed setting data for ${exampleKey}.`, e); - } + ); }; -export const initExamples = (editor: ClassicEditor) => { - const xmpInput = document.getElementById("xmp-input") as HTMLInputElement; - const xmpData = document.getElementById("xmp-data"); - const reloadBtn = document.getElementById("xmp-reload"); - const clearBtn = document.getElementById("xmp-clear"); - - if (!(xmpInput && xmpData && reloadBtn)) { - throw new Error("Required components for Example-Data Loading missing."); - } - - // Clear input on focus (otherwise, only the matched option is shown) - xmpInput.addEventListener("focus", () => { - xmpInput.value = ""; - }); - // On change, set the data – or show an error if data are unknown. - xmpInput.addEventListener("change", () => { - const newValue = xmpInput.value; - if (exampleData.hasOwnProperty(newValue)) { - xmpInput.classList.remove("error"); - setExampleData(editor, newValue); - xmpInput.blur(); - } else { - xmpInput.classList.add("error"); - xmpInput.select(); +const dumpDataViewOnRender = (editor: Editor): void => { + const { data } = editor; + data.once( + "set", + (event, details) => + console.log("CKEditor's Data-Controller received data via 'set'.", { + event, + // eslint-disable-next-line + data: details[0], + }), + { + priority: "lowest", } - }); - // Init the reload-button, to also listen to the value of example input field. - reloadBtn.addEventListener("click", () => { - const newValue = xmpInput.value; - if (exampleData.hasOwnProperty(newValue)) { - xmpInput.classList.remove("error"); - setExampleData(editor, newValue); - xmpInput.blur(); - } - }); + ); +}; - clearBtn?.addEventListener("click", () => { - xmpInput.blur(); - setData(editor, ""); +export const initExamplesAndBindTo = (editor: Editor): void => { + initExamples({ + id: "examples", + examples: exampleData, + default: "Welcome", + onChange: (data: string): void => { + dumpEditingViewOnRender(editor); + dumpDataViewOnRender(editor); + setData(editor, data); + }, }); - - // Now add all examples - for (const exampleKey of Object.keys(exampleData).sort()) { - const option = document.createElement("option"); - // noinspection InnerHTMLJS - option.innerHTML = exampleKey; - option.value = exampleKey; - xmpData?.appendChild(option); - } }; From a3d1f3ce20beddd009e093cfd977dd92d120d861 Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Tue, 26 Sep 2023 15:50:32 +0200 Subject: [PATCH 015/403] chore: Update Copyright --- app/sample/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/sample/index.html b/app/sample/index.html index 89dd58a106..05160caa67 100644 --- a/app/sample/index.html +++ b/app/sample/index.html @@ -75,7 +75,7 @@

CKEditor 5: CoreMedia Plugin Showcase

-

Copyright © 2020-2022, +

Copyright © 2020-2023, CoreMedia

From f847bd39f07e4c30e3bd652de51828dd4318c6fa Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Tue, 26 Sep 2023 16:25:44 +0200 Subject: [PATCH 016/403] refactor: Read-Only Toggle To be able to re-use the read-only toggle button for different example editors, refactored the code, so that it now also adds the required UI-Elements on the fly. --- app/sample/index.html | 3 +- app/src/InitReadOnlyToggle.ts | 79 +++++++++++++++++++++++++++++++++++ app/src/editors/default.ts | 13 +++++- app/src/readOnlySupport.ts | 64 ---------------------------- 4 files changed, 91 insertions(+), 68 deletions(-) create mode 100644 app/src/InitReadOnlyToggle.ts delete mode 100644 app/src/readOnlySupport.ts diff --git a/app/sample/index.html b/app/sample/index.html index 05160caa67..f3c9de5427 100644 --- a/app/sample/index.html +++ b/app/sample/index.html @@ -42,8 +42,7 @@

CKEditor 5: CoreMedia Plugin Showcase

-
- +
diff --git a/app/src/InitReadOnlyToggle.ts b/app/src/InitReadOnlyToggle.ts new file mode 100644 index 0000000000..47a2d4ed74 --- /dev/null +++ b/app/src/InitReadOnlyToggle.ts @@ -0,0 +1,79 @@ +export interface ReadOnlyToggleConfig { + /** + * ID of toolbar button to add the read only toggle button to. + */ + toolbarId: string; + /** + * Callback to trigger on read-only change. + * + * @param readOnly - read only state + */ + onToggle: (readOnly: boolean) => void; +} + +const readOnlyModeButtonId = "readOnlyMode"; +const enableReadOnlyBtnLabel = "Enable Read-Only-Mode"; +const disableReadOnlyBtnLabel = "Disable Read-Only-Mode"; +const readWriteState = "read-write"; +const readOnlyState = "read-only"; + +export const initReadOnlyToggle = (config: ReadOnlyToggleConfig): void => { + const { toolbarId, onToggle } = config; + const toolbar = document.getElementById(toolbarId); + + if (!toolbar) { + throw new Error(`Cannot find toolbar element having ID "${toolbarId}".`); + } + + const button = document.createElement("button"); + + button.id = readOnlyModeButtonId; + button.title = "Delay Modifiers: Ctrl/Cmd: 10s, Shift: 60s, Ctrl/Cmd+Shift: 120s"; + button.textContent = enableReadOnlyBtnLabel; + button.dataset.currentState = readWriteState; + + toolbar.appendChild(button); + + const enableReadOnly = () => { + button.textContent = disableReadOnlyBtnLabel; + button.dataset.currentState = readOnlyState; + onToggle(true); + }; + + const disableReadOnly = () => { + button.textContent = enableReadOnlyBtnLabel; + button.dataset.currentState = readWriteState; + onToggle(false); + }; + + // Naive check, but should be ok. We cannot ask CKEditor directly if **we** + // are responsible for read-only state. + const isReadOnly = () => button.dataset.currentState === readOnlyState; + + let currentToggleDelay: number; + + const toggleState = (countDownSeconds: number) => { + if (countDownSeconds > 0) { + if (isReadOnly()) { + button.textContent = `R/W in ${countDownSeconds} s...`; + } else { + button.textContent = `R/O in ${countDownSeconds} s...`; + } + currentToggleDelay = window.setTimeout(toggleState, 1000, countDownSeconds - 1); + } else { + isReadOnly() ? disableReadOnly() : enableReadOnly(); + } + }; + + button.addEventListener("click", (evt) => { + window.clearTimeout(currentToggleDelay); + let countDownSeconds = 0; + const ctrlOrCommandKey = evt.ctrlKey || evt.metaKey; + if (evt.shiftKey) { + countDownSeconds = ctrlOrCommandKey ? 120 : 60; + } else if (ctrlOrCommandKey) { + countDownSeconds = 10; + } + toggleState(countDownSeconds); + }); +}; diff --git a/app/src/editors/default.ts b/app/src/editors/default.ts index 8fe8ad4232..cfc8357f64 100644 --- a/app/src/editors/default.ts +++ b/app/src/editors/default.ts @@ -31,7 +31,6 @@ import { FontMapper as CoreMediaFontMapper } from "@coremedia/ckeditor5-font-map import MockStudioIntegration from "@coremedia/ckeditor5-coremedia-studio-integration-mock/src/MockStudioIntegration"; import { updatePreview } from "../preview"; -import { initReadOnlyMode } from "../readOnlySupport"; import { initExamplesAndBindTo } from "../example-data"; import { CoreMediaStudioEssentials, @@ -59,6 +58,7 @@ import type { LatestCoreMediaRichTextConfig, V10CoreMediaRichTextConfig, } from "@coremedia/ckeditor5-coremedia-richtext"; +import { initReadOnlyToggle } from "../InitReadOnlyToggle"; /** * Typings for CKEditorInspector, as it does not ship with typings yet. */ @@ -391,7 +391,16 @@ export const createDefaultEditor = (language = "en") => { (newEditor.plugins.get("Differencing") as Differencing)?.activateDifferencing(); - initReadOnlyMode(newEditor); + initReadOnlyToggle({ + toolbarId: "applicationToolbar", + onToggle: (readOnly) => { + if (readOnly) { + newEditor.enableReadOnlyMode("exampleApp"); + } else { + newEditor.disableReadOnlyMode("exampleApp"); + } + }, + }); initExamplesAndBindTo(newEditor); initInputExampleContent(newEditor); diff --git a/app/src/readOnlySupport.ts b/app/src/readOnlySupport.ts deleted file mode 100644 index 9803276de2..0000000000 --- a/app/src/readOnlySupport.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { Editor } from "@ckeditor/ckeditor5-core"; - -const READ_ONLY_MODE_BTN_ID = "readOnlyMode"; -const READ_ONY_MODE_ID = "exampleApplicationReadOnlyMode"; -const ENABLE_BTN_LABEL = "Enable Read-Only-Mode"; -const DISABLE_BTN_LABEL = "Disable Read-Only-Mode"; - -const initReadOnlyMode = (editor: Editor) => { - const toggleButton = document.querySelector(`#${READ_ONLY_MODE_BTN_ID}`) as HTMLElement; - - if (!toggleButton) { - console.error("Failed initializing read-only mode toggle, as required button is not available."); - return; - } - - toggleButton.dataset.currentState = "read-write"; - - const setLabel = (label: string) => (toggleButton.textContent = label); - - const enableReadOnly = () => { - toggleButton.dataset.currentState = "read-only"; - editor.enableReadOnlyMode(READ_ONY_MODE_ID); - setLabel(DISABLE_BTN_LABEL); - }; - - const disableReadOnly = () => { - toggleButton.dataset.currentState = "read-write"; - editor.disableReadOnlyMode(READ_ONY_MODE_ID); - setLabel(ENABLE_BTN_LABEL); - }; - - // Naive check, but should be ok. We cannot ask CKEditor directly, if WE - // are responsible for read-only state. - const isReadOnly = () => toggleButton.dataset.currentState === "read-only"; - - setLabel(ENABLE_BTN_LABEL); - - let currentToggleDelay: number; - - const toggleState = (countDownSeconds: number) => { - if (countDownSeconds > 0) { - setLabel(`Toggling Read-Only-Mode in ${countDownSeconds} s...`); - // eslint-disable-next-line no-restricted-globals - currentToggleDelay = setTimeout(toggleState, 1000, countDownSeconds - 1); - } else { - isReadOnly() ? disableReadOnly() : enableReadOnly(); - } - }; - - toggleButton.addEventListener("click", (evt) => { - // eslint-disable-next-line no-restricted-globals - clearTimeout(currentToggleDelay); - let countDownSeconds = 0; - const ctrlOrCommandKey = evt.ctrlKey || evt.metaKey; - if (evt.shiftKey) { - countDownSeconds = ctrlOrCommandKey ? 120 : 60; - } else if (ctrlOrCommandKey) { - countDownSeconds = 10; - } - toggleState(countDownSeconds); - }); -}; - -export { initReadOnlyMode }; From 7172b9ea6096f7b5ef950e597713c722d4cbee0c Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Wed, 27 Sep 2023 09:02:47 +0200 Subject: [PATCH 017/403] refactor: Minor Adaptations * Move Notifications into Header * Add some comments about "boxes to fill". * Fixing some typos. --- app/sample/index.html | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/app/sample/index.html b/app/sample/index.html index f3c9de5427..452f5663f3 100644 --- a/app/sample/index.html +++ b/app/sample/index.html @@ -8,19 +8,21 @@ -
-
+
+ +

CKEditor 5 logoCKEditor 5

+ src="https://c.cksource.com/a/1/logos/ckeditor5.svg" alt="CKEditor 5 logo">CKEditor 5
+
+ +
@@ -78,7 +82,7 @@

CKEditor 5: CoreMedia Plugin Showcase

CoreMedia

- - + + From 4066752e08d23944c38347423c6da89595d63706 Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Wed, 27 Sep 2023 09:03:35 +0200 Subject: [PATCH 018/403] refactor: Introduce DataFormatter In preparation, to possibly also provide a preview for BBCode (MVP: as plain text), extracted formatting to extra facade. --- app/src/DataFormatter.ts | 18 ++++++++++++++++++ app/src/preview.ts | 8 ++------ 2 files changed, 20 insertions(+), 6 deletions(-) create mode 100644 app/src/DataFormatter.ts diff --git a/app/src/DataFormatter.ts b/app/src/DataFormatter.ts new file mode 100644 index 0000000000..e36f9f4bc5 --- /dev/null +++ b/app/src/DataFormatter.ts @@ -0,0 +1,18 @@ +import { default as formatXml } from "xml-formatter"; + +export type DataFormatter = (data: string, empty?: string) => string; + +export const dataFormatter: { + xml: DataFormatter; + text: DataFormatter; +} = { + xml: (data, empty) => + data + ? formatXml(data, { + indentation: " ", + collapseContent: false, + whiteSpaceAtEndOfSelfclosingTag: true, + }) + : empty ?? "", + text: (data, empty) => (data ? data : empty ?? ""), +}; diff --git a/app/src/preview.ts b/app/src/preview.ts index 72388dc061..8c9fc0a491 100644 --- a/app/src/preview.ts +++ b/app/src/preview.ts @@ -1,4 +1,5 @@ import { default as format } from "xml-formatter"; +import { dataFormatter } from "./DataFormatter"; const WITH_PREVIEW_CLASS = "with-preview"; const getPreviewPanel = (): HTMLElement | null => document.getElementById("preview"); @@ -18,12 +19,7 @@ const updatePreview = (data: string) => { if (!preview) { return; } - preview.innerText = data - ? format(data, { - indentation: " ", - collapseContent: false, - }) - : "empty"; + preview.innerText = dataFormatter.xml(data, "empty"); }; const renderPreviewButton = () => { From 8260e0099a9e182757b0425f3cd8f7c58d8bda46 Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Wed, 27 Sep 2023 09:07:19 +0200 Subject: [PATCH 019/403] refactor: Make Preview Formatter Configurable Now you may configure the formatter to use when updating the preview. --- app/src/preview.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/preview.ts b/app/src/preview.ts index 8c9fc0a491..bdd770712a 100644 --- a/app/src/preview.ts +++ b/app/src/preview.ts @@ -1,4 +1,3 @@ -import { default as format } from "xml-formatter"; import { dataFormatter } from "./DataFormatter"; const WITH_PREVIEW_CLASS = "with-preview"; @@ -14,12 +13,12 @@ const setupPreview = () => { preview.innerText = "waiting for ckeditor changes..."; }; -const updatePreview = (data: string) => { +const updatePreview = (data: string, formatter: keyof typeof dataFormatter = "xml") => { const preview = getPreviewPanel(); if (!preview) { return; } - preview.innerText = dataFormatter.xml(data, "empty"); + preview.innerText = dataFormatter[formatter](data, "empty"); }; const renderPreviewButton = () => { From bd537cd6d2b0d4620a8a13458f4d9d50b8018c72 Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Wed, 27 Sep 2023 10:08:50 +0200 Subject: [PATCH 020/403] refactor: Refactor Preview Initialization To prepare using the preview also for BBCode editor, refactored the preview initialization to be similar to the read-only mode button behavior (like dynamically adding a toggle button to the application toolbar). Moved application toolbar handling to extra file, so that it can be reused. --- app/sample/index.html | 5 +- app/sample/styles.css | 10 ++- app/src/ApplicationToolbar.ts | 20 ++++++ app/src/InitReadOnlyToggle.ts | 17 ++--- app/src/editors/default.ts | 1 - app/src/index.ts | 4 +- app/src/preview.ts | 114 +++++++++++++++++++++++----------- 7 files changed, 111 insertions(+), 60 deletions(-) create mode 100644 app/src/ApplicationToolbar.ts diff --git a/app/sample/index.html b/app/sample/index.html index 452f5663f3..9ebd500a2e 100644 --- a/app/sample/index.html +++ b/app/sample/index.html @@ -47,7 +47,6 @@

CKEditor 5: CoreMedia Plugin Showcase

-
@@ -64,7 +63,9 @@

CKEditor 5: CoreMedia Plugin Showcase

- +
+ +
diff --git a/app/sample/styles.css b/app/sample/styles.css index bc192100b1..493be4450c 100644 --- a/app/sample/styles.css +++ b/app/sample/styles.css @@ -100,7 +100,7 @@ input.error { /* --------- PREVIEW STYLES ---------------------------------------------------------------------------------------- */ .preview { - display: inline-block; + display: none; vertical-align: top; background-color: white; padding: 2px 2em; @@ -114,8 +114,8 @@ input.error { min-height: calc(var(--ck-sample-editor-min-height) + 38.67px); } -.preview.hidden { - display: none; +.with-preview .preview { + display: inline-block; } #input-examples.hidden { @@ -159,7 +159,7 @@ main .ck-editor[role='application'] .ck.ck-content, padding: 1.5em 2em; } -.ck.ck-editor.with-preview { +.with-preview .ck.ck-editor { display: inline-block; vertical-align: top; width: calc(50% - 8px) !important; @@ -383,5 +383,3 @@ span.image-inline[xdiff\:changetype="diff-conflict-image"] > img, span.html-obje .editor-tab.active:hover { opacity: 0.8; } - - diff --git a/app/src/ApplicationToolbar.ts b/app/src/ApplicationToolbar.ts new file mode 100644 index 0000000000..a763ac7d9e --- /dev/null +++ b/app/src/ApplicationToolbar.ts @@ -0,0 +1,20 @@ +export const defaultApplicationToolbarId = "applicationToolbar"; + +export interface ApplicationToolbarConfig { + /** + * ID of toolbar button to add the preview button to. + * Defaults to: `applicationToolbar`. + */ + toolbarId?: string; +} + +export const requireApplicationToolbar = (config?: ApplicationToolbarConfig): HTMLElement => { + const { toolbarId = defaultApplicationToolbarId} = config ?? {}; + const toolbar = document.getElementById(toolbarId); + + if (!toolbar) { + throw new Error(`Cannot find toolbar element having ID "${toolbarId}".`); + } + + return toolbar; +}; diff --git a/app/src/InitReadOnlyToggle.ts b/app/src/InitReadOnlyToggle.ts index 47a2d4ed74..d9c1f074d3 100644 --- a/app/src/InitReadOnlyToggle.ts +++ b/app/src/InitReadOnlyToggle.ts @@ -1,8 +1,6 @@ -export interface ReadOnlyToggleConfig { - /** - * ID of toolbar button to add the read only toggle button to. - */ - toolbarId: string; +import { ApplicationToolbarConfig, requireApplicationToolbar } from "./ApplicationToolbar"; + +export interface ReadOnlyToggleConfig extends ApplicationToolbarConfig { /** * Callback to trigger on read-only change. * @@ -18,13 +16,8 @@ const readWriteState = "read-write"; const readOnlyState = "read-only"; export const initReadOnlyToggle = (config: ReadOnlyToggleConfig): void => { - const { toolbarId, onToggle } = config; - const toolbar = document.getElementById(toolbarId); - - if (!toolbar) { - throw new Error(`Cannot find toolbar element having ID "${toolbarId}".`); - } - + const { onToggle } = config; + const toolbar = requireApplicationToolbar(config); const button = document.createElement("button"); button.id = readOnlyModeButtonId; diff --git a/app/src/editors/default.ts b/app/src/editors/default.ts index cfc8357f64..ed845784d8 100644 --- a/app/src/editors/default.ts +++ b/app/src/editors/default.ts @@ -392,7 +392,6 @@ export const createDefaultEditor = (language = "en") => { (newEditor.plugins.get("Differencing") as Differencing)?.activateDifferencing(); initReadOnlyToggle({ - toolbarId: "applicationToolbar", onToggle: (readOnly) => { if (readOnly) { newEditor.enableReadOnlyMode("exampleApp"); diff --git a/app/src/index.ts b/app/src/index.ts index e5443bb839..183be55b8a 100644 --- a/app/src/index.ts +++ b/app/src/index.ts @@ -1,4 +1,4 @@ -import { setupPreview } from "./preview"; +import { initPreview } from "./preview"; import { createDefaultEditor } from "./editors/default"; import { createBBCodeEditor } from "./editors/bbCode"; @@ -79,6 +79,6 @@ initToggleEditorTabs(); const lang = initLanguage(); -setupPreview(); +initPreview(); createDefaultEditor(lang); createBBCodeEditor(lang); diff --git a/app/src/preview.ts b/app/src/preview.ts index bdd770712a..19bdf99f36 100644 --- a/app/src/preview.ts +++ b/app/src/preview.ts @@ -1,51 +1,91 @@ import { dataFormatter } from "./DataFormatter"; +import { ApplicationToolbarConfig, requireApplicationToolbar } from "./ApplicationToolbar"; -const WITH_PREVIEW_CLASS = "with-preview"; -const getPreviewPanel = (): HTMLElement | null => document.getElementById("preview"); +const previewToggleButtonId = "previewToggle"; +const showPreviewBtnLabel = "Show Preview"; +const hidePreviewBtnLabel = "Hide Preview"; +const visibleState = "visible"; +const hiddenState = "hidden"; -const getEditor = () => document.getElementsByClassName("ck-editor")[0]; +const withPreviewClass = "with-preview"; +const defaultPreviewPanelId = "preview"; +const previewClass = "preview"; -const setupPreview = () => { - const preview = getPreviewPanel(); - if (!preview) { - throw new Error("No Preview Panel found."); - } - preview.innerText = "waiting for ckeditor changes..."; -}; +export interface PreviewConfig extends ApplicationToolbarConfig { + /** + * ID of the preview panel. Defaults to `preview`. + */ + previewId?: string; +} -const updatePreview = (data: string, formatter: keyof typeof dataFormatter = "xml") => { - const preview = getPreviewPanel(); - if (!preview) { - return; - } - preview.innerText = dataFormatter[formatter](data, "empty"); -}; +export const initPreview = (config?: PreviewConfig) => { + const { previewId = defaultPreviewPanelId } = config ?? {}; + + const toolbar = requireApplicationToolbar(config); + const preview = document.getElementById(previewId); + const previewParent = preview?.parentElement; -const renderPreviewButton = () => { - const preview = getPreviewPanel(); if (!preview) { - return; + throw new Error(`Cannot find preview element having ID "${previewId}".`); } - const previewButton = document.querySelector("#previewButton"); - if (!previewButton) { - return; + + if (!previewParent) { + throw new Error(`Preview with ID "${previewId}" misses required parent element.`); } - previewButton.addEventListener("click", () => { - preview.hidden = !preview.hidden; - if (preview.hidden) { - // remove preview-mode - getEditor().classList.remove(WITH_PREVIEW_CLASS); - previewButton.textContent = "Show XML Preview"; - preview.classList.add("hidden"); + + const button = document.createElement("button"); + + button.id = previewToggleButtonId; + button.title = `Shows preview of data as they would be stored via external service like CoreMedia CMS.`; + button.textContent = showPreviewBtnLabel; + button.dataset.currentState = hiddenState; + + document.body.dataset.previewId = previewId; + preview.classList.add(previewClass); + preview.innerText = "No data received yet."; + + toolbar.appendChild(button); + + const showPreview = () => { + previewParent.classList.add(withPreviewClass); + button.textContent = hidePreviewBtnLabel; + button.dataset.currentState = visibleState; + }; + + const hidePreview = () => { + previewParent.classList.remove(withPreviewClass); + button.textContent = showPreviewBtnLabel; + button.dataset.currentState = hiddenState; + }; + + const isVisible = () => button.dataset.currentState === visibleState; + + const togglePreview = () => { + if (isVisible()) { + hidePreview(); } else { - // set preview-mode - getEditor().classList.add(WITH_PREVIEW_CLASS); - previewButton.textContent = "Hide XML Preview"; - preview.classList.remove("hidden"); + showPreview(); } - }); + }; + + button.addEventListener("click", togglePreview); + + // The initial state of the preview. + hidePreview(); }; -renderPreviewButton(); +const getPreviewPanel = (): HTMLElement => { + const previewId = document.body.dataset.previewId; + if (!previewId) { + throw new Error(`Preview ID not exposed at body.`); + } + const preview = document.getElementById(previewId); + if (!preview) { + throw new Error(`Preview with ID ${previewId} as denoted by body does not exist.`); + } + return preview; +}; -export { setupPreview, updatePreview }; +export const updatePreview = (data: string, formatter: keyof typeof dataFormatter = "xml") => { + getPreviewPanel().innerText = dataFormatter[formatter](data, "empty"); +}; From 88c8aca193638acd9f52a33e52d5df11846292b6 Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Wed, 27 Sep 2023 17:16:21 +0200 Subject: [PATCH 021/403] refactor: Refactor To Toggle By Data Type Introduced a general concept for toggling states with focus on being able to switch the editor depending on the data-type. State is stored in hash-parameters, so that it can be restored on reload or even triggered directly. --- app/sample/index.html | 14 -- app/sample/styles.css | 23 --- app/src/ApplicationState.ts | 73 +++++++++ app/src/CKEditorInstanceFactory.ts | 4 + app/src/DataTypeSwitch.ts | 16 ++ app/src/HashParams.ts | 94 +++++++++--- app/src/SwitchButton.ts | 57 +++++++ app/src/UiLanguageSwitch.ts | 16 ++ app/src/createCKEditorInstance.ts | 155 ++++++++++++++++++++ app/src/editors/bbCode.ts | 47 ++---- app/src/editors/{default.ts => richtext.ts} | 85 ++--------- app/src/index.ts | 76 +--------- 12 files changed, 429 insertions(+), 231 deletions(-) create mode 100644 app/src/ApplicationState.ts create mode 100644 app/src/CKEditorInstanceFactory.ts create mode 100644 app/src/DataTypeSwitch.ts create mode 100644 app/src/SwitchButton.ts create mode 100644 app/src/UiLanguageSwitch.ts create mode 100644 app/src/createCKEditorInstance.ts rename app/src/editors/{default.ts => richtext.ts} (82%) diff --git a/app/sample/index.html b/app/sample/index.html index 9ebd500a2e..d634879441 100644 --- a/app/sample/index.html +++ b/app/sample/index.html @@ -29,8 +29,6 @@

Website
  • Debug Logging
  • -
  • Language:
  • @@ -54,27 +52,15 @@

    CKEditor 5: CoreMedia Plugin Showcase

    -
    - - -
    -
    -
    -
    - - -
    -
    -
    diff --git a/app/sample/styles.css b/app/sample/styles.css index 493be4450c..0c1a7d52a3 100644 --- a/app/sample/styles.css +++ b/app/sample/styles.css @@ -360,26 +360,3 @@ span.image-inline[xdiff\:changetype="diff-conflict-image"] > img, span.html-obje font-size: 1.5em; } } - - -.editor-tab { - padding: 10px 20px; - background-color: white; - color: #333; - border: 1px solid var(--ck-color-base-border); - margin-top: 16px; - margin-bottom: 4px; - opacity: 0.5; -} - -.editor-tab:hover { - opacity: 0.3; -} - -.editor-tab.active { - opacity: 1; -} - -.editor-tab.active:hover { - opacity: 0.8; -} diff --git a/app/src/ApplicationState.ts b/app/src/ApplicationState.ts new file mode 100644 index 0000000000..a5b7adc036 --- /dev/null +++ b/app/src/ApplicationState.ts @@ -0,0 +1,73 @@ +import { setHashParam } from "./HashParams"; + +export type InspectorState = "expanded" | "collapsed"; +export type CompatibilityMode = "v10" | "latest"; +export type DataType = "richtext" | "bbcode"; +export type UiLanguage = "en" | "de"; + +export class ApplicationState { + /** + * Language for CKEditor 5 UI. + */ + readonly #uiLanguage: UiLanguage; + /** + * Signals, if to open the inspector expanded or collapsed by default. + */ + readonly #inspector: InspectorState; + /** + * Plugin version compatibility mode to apply. + * + * * `v10`: Up to version 10 we provided different data-processing for + * CoreMedia Rich Text. + * * `latest`: Just assume the latest plugin version. + */ + readonly #compatibility: "v10" | "latest"; + /** + * The data type to support. + */ + readonly #dataType: "richtext" | "bbcode"; + + constructor(config: Record = {}) { + const { uiLanguage, inspector, compatibility, dataType } = config; + + this.#uiLanguage = typeof uiLanguage === "string" && uiLanguage.toLowerCase() === "de" ? "de" : "en"; + this.#inspector = + typeof inspector === "string" && inspector.toLowerCase() === "expanded" ? "expanded" : "collapsed"; + this.#compatibility = typeof compatibility === "string" && compatibility.toLowerCase() === "v10" ? "v10" : "latest"; + this.#dataType = typeof dataType === "string" && dataType.toLowerCase() === "bbcode" ? "bbcode" : "richtext"; + } + + get uiLanguage(): UiLanguage { + return this.#uiLanguage; + } + + set uiLanguage(language: UiLanguage) { + if (language !== this.#uiLanguage) { + setHashParam("uiLanguage", language, true); + } + } + + get inspector(): InspectorState { + return this.#inspector; + } + + get compatibility(): CompatibilityMode { + return this.#compatibility; + } + + set compatibility(mode) { + if (this.#compatibility !== mode) { + setHashParam("compatibility", mode, true); + } + } + + get dataType(): DataType { + return this.#dataType; + } + + set dataType(dataType) { + if (this.#dataType !== dataType) { + setHashParam("dataType", dataType, true); + } + } +} diff --git a/app/src/CKEditorInstanceFactory.ts b/app/src/CKEditorInstanceFactory.ts new file mode 100644 index 0000000000..7629a576d0 --- /dev/null +++ b/app/src/CKEditorInstanceFactory.ts @@ -0,0 +1,4 @@ +import { ApplicationState } from "./ApplicationState"; +import { ClassicEditor } from "@ckeditor/ckeditor5-editor-classic"; + +export type CKEditorInstanceFactory = (sourceElement: HTMLElement, state: ApplicationState) => Promise; diff --git a/app/src/DataTypeSwitch.ts b/app/src/DataTypeSwitch.ts new file mode 100644 index 0000000000..a7765c87b0 --- /dev/null +++ b/app/src/DataTypeSwitch.ts @@ -0,0 +1,16 @@ +import { SwitchButton, SwitchButtonConfig } from "./SwitchButton"; + +const dataTypes = { + ["richtext" as const]: "Rich Text", + ["bbcode" as const]: "BBCode", +}; + +export const initDataTypeSwitch = (config: SwitchButtonConfig): void => { + new SwitchButton({ + id: "dataTypeSwitch", + default: "richtext", + states: dataTypes, + label: "Data Type", + ...config, + }).init(); +}; diff --git a/app/src/HashParams.ts b/app/src/HashParams.ts index e68f22a3f7..411758f219 100644 --- a/app/src/HashParams.ts +++ b/app/src/HashParams.ts @@ -4,6 +4,79 @@ */ export const hashParamRegExp = /([^=]*)=(.*)/; +export const getHashParams = (): Record => { + // Check for `window`: Required when used from within Jest tests, where + // 'jsdom' is not available. + const { location } = window ?? {}; + if (!location) { + return {}; + } + const { hash: rawHash } = location; + if (rawHash.length === 0) { + return {}; + } + // substring: Remove hash + const hash: string = rawHash.substring(1); + const hashParams: string[] = hash.split(/&/); + const parsedHashParams: Record = {}; + for (const hashParam of hashParams) { + const paramMatch: RegExpExecArray | null = hashParamRegExp.exec(hashParam); + let key: string; + let value: string | boolean; + if (paramMatch) { + key = paramMatch[1]; + + const rawValue = paramMatch[2]; + + switch (rawValue.trim().toLowerCase()) { + case "": + // Map empty String to truthy value. + value = true; + break; + case "true": + case "on": + value = true; + break; + case "false": + case "off": + value = false; + break; + default: + value = rawValue; + } + } else { + // We have a toggle hash param. + key = hashParam; + value = true; + } + parsedHashParams[key] = value; + } + return parsedHashParams; +}; + +export const toHashParam = (hashParams: Record): string => { + let result = ""; + for (const [key, value] of Object.entries(hashParams)) { + result = `${result}${result ? "&" : ""}${encodeURIComponent(key)}=${encodeURIComponent(value)}`; + } + return result; +}; + +export const setHashParam = (key: string, value: string | boolean, reload = false): void => { + const { location, history } = window ?? {}; + if (!location) { + console.info(`Skipped setting hash parameter ${key} to ${value} as window location and/or history is unknown.`); + return; + } + + const hashParams = getHashParams(); + hashParams[key] = value; + location.hash = toHashParam(hashParams); + if (reload) { + location.reload(); + } +}; + /** * Get hash parameter value from `window.location`. * @@ -12,25 +85,8 @@ export const hashParamRegExp = /([^=]*)=(.*)/; export const getHashParam = (key: string | undefined): string | boolean => { // Check for `window`: Required when used from within Jest tests, where // 'jsdom' is not available. - if (key === undefined || typeof window === "undefined") { + if (key === undefined) { return false; } - if (window.location?.hash) { - // substring: Remove hash - const hash: string = window.location.hash.substring(1); - const hashParams: string[] = hash.split(/&/); - for (const hashParam of hashParams) { - if (key === hashParam) { - return true; - } - const paramMatch: RegExpExecArray | null = hashParamRegExp.exec(hashParam); - if (paramMatch) { - if (paramMatch[1] === key) { - // Map empty String to truthy value. - return paramMatch[2] || true; - } - } - } - } - return false; + return getHashParams()[key] ?? false; }; diff --git a/app/src/SwitchButton.ts b/app/src/SwitchButton.ts new file mode 100644 index 0000000000..3da5bb2156 --- /dev/null +++ b/app/src/SwitchButton.ts @@ -0,0 +1,57 @@ +import { ApplicationToolbarConfig, requireApplicationToolbar } from "./ApplicationToolbar"; + +export interface SwitchButtonConfig extends ApplicationToolbarConfig { + id?: string; + default?: T; + states?: Record; + label?: string; + onSwitch: (state: T) => void; +} + +const sortKeysByValue = (states: Record): T[] => + Object.entries(states) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([k]) => k as T); + +export type StrictSwitchButtonConfig = Required, "toolbarId">> & + ApplicationToolbarConfig; + +export class SwitchButton { + readonly config: StrictSwitchButtonConfig; + + constructor(config: StrictSwitchButtonConfig) { + this.config = config; + } + + init() { + const { id, default: defaultState, states, label, onSwitch } = this.config; + const toolbar = requireApplicationToolbar(this.config); + const button = document.createElement("button"); + const keys = sortKeysByValue(states); + + button.id = id; + toolbar.appendChild(button); + + const nextState = (current: T): T => { + const nextIdx = keys.indexOf(current) + 1; + if (nextIdx >= keys.length) { + return keys[0]; + } + return keys[nextIdx]; + }; + + const switchState = () => { + const switchTo = (button.dataset.next ?? defaultState) as T; + const switchNext = nextState(switchTo); + button.title = `Press to switch to ${states[switchNext]}.`; + button.textContent = `${label}: ${states[switchTo]}`; + button.dataset.next = switchNext; + onSwitch(switchTo); + }; + + button.addEventListener("click", () => switchState()); + + // Init with default state. + switchState(); + } +} diff --git a/app/src/UiLanguageSwitch.ts b/app/src/UiLanguageSwitch.ts new file mode 100644 index 0000000000..f70eb5fa00 --- /dev/null +++ b/app/src/UiLanguageSwitch.ts @@ -0,0 +1,16 @@ +import { SwitchButton, SwitchButtonConfig } from "./SwitchButton"; + +const uiLanguages = { + ["en" as const]: "English", + ["de" as const]: "German", +}; + +export const initUiLanguageSwitch = (config: SwitchButtonConfig): void => { + new SwitchButton({ + id: "uiLanguageSwitch", + default: "en", + states: uiLanguages, + label: "UI Language", + ...config, + }).init(); +}; diff --git a/app/src/createCKEditorInstance.ts b/app/src/createCKEditorInstance.ts new file mode 100644 index 0000000000..370dc61670 --- /dev/null +++ b/app/src/createCKEditorInstance.ts @@ -0,0 +1,155 @@ +import { ApplicationState } from "./ApplicationState"; +import { ClassicEditor } from "@ckeditor/ckeditor5-editor-classic"; +import { Command, Editor } from "@ckeditor/ckeditor5-core"; +import { CKEditorInstanceFactory } from "./CKEditorInstanceFactory"; +import { Differencing } from "@coremedia/ckeditor5-coremedia-differencing"; +import { initReadOnlyToggle } from "./InitReadOnlyToggle"; +import { updatePreview } from "./preview"; +import { createRichTextEditor } from "./editors/richtext"; +import { createBBCodeEditor } from "./editors/bbCode"; +import { initDataTypeSwitch } from "./DataTypeSwitch"; +import { initUiLanguageSwitch } from "./UiLanguageSwitch"; + +/** + * Typings for CKEditorInspector, as it does not ship with typings yet. + */ +// See https://github.com/ckeditor/ckeditor5-inspector/issues/173 +// eslint-disable-next-line @typescript-eslint/no-extraneous-class +declare class CKEditorInspector { + static attach(editorOrConfig: Editor | Record, options?: { isCollapsed?: boolean }): string[]; +} + +export const editorElementId = "editor"; + +export interface CKEditorInstanceFactories { + bbcode: CKEditorInstanceFactory; + richtext: CKEditorInstanceFactory; +} + +export const ckEditorInstanceFactories: CKEditorInstanceFactories = { + bbcode: createBBCodeEditor, + richtext: createRichTextEditor, +}; + +const attachInspector = (editor: Editor, { dataType, inspector }: ApplicationState): string[] => + CKEditorInspector.attach( + { + [dataType]: editor, + }, + { + // With hash parameter #expandInspector you may expand the + // inspector by default. + isCollapsed: inspector === "collapsed", + } + ); + +const optionallyActivateDifferencing = (editor: Editor): void => { + if (editor.plugins.has(Differencing)) { + editor.plugins.get(Differencing).activateDifferencing(); + } +}; + +/** + * Convenience method to help test with the undo stack. + * + * @param editor - editor to apply new method to. + */ +const registerResetUndo = (editor: Editor): void => { + const undoCommand: Command | undefined = editor.commands.get("undo"); + + if (undoCommand) { + //@ts-expect-error Editor extension, no typing available. + // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-return + editor.resetUndo = () => undoCommand.clearStack(); + console.log("Registered `editor.resetUndo()` to clear undo history."); + } +}; + +/** + * Register the editor as global variable for better support when + * debugging in DevTools. + * + * May also be used by tests that may rely on setting the global variable + * late when all adaptations have been applied. + * + * @param editor - editor to register globally + */ +const registerGlobalEditor = (editor: Editor): void => { + // Do it late, so that we also have a clear signal (e.g., to integration + // tests), that the editor is ready. + //@ts-expect-error Unknown, but we set it. + window.editor = editor; + console.log("Exposed editor instance as `editor`."); +}; + +/** + * Update preview with data from the editor initially. + * + * @param editor - editor to get data from + * @param dataType - type of data + */ +const initializePreviewData = (editor: ClassicEditor, { dataType }: ApplicationState): void => { + switch (dataType) { + case "richtext": + updatePreview(editor.getData(), "xml"); + break; + default: + updatePreview(editor.getData(), "text"); + } +}; + +export const createCKEditorInstance = (state: ApplicationState): Promise => { + const sourceElement = document.getElementById(editorElementId); + + if (!sourceElement) { + throw new Error(`Required element with id ${editorElementId} not defined in HTML.`); + } + + const { dataType } = state; + let factory: CKEditorInstanceFactory; + switch (dataType) { + case "richtext": + factory = ckEditorInstanceFactories.richtext; + break; + case "bbcode": + factory = ckEditorInstanceFactories.bbcode; + break; + default: + throw new Error(`Unknown data type: ${dataType}`); + } + return factory(sourceElement, state).then(async (editor) => { + const { dataType, uiLanguage } = state; + + initDataTypeSwitch({ + default: dataType, + onSwitch(mode): void { + state.dataType = mode; + }, + }); + + initUiLanguageSwitch({ + default: uiLanguage, + onSwitch(lang): void { + state.uiLanguage = lang; + }, + }); + + attachInspector(editor, state); + optionallyActivateDifferencing(editor); + initReadOnlyToggle({ + onToggle: (readOnly) => { + if (readOnly) { + editor.enableReadOnlyMode("exampleApp"); + } else { + editor.disableReadOnlyMode("exampleApp"); + } + }, + }); + registerResetUndo(editor); + initializePreviewData(editor, state); + + registerGlobalEditor(editor); + + return editor; + }); +}; diff --git a/app/src/editors/bbCode.ts b/app/src/editors/bbCode.ts index 09c3d29f80..6d068b8f53 100644 --- a/app/src/editors/bbCode.ts +++ b/app/src/editors/bbCode.ts @@ -7,34 +7,22 @@ import { Heading } from "@ckeditor/ckeditor5-heading"; import { Paragraph } from "@ckeditor/ckeditor5-paragraph"; import { SourceEditing } from "@ckeditor/ckeditor5-source-editing"; -import { Editor } from "@ckeditor/ckeditor5-core"; -import { getHashParam } from "../HashParams"; import { Link } from "@ckeditor/ckeditor5-link"; +import { CKEditorInstanceFactory } from "../CKEditorInstanceFactory"; +import { ApplicationState } from "../ApplicationState"; -/** - * Typings for CKEditorInspector, as it does not ship with typings yet. - */ -// See https://github.com/ckeditor/ckeditor5-inspector/issues/173 -// eslint-disable-next-line @typescript-eslint/no-extraneous-class -declare class CKEditorInspector { - static attach(editorOrConfig: Editor | Record, options?: { isCollapsed?: boolean }): string[]; -} - -const editorElementSelector = "#bbcodeEditor"; - -export const createBBCodeEditor = (language = "en") => { - const sourceElement = document.querySelector(editorElementSelector) as HTMLElement; - if (!sourceElement) { - throw new Error(`No element with id ${editorElementSelector} defined in html. Nothing to create the editor in.`); - } - - ClassicEditor.create(document.querySelector(editorElementSelector) as HTMLElement, { +export const createBBCodeEditor: CKEditorInstanceFactory = ( + sourceElement: HTMLElement, + state: ApplicationState +): Promise => { + const { uiLanguage } = state; + return ClassicEditor.create(sourceElement, { placeholder: "Type your text here...", plugins: [Autosave, Bold, Essentials, Heading, Italic, Underline, Paragraph, SourceEditing, Link, BBCode], toolbar: ["undo", "redo", "|", "heading", "|", "bold", "italic", "underline", "|", "link", "|", "sourceEditing"], language: { // Language switch only applies to editor instance. - ui: language, + ui: uiLanguage, // Won't change the language of content. content: "en", }, @@ -45,20 +33,5 @@ export const createBBCodeEditor = (language = "en") => { return Promise.resolve(); }, }, - }) - .then((newEditor: ClassicEditor) => { - CKEditorInspector.attach( - { - "bbcode-editor": newEditor, - }, - { - // With hash parameter #expandInspector you may expand the - // inspector by default. - isCollapsed: !getHashParam("expandInspector"), - } - ); - }) - .catch((error) => { - console.error(error); - }); + }); }; diff --git a/app/src/editors/default.ts b/app/src/editors/richtext.ts similarity index 82% rename from app/src/editors/default.ts rename to app/src/editors/richtext.ts index ed845784d8..8115bdc119 100644 --- a/app/src/editors/default.ts +++ b/app/src/editors/richtext.ts @@ -30,7 +30,6 @@ import { ContentImagePlugin } from "@coremedia/ckeditor5-coremedia-images"; import { FontMapper as CoreMediaFontMapper } from "@coremedia/ckeditor5-font-mapper"; import MockStudioIntegration from "@coremedia/ckeditor5-coremedia-studio-integration-mock/src/MockStudioIntegration"; -import { updatePreview } from "../preview"; import { initExamplesAndBindTo } from "../example-data"; import { CoreMediaStudioEssentials, @@ -41,7 +40,7 @@ import { import { initInputExampleContent } from "../inputExampleContents"; import { COREMEDIA_MOCK_CONTENT_PLUGIN } from "@coremedia/ckeditor5-coremedia-studio-integration-mock/src/content/MockContentPlugin"; -import { Command, Editor, icons, PluginConstructor } from "@ckeditor/ckeditor5-core"; +import { Editor, icons, PluginConstructor } from "@ckeditor/ckeditor5-core"; import { saveData } from "../dataFacade"; import MockInputExamplePlugin from "@coremedia/ckeditor5-coremedia-studio-integration-mock/src/content/MockInputExamplePlugin"; import PasteContentPlugin from "@coremedia/ckeditor5-coremedia-content-clipboard/src/paste/PasteContentPlugin"; @@ -58,15 +57,8 @@ import type { LatestCoreMediaRichTextConfig, V10CoreMediaRichTextConfig, } from "@coremedia/ckeditor5-coremedia-richtext"; -import { initReadOnlyToggle } from "../InitReadOnlyToggle"; -/** - * Typings for CKEditorInspector, as it does not ship with typings yet. - */ -// See https://github.com/ckeditor/ckeditor5-inspector/issues/173 -// eslint-disable-next-line @typescript-eslint/no-extraneous-class -declare class CKEditorInspector { - static attach(editorOrConfig: Editor | Record, options?: { isCollapsed?: boolean }): string[]; -} +import { CKEditorInstanceFactory } from "../CKEditorInstanceFactory"; +import { ApplicationState } from "../ApplicationState"; const { objectInline: withinTextIcon, @@ -135,8 +127,6 @@ const linkAttributesConfig: LinkAttributesConfig = getHashParam("skipLinkAttribu ], }; -const editorElementSelector = "#editor"; - const getRichTextConfig = ( richTextCompatibility: string | true ): Partial | V10CoreMediaRichTextConfig => { @@ -156,13 +146,12 @@ const getRichTextConfig = ( }; }; -export const createDefaultEditor = (language = "en") => { - const sourceElement = document.querySelector(editorElementSelector) as HTMLElement; - if (!sourceElement) { - throw new Error(`No element with id ${editorElementSelector} defined in html. Nothing to create the editor in.`); - } - - ClassicEditor.create(sourceElement, { +export const createRichTextEditor: CKEditorInstanceFactory = ( + sourceElement: HTMLElement, + state: ApplicationState +): Promise => { + const { uiLanguage } = state; + return ClassicEditor.create(sourceElement, { placeholder: "Type your text here...", plugins: [ ...imagePlugins, @@ -340,7 +329,7 @@ export const createDefaultEditor = (language = "en") => { }, language: { // Language switch only applies to editor instance. - ui: language, + ui: uiLanguage, // Won't change the language of content. content: "en", }, @@ -364,6 +353,7 @@ export const createDefaultEditor = (language = "en") => { { name: "mark", inherit: "span" }, ], }, + // @ts-expect-error - TODO: Typing issues as it seems. [COREMEDIA_LINK_CONFIG_KEY]: { linkBalloon: { keepOpen: { @@ -376,52 +366,9 @@ export const createDefaultEditor = (language = "en") => { // Demonstrates, how you may add more contents on the fly. contents: [{ id: 2, name: "Some Example Document", type: "document" }], }, - }) - .then((newEditor: ClassicEditor) => { - CKEditorInspector.attach( - { - "main-editor": newEditor, - }, - { - // With hash parameter #expandInspector you may expand the - // inspector by default. - isCollapsed: !getHashParam("expandInspector"), - } - ); - - (newEditor.plugins.get("Differencing") as Differencing)?.activateDifferencing(); - - initReadOnlyToggle({ - onToggle: (readOnly) => { - if (readOnly) { - newEditor.enableReadOnlyMode("exampleApp"); - } else { - newEditor.disableReadOnlyMode("exampleApp"); - } - }, - }); - initExamplesAndBindTo(newEditor); - initInputExampleContent(newEditor); - - const undoCommand: Command | undefined = newEditor.commands.get("undo"); - - if (undoCommand) { - //@ts-expect-error Editor extension, no typing available. - // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-return - newEditor.resetUndo = () => undoCommand.clearStack(); - console.log("Registered `editor.resetUndo()` to clear undo history."); - } - - // Do it late, so that we also have a clear signal (e.g., to integration - // tests), that the editor is ready. - //@ts-expect-error Unknown, but we set it. - window.editor = newEditor; - console.log("Exposed editor instance as `editor`."); - - // Initialize Preview - updatePreview(newEditor.getData()); - }) - .catch((error) => { - console.error(error); - }); + }).then((newEditor: ClassicEditor) => { + initExamplesAndBindTo(newEditor); + initInputExampleContent(newEditor); + return newEditor; + }); }; diff --git a/app/src/index.ts b/app/src/index.ts index 183be55b8a..4ff20207ea 100644 --- a/app/src/index.ts +++ b/app/src/index.ts @@ -1,6 +1,7 @@ import { initPreview } from "./preview"; -import { createDefaultEditor } from "./editors/default"; -import { createBBCodeEditor } from "./editors/bbCode"; +import { createCKEditorInstance } from "./createCKEditorInstance"; +import { ApplicationState } from "./ApplicationState"; +import { getHashParams } from "./HashParams"; // setup input example content IFrame const showHideExampleContentButton = document.querySelector("#inputExampleContentButton"); @@ -14,71 +15,8 @@ if (showHideExampleContentButton && inputExampleContentFrame) { }); } -const initLanguage = () => { - const queryString = window.location.search; - const urlParams = new URLSearchParams(queryString); - const languageFlag = "lang"; - const language = urlParams.get(languageFlag)?.toLowerCase() ?? "en"; - const languageToggle = document.getElementById(languageFlag); - if (!languageToggle) { - throw Error("No language toggle element found."); - } - let label, hrefLang; - if (language === "de") { - label = "EN | DE"; - hrefLang = "en"; - } else { - label = "EN | DE"; - hrefLang = "de"; - } - languageToggle.setAttribute("href", `.?lang=${hrefLang}`); - languageToggle.innerHTML = label; - return language; -}; - -const initToggleEditorTabs = () => { - const hideAllEditorRows = () => { - const rows = document.getElementsByClassName("editor-row"); - Array.from(rows).forEach((rowEl) => { - const divElement = rowEl as HTMLDivElement; - if (divElement) { - divElement.style.display = "none"; - } - }); - - const tabs = document.getElementsByClassName("editor-tab"); - Array.from(tabs).forEach((tabsEl) => { - const divElement = tabsEl as HTMLDivElement; - if (divElement) { - divElement.classList.remove("active"); - } - }); - }; - - const initToggleEditorTab = (buttonSelector: string, editorRowSelector: string) => { - const editorTab = document.querySelector(buttonSelector) as HTMLButtonElement; - editorTab.addEventListener("click", () => { - const editorRow = document.querySelector(editorRowSelector) as HTMLDivElement; - hideAllEditorRows(); - editorTab.classList.add("active"); - editorRow.style.display = "block"; - }); - }; - - initToggleEditorTab("#defaultEditorTab", "#defaultEditorRow"); - initToggleEditorTab("#bbcodeEditorTab", "#bbcodeEditorRow"); - - hideAllEditorRows(); - const defaultEditorRow = document.querySelector("#defaultEditorRow") as HTMLDivElement; - const defaultEditorTab = document.querySelector("#defaultEditorTab") as HTMLButtonElement; - defaultEditorTab.classList.add("active"); - defaultEditorRow.style.display = "block"; -}; - -initToggleEditorTabs(); - -const lang = initLanguage(); - initPreview(); -createDefaultEditor(lang); -createBBCodeEditor(lang); + +void createCKEditorInstance(new ApplicationState(getHashParams())).catch((error) => { + console.error(error); +}); From 11c8ab3b9a9086d034781471db25c279258928d4 Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Thu, 28 Sep 2023 08:53:35 +0200 Subject: [PATCH 022/403] refactor: Refactor Read Only Toggle Now also using the SwitchButton. We decided to extend the SwitchButton, so that it ships with the option to delay switching on demand, to cover a relevant use-case for switching read-only mode (we require the delay, so that we may, for example, open some dialogs and see, that they respond for a switch of the read-only mode). --- app/src/DataTypeSwitch.ts | 2 +- app/src/InitReadOnlyToggle.ts | 72 ------------------------------- app/src/ReadOnlySwitch.ts | 19 ++++++++ app/src/SwitchButton.ts | 41 ++++++++++++++---- app/src/UiLanguageSwitch.ts | 2 +- app/src/createCKEditorInstance.ts | 8 ++-- 6 files changed, 58 insertions(+), 86 deletions(-) delete mode 100644 app/src/InitReadOnlyToggle.ts create mode 100644 app/src/ReadOnlySwitch.ts diff --git a/app/src/DataTypeSwitch.ts b/app/src/DataTypeSwitch.ts index a7765c87b0..9c2a9ba06c 100644 --- a/app/src/DataTypeSwitch.ts +++ b/app/src/DataTypeSwitch.ts @@ -1,6 +1,6 @@ import { SwitchButton, SwitchButtonConfig } from "./SwitchButton"; -const dataTypes = { +export const dataTypes = { ["richtext" as const]: "Rich Text", ["bbcode" as const]: "BBCode", }; diff --git a/app/src/InitReadOnlyToggle.ts b/app/src/InitReadOnlyToggle.ts deleted file mode 100644 index d9c1f074d3..0000000000 --- a/app/src/InitReadOnlyToggle.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { ApplicationToolbarConfig, requireApplicationToolbar } from "./ApplicationToolbar"; - -export interface ReadOnlyToggleConfig extends ApplicationToolbarConfig { - /** - * Callback to trigger on read-only change. - * - * @param readOnly - read only state - */ - onToggle: (readOnly: boolean) => void; -} - -const readOnlyModeButtonId = "readOnlyMode"; -const enableReadOnlyBtnLabel = "Enable Read-Only-Mode"; -const disableReadOnlyBtnLabel = "Disable Read-Only-Mode"; -const readWriteState = "read-write"; -const readOnlyState = "read-only"; - -export const initReadOnlyToggle = (config: ReadOnlyToggleConfig): void => { - const { onToggle } = config; - const toolbar = requireApplicationToolbar(config); - const button = document.createElement("button"); - - button.id = readOnlyModeButtonId; - button.title = "Delay Modifiers: Ctrl/Cmd: 10s, Shift: 60s, Ctrl/Cmd+Shift: 120s"; - button.textContent = enableReadOnlyBtnLabel; - button.dataset.currentState = readWriteState; - - toolbar.appendChild(button); - - const enableReadOnly = () => { - button.textContent = disableReadOnlyBtnLabel; - button.dataset.currentState = readOnlyState; - onToggle(true); - }; - - const disableReadOnly = () => { - button.textContent = enableReadOnlyBtnLabel; - button.dataset.currentState = readWriteState; - onToggle(false); - }; - - // Naive check, but should be ok. We cannot ask CKEditor directly if **we** - // are responsible for read-only state. - const isReadOnly = () => button.dataset.currentState === readOnlyState; - - let currentToggleDelay: number; - - const toggleState = (countDownSeconds: number) => { - if (countDownSeconds > 0) { - if (isReadOnly()) { - button.textContent = `R/W in ${countDownSeconds} s...`; - } else { - button.textContent = `R/O in ${countDownSeconds} s...`; - } - currentToggleDelay = window.setTimeout(toggleState, 1000, countDownSeconds - 1); - } else { - isReadOnly() ? disableReadOnly() : enableReadOnly(); - } - }; - - button.addEventListener("click", (evt) => { - window.clearTimeout(currentToggleDelay); - let countDownSeconds = 0; - const ctrlOrCommandKey = evt.ctrlKey || evt.metaKey; - if (evt.shiftKey) { - countDownSeconds = ctrlOrCommandKey ? 120 : 60; - } else if (ctrlOrCommandKey) { - countDownSeconds = 10; - } - toggleState(countDownSeconds); - }); -}; diff --git a/app/src/ReadOnlySwitch.ts b/app/src/ReadOnlySwitch.ts new file mode 100644 index 0000000000..bd84be3422 --- /dev/null +++ b/app/src/ReadOnlySwitch.ts @@ -0,0 +1,19 @@ +import { SwitchButton, SwitchButtonConfig } from "./SwitchButton"; + +export const readOnlyStates = { + ["rw" as const]: "Read Write", + ["ro" as const]: "Read Only", +}; + +const readOnlyModeButtonId = "readOnlyMode"; + +export const initReadOnlyToggle = (config: SwitchButtonConfig): void => { + new SwitchButton({ + id: readOnlyModeButtonId, + default: "rw", + states: readOnlyStates, + label: "Mode", + enableDelay: true, + ...config, + }).init(); +}; diff --git a/app/src/SwitchButton.ts b/app/src/SwitchButton.ts index 3da5bb2156..4daf23625f 100644 --- a/app/src/SwitchButton.ts +++ b/app/src/SwitchButton.ts @@ -5,6 +5,7 @@ export interface SwitchButtonConfig extends Applicati default?: T; states?: Record; label?: string; + enableDelay?: boolean; onSwitch: (state: T) => void; } @@ -13,9 +14,14 @@ const sortKeysByValue = (states: Record): .sort(([a], [b]) => a.localeCompare(b)) .map(([k]) => k as T); -export type StrictSwitchButtonConfig = Required, "toolbarId">> & +export type StrictSwitchButtonConfig = Required< + Omit, "toolbarId" | "enableDelay"> +> & + Pick, "enableDelay"> & ApplicationToolbarConfig; +const delayTitle = "Delay Modifiers: Ctrl/Cmd: 10s, Shift: 60s, Ctrl/Cmd+Shift: 120s"; + export class SwitchButton { readonly config: StrictSwitchButtonConfig; @@ -24,7 +30,7 @@ export class SwitchButton { } init() { - const { id, default: defaultState, states, label, onSwitch } = this.config; + const { id, default: defaultState, states, label, onSwitch, enableDelay = false } = this.config; const toolbar = requireApplicationToolbar(this.config); const button = document.createElement("button"); const keys = sortKeysByValue(states); @@ -40,16 +46,35 @@ export class SwitchButton { return keys[nextIdx]; }; - const switchState = () => { + let switchDelayTimer: number; + + const switchState = (countDownSeconds = 0) => { const switchTo = (button.dataset.next ?? defaultState) as T; const switchNext = nextState(switchTo); - button.title = `Press to switch to ${states[switchNext]}.`; - button.textContent = `${label}: ${states[switchTo]}`; - button.dataset.next = switchNext; - onSwitch(switchTo); + if (countDownSeconds <= 0) { + button.title = `Press to switch to ${states[switchNext]}.${enableDelay ? ` ${delayTitle}` : ""}`; + button.textContent = `${label}: ${states[switchTo]}`; + button.dataset.next = switchNext; + onSwitch(switchTo); + } else { + button.textContent = `${label}: ${states[switchTo]} (in ${countDownSeconds} s)`; + switchDelayTimer = window.setTimeout(switchState, 1000, countDownSeconds - 1); + } }; - button.addEventListener("click", () => switchState()); + button.addEventListener("click", (evt: MouseEvent): void => { + let countDownSeconds = 0; + if (enableDelay) { + window.clearTimeout(switchDelayTimer); + const ctrlOrCommandKey = evt.ctrlKey || evt.metaKey; + if (evt.shiftKey) { + countDownSeconds = ctrlOrCommandKey ? 120 : 60; + } else if (ctrlOrCommandKey) { + countDownSeconds = 10; + } + } + switchState(countDownSeconds); + }); // Init with default state. switchState(); diff --git a/app/src/UiLanguageSwitch.ts b/app/src/UiLanguageSwitch.ts index f70eb5fa00..0526b501df 100644 --- a/app/src/UiLanguageSwitch.ts +++ b/app/src/UiLanguageSwitch.ts @@ -1,6 +1,6 @@ import { SwitchButton, SwitchButtonConfig } from "./SwitchButton"; -const uiLanguages = { +export const uiLanguages = { ["en" as const]: "English", ["de" as const]: "German", }; diff --git a/app/src/createCKEditorInstance.ts b/app/src/createCKEditorInstance.ts index 370dc61670..b6091ff489 100644 --- a/app/src/createCKEditorInstance.ts +++ b/app/src/createCKEditorInstance.ts @@ -3,7 +3,7 @@ import { ClassicEditor } from "@ckeditor/ckeditor5-editor-classic"; import { Command, Editor } from "@ckeditor/ckeditor5-core"; import { CKEditorInstanceFactory } from "./CKEditorInstanceFactory"; import { Differencing } from "@coremedia/ckeditor5-coremedia-differencing"; -import { initReadOnlyToggle } from "./InitReadOnlyToggle"; +import { initReadOnlyToggle } from "./ReadOnlySwitch"; import { updatePreview } from "./preview"; import { createRichTextEditor } from "./editors/richtext"; import { createBBCodeEditor } from "./editors/bbCode"; @@ -117,7 +117,7 @@ export const createCKEditorInstance = (state: ApplicationState): Promise { + return factory(sourceElement, state).then((editor) => { const { dataType, uiLanguage } = state; initDataTypeSwitch({ @@ -137,8 +137,8 @@ export const createCKEditorInstance = (state: ApplicationState): Promise { - if (readOnly) { + onSwitch: (mode) => { + if (mode === "ro") { editor.enableReadOnlyMode("exampleApp"); } else { editor.disableReadOnlyMode("exampleApp"); From df10a914019679d0f95343dae6a0ed6b7c611450 Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Thu, 28 Sep 2023 08:55:32 +0200 Subject: [PATCH 023/403] refactor: Convert to async function --- app/src/createCKEditorInstance.ts | 72 ++++++++++++++++--------------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/app/src/createCKEditorInstance.ts b/app/src/createCKEditorInstance.ts index b6091ff489..d1f2cbb15a 100644 --- a/app/src/createCKEditorInstance.ts +++ b/app/src/createCKEditorInstance.ts @@ -98,7 +98,7 @@ const initializePreviewData = (editor: ClassicEditor, { dataType }: ApplicationS } }; -export const createCKEditorInstance = (state: ApplicationState): Promise => { +export const createCKEditorInstance = async (state: ApplicationState): Promise => { const sourceElement = document.getElementById(editorElementId); if (!sourceElement) { @@ -117,39 +117,41 @@ export const createCKEditorInstance = (state: ApplicationState): Promise { - const { dataType, uiLanguage } = state; - - initDataTypeSwitch({ - default: dataType, - onSwitch(mode): void { - state.dataType = mode; - }, - }); - - initUiLanguageSwitch({ - default: uiLanguage, - onSwitch(lang): void { - state.uiLanguage = lang; - }, - }); - - attachInspector(editor, state); - optionallyActivateDifferencing(editor); - initReadOnlyToggle({ - onSwitch: (mode) => { - if (mode === "ro") { - editor.enableReadOnlyMode("exampleApp"); - } else { - editor.disableReadOnlyMode("exampleApp"); - } - }, - }); - registerResetUndo(editor); - initializePreviewData(editor, state); - - registerGlobalEditor(editor); - - return editor; + + const editor = await factory(sourceElement, state); + + const { uiLanguage } = state; + + initDataTypeSwitch({ + default: dataType, + onSwitch(mode): void { + state.dataType = mode; + }, + }); + + initUiLanguageSwitch({ + default: uiLanguage, + onSwitch(lang): void { + state.uiLanguage = lang; + }, }); + + attachInspector(editor, state); + + optionallyActivateDifferencing(editor); + + initReadOnlyToggle({ + onSwitch: (mode) => { + if (mode === "ro") { + editor.enableReadOnlyMode("exampleApp"); + } else { + editor.disableReadOnlyMode("exampleApp"); + } + }, + }); + + registerResetUndo(editor); + initializePreviewData(editor, state); + registerGlobalEditor(editor); + return editor; }; From b86aadeb26b0ba29d0b770e36f173a7a7e5b14cc Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Thu, 28 Sep 2023 09:44:14 +0200 Subject: [PATCH 024/403] feat: Remember Read-Only State on Reload The current read-only mode is now also stored in hash-parameters and helps to start the application in a given mode right away. --- app/src/ApplicationState.ts | 18 +++++++++++++++--- app/src/createCKEditorInstance.ts | 6 +++--- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/app/src/ApplicationState.ts b/app/src/ApplicationState.ts index a5b7adc036..92705febea 100644 --- a/app/src/ApplicationState.ts +++ b/app/src/ApplicationState.ts @@ -4,6 +4,7 @@ export type InspectorState = "expanded" | "collapsed"; export type CompatibilityMode = "v10" | "latest"; export type DataType = "richtext" | "bbcode"; export type UiLanguage = "en" | "de"; +export type ReadOnlyMode = "rw" | "ro"; export class ApplicationState { /** @@ -21,20 +22,22 @@ export class ApplicationState { * CoreMedia Rich Text. * * `latest`: Just assume the latest plugin version. */ - readonly #compatibility: "v10" | "latest"; + readonly #compatibility: CompatibilityMode; /** * The data type to support. */ - readonly #dataType: "richtext" | "bbcode"; + readonly #dataType: DataType; + #readOnlyMode: ReadOnlyMode; constructor(config: Record = {}) { - const { uiLanguage, inspector, compatibility, dataType } = config; + const { uiLanguage, inspector, compatibility, dataType, readOnly } = config; this.#uiLanguage = typeof uiLanguage === "string" && uiLanguage.toLowerCase() === "de" ? "de" : "en"; this.#inspector = typeof inspector === "string" && inspector.toLowerCase() === "expanded" ? "expanded" : "collapsed"; this.#compatibility = typeof compatibility === "string" && compatibility.toLowerCase() === "v10" ? "v10" : "latest"; this.#dataType = typeof dataType === "string" && dataType.toLowerCase() === "bbcode" ? "bbcode" : "richtext"; + this.#readOnlyMode = typeof readOnly === "boolean" && readOnly ? "ro" : "rw"; } get uiLanguage(): UiLanguage { @@ -70,4 +73,13 @@ export class ApplicationState { setHashParam("dataType", dataType, true); } } + + get readOnlyMode(): ReadOnlyMode { + return this.#readOnlyMode; + } + + set readOnlyMode(mode) { + this.#readOnlyMode = mode; + setHashParam("readOnly", mode === "ro"); + } } diff --git a/app/src/createCKEditorInstance.ts b/app/src/createCKEditorInstance.ts index d1f2cbb15a..8cdfa0bcdb 100644 --- a/app/src/createCKEditorInstance.ts +++ b/app/src/createCKEditorInstance.ts @@ -105,7 +105,7 @@ export const createCKEditorInstance = async (state: ApplicationState): Promise { if (mode === "ro") { editor.enableReadOnlyMode("exampleApp"); } else { editor.disableReadOnlyMode("exampleApp"); } + state.readOnlyMode = mode; }, }); From 661f2fc490d2104571ba9d5458b30b60e33500b6 Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Thu, 28 Sep 2023 10:12:30 +0200 Subject: [PATCH 025/403] refactor: Use SwitchButton for Preview --- app/src/PreviewSwitch.ts | 18 ++++++++++++++ app/src/preview.ts | 51 +++++++++++++--------------------------- 2 files changed, 34 insertions(+), 35 deletions(-) create mode 100644 app/src/PreviewSwitch.ts diff --git a/app/src/PreviewSwitch.ts b/app/src/PreviewSwitch.ts new file mode 100644 index 0000000000..afdaef6f71 --- /dev/null +++ b/app/src/PreviewSwitch.ts @@ -0,0 +1,18 @@ +import { SwitchButton, SwitchButtonConfig } from "./SwitchButton"; + +export const previewStates = { + ["hidden" as const]: "Hidden", + ["visible" as const]: "Visible", +}; + +const previewSwitchBtnId = "previewSwitch"; + +export const initPreviewSwitch = (config: SwitchButtonConfig): void => { + new SwitchButton({ + id: previewSwitchBtnId, + default: "hidden", + states: previewStates, + label: "Preview", + ...config, + }).init(); +}; diff --git a/app/src/preview.ts b/app/src/preview.ts index 19bdf99f36..26aae64d29 100644 --- a/app/src/preview.ts +++ b/app/src/preview.ts @@ -1,12 +1,8 @@ import { dataFormatter } from "./DataFormatter"; -import { ApplicationToolbarConfig, requireApplicationToolbar } from "./ApplicationToolbar"; +import { ApplicationToolbarConfig } from "./ApplicationToolbar"; +import { initPreviewSwitch } from "./PreviewSwitch"; const previewToggleButtonId = "previewToggle"; -const showPreviewBtnLabel = "Show Preview"; -const hidePreviewBtnLabel = "Hide Preview"; -const visibleState = "visible"; -const hiddenState = "hidden"; - const withPreviewClass = "with-preview"; const defaultPreviewPanelId = "preview"; const previewClass = "preview"; @@ -21,7 +17,6 @@ export interface PreviewConfig extends ApplicationToolbarConfig { export const initPreview = (config?: PreviewConfig) => { const { previewId = defaultPreviewPanelId } = config ?? {}; - const toolbar = requireApplicationToolbar(config); const preview = document.getElementById(previewId); const previewParent = preview?.parentElement; @@ -33,45 +28,31 @@ export const initPreview = (config?: PreviewConfig) => { throw new Error(`Preview with ID "${previewId}" misses required parent element.`); } - const button = document.createElement("button"); - - button.id = previewToggleButtonId; - button.title = `Shows preview of data as they would be stored via external service like CoreMedia CMS.`; - button.textContent = showPreviewBtnLabel; - button.dataset.currentState = hiddenState; - document.body.dataset.previewId = previewId; - preview.classList.add(previewClass); - preview.innerText = "No data received yet."; - - toolbar.appendChild(button); const showPreview = () => { previewParent.classList.add(withPreviewClass); - button.textContent = hidePreviewBtnLabel; - button.dataset.currentState = visibleState; }; const hidePreview = () => { previewParent.classList.remove(withPreviewClass); - button.textContent = showPreviewBtnLabel; - button.dataset.currentState = hiddenState; }; - const isVisible = () => button.dataset.currentState === visibleState; + initPreviewSwitch({ + id: previewToggleButtonId, + default: "hidden", + onSwitch(state) { + if (state === "visible") { + showPreview(); + } else { + hidePreview(); + } + }, + toolbarId: config?.toolbarId, + }); - const togglePreview = () => { - if (isVisible()) { - hidePreview(); - } else { - showPreview(); - } - }; - - button.addEventListener("click", togglePreview); - - // The initial state of the preview. - hidePreview(); + preview.classList.add(previewClass); + preview.innerText = "No data received yet."; }; const getPreviewPanel = (): HTMLElement => { From 386501615955c6a2e91d65d9296e602798793dc7 Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Thu, 28 Sep 2023 10:27:24 +0200 Subject: [PATCH 026/403] feat: Remember Preview State We now also store the preview state as hash parameter. --- app/src/ApplicationState.ts | 18 +++++++++++++++++- app/src/createCKEditorInstance.ts | 4 +++- app/src/index.ts | 3 --- app/src/preview.ts | 23 ++++++++--------------- 4 files changed, 28 insertions(+), 20 deletions(-) diff --git a/app/src/ApplicationState.ts b/app/src/ApplicationState.ts index 92705febea..21272653b5 100644 --- a/app/src/ApplicationState.ts +++ b/app/src/ApplicationState.ts @@ -1,10 +1,15 @@ import { setHashParam } from "./HashParams"; +// Keeping the following values in line with corresponding switch-buttons helps +// to ease the integration. We may also refactor this to some more explicit +// reusable set of types to choose from. + export type InspectorState = "expanded" | "collapsed"; export type CompatibilityMode = "v10" | "latest"; export type DataType = "richtext" | "bbcode"; export type UiLanguage = "en" | "de"; export type ReadOnlyMode = "rw" | "ro"; +export type PreviewState = "hidden" | "visible"; export class ApplicationState { /** @@ -28,9 +33,10 @@ export class ApplicationState { */ readonly #dataType: DataType; #readOnlyMode: ReadOnlyMode; + #previewState: PreviewState; constructor(config: Record = {}) { - const { uiLanguage, inspector, compatibility, dataType, readOnly } = config; + const { uiLanguage, inspector, compatibility, dataType, readOnly, showPreview } = config; this.#uiLanguage = typeof uiLanguage === "string" && uiLanguage.toLowerCase() === "de" ? "de" : "en"; this.#inspector = @@ -38,6 +44,7 @@ export class ApplicationState { this.#compatibility = typeof compatibility === "string" && compatibility.toLowerCase() === "v10" ? "v10" : "latest"; this.#dataType = typeof dataType === "string" && dataType.toLowerCase() === "bbcode" ? "bbcode" : "richtext"; this.#readOnlyMode = typeof readOnly === "boolean" && readOnly ? "ro" : "rw"; + this.#previewState = typeof showPreview === "boolean" && showPreview ? "visible" : "hidden"; } get uiLanguage(): UiLanguage { @@ -82,4 +89,13 @@ export class ApplicationState { this.#readOnlyMode = mode; setHashParam("readOnly", mode === "ro"); } + + get previewState(): PreviewState { + return this.#previewState; + } + + set previewState(state) { + this.#previewState = state; + setHashParam("showPreview", state === "visible"); + } } diff --git a/app/src/createCKEditorInstance.ts b/app/src/createCKEditorInstance.ts index 8cdfa0bcdb..7363a85128 100644 --- a/app/src/createCKEditorInstance.ts +++ b/app/src/createCKEditorInstance.ts @@ -4,7 +4,7 @@ import { Command, Editor } from "@ckeditor/ckeditor5-core"; import { CKEditorInstanceFactory } from "./CKEditorInstanceFactory"; import { Differencing } from "@coremedia/ckeditor5-coremedia-differencing"; import { initReadOnlyToggle } from "./ReadOnlySwitch"; -import { updatePreview } from "./preview"; +import { initPreview, updatePreview } from "./preview"; import { createRichTextEditor } from "./editors/richtext"; import { createBBCodeEditor } from "./editors/bbCode"; import { initDataTypeSwitch } from "./DataTypeSwitch"; @@ -150,6 +150,8 @@ export const createCKEditorInstance = async (state: ApplicationState): Promise { console.error(error); }); diff --git a/app/src/preview.ts b/app/src/preview.ts index 26aae64d29..4da6a9cce6 100644 --- a/app/src/preview.ts +++ b/app/src/preview.ts @@ -1,21 +1,14 @@ import { dataFormatter } from "./DataFormatter"; -import { ApplicationToolbarConfig } from "./ApplicationToolbar"; import { initPreviewSwitch } from "./PreviewSwitch"; +import { ApplicationState } from "./ApplicationState"; const previewToggleButtonId = "previewToggle"; const withPreviewClass = "with-preview"; -const defaultPreviewPanelId = "preview"; +const previewPanelId = "preview"; const previewClass = "preview"; -export interface PreviewConfig extends ApplicationToolbarConfig { - /** - * ID of the preview panel. Defaults to `preview`. - */ - previewId?: string; -} - -export const initPreview = (config?: PreviewConfig) => { - const { previewId = defaultPreviewPanelId } = config ?? {}; +export const initPreview = (state: ApplicationState) => { + const previewId = previewPanelId; const preview = document.getElementById(previewId); const previewParent = preview?.parentElement; @@ -40,15 +33,15 @@ export const initPreview = (config?: PreviewConfig) => { initPreviewSwitch({ id: previewToggleButtonId, - default: "hidden", - onSwitch(state) { - if (state === "visible") { + default: state.previewState, + onSwitch(previewState) { + if (previewState === "visible") { showPreview(); } else { hidePreview(); } + state.previewState = previewState; }, - toolbarId: config?.toolbarId, }); preview.classList.add(previewClass); From f7c705681401f15eaaa2ec334b43a88a5f6c8c87 Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Fri, 29 Sep 2023 07:42:57 +0200 Subject: [PATCH 027/403] refactor: Extract Content Link Data In preparation to also have example data for BBCode, moving content link data to example-data module for better separation of concerns. --- app/src/example-data.ts | 131 +---------------- .../src/data/ContentLinkData.ts | 132 ++++++++++++++++++ 2 files changed, 134 insertions(+), 129 deletions(-) create mode 100644 packages/ckeditor5-coremedia-example-data/src/data/ContentLinkData.ts diff --git a/app/src/example-data.ts b/app/src/example-data.ts index 53507e8dc6..8cd118a2e3 100644 --- a/app/src/example-data.ts +++ b/app/src/example-data.ts @@ -10,138 +10,11 @@ import { grsData } from "@coremedia-internal/ckeditor5-coremedia-example-data/sr import { loremIpsumData } from "@coremedia-internal/ckeditor5-coremedia-example-data/src/data/LoremIpsumData"; import { linkTargetData } from "@coremedia-internal/ckeditor5-coremedia-example-data/src/data/LinkTargetData"; import { h1, richtext } from "@coremedia-internal/ckeditor5-coremedia-example-data/src/RichText"; -import { richTextDocument } from "@coremedia-internal/ckeditor5-coremedia-example-data/src/RichTextDOM"; import { entitiesData } from "@coremedia-internal/ckeditor5-coremedia-example-data/src/data/EntitiesData"; import { View } from "@ckeditor/ckeditor5-engine"; import { initExamples } from "@coremedia-internal/ckeditor5-coremedia-example-data"; import { Editor } from "@ckeditor/ckeditor5-core"; - -const CM_RICHTEXT = "http://www.coremedia.com/2003/richtext-1.0"; -const XLINK = "http://www.w3.org/1999/xlink"; -const EXAMPLE_URL = "https://example.org/"; -const LINK_TEXT = "Link"; -const serializer = new XMLSerializer(); -const tableHeader = (...headers: string[]) => - `${headers.map((h) => `${h}`).join("")}`; -// TODO: Should use `RichText.a` in the end, as soon as proper escaping is -// supported. See also: `LinkTargetData.createLink` which is currently a -// duplicate. -function createLink(show: string, role: string, href = EXAMPLE_URL) { - const a = richTextDocument.createElement("a"); - a.textContent = LINK_TEXT; - a.setAttribute("xlink:href", href); - show && a.setAttribute("xlink:show", show); - role && a.setAttribute("xlink:role", role); - return serializer.serializeToString(a); -} - -function createContentLinkTableHeading() { - return tableHeader("Link", "Comment"); -} - -function createContentLinkTableRow({ comment, id }: { comment: string; id: number }) { - return `${createLink("", "", `content:${id}`)}${comment || ""}`; -} - -function createContentLinkScenario(title: string, scenarios: { comment: string; id: number }[]) { - const scenarioTitle = h1(title); - const scenarioHeader = createContentLinkTableHeading(); - const scenarioRows = scenarios.map(createContentLinkTableRow).join(""); - return `${scenarioTitle}${scenarioHeader}${scenarioRows}
    `; -} - -function contentLinkExamples() { - const standardScenarios: { comment: string; id: number }[] = [ - { - comment: "Root Folder", - id: 1, - }, - { - comment: "Folder 1", - id: 11, - }, - { - comment: "Folder 2", - id: 13, - }, - { - comment: "Document 1", - id: 10, - }, - { - comment: "Document 2", - id: 12, - }, - ]; - const nameChangeScenarios = [ - { - comment: "Folder (changing names)", - id: 103, - }, - { - comment: "Document (changing names)", - id: 102, - }, - ]; - const unreadableScenarios = [ - { - comment: "Folder 1 (unreadable)", - id: 105, - }, - { - comment: "Folder 2 (unreadable/readable toggle)", - id: 107, - }, - { - comment: "Document 1 (unreadable)", - id: 104, - }, - { - comment: "Document 2 (unreadable/readable toggle)", - id: 106, - }, - ]; - const stateScenarios = [ - { - comment: "Document 1 (checked-in)", - id: 100, - }, - { - comment: "Document 2 (checked-out)", - id: 108, - }, - { - comment: "Document (being edited; toggles checked-out/-in)", - id: 110, - }, - ]; - const xssScenarios = [ - { - comment: "Document 1", - id: 606, - }, - ]; - const slowScenarios = [ - { - comment: "Slow Document", - id: 800, - }, - { - comment: "Very Slow Document", - id: 802, - }, - ]; - const scenarios = [ - createContentLinkScenario("Standard Links", standardScenarios), - createContentLinkScenario("Name Change Scenarios", nameChangeScenarios), - createContentLinkScenario("Unreadable Scenarios", unreadableScenarios), - createContentLinkScenario("Content State Scenarios", stateScenarios), - createContentLinkScenario("XSS Scenarios", xssScenarios), - createContentLinkScenario("Slow Loading Scenarios", slowScenarios), - ].join(""); - // noinspection XmlUnusedNamespaceDeclaration - return `
    ${scenarios}
    `; -} +import { contentLinkData } from "@coremedia-internal/ckeditor5-coremedia-example-data/src/data/ContentLinkData"; // noinspection HtmlUnknownAttribute const exampleData: Record = { @@ -151,7 +24,7 @@ const exampleData: Record = { ...loremIpsumData, ...grsData, ...welcomeTextData, - "Content Links": contentLinkExamples(), + ...contentLinkData, "Various Links": PREDEFINED_MOCK_LINK_DATA, "Various Images": PREDEFINED_MOCK_BLOB_DATA, "Empty": "", diff --git a/packages/ckeditor5-coremedia-example-data/src/data/ContentLinkData.ts b/packages/ckeditor5-coremedia-example-data/src/data/ContentLinkData.ts new file mode 100644 index 0000000000..5dc848396b --- /dev/null +++ b/packages/ckeditor5-coremedia-example-data/src/data/ContentLinkData.ts @@ -0,0 +1,132 @@ +import { ExampleData } from "../ExampleData"; +import { richTextDocument } from "../RichTextDOM"; +import { h1 } from "../RichTextConvenience"; + +const EXAMPLE_URL = "https://example.org/"; +const LINK_TEXT = "Link"; +const CM_RICHTEXT = "http://www.coremedia.com/2003/richtext-1.0"; +const XLINK = "http://www.w3.org/1999/xlink"; +const serializer = new XMLSerializer(); + +const tableHeader = (...headers: string[]) => + `${headers.map((h) => `${h}`).join("")}`; + +const createContentLinkTableHeading = () => tableHeader("Link", "Comment"); + +// TODO: Should use `RichText.a` in the end, as soon as proper escaping is +// supported. See also: `LinkTargetData.createLink` which is currently a +// duplicate. +const createLink = (show: string, role: string, href = EXAMPLE_URL) => { + const a = richTextDocument.createElement("a"); + a.textContent = LINK_TEXT; + a.setAttribute("xlink:href", href); + show && a.setAttribute("xlink:show", show); + role && a.setAttribute("xlink:role", role); + return serializer.serializeToString(a); +}; + +const createContentLinkTableRow = ({ comment, id }: { comment: string; id: number }) => `${createLink("", "", `content:${id}`)}${comment || ""}`; + +const createContentLinkScenario = (title: string, scenarios: { comment: string; id: number }[]) => { + const scenarioTitle = h1(title); + const scenarioHeader = createContentLinkTableHeading(); + const scenarioRows = scenarios.map(createContentLinkTableRow).join(""); + return `${scenarioTitle}${scenarioHeader}${scenarioRows}
    `; +}; + +const contentLinkExamples = () => { + const standardScenarios: { comment: string; id: number }[] = [ + { + comment: "Root Folder", + id: 1, + }, + { + comment: "Folder 1", + id: 11, + }, + { + comment: "Folder 2", + id: 13, + }, + { + comment: "Document 1", + id: 10, + }, + { + comment: "Document 2", + id: 12, + }, + ]; + const nameChangeScenarios = [ + { + comment: "Folder (changing names)", + id: 103, + }, + { + comment: "Document (changing names)", + id: 102, + }, + ]; + const unreadableScenarios = [ + { + comment: "Folder 1 (unreadable)", + id: 105, + }, + { + comment: "Folder 2 (unreadable/readable toggle)", + id: 107, + }, + { + comment: "Document 1 (unreadable)", + id: 104, + }, + { + comment: "Document 2 (unreadable/readable toggle)", + id: 106, + }, + ]; + const stateScenarios = [ + { + comment: "Document 1 (checked-in)", + id: 100, + }, + { + comment: "Document 2 (checked-out)", + id: 108, + }, + { + comment: "Document (being edited; toggles checked-out/-in)", + id: 110, + }, + ]; + const xssScenarios = [ + { + comment: "Document 1", + id: 606, + }, + ]; + const slowScenarios = [ + { + comment: "Slow Document", + id: 800, + }, + { + comment: "Very Slow Document", + id: 802, + }, + ]; + const scenarios = [ + createContentLinkScenario("Standard Links", standardScenarios), + createContentLinkScenario("Name Change Scenarios", nameChangeScenarios), + createContentLinkScenario("Unreadable Scenarios", unreadableScenarios), + createContentLinkScenario("Content State Scenarios", stateScenarios), + createContentLinkScenario("XSS Scenarios", xssScenarios), + createContentLinkScenario("Slow Loading Scenarios", slowScenarios), + ].join(""); + // noinspection XmlUnusedNamespaceDeclaration + return `
    ${scenarios}
    `; +}; + +export const contentLinkData: ExampleData = { + "Content Links": contentLinkExamples(), +}; From 2f7661731e18d8dd77e227bf3b3db3dff125cb43 Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Fri, 29 Sep 2023 07:47:56 +0200 Subject: [PATCH 028/403] refactor: Extract Invalid Rich Text Data In preparation to also have example data for BBCode, moving invalid rich text data to example-data module for better separation of concerns. --- app/src/example-data.ts | 7 ++----- .../src/data/InvalidData.ts | 12 ++++++++++++ 2 files changed, 14 insertions(+), 5 deletions(-) create mode 100644 packages/ckeditor5-coremedia-example-data/src/data/InvalidData.ts diff --git a/app/src/example-data.ts b/app/src/example-data.ts index 8cd118a2e3..241be10edd 100644 --- a/app/src/example-data.ts +++ b/app/src/example-data.ts @@ -15,6 +15,7 @@ import { View } from "@ckeditor/ckeditor5-engine"; import { initExamples } from "@coremedia-internal/ckeditor5-coremedia-example-data"; import { Editor } from "@ckeditor/ckeditor5-core"; import { contentLinkData } from "@coremedia-internal/ckeditor5-coremedia-example-data/src/data/ContentLinkData"; +import { invalidData } from "@coremedia-internal/ckeditor5-coremedia-example-data/src/data/InvalidData"; // noinspection HtmlUnknownAttribute const exampleData: Record = { @@ -25,15 +26,11 @@ const exampleData: Record = { ...grsData, ...welcomeTextData, ...contentLinkData, + ...invalidData, "Various Links": PREDEFINED_MOCK_LINK_DATA, "Various Images": PREDEFINED_MOCK_BLOB_DATA, "Empty": "", "Hello": richtext(`

    Hello World!

    `), - "Invalid RichText": richtext( - `${h1( - "Invalid RichText", - )}

    Parsing cannot succeed below, because xlink-namespace declaration is missing.

    LINK

    `, - ).replace("LINK", `Link`), }; const dumpEditingViewOnRender = (editor: Editor): void => { diff --git a/packages/ckeditor5-coremedia-example-data/src/data/InvalidData.ts b/packages/ckeditor5-coremedia-example-data/src/data/InvalidData.ts new file mode 100644 index 0000000000..c8d8c41e10 --- /dev/null +++ b/packages/ckeditor5-coremedia-example-data/src/data/InvalidData.ts @@ -0,0 +1,12 @@ +import { ExampleData } from "../ExampleData"; +import { richtext } from "../RichTextBase"; +import { h1 } from "../RichTextConvenience"; + +// noinspection HtmlUnknownAttribute +export const invalidData: ExampleData = { + "Invalid RichText": richtext( + `${h1( + "Invalid RichText" + )}

    Parsing cannot succeed below, because xlink-namespace declaration is missing.

    LINK

    ` + ).replace("LINK", `Link`), +}; From d62f793ff97994fedc4d0aae235c65a85b2bae08 Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Fri, 29 Sep 2023 08:08:37 +0200 Subject: [PATCH 029/403] refactor: Extract Simple Rich Text Data In preparation to also have example data for BBCode, moving some simple rich text data to example-data module for better separation of concerns. --- app/src/example-data.ts | 5 ++--- .../src/data/SimpleData.ts | 9 +++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 packages/ckeditor5-coremedia-example-data/src/data/SimpleData.ts diff --git a/app/src/example-data.ts b/app/src/example-data.ts index 241be10edd..a2f8a585dc 100644 --- a/app/src/example-data.ts +++ b/app/src/example-data.ts @@ -9,13 +9,13 @@ import { differencingData } from "@coremedia-internal/ckeditor5-coremedia-exampl import { grsData } from "@coremedia-internal/ckeditor5-coremedia-example-data/src/data/GrsData"; import { loremIpsumData } from "@coremedia-internal/ckeditor5-coremedia-example-data/src/data/LoremIpsumData"; import { linkTargetData } from "@coremedia-internal/ckeditor5-coremedia-example-data/src/data/LinkTargetData"; -import { h1, richtext } from "@coremedia-internal/ckeditor5-coremedia-example-data/src/RichText"; import { entitiesData } from "@coremedia-internal/ckeditor5-coremedia-example-data/src/data/EntitiesData"; import { View } from "@ckeditor/ckeditor5-engine"; import { initExamples } from "@coremedia-internal/ckeditor5-coremedia-example-data"; import { Editor } from "@ckeditor/ckeditor5-core"; import { contentLinkData } from "@coremedia-internal/ckeditor5-coremedia-example-data/src/data/ContentLinkData"; import { invalidData } from "@coremedia-internal/ckeditor5-coremedia-example-data/src/data/InvalidData"; +import { simpleData } from "@coremedia-internal/ckeditor5-coremedia-example-data/src/data/SimpleData"; // noinspection HtmlUnknownAttribute const exampleData: Record = { @@ -27,10 +27,9 @@ const exampleData: Record = { ...welcomeTextData, ...contentLinkData, ...invalidData, + ...simpleData, "Various Links": PREDEFINED_MOCK_LINK_DATA, "Various Images": PREDEFINED_MOCK_BLOB_DATA, - "Empty": "", - "Hello": richtext(`

    Hello World!

    `), }; const dumpEditingViewOnRender = (editor: Editor): void => { diff --git a/packages/ckeditor5-coremedia-example-data/src/data/SimpleData.ts b/packages/ckeditor5-coremedia-example-data/src/data/SimpleData.ts new file mode 100644 index 0000000000..8587e7313e --- /dev/null +++ b/packages/ckeditor5-coremedia-example-data/src/data/SimpleData.ts @@ -0,0 +1,9 @@ +import { ExampleData } from "../ExampleData"; +import { richtext } from "../RichTextBase"; + +export const simpleData: ExampleData = { + "Empty: ''": "", + "Empty Paragraph: '

    '": richtext(`

    `), + "Empty Paragraph: '

    '": richtext(`

    `), + "Hello": richtext(`

    Hello World!

    `), +}; From ad1d4a0aea72672323a4bbcc933b6eda1d833116 Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Fri, 29 Sep 2023 08:10:12 +0200 Subject: [PATCH 030/403] fix: Fix Possible "XSS" Attack Vector Using `innerHtml` for defining option may break the layout, if the example title contains HTML elements or entities. --- packages/ckeditor5-coremedia-example-data/src/InitExamples.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/ckeditor5-coremedia-example-data/src/InitExamples.ts b/packages/ckeditor5-coremedia-example-data/src/InitExamples.ts index c033d6d146..60d7cc2e1e 100644 --- a/packages/ckeditor5-coremedia-example-data/src/InitExamples.ts +++ b/packages/ckeditor5-coremedia-example-data/src/InitExamples.ts @@ -100,8 +100,7 @@ const addExampleOptions = ( // Now add all examples for (const exampleKey of exampleKeys.sort()) { const option = document.createElement("option"); - // noinspection InnerHTMLJS - option.innerHTML = exampleKey; + option.textContent = exampleKey; option.value = exampleKey; option.defaultSelected = exampleKey === defaultKey; dataList.appendChild(option); From 99a44fda38640a28c0176b68332f2dcdf7d1df70 Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Fri, 29 Sep 2023 08:29:17 +0200 Subject: [PATCH 031/403] refactor: Bundle Rich Text Data Bundle rich text example data into one constant. --- app/src/example-data.ts | 21 ++---------------- .../src/data/RichTextData.ts | 22 +++++++++++++++++++ .../src/index.ts | 2 ++ 3 files changed, 26 insertions(+), 19 deletions(-) create mode 100644 packages/ckeditor5-coremedia-example-data/src/data/RichTextData.ts diff --git a/app/src/example-data.ts b/app/src/example-data.ts index a2f8a585dc..d6e9d09d17 100644 --- a/app/src/example-data.ts +++ b/app/src/example-data.ts @@ -4,30 +4,13 @@ import { PREDEFINED_MOCK_LINK_DATA, } from "@coremedia/ckeditor5-coremedia-studio-integration-mock/src/content/PredefinedMockContents"; import { setData } from "./dataFacade"; -import { welcomeTextData } from "@coremedia-internal/ckeditor5-coremedia-example-data/src/data/WelcomeTextData"; -import { differencingData } from "@coremedia-internal/ckeditor5-coremedia-example-data/src/data/DifferencingData"; -import { grsData } from "@coremedia-internal/ckeditor5-coremedia-example-data/src/data/GrsData"; -import { loremIpsumData } from "@coremedia-internal/ckeditor5-coremedia-example-data/src/data/LoremIpsumData"; -import { linkTargetData } from "@coremedia-internal/ckeditor5-coremedia-example-data/src/data/LinkTargetData"; -import { entitiesData } from "@coremedia-internal/ckeditor5-coremedia-example-data/src/data/EntitiesData"; import { View } from "@ckeditor/ckeditor5-engine"; -import { initExamples } from "@coremedia-internal/ckeditor5-coremedia-example-data"; +import { initExamples, richTextData } from "@coremedia-internal/ckeditor5-coremedia-example-data"; import { Editor } from "@ckeditor/ckeditor5-core"; -import { contentLinkData } from "@coremedia-internal/ckeditor5-coremedia-example-data/src/data/ContentLinkData"; -import { invalidData } from "@coremedia-internal/ckeditor5-coremedia-example-data/src/data/InvalidData"; -import { simpleData } from "@coremedia-internal/ckeditor5-coremedia-example-data/src/data/SimpleData"; // noinspection HtmlUnknownAttribute const exampleData: Record = { - ...differencingData, - ...entitiesData, - ...linkTargetData, - ...loremIpsumData, - ...grsData, - ...welcomeTextData, - ...contentLinkData, - ...invalidData, - ...simpleData, + ...richTextData, "Various Links": PREDEFINED_MOCK_LINK_DATA, "Various Images": PREDEFINED_MOCK_BLOB_DATA, }; diff --git a/packages/ckeditor5-coremedia-example-data/src/data/RichTextData.ts b/packages/ckeditor5-coremedia-example-data/src/data/RichTextData.ts new file mode 100644 index 0000000000..e048b7dc63 --- /dev/null +++ b/packages/ckeditor5-coremedia-example-data/src/data/RichTextData.ts @@ -0,0 +1,22 @@ +import { contentLinkData } from "./ContentLinkData"; +import { differencingData } from "./DifferencingData"; +import { entitiesData } from "./EntitiesData"; +import { grsData } from "./GrsData"; +import { invalidData } from "./InvalidData"; +import { linkTargetData } from "./LinkTargetData"; +import { loremIpsumData } from "./LoremIpsumData"; +import { simpleData } from "./SimpleData"; +import { welcomeTextData } from "./WelcomeTextData"; +import { ExampleData } from "../ExampleData"; + +export const richTextData: ExampleData = { + ...contentLinkData, + ...differencingData, + ...entitiesData, + ...grsData, + ...invalidData, + ...linkTargetData, + ...loremIpsumData, + ...simpleData, + ...welcomeTextData, +}; diff --git a/packages/ckeditor5-coremedia-example-data/src/index.ts b/packages/ckeditor5-coremedia-example-data/src/index.ts index fbe96d6d6b..39b3f6891b 100644 --- a/packages/ckeditor5-coremedia-example-data/src/index.ts +++ b/packages/ckeditor5-coremedia-example-data/src/index.ts @@ -2,4 +2,6 @@ * @module ckeditor5-coremedia-example-data */ +export { ExampleData } from "./ExampleData"; export { initExamples, type ExamplesConfig } from "./InitExamples"; +export { richTextData } from "./data/RichTextData"; From 1ca4507efce6144d9b2d643c9f71c6d0d36aa4a7 Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Fri, 29 Sep 2023 08:30:33 +0200 Subject: [PATCH 032/403] refactor: Rename "data" folder As we will have an extra folder for BBCode data, renaming the generic "data" folder to "richtext". --- packages/ckeditor5-coremedia-example-data/src/index.ts | 2 +- .../src/{data => richtext}/ChallengingData.ts | 0 .../src/{data => richtext}/ContentLinkData.ts | 0 .../src/{data => richtext}/DifferencingData.ts | 0 .../src/{data => richtext}/EntitiesData.ts | 0 .../src/{data => richtext}/GrsData.ts | 0 .../src/{data => richtext}/InvalidData.ts | 0 .../src/{data => richtext}/LinkTargetData.ts | 0 .../src/{data => richtext}/LoremIpsumData.ts | 0 .../src/{data => richtext}/RichTextData.ts | 0 .../src/{data => richtext}/SimpleData.ts | 0 .../src/{data => richtext}/WelcomeTextData.ts | 0 12 files changed, 1 insertion(+), 1 deletion(-) rename packages/ckeditor5-coremedia-example-data/src/{data => richtext}/ChallengingData.ts (100%) rename packages/ckeditor5-coremedia-example-data/src/{data => richtext}/ContentLinkData.ts (100%) rename packages/ckeditor5-coremedia-example-data/src/{data => richtext}/DifferencingData.ts (100%) rename packages/ckeditor5-coremedia-example-data/src/{data => richtext}/EntitiesData.ts (100%) rename packages/ckeditor5-coremedia-example-data/src/{data => richtext}/GrsData.ts (100%) rename packages/ckeditor5-coremedia-example-data/src/{data => richtext}/InvalidData.ts (100%) rename packages/ckeditor5-coremedia-example-data/src/{data => richtext}/LinkTargetData.ts (100%) rename packages/ckeditor5-coremedia-example-data/src/{data => richtext}/LoremIpsumData.ts (100%) rename packages/ckeditor5-coremedia-example-data/src/{data => richtext}/RichTextData.ts (100%) rename packages/ckeditor5-coremedia-example-data/src/{data => richtext}/SimpleData.ts (100%) rename packages/ckeditor5-coremedia-example-data/src/{data => richtext}/WelcomeTextData.ts (100%) diff --git a/packages/ckeditor5-coremedia-example-data/src/index.ts b/packages/ckeditor5-coremedia-example-data/src/index.ts index 39b3f6891b..87a6c5e8e6 100644 --- a/packages/ckeditor5-coremedia-example-data/src/index.ts +++ b/packages/ckeditor5-coremedia-example-data/src/index.ts @@ -4,4 +4,4 @@ export { ExampleData } from "./ExampleData"; export { initExamples, type ExamplesConfig } from "./InitExamples"; -export { richTextData } from "./data/RichTextData"; +export { richTextData } from "./richtext/RichTextData"; diff --git a/packages/ckeditor5-coremedia-example-data/src/data/ChallengingData.ts b/packages/ckeditor5-coremedia-example-data/src/richtext/ChallengingData.ts similarity index 100% rename from packages/ckeditor5-coremedia-example-data/src/data/ChallengingData.ts rename to packages/ckeditor5-coremedia-example-data/src/richtext/ChallengingData.ts diff --git a/packages/ckeditor5-coremedia-example-data/src/data/ContentLinkData.ts b/packages/ckeditor5-coremedia-example-data/src/richtext/ContentLinkData.ts similarity index 100% rename from packages/ckeditor5-coremedia-example-data/src/data/ContentLinkData.ts rename to packages/ckeditor5-coremedia-example-data/src/richtext/ContentLinkData.ts diff --git a/packages/ckeditor5-coremedia-example-data/src/data/DifferencingData.ts b/packages/ckeditor5-coremedia-example-data/src/richtext/DifferencingData.ts similarity index 100% rename from packages/ckeditor5-coremedia-example-data/src/data/DifferencingData.ts rename to packages/ckeditor5-coremedia-example-data/src/richtext/DifferencingData.ts diff --git a/packages/ckeditor5-coremedia-example-data/src/data/EntitiesData.ts b/packages/ckeditor5-coremedia-example-data/src/richtext/EntitiesData.ts similarity index 100% rename from packages/ckeditor5-coremedia-example-data/src/data/EntitiesData.ts rename to packages/ckeditor5-coremedia-example-data/src/richtext/EntitiesData.ts diff --git a/packages/ckeditor5-coremedia-example-data/src/data/GrsData.ts b/packages/ckeditor5-coremedia-example-data/src/richtext/GrsData.ts similarity index 100% rename from packages/ckeditor5-coremedia-example-data/src/data/GrsData.ts rename to packages/ckeditor5-coremedia-example-data/src/richtext/GrsData.ts diff --git a/packages/ckeditor5-coremedia-example-data/src/data/InvalidData.ts b/packages/ckeditor5-coremedia-example-data/src/richtext/InvalidData.ts similarity index 100% rename from packages/ckeditor5-coremedia-example-data/src/data/InvalidData.ts rename to packages/ckeditor5-coremedia-example-data/src/richtext/InvalidData.ts diff --git a/packages/ckeditor5-coremedia-example-data/src/data/LinkTargetData.ts b/packages/ckeditor5-coremedia-example-data/src/richtext/LinkTargetData.ts similarity index 100% rename from packages/ckeditor5-coremedia-example-data/src/data/LinkTargetData.ts rename to packages/ckeditor5-coremedia-example-data/src/richtext/LinkTargetData.ts diff --git a/packages/ckeditor5-coremedia-example-data/src/data/LoremIpsumData.ts b/packages/ckeditor5-coremedia-example-data/src/richtext/LoremIpsumData.ts similarity index 100% rename from packages/ckeditor5-coremedia-example-data/src/data/LoremIpsumData.ts rename to packages/ckeditor5-coremedia-example-data/src/richtext/LoremIpsumData.ts diff --git a/packages/ckeditor5-coremedia-example-data/src/data/RichTextData.ts b/packages/ckeditor5-coremedia-example-data/src/richtext/RichTextData.ts similarity index 100% rename from packages/ckeditor5-coremedia-example-data/src/data/RichTextData.ts rename to packages/ckeditor5-coremedia-example-data/src/richtext/RichTextData.ts diff --git a/packages/ckeditor5-coremedia-example-data/src/data/SimpleData.ts b/packages/ckeditor5-coremedia-example-data/src/richtext/SimpleData.ts similarity index 100% rename from packages/ckeditor5-coremedia-example-data/src/data/SimpleData.ts rename to packages/ckeditor5-coremedia-example-data/src/richtext/SimpleData.ts diff --git a/packages/ckeditor5-coremedia-example-data/src/data/WelcomeTextData.ts b/packages/ckeditor5-coremedia-example-data/src/richtext/WelcomeTextData.ts similarity index 100% rename from packages/ckeditor5-coremedia-example-data/src/data/WelcomeTextData.ts rename to packages/ckeditor5-coremedia-example-data/src/richtext/WelcomeTextData.ts From 8bfbe6042db9da14f6bd65a86f98c607d02f9913 Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Fri, 29 Sep 2023 10:41:41 +0200 Subject: [PATCH 033/403] feat: Introduce WithRequired Utility Type --- packages/ckeditor5-common/package.json | 5 +++++ packages/ckeditor5-common/src/WithRequired.ts | 3 +++ packages/ckeditor5-common/src/index.ts | 3 +-- 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 packages/ckeditor5-common/src/WithRequired.ts diff --git a/packages/ckeditor5-common/package.json b/packages/ckeditor5-common/package.json index 0284408aa5..78c74579c6 100644 --- a/packages/ckeditor5-common/package.json +++ b/packages/ckeditor5-common/package.json @@ -9,6 +9,11 @@ "node": "18", "pnpm": "^8.6.9" }, + "main": "./src/index.ts", + "publishConfig": { + "main": "./src/index.js", + "types": "./src/index.d.ts" + }, "license": "Apache-2.0", "main": "./src/index.ts", "publishConfig": { diff --git a/packages/ckeditor5-common/src/WithRequired.ts b/packages/ckeditor5-common/src/WithRequired.ts new file mode 100644 index 0000000000..e1910bc466 --- /dev/null +++ b/packages/ckeditor5-common/src/WithRequired.ts @@ -0,0 +1,3 @@ +export type WithRequired = T & { + [P in K]-?: T[P]; +}; diff --git a/packages/ckeditor5-common/src/index.ts b/packages/ckeditor5-common/src/index.ts index a27d3ac7cf..1af730daa8 100644 --- a/packages/ckeditor5-common/src/index.ts +++ b/packages/ckeditor5-common/src/index.ts @@ -1,7 +1,6 @@ /** - * General utilities, e.g., for TypeScript Support. - * * @module ckeditor5-common */ +export type { WithRequired } from "./WithRequired"; export type { RequireSelected } from "./AdvancedTypes"; From 06428b9ce3cb66a5a2f2cb4aff084affc87a84b9 Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Fri, 29 Sep 2023 10:43:32 +0200 Subject: [PATCH 034/403] refactor: Use ExampleData Type for examples --- packages/ckeditor5-coremedia-example-data/src/InitExamples.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-coremedia-example-data/src/InitExamples.ts b/packages/ckeditor5-coremedia-example-data/src/InitExamples.ts index 60d7cc2e1e..3b85161344 100644 --- a/packages/ckeditor5-coremedia-example-data/src/InitExamples.ts +++ b/packages/ckeditor5-coremedia-example-data/src/InitExamples.ts @@ -1,3 +1,5 @@ +import { ExampleData } from "./ExampleData"; + const createLabelFor = (inputElement: HTMLInputElement): HTMLLabelElement => { const { id: inputId } = inputElement; const element = document.createElement("label"); @@ -188,7 +190,7 @@ export interface ExamplesConfig { * Set of examples to initialize. The key is the title to render within * the option selection; the value represents the data to set. */ - examples: Record; + examples: ExampleData; /** * Listener, that will be informed on any valid selection about the data * to set at the editor. From a11251cb41e2f09624fab96163c23f926a15dc3b Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Fri, 29 Sep 2023 10:44:29 +0200 Subject: [PATCH 035/403] feat: Provide First BBCode Example Data --- .../package.json | 3 + .../src/bbcode/BBCode.ts | 78 +++++++++++++++++++ .../src/bbcode/BBCodeData.ts | 8 ++ .../src/bbcode/InlineFormatData.ts | 27 +++++++ .../src/bbcode/SimpleData.ts | 6 ++ .../src/bbcode/WelcomeTextData.ts | 21 +++++ .../src/index.ts | 1 + 7 files changed, 144 insertions(+) create mode 100644 packages/ckeditor5-coremedia-example-data/src/bbcode/BBCode.ts create mode 100644 packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeData.ts create mode 100644 packages/ckeditor5-coremedia-example-data/src/bbcode/InlineFormatData.ts create mode 100644 packages/ckeditor5-coremedia-example-data/src/bbcode/SimpleData.ts create mode 100644 packages/ckeditor5-coremedia-example-data/src/bbcode/WelcomeTextData.ts diff --git a/packages/ckeditor5-coremedia-example-data/package.json b/packages/ckeditor5-coremedia-example-data/package.json index 5e4c0f42d0..1b56a6e8d0 100644 --- a/packages/ckeditor5-coremedia-example-data/package.json +++ b/packages/ckeditor5-coremedia-example-data/package.json @@ -28,6 +28,9 @@ "rimraf": "^5.0.1", "typescript": "^4.9.5" }, + "dependencies": { + "@coremedia/ckeditor5-common": "15.0.2-rc.2" + }, "scripts": { "build": "tsc --project ./tsconfig.release.json", "clean": "pnpm clean:src && pnpm clean:dist", diff --git a/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCode.ts b/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCode.ts new file mode 100644 index 0000000000..021600438c --- /dev/null +++ b/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCode.ts @@ -0,0 +1,78 @@ +import { WithRequired } from "@coremedia/ckeditor5-common"; + +export interface StyleOptions { + size?: string; + color?: string; +} + +/** + * BBCode formatting options. This API is just meant for internal use. It is + * not robust for malicious BBCode. + * + * There is no official "escape mechanism" for BBCode. + * For details, see: https://www.phpbb.com/community/viewtopic.php?t=1721345. + * + * The provided factory methods are not meant to align with any used BBCode + * parser. It just summarizes some possible options for rendering BBCode. + * Skipped, for example, `[pipe]` which provides an alternative syntax for + * tables, supported by some parsers. + * + * See also: + * + * * https://en.wikipedia.org/wiki/BBCode. + * * https://www.bbcode.org/reference.php + * * https://www.bbcode.org/how-to-use-bbcode-a-complete-guide.php + */ +export const bbCode = { + heading: (text: string, level: 1 | 2 | 3 | 4 | 5 | 6) => `[h${level}]${text}[/h${level}]`, + h1: (text: string) => bbCode.heading(text, 1), + h2: (text: string) => bbCode.heading(text, 2), + h3: (text: string) => bbCode.heading(text, 3), + h4: (text: string) => bbCode.heading(text, 4), + h5: (text: string) => bbCode.heading(text, 5), + h6: (text: string) => bbCode.heading(text, 6), + bold: (text: string) => `[b]${text}[/b]`, + italic: (text: string) => `[i]${text}[/i]`, + underline: (text: string) => `[u]${text}[/u]`, + strikethrough: (text: string) => `[s]${text}[/s]`, + url: (textOrUrl: string, url?: string) => (url ? `[url=${url}]${textOrUrl}[/url]` : `[url]${textOrUrl}[/url]`), + img: (url: string) => `[img]${url}[/img]`, + emoji: (emoji: string) => `[${emoji}]`, + quote: (text: string, author?: string) => (author ? `[quote="${author}"]${text}[/quote]` : `[quote]${text}[/quote]`), + code: (text: string) => `[code]${text}[/code]`, + style: (text: string, style: WithRequired | WithRequired) => { + let styleOptions = ``; + if (style.size) { + styleOptions = `${styleOptions} size="${style.size}"`; + } + if (style.color) { + if (style.color.startsWith("#")) { + styleOptions = `${styleOptions} color=${style.color}`; + } else { + styleOptions = `${styleOptions} color="${style.color}"`; + } + } + return `[style ${styleOptions.trim()}]${text}[/style]`; + }, + color: (text: string, color: string) => color.startsWith("#") ? `[color=${color}]${text}[/color]` : `[color="${color}"]${text}[/color]`, + list: (entries: string[], listType: "ordered" | "unordered" = "unordered") => { + let result = listType === "unordered" ? `[list]` : `[list=1]`; + entries.forEach((entry) => { + result = `${result}\n[*]${entry}`; + }); + result = `${result}\n[/list]\n`; + return result; + }, + table: (entries: string[][]) => { + let result = `[table]`; + for (const row of entries) { + result = `${result}\n[tr]`; + for (const column of row) { + result = `${result}\n[td]${column}[/td]`; + } + result = `${result}\n[/tr]`; + } + result = `${result}\n[/table]`; + return result; + }, +}; diff --git a/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeData.ts b/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeData.ts new file mode 100644 index 0000000000..cac5240bab --- /dev/null +++ b/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeData.ts @@ -0,0 +1,8 @@ +import { ExampleData } from "../ExampleData"; +import { inlineFormatData } from "./InlineFormatData"; +import { welcomeTextData } from "./WelcomeTextData"; + +export const bbCodeData: ExampleData = { + ...inlineFormatData, + ...welcomeTextData, +}; diff --git a/packages/ckeditor5-coremedia-example-data/src/bbcode/InlineFormatData.ts b/packages/ckeditor5-coremedia-example-data/src/bbcode/InlineFormatData.ts new file mode 100644 index 0000000000..ab44a3dae1 --- /dev/null +++ b/packages/ckeditor5-coremedia-example-data/src/bbcode/InlineFormatData.ts @@ -0,0 +1,27 @@ +import { ExampleData } from "../ExampleData"; +import { bbCode } from "./BBCode"; + +const paragraphs = (...texts: string[]): string => texts.join(`\n\n`); + +export const inlineFormatData: ExampleData = { + Bold: paragraphs(`${bbCode.h1("Bold Text")}`, `Lorem ${bbCode.bold("ipsum")} dolor`), + Italic: paragraphs(`${bbCode.h1("Italic Text")}`, `Lorem ${bbCode.italic("ipsum")} dolor`), + Underline: paragraphs(`${bbCode.h1("Underline Text")}`, `Lorem ${bbCode.underline("ipsum")} dolor`), + Strikethrough: paragraphs(`${bbCode.h1("Strikethrough Text")}`, `Lorem ${bbCode.strikethrough("ipsum")} dolor`), + Code: paragraphs(`${bbCode.h1("Code Text")}`, `Lorem ${bbCode.code("ipsum")} dolor`), + Styles: paragraphs( + `${bbCode.h1("Text Using Styles")}`, + `Lorem ${bbCode.style("ipsum", { size: "1.5em" })} dolor`, + `Lorem ${bbCode.style("ipsum", { color: "fuchsia" })} dolor`, + `Lorem ${bbCode.style("ipsum", { color: "#ff0000" })} dolor`, + `Lorem ${bbCode.style("ipsum", { + size: "1.5em", + color: "fuchsia", + })} dolor` + ), + Color: paragraphs( + `${bbCode.h1("Colored Text")}`, + `Lorem ${bbCode.color("ipsum", "fuchsia")} dolor`, + `Lorem ${bbCode.color("ipsum", "#ff0000")} dolor` + ), +}; diff --git a/packages/ckeditor5-coremedia-example-data/src/bbcode/SimpleData.ts b/packages/ckeditor5-coremedia-example-data/src/bbcode/SimpleData.ts new file mode 100644 index 0000000000..40b8d9e284 --- /dev/null +++ b/packages/ckeditor5-coremedia-example-data/src/bbcode/SimpleData.ts @@ -0,0 +1,6 @@ +import { ExampleData } from "../ExampleData"; + +export const simpleData: ExampleData = { + Empty: "", + Hello: `Hello World!`, +}; diff --git a/packages/ckeditor5-coremedia-example-data/src/bbcode/WelcomeTextData.ts b/packages/ckeditor5-coremedia-example-data/src/bbcode/WelcomeTextData.ts new file mode 100644 index 0000000000..6318a4ed2a --- /dev/null +++ b/packages/ckeditor5-coremedia-example-data/src/bbcode/WelcomeTextData.ts @@ -0,0 +1,21 @@ +import { ExampleData } from "../ExampleData"; +import { bbCode } from "./BBCode"; + +export const welcomeText = `${bbCode.h1("CKEditor 5: CoreMedia Plugin Showcase")} + +This example instance of CKEditor 5 serves as a showcase for plugins provided +by CoreMedia. Most of these plugins are mandatory to use CKEditor 5 as editor +within CoreMedia Studio. Others are required to edit CoreMedia RichText, and +then again others provide more additional functionality. For details see +corresponding documentation. + +${bbCode.h2("Example Data")} + +For testing several use-cases you will see buttons at the top, which load +different data-sets for testing and for experimenting with this CKEditor +instance and its plugins. +`; + +export const welcomeTextData: ExampleData = { + Welcome: welcomeText, +}; diff --git a/packages/ckeditor5-coremedia-example-data/src/index.ts b/packages/ckeditor5-coremedia-example-data/src/index.ts index 87a6c5e8e6..d93443532e 100644 --- a/packages/ckeditor5-coremedia-example-data/src/index.ts +++ b/packages/ckeditor5-coremedia-example-data/src/index.ts @@ -5,3 +5,4 @@ export { ExampleData } from "./ExampleData"; export { initExamples, type ExamplesConfig } from "./InitExamples"; export { richTextData } from "./richtext/RichTextData"; +export { bbCodeData } from "./bbcode/BBCodeData"; From 39b8be7474e3cf13e163d42d17ebbb301ef791de Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Fri, 29 Sep 2023 10:45:11 +0200 Subject: [PATCH 036/403] feat: Use BBCode Example Data Refactored initialization to `createCKEditorInstance` factory method. --- app/src/createCKEditorInstance.ts | 3 +++ app/src/editors/richtext.ts | 2 -- app/src/example-data.ts | 28 ++++++++++++++++++++-------- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/app/src/createCKEditorInstance.ts b/app/src/createCKEditorInstance.ts index 7363a85128..52ea08ea32 100644 --- a/app/src/createCKEditorInstance.ts +++ b/app/src/createCKEditorInstance.ts @@ -9,6 +9,7 @@ import { createRichTextEditor } from "./editors/richtext"; import { createBBCodeEditor } from "./editors/bbCode"; import { initDataTypeSwitch } from "./DataTypeSwitch"; import { initUiLanguageSwitch } from "./UiLanguageSwitch"; +import { initExamplesAndBindTo } from "./example-data"; /** * Typings for CKEditorInspector, as it does not ship with typings yet. @@ -120,6 +121,8 @@ export const createCKEditorInstance = async (state: ApplicationState): Promise { - initExamplesAndBindTo(newEditor); initInputExampleContent(newEditor); return newEditor; }); diff --git a/app/src/example-data.ts b/app/src/example-data.ts index d6e9d09d17..57f7043438 100644 --- a/app/src/example-data.ts +++ b/app/src/example-data.ts @@ -5,16 +5,28 @@ import { } from "@coremedia/ckeditor5-coremedia-studio-integration-mock/src/content/PredefinedMockContents"; import { setData } from "./dataFacade"; import { View } from "@ckeditor/ckeditor5-engine"; -import { initExamples, richTextData } from "@coremedia-internal/ckeditor5-coremedia-example-data"; +import { + bbCodeData, + ExampleData, + initExamples, + richTextData, +} from "@coremedia-internal/ckeditor5-coremedia-example-data"; import { Editor } from "@ckeditor/ckeditor5-core"; -// noinspection HtmlUnknownAttribute -const exampleData: Record = { - ...richTextData, - "Various Links": PREDEFINED_MOCK_LINK_DATA, - "Various Images": PREDEFINED_MOCK_BLOB_DATA, +const exampleData: { + richtext: ExampleData; + bbcode: ExampleData; +} = { + richtext: { + ...richTextData, + "Various Links": PREDEFINED_MOCK_LINK_DATA, + "Various Images": PREDEFINED_MOCK_BLOB_DATA, + }, + bbcode: bbCodeData, }; +export type ExampleDataType = keyof typeof exampleData; + const dumpEditingViewOnRender = (editor: Editor): void => { const { editing: { view }, @@ -72,10 +84,10 @@ const dumpDataViewOnRender = (editor: Editor): void => { ); }; -export const initExamplesAndBindTo = (editor: Editor): void => { +export const initExamplesAndBindTo = (editor: Editor, examplesType: ExampleDataType = "richtext"): void => { initExamples({ id: "examples", - examples: exampleData, + examples: exampleData[examplesType], default: "Welcome", onChange: (data: string): void => { dumpEditingViewOnRender(editor); From 8fa8675d14fb3a558abca4ca06d58f78b755e182 Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Fri, 29 Sep 2023 11:38:27 +0200 Subject: [PATCH 037/403] fix: Typings for BBob Parser --- .../src/bbcode2html/bbcode2html.ts | 4 +--- packages/ckeditor5-coremedia-bbcode/tsconfig.json | 10 ++++++++-- .../types/@bbob/html/es/index.d.ts | 12 ++++++++++++ .../types/@bbob/preset-html5/index.d.ts | 4 ++++ 4 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 packages/ckeditor5-coremedia-bbcode/types/@bbob/html/es/index.d.ts create mode 100644 packages/ckeditor5-coremedia-bbcode/types/@bbob/preset-html5/index.d.ts diff --git a/packages/ckeditor5-coremedia-bbcode/src/bbcode2html/bbcode2html.ts b/packages/ckeditor5-coremedia-bbcode/src/bbcode2html/bbcode2html.ts index db89db94cc..5f57544405 100644 --- a/packages/ckeditor5-coremedia-bbcode/src/bbcode2html/bbcode2html.ts +++ b/packages/ckeditor5-coremedia-bbcode/src/bbcode2html/bbcode2html.ts @@ -4,6 +4,4 @@ import presetHTML5 from "@bbob/preset-html5"; /** * Parses BBCode to HTML. */ -export const bbcode2html = (bbcode: string): string => { - return bbobHTML(bbcode, presetHTML5()); -}; +export const bbcode2html = (bbcode: string): string => bbobHTML(bbcode, presetHTML5()); diff --git a/packages/ckeditor5-coremedia-bbcode/tsconfig.json b/packages/ckeditor5-coremedia-bbcode/tsconfig.json index 1b6674928a..d7f6a45c01 100644 --- a/packages/ckeditor5-coremedia-bbcode/tsconfig.json +++ b/packages/ckeditor5-coremedia-bbcode/tsconfig.json @@ -2,6 +2,12 @@ "extends": "../../tsconfig.json", "include": [ "./__tests__", - "./src", - ] + "./src" + ], + "compilerOptions": { + "paths": { + "@bbob/html/es": ["./types/@bbob/html/es/index.d.ts"], + "@bbob/preset-html5": ["./types/@bbob/preset-html5/index.d.ts"], + } + } } diff --git a/packages/ckeditor5-coremedia-bbcode/types/@bbob/html/es/index.d.ts b/packages/ckeditor5-coremedia-bbcode/types/@bbob/html/es/index.d.ts new file mode 100644 index 0000000000..1591fd9fb3 --- /dev/null +++ b/packages/ckeditor5-coremedia-bbcode/types/@bbob/html/es/index.d.ts @@ -0,0 +1,12 @@ +declare module "@bbob/html/es" { + export default function toHTML( + source: string, + // eslint-disable-next-line @typescript-eslint/ban-types + plugins?: Function | Function[], + options?: { + onlyAllowTags?: string[]; + contextFreeTags?: string[]; + enableEscapeTags?: boolean; + } + ): string; +} diff --git a/packages/ckeditor5-coremedia-bbcode/types/@bbob/preset-html5/index.d.ts b/packages/ckeditor5-coremedia-bbcode/types/@bbob/preset-html5/index.d.ts new file mode 100644 index 0000000000..0f22f991cd --- /dev/null +++ b/packages/ckeditor5-coremedia-bbcode/types/@bbob/preset-html5/index.d.ts @@ -0,0 +1,4 @@ +declare module "@bbob/preset-html5" { + // eslint-disable-next-line @typescript-eslint/ban-types + export default function presetHTML5(): Function; +} From 1b075e8f2e7f9ef03fe0b70744042726fa22bc7c Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Fri, 29 Sep 2023 12:32:00 +0200 Subject: [PATCH 038/403] chore: Fix Package Versions After Rebase To Main --- packages/ckeditor5-coremedia-bbcode/package.json | 6 +++--- packages/ckeditor5-coremedia-example-data/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ckeditor5-coremedia-bbcode/package.json b/packages/ckeditor5-coremedia-bbcode/package.json index 5e2c3ea262..c537ab60d9 100644 --- a/packages/ckeditor5-coremedia-bbcode/package.json +++ b/packages/ckeditor5-coremedia-bbcode/package.json @@ -1,6 +1,6 @@ { "name": "@coremedia/ckeditor5-coremedia-bbcode", - "version": "15.0.2-rc.2", + "version": "16.0.1-rc.2", "description": "BBCode Data-Processor", "keywords": [ "coremedia", @@ -26,7 +26,7 @@ "devDependencies": { "@ckeditor/ckeditor5-core": "^37.1.0", "@ckeditor/ckeditor5-engine": "^37.1.0", - "@coremedia/ckeditor5-dom-support": "15.0.2-rc.2", + "@coremedia/ckeditor5-dom-support": "16.0.1-rc.2", "@types/jest": "^29.5.1", "jest": "^29.5.0", "jest-each": "^29.5.0", @@ -46,6 +46,6 @@ "dependencies": { "@bbob/html": "^3.0.0", "@bbob/preset-html5": "^3.0.0", - "@coremedia/ckeditor5-core-common": "15.0.2-rc.2" + "@coremedia/ckeditor5-core-common": "16.0.1-rc.2" } } diff --git a/packages/ckeditor5-coremedia-example-data/package.json b/packages/ckeditor5-coremedia-example-data/package.json index 1b56a6e8d0..34d60ea959 100644 --- a/packages/ckeditor5-coremedia-example-data/package.json +++ b/packages/ckeditor5-coremedia-example-data/package.json @@ -29,7 +29,7 @@ "typescript": "^4.9.5" }, "dependencies": { - "@coremedia/ckeditor5-common": "15.0.2-rc.2" + "@coremedia/ckeditor5-common": "16.0.1-rc.2" }, "scripts": { "build": "tsc --project ./tsconfig.release.json", From a35f27a37b1aa39ccdf40b253339affcf55db481 Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Fri, 29 Sep 2023 12:32:23 +0200 Subject: [PATCH 039/403] chore: Update pnpm-lock.yaml --- pnpm-lock.yaml | 191 ++++++++++++++++++++++++++++++------------------- 1 file changed, 117 insertions(+), 74 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 743ff1e52f..a2ca38c772 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -144,6 +144,9 @@ importers: '@coremedia-internal/ckeditor5-coremedia-example-data': specifier: ^1.0.0 version: link:../packages/ckeditor5-coremedia-example-data + '@coremedia/ckeditor5-coremedia-bbcode': + specifier: 16.0.1-rc.2 + version: link:../packages/ckeditor5-coremedia-bbcode '@coremedia/ckeditor5-coremedia-blocklist': specifier: 16.0.1-rc.2 version: link:../packages/ckeditor5-coremedia-blocklist @@ -427,6 +430,46 @@ importers: specifier: ^4.9.5 version: 4.9.5 + packages/ckeditor5-coremedia-bbcode: + dependencies: + '@bbob/html': + specifier: ^3.0.0 + version: 3.0.2 + '@bbob/preset-html5': + specifier: ^3.0.0 + version: 3.0.2 + '@coremedia/ckeditor5-core-common': + specifier: 16.0.1-rc.2 + version: link:../ckeditor5-core-common + devDependencies: + '@ckeditor/ckeditor5-core': + specifier: ^37.1.0 + version: 37.1.0 + '@ckeditor/ckeditor5-engine': + specifier: ^37.1.0 + version: 37.1.0 + '@coremedia/ckeditor5-dom-support': + specifier: 16.0.1-rc.2 + version: link:../ckeditor5-dom-support + '@types/jest': + specifier: ^29.5.1 + version: 29.5.5 + jest: + specifier: ^29.5.0 + version: 29.7.0(@types/node@18.17.18) + jest-each: + specifier: ^29.5.0 + version: 29.7.0 + jest-xml-matcher: + specifier: ^1.2.0 + version: 1.2.0 + rimraf: + specifier: ^5.0.0 + version: 5.0.1 + typescript: + specifier: ^4.9.5 + version: 4.9.5 + packages/ckeditor5-coremedia-blocklist: dependencies: '@ckeditor/ckeditor5-find-and-replace': @@ -633,6 +676,10 @@ importers: version: 4.9.5 packages/ckeditor5-coremedia-example-data: + dependencies: + '@coremedia/ckeditor5-common': + specifier: 16.0.1-rc.2 + version: link:../ckeditor5-common devDependencies: '@coremedia-internal/ckeditor5-jest-test-helpers': specifier: ^1.0.0 @@ -1483,7 +1530,7 @@ packages: resolution: {integrity: sha512-qLNsZbgrNh0fDQBCPocSL8guki1hcPvltGDv/NxvUoABwFq7GkKSu1nRXeJkVZc+wJvne2E0RKQz+2SQrz6eAA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.22.17 + '@babel/types': 7.22.19 dev: false /@babel/helper-module-imports@7.22.15: @@ -1512,10 +1559,6 @@ packages: '@babel/types': 7.22.17 dev: false - /@babel/helper-plugin-utils@7.21.5: - resolution: {integrity: sha512-0WDaIlXKOX/3KfBK/dwP1oQGiPh6rjMkT7HIRv7i5RR2VUMwrx5ZL0dwBkKx7+SW1zwNdgjHd34IMk5ZjTeHVg==} - engines: {node: '>=6.9.0'} - /@babel/helper-plugin-utils@7.22.5: resolution: {integrity: sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==} engines: {node: '>=6.9.0'} @@ -1554,7 +1597,7 @@ packages: resolution: {integrity: sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.22.17 + '@babel/types': 7.22.19 dev: false /@babel/helper-split-export-declaration@7.22.6: @@ -1602,7 +1645,7 @@ packages: resolution: {integrity: sha512-C/BaXcnnvBCmHTpz/VGZ8jgtE2aYlW4hxDhseJAWZb7gqGM/qtCK6iZUb0TyKFf7BOUsBH7Q7fkRsDRhg1XklQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/helper-validator-identifier': 7.22.15 + '@babel/helper-validator-identifier': 7.22.20 chalk: 2.4.2 js-tokens: 4.0.0 @@ -1658,7 +1701,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.22.20 - '@babel/helper-plugin-utils': 7.21.5 + '@babel/helper-plugin-utils': 7.22.5 /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.22.20): resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} @@ -2559,6 +2602,40 @@ packages: '@babel/helper-validator-identifier': 7.22.20 to-fast-properties: 2.0.0 + /@bbob/core@3.0.2: + resolution: {integrity: sha512-ru0Jd+jPGxiwyMR3hNgYqbVx9sJ4L8Hv3VoqW0svzC+rZpgKnorkCkwxIj2nlq5+z+0C1hS4lho5pn4Vandlag==} + dependencies: + '@bbob/parser': 3.0.2 + dev: false + + /@bbob/html@3.0.2: + resolution: {integrity: sha512-ILvlTUlmaM89bW7agZOmyiE9O+qY212YeFNWH7s9MzLmwykUMEtq4O5DTzawbLlyOhf2fbP8OHV4ljgGf8Cq0A==} + dependencies: + '@bbob/core': 3.0.2 + '@bbob/plugin-helper': 3.0.2 + dev: false + + /@bbob/parser@3.0.2: + resolution: {integrity: sha512-3emLznDJWFJky+06AaTMpJ0IjY/mVpPN5+cnzwDKQgZ3wSFPbeEmdJhfh+gbiR30/UCWgwI1Ts+WUNJtUx1eaw==} + dependencies: + '@bbob/plugin-helper': 3.0.2 + dev: false + + /@bbob/plugin-helper@3.0.2: + resolution: {integrity: sha512-t1wFB3hbUg3pKq7TXk+mnnMcKjX6D29HxTAxU/+2mIbsWXaqgHLfH9FFxhZ8s/sGaLH4qgOSH+lgynpF3zy62Q==} + dev: false + + /@bbob/preset-html5@3.0.2: + resolution: {integrity: sha512-J+c3aOQKD6n1HXGxB+o3tQuRTeqHLXBifBMFY4rT/y19tU3dZySI8TT/07pDqJVeh1PAa1KPaCww7m3O4QS0oQ==} + dependencies: + '@bbob/plugin-helper': 3.0.2 + '@bbob/preset': 3.0.2 + dev: false + + /@bbob/preset@3.0.2: + resolution: {integrity: sha512-ZsHowFvvOx1tpem76SjYirsnO3bhaWnuh44Huj82YZBSftCgTaaH5WBleWk0TfNAu1gv44L1mHZyUBDLz3wgEg==} + dev: false + /@bcoe/v8-coverage@0.2.3: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} @@ -2608,6 +2685,15 @@ packages: ckeditor5: 39.0.2 dev: false + /@ckeditor/ckeditor5-core@37.1.0: + resolution: {integrity: sha512-edewiWlMCK5BPN9Can0A9skob9dNDMrv09khiKaUYK5PEobZZQSyUBck52vXpt255u2rnlmhF5phTqsQo5EiOw==} + engines: {node: '>=16.0.0', npm: '>=5.7.1'} + dependencies: + '@ckeditor/ckeditor5-engine': 37.1.0 + '@ckeditor/ckeditor5-utils': 37.1.0 + lodash-es: 4.17.21 + dev: true + /@ckeditor/ckeditor5-core@39.0.2: resolution: {integrity: sha512-/xtor5vIXgwBVsAj+yO/wyzezQUmXabdkb/T8aSXtO2665zeOVbDbtSsJ1Ov7Tz5A4Ia1pA9d7iDCt7E8Kva7A==} dependencies: @@ -2667,6 +2753,14 @@ packages: ckeditor5: 39.0.2 lodash-es: 4.17.21 + /@ckeditor/ckeditor5-engine@37.1.0: + resolution: {integrity: sha512-D/xWNOgqk3G1qtv8P2UCmpHcIONjJE0NRJeJuJ8jppIgOYpbVG/7KSuzJYV7G1M9oGSBAeNb7U+lz7y/eg38Hw==} + engines: {node: '>=16.0.0', npm: '>=5.7.1'} + dependencies: + '@ckeditor/ckeditor5-utils': 37.1.0 + lodash-es: 4.17.21 + dev: true + /@ckeditor/ckeditor5-engine@39.0.2: resolution: {integrity: sha512-ERcEpIrmTML0/uhukkC+ZJSOx4mRaPbNG5vPEBXIentfDpzu1NrmUhGZRGXaw5lltL+NJbuTI0wjEINap0Hl3w==} dependencies: @@ -2828,6 +2922,13 @@ packages: '@ckeditor/ckeditor5-ui': 39.0.2 '@ckeditor/ckeditor5-utils': 39.0.2 + /@ckeditor/ckeditor5-utils@37.1.0: + resolution: {integrity: sha512-r4rSbzMy0WFSuP0IRd+yYUMjzb279eiICksOEiHViiqoKQ8RqcGDlh+zOaACkgw6xvLxj96C5MwG2wsZsGJqcA==} + engines: {node: '>=16.0.0', npm: '>=5.7.1'} + dependencies: + lodash-es: 4.17.21 + dev: true + /@ckeditor/ckeditor5-utils@39.0.2: resolution: {integrity: sha512-aqiGhPJxEihSLW21lGWcAvjVTTwJYxEbfMk1eLf/BEY3euy6iltRC6EqbXkyJDcKGU7cQtk6JXAIkH+D2FF87g==} dependencies: @@ -3446,13 +3547,6 @@ packages: '@types/node': 18.17.18 jest-mock: 29.7.0 - /@jest/expect-utils@29.5.0: - resolution: {integrity: sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - jest-get-type: 29.6.3 - dev: true - /@jest/expect-utils@29.7.0: resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -3526,13 +3620,6 @@ packages: transitivePeerDependencies: - supports-color - /@jest/schemas@29.4.3: - resolution: {integrity: sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@sinclair/typebox': 0.25.24 - dev: true - /@jest/schemas@29.6.3: resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -3726,10 +3813,6 @@ packages: resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==} dev: true - /@sinclair/typebox@0.25.24: - resolution: {integrity: sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==} - dev: true - /@sinclair/typebox@0.27.8: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} @@ -3765,18 +3848,18 @@ packages: /@types/babel__generator@7.6.4: resolution: {integrity: sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==} dependencies: - '@babel/types': 7.22.17 + '@babel/types': 7.22.19 /@types/babel__template@7.4.1: resolution: {integrity: sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==} dependencies: '@babel/parser': 7.22.16 - '@babel/types': 7.22.17 + '@babel/types': 7.22.19 /@types/babel__traverse@7.18.3: resolution: {integrity: sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w==} dependencies: - '@babel/types': 7.22.17 + '@babel/types': 7.22.19 /@types/body-parser@1.19.2: resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} @@ -4540,7 +4623,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/template': 7.22.15 - '@babel/types': 7.22.17 + '@babel/types': 7.22.19 '@types/babel__core': 7.20.0 '@types/babel__traverse': 7.18.3 @@ -5424,11 +5507,6 @@ packages: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} - /diff-sequences@29.4.3: - resolution: {integrity: sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dev: true - /diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -6116,10 +6194,10 @@ packages: resolution: {integrity: sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/expect-utils': 29.5.0 + '@jest/expect-utils': 29.7.0 jest-get-type: 29.6.3 - jest-matcher-utils: 29.5.0 - jest-message-util: 29.5.0 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 jest-util: 29.7.0 dev: true @@ -7288,16 +7366,6 @@ packages: - babel-plugin-macros - supports-color - /jest-diff@29.5.0: - resolution: {integrity: sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - chalk: 4.1.2 - diff-sequences: 29.4.3 - jest-get-type: 29.6.3 - pretty-format: 29.7.0 - dev: true - /jest-diff@29.7.0: resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -7386,16 +7454,6 @@ packages: jest-get-type: 29.6.3 pretty-format: 29.7.0 - /jest-matcher-utils@29.5.0: - resolution: {integrity: sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - chalk: 4.1.2 - jest-diff: 29.5.0 - jest-get-type: 29.6.3 - pretty-format: 29.7.0 - dev: true - /jest-matcher-utils@29.7.0: resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -7405,21 +7463,6 @@ packages: jest-get-type: 29.6.3 pretty-format: 29.7.0 - /jest-message-util@29.5.0: - resolution: {integrity: sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@babel/code-frame': 7.22.13 - '@jest/types': 29.6.3 - '@types/stack-utils': 2.0.1 - chalk: 4.1.2 - graceful-fs: 4.2.11 - micromatch: 4.0.5 - pretty-format: 29.7.0 - slash: 3.0.0 - stack-utils: 2.0.6 - dev: true - /jest-message-util@29.7.0: resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -9015,7 +9058,7 @@ packages: resolution: {integrity: sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/schemas': 29.4.3 + '@jest/schemas': 29.6.3 ansi-styles: 5.2.0 react-is: 18.2.0 dev: true From 08c34c6ecde203948a75e45477942d78164eb750 Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Fri, 29 Sep 2023 12:36:43 +0200 Subject: [PATCH 040/403] chore: Fix Dependency Versions After Rebase --- packages/ckeditor5-coremedia-bbcode/package.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/ckeditor5-coremedia-bbcode/package.json b/packages/ckeditor5-coremedia-bbcode/package.json index c537ab60d9..b6c21a859b 100644 --- a/packages/ckeditor5-coremedia-bbcode/package.json +++ b/packages/ckeditor5-coremedia-bbcode/package.json @@ -24,14 +24,14 @@ "npm-check-updates": "npm-check-updates --upgrade" }, "devDependencies": { - "@ckeditor/ckeditor5-core": "^37.1.0", - "@ckeditor/ckeditor5-engine": "^37.1.0", + "@ckeditor/ckeditor5-core": "^39.0.2", + "@ckeditor/ckeditor5-engine": "^39.0.2", "@coremedia/ckeditor5-dom-support": "16.0.1-rc.2", - "@types/jest": "^29.5.1", - "jest": "^29.5.0", - "jest-each": "^29.5.0", + "@types/jest": "^29.5.4", + "jest": "^29.7.0", + "jest-each": "^29.7.0", "jest-xml-matcher": "^1.2.0", - "rimraf": "^5.0.0", + "rimraf": "^5.0.1", "typescript": "^4.9.5" }, "main": "./src/index.ts", @@ -40,8 +40,8 @@ "types": "./src/index.d.ts" }, "peerDependencies": { - "@ckeditor/ckeditor5-core": "^37.0.1", - "@ckeditor/ckeditor5-engine": "^37.0.1" + "@ckeditor/ckeditor5-core": "^39.0.2", + "@ckeditor/ckeditor5-engine": "^39.0.2" }, "dependencies": { "@bbob/html": "^3.0.0", From 724ddc921564adf67b4757c7e0bef43c3a5ace2b Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Fri, 29 Sep 2023 12:36:55 +0200 Subject: [PATCH 041/403] chore: Update pnpm-lock.yaml --- pnpm-lock.yaml | 40 ++++++++-------------------------------- 1 file changed, 8 insertions(+), 32 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a2ca38c772..af126d73f8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -443,28 +443,28 @@ importers: version: link:../ckeditor5-core-common devDependencies: '@ckeditor/ckeditor5-core': - specifier: ^37.1.0 - version: 37.1.0 + specifier: ^39.0.2 + version: 39.0.2 '@ckeditor/ckeditor5-engine': - specifier: ^37.1.0 - version: 37.1.0 + specifier: ^39.0.2 + version: 39.0.2 '@coremedia/ckeditor5-dom-support': specifier: 16.0.1-rc.2 version: link:../ckeditor5-dom-support '@types/jest': - specifier: ^29.5.1 + specifier: ^29.5.4 version: 29.5.5 jest: - specifier: ^29.5.0 + specifier: ^29.7.0 version: 29.7.0(@types/node@18.17.18) jest-each: - specifier: ^29.5.0 + specifier: ^29.7.0 version: 29.7.0 jest-xml-matcher: specifier: ^1.2.0 version: 1.2.0 rimraf: - specifier: ^5.0.0 + specifier: ^5.0.1 version: 5.0.1 typescript: specifier: ^4.9.5 @@ -2685,15 +2685,6 @@ packages: ckeditor5: 39.0.2 dev: false - /@ckeditor/ckeditor5-core@37.1.0: - resolution: {integrity: sha512-edewiWlMCK5BPN9Can0A9skob9dNDMrv09khiKaUYK5PEobZZQSyUBck52vXpt255u2rnlmhF5phTqsQo5EiOw==} - engines: {node: '>=16.0.0', npm: '>=5.7.1'} - dependencies: - '@ckeditor/ckeditor5-engine': 37.1.0 - '@ckeditor/ckeditor5-utils': 37.1.0 - lodash-es: 4.17.21 - dev: true - /@ckeditor/ckeditor5-core@39.0.2: resolution: {integrity: sha512-/xtor5vIXgwBVsAj+yO/wyzezQUmXabdkb/T8aSXtO2665zeOVbDbtSsJ1Ov7Tz5A4Ia1pA9d7iDCt7E8Kva7A==} dependencies: @@ -2753,14 +2744,6 @@ packages: ckeditor5: 39.0.2 lodash-es: 4.17.21 - /@ckeditor/ckeditor5-engine@37.1.0: - resolution: {integrity: sha512-D/xWNOgqk3G1qtv8P2UCmpHcIONjJE0NRJeJuJ8jppIgOYpbVG/7KSuzJYV7G1M9oGSBAeNb7U+lz7y/eg38Hw==} - engines: {node: '>=16.0.0', npm: '>=5.7.1'} - dependencies: - '@ckeditor/ckeditor5-utils': 37.1.0 - lodash-es: 4.17.21 - dev: true - /@ckeditor/ckeditor5-engine@39.0.2: resolution: {integrity: sha512-ERcEpIrmTML0/uhukkC+ZJSOx4mRaPbNG5vPEBXIentfDpzu1NrmUhGZRGXaw5lltL+NJbuTI0wjEINap0Hl3w==} dependencies: @@ -2922,13 +2905,6 @@ packages: '@ckeditor/ckeditor5-ui': 39.0.2 '@ckeditor/ckeditor5-utils': 39.0.2 - /@ckeditor/ckeditor5-utils@37.1.0: - resolution: {integrity: sha512-r4rSbzMy0WFSuP0IRd+yYUMjzb279eiICksOEiHViiqoKQ8RqcGDlh+zOaACkgw6xvLxj96C5MwG2wsZsGJqcA==} - engines: {node: '>=16.0.0', npm: '>=5.7.1'} - dependencies: - lodash-es: 4.17.21 - dev: true - /@ckeditor/ckeditor5-utils@39.0.2: resolution: {integrity: sha512-aqiGhPJxEihSLW21lGWcAvjVTTwJYxEbfMk1eLf/BEY3euy6iltRC6EqbXkyJDcKGU7cQtk6JXAIkH+D2FF87g==} dependencies: From a0ae032f93d9e1042c0dce1e8b84b916ce284a14 Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Fri, 29 Sep 2023 12:39:33 +0200 Subject: [PATCH 042/403] fix: Broken example-data after Rebase --- app/src/example-data.ts | 30 ++++++------------------------ 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/app/src/example-data.ts b/app/src/example-data.ts index 57f7043438..fc870987ad 100644 --- a/app/src/example-data.ts +++ b/app/src/example-data.ts @@ -31,7 +31,6 @@ const dumpEditingViewOnRender = (editor: Editor): void => { const { editing: { view }, } = editor; - }, // noinspection InnerHTMLJS view.once( @@ -43,28 +42,11 @@ const dumpEditingViewOnRender = (editor: Editor): void => { source, innerHtml: source.getDomRoot()?.innerHTML, }); - ); - editor.data.once( - "set", - (event, details) => - console.log("CKEditor's Data-Controller received data via 'set'.", { - event, - // eslint-disable-next-line - data: details[0], - }), - { - priority: "lowest", - }, - ); - - const data = exampleData[exampleKey]; - console.log("Setting Example Data.", { [exampleKey]: data }); - setData(editor, data); - - const xmpInput = document.getElementById("xmp-input") as HTMLInputElement; - if (xmpInput) { - xmpInput.value = exampleKey; - } + } + }, + { + priority: "lowest", + }, ); }; @@ -80,7 +62,7 @@ const dumpDataViewOnRender = (editor: Editor): void => { }), { priority: "lowest", - } + }, ); }; From da1f45fd448924ac23b34ba4cbc9db1185b77970 Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Fri, 29 Sep 2023 12:44:18 +0200 Subject: [PATCH 043/403] fix: Add Accidentally Dropped Challenging Data Fix after rebase to main. --- .../src/richtext/RichTextData.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/ckeditor5-coremedia-example-data/src/richtext/RichTextData.ts b/packages/ckeditor5-coremedia-example-data/src/richtext/RichTextData.ts index e048b7dc63..4b62d99170 100644 --- a/packages/ckeditor5-coremedia-example-data/src/richtext/RichTextData.ts +++ b/packages/ckeditor5-coremedia-example-data/src/richtext/RichTextData.ts @@ -8,6 +8,7 @@ import { loremIpsumData } from "./LoremIpsumData"; import { simpleData } from "./SimpleData"; import { welcomeTextData } from "./WelcomeTextData"; import { ExampleData } from "../ExampleData"; +import { challengingData } from "./ChallengingData"; export const richTextData: ExampleData = { ...contentLinkData, @@ -19,4 +20,5 @@ export const richTextData: ExampleData = { ...loremIpsumData, ...simpleData, ...welcomeTextData, + ...challengingData, }; From b77687264cfe060eec42a953fb3f1aa797a4bd4c Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Mon, 2 Oct 2023 08:05:20 +0200 Subject: [PATCH 044/403] fix: Adapt Rich Text Editor Config Adapt Rich Text to align with current state on main and also enable the default target mode for some artificial URL that ends on `#newTab`. --- app/src/editors/richtext.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app/src/editors/richtext.ts b/app/src/editors/richtext.ts index 193a62a88f..c480a7f196 100644 --- a/app/src/editors/richtext.ts +++ b/app/src/editors/richtext.ts @@ -58,6 +58,7 @@ import type { } from "@coremedia/ckeditor5-coremedia-richtext"; import { CKEditorInstanceFactory } from "../CKEditorInstanceFactory"; import { ApplicationState } from "../ApplicationState"; +import { Blocklist } from "@coremedia/ckeditor5-coremedia-blocklist"; const { objectInline: withinTextIcon, @@ -127,7 +128,7 @@ const linkAttributesConfig: LinkAttributesConfig = getHashParam("skipLinkAttribu }; const getRichTextConfig = ( - richTextCompatibility: string | true + richTextCompatibility: string | true, ): Partial | V10CoreMediaRichTextConfig => { // Use v10 for first data-processor architecture, for example. if (richTextCompatibility === "v10") { @@ -147,7 +148,7 @@ const getRichTextConfig = ( export const createRichTextEditor: CKEditorInstanceFactory = ( sourceElement: HTMLElement, - state: ApplicationState + state: ApplicationState, ): Promise => { const { uiLanguage } = state; return ClassicEditor.create(sourceElement, { @@ -157,6 +158,7 @@ export const createRichTextEditor: CKEditorInstanceFactory = ( Alignment, Autoformat, Autosave, + Blocklist, BlockQuote, Bold, Code, @@ -224,6 +226,7 @@ export const createRichTextEditor: CKEditorInstanceFactory = ( "|", "pasteContent", "findAndReplace", + "blocklist", "|", "sourceEditing", ], @@ -266,6 +269,13 @@ export const createRichTextEditor: CKEditorInstanceFactory = ( }, link: { defaultProtocol: "https://", + defaultTargets: [ + { + // May be used to experiment with default target selection. + filter: (url) => url.endsWith("#newTab"), + target: "_blank", + }, + ], ...linkAttributesConfig, /*decorators: { hasTitle: { From f2d5d16a83ce508afefa95d8c52d2a3465287b63 Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Mon, 2 Oct 2023 08:37:09 +0200 Subject: [PATCH 045/403] feat: Integrate Data Facade Integrating data facade for Rich Text and BBCode editor. Also enabled preview update from BBCode editor. --- app/package.json | 1 + app/src/dataFacade.ts | 82 ------------------------------------- app/src/editors/bbCode.ts | 27 ++++++++++-- app/src/editors/richtext.ts | 16 +++++--- app/src/example-data.ts | 4 +- 5 files changed, 36 insertions(+), 94 deletions(-) delete mode 100644 app/src/dataFacade.ts diff --git a/app/package.json b/app/package.json index 2dc9401cd6..5f33791492 100644 --- a/app/package.json +++ b/app/package.json @@ -46,6 +46,7 @@ "@coremedia/ckeditor5-coremedia-richtext": "16.0.1-rc.2", "@coremedia/ckeditor5-coremedia-studio-essentials": "16.0.1-rc.2", "@coremedia/ckeditor5-coremedia-studio-integration-mock": "16.0.1-rc.2", + "@coremedia/ckeditor5-data-facade": "16.0.1-rc.2", "@coremedia/ckeditor5-dataprocessor-support": "16.0.1-rc.2", "@coremedia/ckeditor5-dom-converter": "16.0.1-rc.2", "@coremedia/ckeditor5-font-mapper": "16.0.1-rc.2", diff --git a/app/src/dataFacade.ts b/app/src/dataFacade.ts deleted file mode 100644 index ecd6f618bd..0000000000 --- a/app/src/dataFacade.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import { updatePreview } from "./preview"; -import { Editor } from "@ckeditor/ckeditor5-core"; - -const LastSetVersion = Symbol("LastSetVersion"); -const LastSetData = Symbol("LastSetData"); - -/** - * A small facade around editor.setData, which remembers the last data - * set explicitly. This simulates a similar approach as in studio-client. - */ -export const setData = (editor: Editor, data: string) => { - const { document } = editor.model; - const { data: dataController } = editor; - - const versionBefore = document.version; - dataController.set(data); - const versionAfter = document.version; - - //@ts-expect-error problem with symbols - window[LastSetData] = data; - - //@ts-expect-error problem with symbols - window[LastSetVersion] = versionAfter; - - console.log(`Editor Data Set.`, { - data, - transformedData: dataController.get(), - versionBefore, - versionAfter, - }); -}; - -/** - * Save method with additional recognition, if there is an actual change. - * This represents how we could prevent auto-checkout in CoreMedia - * Studio for irrelevant changes, because they are semantically equivalent. - * - * @param editor - the editor instance whose data to save - * @param source - which editor stored the data - */ -// async: In production scenarios, this will be an asynchronous call. -// eslint-disable-next-line @typescript-eslint/require-await -export const saveData = async (editor: Editor, source: string) => { - const data = editor.data.get({ - // set to `none`, to trigger data-processing for empty text, too - // possible values: empty, none (default: empty) - trim: "empty", - }); - const currentVersion = editor.model.document.version; - //@ts-expect-error problem with symbols - const lastSetVersion = window[LastSetVersion]; - //@ts-expect-error problem with symbols - const lastSetData = window[LastSetData]; - - const logInfo = (isUpdate: boolean) => ({ - isUpdate, - currentVersion, - lastSetVersion, - data, - lastSetData, - }); - - let previewData: string; - - if (lastSetVersion !== undefined && lastSetVersion === currentVersion) { - console.log( - `Would skip saving data triggered by ${source} as they represent the same data as set originally. Note, that the actual data may differ, but they are semantically equivalent.`, - logInfo(false), - ); - previewData = lastSetData; - } else { - console.log(`Saving data triggered by ${source}.`, logInfo(true)); - previewData = data; - } - - // Similar to CoreMedia Studio, we prefer the originally set data, when - // there is no semantic difference compared to the data as returned by - // CKEditor. - console.log(`Update Preview triggered by ${source}.`, { previewData }); - updatePreview(previewData); -}; diff --git a/app/src/editors/bbCode.ts b/app/src/editors/bbCode.ts index 6d068b8f53..a65e9310d0 100644 --- a/app/src/editors/bbCode.ts +++ b/app/src/editors/bbCode.ts @@ -10,15 +10,29 @@ import { SourceEditing } from "@ckeditor/ckeditor5-source-editing"; import { Link } from "@ckeditor/ckeditor5-link"; import { CKEditorInstanceFactory } from "../CKEditorInstanceFactory"; import { ApplicationState } from "../ApplicationState"; +import { DataFacade } from "@coremedia/ckeditor5-data-facade"; +import { updatePreview } from "../preview"; export const createBBCodeEditor: CKEditorInstanceFactory = ( sourceElement: HTMLElement, - state: ApplicationState + state: ApplicationState, ): Promise => { const { uiLanguage } = state; return ClassicEditor.create(sourceElement, { placeholder: "Type your text here...", - plugins: [Autosave, Bold, Essentials, Heading, Italic, Underline, Paragraph, SourceEditing, Link, BBCode], + plugins: [ + Autosave, + BBCode, + Bold, + DataFacade, + Essentials, + Heading, + Italic, + Link, + Paragraph, + SourceEditing, + Underline, + ], toolbar: ["undo", "redo", "|", "heading", "|", "bold", "italic", "underline", "|", "link", "|", "sourceEditing"], language: { // Language switch only applies to editor instance. @@ -28,8 +42,13 @@ export const createBBCodeEditor: CKEditorInstanceFactory = ( }, autosave: { waitingTime: 1000, // in ms - save() { - console.log("BBCode Save triggered..."); + }, + dataFacade: { + save(dataApi): Promise { + console.log("Save triggered..."); + const start = performance.now(); + updatePreview(dataApi.getData(), "text"); + console.log(`Saved data within ${performance.now() - start} ms.`); return Promise.resolve(); }, }, diff --git a/app/src/editors/richtext.ts b/app/src/editors/richtext.ts index c480a7f196..aa9bd5b4a0 100644 --- a/app/src/editors/richtext.ts +++ b/app/src/editors/richtext.ts @@ -39,8 +39,7 @@ import { import { initInputExampleContent } from "../inputExampleContents"; import { COREMEDIA_MOCK_CONTENT_PLUGIN } from "@coremedia/ckeditor5-coremedia-studio-integration-mock/src/content/MockContentPlugin"; -import { Editor, icons, PluginConstructor } from "@ckeditor/ckeditor5-core"; -import { saveData } from "../dataFacade"; +import { icons, PluginConstructor } from "@ckeditor/ckeditor5-core"; import MockInputExamplePlugin from "@coremedia/ckeditor5-coremedia-studio-integration-mock/src/content/MockInputExamplePlugin"; import PasteContentPlugin from "@coremedia/ckeditor5-coremedia-content-clipboard/src/paste/PasteContentPlugin"; import { RuleConfig } from "@coremedia/ckeditor5-dom-converter/src/Rule"; @@ -59,6 +58,8 @@ import type { import { CKEditorInstanceFactory } from "../CKEditorInstanceFactory"; import { ApplicationState } from "../ApplicationState"; import { Blocklist } from "@coremedia/ckeditor5-coremedia-blocklist"; +import { DataFacade } from "@coremedia/ckeditor5-data-facade"; +import { updatePreview } from "../preview"; const { objectInline: withinTextIcon, @@ -165,6 +166,7 @@ export const createRichTextEditor: CKEditorInstanceFactory = ( CodeBlock, ContentLinks, ContentClipboard, + DataFacade, Differencing, Essentials, FindAndReplace, @@ -344,12 +346,14 @@ export const createRichTextEditor: CKEditorInstanceFactory = ( }, autosave: { waitingTime: 1000, // in ms - save(currentEditor: Editor) { + }, + dataFacade: { + save(dataApi): Promise { console.log("Save triggered..."); const start = performance.now(); - return saveData(currentEditor, "autosave").then(() => { - console.log(`Saved data within ${performance.now() - start} ms.`); - }); + updatePreview(dataApi.getData(), "xml"); + console.log(`Saved data within ${performance.now() - start} ms.`); + return Promise.resolve(); }, }, [COREMEDIA_RICHTEXT_CONFIG_KEY]: getRichTextConfig(richTextCompatibility), diff --git a/app/src/example-data.ts b/app/src/example-data.ts index fc870987ad..550023962a 100644 --- a/app/src/example-data.ts +++ b/app/src/example-data.ts @@ -3,7 +3,6 @@ import { PREDEFINED_MOCK_BLOB_DATA, PREDEFINED_MOCK_LINK_DATA, } from "@coremedia/ckeditor5-coremedia-studio-integration-mock/src/content/PredefinedMockContents"; -import { setData } from "./dataFacade"; import { View } from "@ckeditor/ckeditor5-engine"; import { bbCodeData, @@ -12,6 +11,7 @@ import { richTextData, } from "@coremedia-internal/ckeditor5-coremedia-example-data"; import { Editor } from "@ckeditor/ckeditor5-core"; +import { DataFacade } from "@coremedia/ckeditor5-data-facade"; const exampleData: { richtext: ExampleData; @@ -74,7 +74,7 @@ export const initExamplesAndBindTo = (editor: Editor, examplesType: ExampleDataT onChange: (data: string): void => { dumpEditingViewOnRender(editor); dumpDataViewOnRender(editor); - setData(editor, data); + editor.plugins.get(DataFacade).setData(data); }, }); }; From 529ca6ea9bfdf90d5d6d27ab6cde8d7fc96657c2 Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Mon, 2 Oct 2023 08:37:19 +0200 Subject: [PATCH 046/403] chore: Update pnpm-lock.yaml --- pnpm-lock.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index af126d73f8..f506f7a963 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -171,6 +171,9 @@ importers: '@coremedia/ckeditor5-coremedia-studio-integration-mock': specifier: 16.0.1-rc.2 version: link:../packages/ckeditor5-coremedia-studio-integration-mock + '@coremedia/ckeditor5-data-facade': + specifier: 16.0.1-rc.2 + version: link:../packages/ckeditor5-data-facade '@coremedia/ckeditor5-dataprocessor-support': specifier: 16.0.1-rc.2 version: link:../packages/ckeditor5-dataprocessor-support From 02c4f54acc5a948a10e0b4b1d74f4b59526d1a37 Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Mon, 2 Oct 2023 08:45:06 +0200 Subject: [PATCH 047/403] chore: Fix Lint Issues --- app/src/ApplicationToolbar.ts | 2 +- app/src/HashParams.ts | 2 +- app/src/createCKEditorInstance.ts | 4 ++-- .../ckeditor5-coremedia-bbcode/src/BBCodeDataProcessor.ts | 4 ++-- .../ckeditor5-coremedia-bbcode/types/@bbob/html/es/index.d.ts | 2 +- packages/ckeditor5-coremedia-example-data/src/InitExamples.ts | 2 +- .../ckeditor5-coremedia-example-data/src/bbcode/BBCode.ts | 3 ++- .../src/bbcode/InlineFormatData.ts | 4 ++-- .../src/richtext/ContentLinkData.ts | 3 ++- .../src/richtext/InvalidData.ts | 4 ++-- 10 files changed, 16 insertions(+), 14 deletions(-) diff --git a/app/src/ApplicationToolbar.ts b/app/src/ApplicationToolbar.ts index a763ac7d9e..7fb2222e83 100644 --- a/app/src/ApplicationToolbar.ts +++ b/app/src/ApplicationToolbar.ts @@ -9,7 +9,7 @@ export interface ApplicationToolbarConfig { } export const requireApplicationToolbar = (config?: ApplicationToolbarConfig): HTMLElement => { - const { toolbarId = defaultApplicationToolbarId} = config ?? {}; + const { toolbarId = defaultApplicationToolbarId } = config ?? {}; const toolbar = document.getElementById(toolbarId); if (!toolbar) { diff --git a/app/src/HashParams.ts b/app/src/HashParams.ts index 411758f219..5cead9f54b 100644 --- a/app/src/HashParams.ts +++ b/app/src/HashParams.ts @@ -63,7 +63,7 @@ export const toHashParam = (hashParams: Record): strin }; export const setHashParam = (key: string, value: string | boolean, reload = false): void => { - const { location, history } = window ?? {}; + const { location } = window ?? {}; if (!location) { console.info(`Skipped setting hash parameter ${key} to ${value} as window location and/or history is unknown.`); return; diff --git a/app/src/createCKEditorInstance.ts b/app/src/createCKEditorInstance.ts index 52ea08ea32..d852c8d34e 100644 --- a/app/src/createCKEditorInstance.ts +++ b/app/src/createCKEditorInstance.ts @@ -41,7 +41,7 @@ const attachInspector = (editor: Editor, { dataType, inspector }: ApplicationSta // With hash parameter #expandInspector you may expand the // inspector by default. isCollapsed: inspector === "collapsed", - } + }, ); const optionallyActivateDifferencing = (editor: Editor): void => { @@ -87,7 +87,7 @@ const registerGlobalEditor = (editor: Editor): void => { * Update preview with data from the editor initially. * * @param editor - editor to get data from - * @param dataType - type of data + * @param { dataType } - type of data */ const initializePreviewData = (editor: ClassicEditor, { dataType }: ApplicationState): void => { switch (dataType) { diff --git a/packages/ckeditor5-coremedia-bbcode/src/BBCodeDataProcessor.ts b/packages/ckeditor5-coremedia-bbcode/src/BBCodeDataProcessor.ts index 18bf5e355d..965d19411d 100644 --- a/packages/ckeditor5-coremedia-bbcode/src/BBCodeDataProcessor.ts +++ b/packages/ckeditor5-coremedia-bbcode/src/BBCodeDataProcessor.ts @@ -1,5 +1,6 @@ import { - DataProcessor, DomConverter, + DataProcessor, + DomConverter, HtmlDataProcessor, MatcherPattern, ViewDocument, @@ -9,7 +10,6 @@ import { import { bbcode2html } from "./bbcode2html/bbcode2html"; import { html2bbcode } from "./html2bbcode/html2bbcode"; import { defaultRules, HTML2BBCodeRule } from "./html2bbcode/rules/DefaultRules"; -import BasicHtmlWriter from "@ckeditor/ckeditor5-engine/src/dataprocessor/basichtmlwriter"; /** * Data processor for BBCode. diff --git a/packages/ckeditor5-coremedia-bbcode/types/@bbob/html/es/index.d.ts b/packages/ckeditor5-coremedia-bbcode/types/@bbob/html/es/index.d.ts index 1591fd9fb3..131c77a7a9 100644 --- a/packages/ckeditor5-coremedia-bbcode/types/@bbob/html/es/index.d.ts +++ b/packages/ckeditor5-coremedia-bbcode/types/@bbob/html/es/index.d.ts @@ -7,6 +7,6 @@ declare module "@bbob/html/es" { onlyAllowTags?: string[]; contextFreeTags?: string[]; enableEscapeTags?: boolean; - } + }, ): string; } diff --git a/packages/ckeditor5-coremedia-example-data/src/InitExamples.ts b/packages/ckeditor5-coremedia-example-data/src/InitExamples.ts index 3b85161344..c9229cfcf4 100644 --- a/packages/ckeditor5-coremedia-example-data/src/InitExamples.ts +++ b/packages/ckeditor5-coremedia-example-data/src/InitExamples.ts @@ -97,7 +97,7 @@ const initExamplesUi = (parent: ParentNode): ExamplesUiElements => { const addExampleOptions = ( dataList: HTMLDataListElement, defaultKey: string | undefined, - exampleKeys: string[] + exampleKeys: string[], ): void => { // Now add all examples for (const exampleKey of exampleKeys.sort()) { diff --git a/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCode.ts b/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCode.ts index 021600438c..98cf199685 100644 --- a/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCode.ts +++ b/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCode.ts @@ -54,7 +54,8 @@ export const bbCode = { } return `[style ${styleOptions.trim()}]${text}[/style]`; }, - color: (text: string, color: string) => color.startsWith("#") ? `[color=${color}]${text}[/color]` : `[color="${color}"]${text}[/color]`, + color: (text: string, color: string) => + color.startsWith("#") ? `[color=${color}]${text}[/color]` : `[color="${color}"]${text}[/color]`, list: (entries: string[], listType: "ordered" | "unordered" = "unordered") => { let result = listType === "unordered" ? `[list]` : `[list=1]`; entries.forEach((entry) => { diff --git a/packages/ckeditor5-coremedia-example-data/src/bbcode/InlineFormatData.ts b/packages/ckeditor5-coremedia-example-data/src/bbcode/InlineFormatData.ts index ab44a3dae1..e68ced7ecf 100644 --- a/packages/ckeditor5-coremedia-example-data/src/bbcode/InlineFormatData.ts +++ b/packages/ckeditor5-coremedia-example-data/src/bbcode/InlineFormatData.ts @@ -17,11 +17,11 @@ export const inlineFormatData: ExampleData = { `Lorem ${bbCode.style("ipsum", { size: "1.5em", color: "fuchsia", - })} dolor` + })} dolor`, ), Color: paragraphs( `${bbCode.h1("Colored Text")}`, `Lorem ${bbCode.color("ipsum", "fuchsia")} dolor`, - `Lorem ${bbCode.color("ipsum", "#ff0000")} dolor` + `Lorem ${bbCode.color("ipsum", "#ff0000")} dolor`, ), }; diff --git a/packages/ckeditor5-coremedia-example-data/src/richtext/ContentLinkData.ts b/packages/ckeditor5-coremedia-example-data/src/richtext/ContentLinkData.ts index 5dc848396b..8cfd2623ae 100644 --- a/packages/ckeditor5-coremedia-example-data/src/richtext/ContentLinkData.ts +++ b/packages/ckeditor5-coremedia-example-data/src/richtext/ContentLinkData.ts @@ -25,7 +25,8 @@ const createLink = (show: string, role: string, href = EXAMPLE_URL) => { return serializer.serializeToString(a); }; -const createContentLinkTableRow = ({ comment, id }: { comment: string; id: number }) => `${createLink("", "", `content:${id}`)}${comment || ""}`; +const createContentLinkTableRow = ({ comment, id }: { comment: string; id: number }) => + `${createLink("", "", `content:${id}`)}${comment || ""}`; const createContentLinkScenario = (title: string, scenarios: { comment: string; id: number }[]) => { const scenarioTitle = h1(title); diff --git a/packages/ckeditor5-coremedia-example-data/src/richtext/InvalidData.ts b/packages/ckeditor5-coremedia-example-data/src/richtext/InvalidData.ts index c8d8c41e10..1bfc8c4b92 100644 --- a/packages/ckeditor5-coremedia-example-data/src/richtext/InvalidData.ts +++ b/packages/ckeditor5-coremedia-example-data/src/richtext/InvalidData.ts @@ -6,7 +6,7 @@ import { h1 } from "../RichTextConvenience"; export const invalidData: ExampleData = { "Invalid RichText": richtext( `${h1( - "Invalid RichText" - )}

    Parsing cannot succeed below, because xlink-namespace declaration is missing.

    LINK

    ` + "Invalid RichText", + )}

    Parsing cannot succeed below, because xlink-namespace declaration is missing.

    LINK

    `, ).replace("LINK", `Link`), }; From 44201a04a4d02f63b519d02ff1e4688bc9b4fb41 Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Mon, 2 Oct 2023 09:11:38 +0200 Subject: [PATCH 048/403] fix: Address Remote property injection Addressing report of "Remote property injection" alert by replacing storage for hash parameters to use a Map instead. --- app/src/ApplicationState.ts | 9 +++++++-- app/src/HashParams.ts | 20 ++++++++++---------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/app/src/ApplicationState.ts b/app/src/ApplicationState.ts index 21272653b5..86a30154aa 100644 --- a/app/src/ApplicationState.ts +++ b/app/src/ApplicationState.ts @@ -35,8 +35,13 @@ export class ApplicationState { #readOnlyMode: ReadOnlyMode; #previewState: PreviewState; - constructor(config: Record = {}) { - const { uiLanguage, inspector, compatibility, dataType, readOnly, showPreview } = config; + constructor(config?: Map) { + const uiLanguage = config?.get("uiLanguage") ?? "en"; + const inspector = config?.get("inspector") ?? "collapsed"; + const compatibility = config?.get("compatibility") ?? "latest"; + const dataType = config?.get("dataType") ?? "richtext"; + const readOnly = config?.get("readOnly") ?? false; + const showPreview = config?.get("showPreview") ?? false; this.#uiLanguage = typeof uiLanguage === "string" && uiLanguage.toLowerCase() === "de" ? "de" : "en"; this.#inspector = diff --git a/app/src/HashParams.ts b/app/src/HashParams.ts index 5cead9f54b..08109792c1 100644 --- a/app/src/HashParams.ts +++ b/app/src/HashParams.ts @@ -4,21 +4,21 @@ */ export const hashParamRegExp = /([^=]*)=(.*)/; -export const getHashParams = (): Record => { +export const getHashParams = (): Map => { // Check for `window`: Required when used from within Jest tests, where // 'jsdom' is not available. const { location } = window ?? {}; if (!location) { - return {}; + return new Map(); } const { hash: rawHash } = location; if (rawHash.length === 0) { - return {}; + return new Map(); } // substring: Remove hash const hash: string = rawHash.substring(1); const hashParams: string[] = hash.split(/&/); - const parsedHashParams: Record = {}; + const parsedHashParams = new Map(); for (const hashParam of hashParams) { const paramMatch: RegExpExecArray | null = hashParamRegExp.exec(hashParam); let key: string; @@ -49,16 +49,16 @@ export const getHashParams = (): Record => { key = hashParam; value = true; } - parsedHashParams[key] = value; + parsedHashParams.set(key, value); } return parsedHashParams; }; -export const toHashParam = (hashParams: Record): string => { +export const toHashParam = (hashParams: Map): string => { let result = ""; - for (const [key, value] of Object.entries(hashParams)) { + hashParams.forEach((value, key) => { result = `${result}${result ? "&" : ""}${encodeURIComponent(key)}=${encodeURIComponent(value)}`; - } + }); return result; }; @@ -70,7 +70,7 @@ export const setHashParam = (key: string, value: string | boolean, reload = fals } const hashParams = getHashParams(); - hashParams[key] = value; + hashParams.set(key, value); location.hash = toHashParam(hashParams); if (reload) { location.reload(); @@ -88,5 +88,5 @@ export const getHashParam = (key: string | undefined): string | boolean => { if (key === undefined) { return false; } - return getHashParams()[key] ?? false; + return getHashParams().get(key) ?? false; }; From 510b6cbfefa3fb859631fbb0db4d2dcb0f8060c5 Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Mon, 2 Oct 2023 09:37:01 +0200 Subject: [PATCH 049/403] feat: Extend BBCode Example Data Extend utility methods for "paragraphs" (feels more comfortable when developing richtext example-data in parallel). And, most important, add "challenging data", that also demonstrate the advantage of using the data facade also for BBCode. --- .../src/bbcode/BBCode.ts | 20 +++++--- .../src/bbcode/BBCodeData.ts | 2 + .../src/bbcode/ChallengingData.ts | 50 +++++++++++++++++++ .../src/bbcode/WelcomeTextData.ts | 22 ++++---- 4 files changed, 74 insertions(+), 20 deletions(-) create mode 100644 packages/ckeditor5-coremedia-example-data/src/bbcode/ChallengingData.ts diff --git a/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCode.ts b/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCode.ts index 98cf199685..c032d3e82a 100644 --- a/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCode.ts +++ b/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCode.ts @@ -5,6 +5,8 @@ export interface StyleOptions { color?: string; } +const replaceTrailingNewline = (text: string, replacement = ""): string => text.replace(/[\n\r]*$/, replacement); + /** * BBCode formatting options. This API is just meant for internal use. It is * not robust for malicious BBCode. @@ -24,13 +26,15 @@ export interface StyleOptions { * * https://www.bbcode.org/how-to-use-bbcode-a-complete-guide.php */ export const bbCode = { - heading: (text: string, level: 1 | 2 | 3 | 4 | 5 | 6) => `[h${level}]${text}[/h${level}]`, + heading: (text: string, level: 1 | 2 | 3 | 4 | 5 | 6) => + `\n[h${level}]${replaceTrailingNewline(text)}[/h${level}]\n`, h1: (text: string) => bbCode.heading(text, 1), h2: (text: string) => bbCode.heading(text, 2), h3: (text: string) => bbCode.heading(text, 3), h4: (text: string) => bbCode.heading(text, 4), h5: (text: string) => bbCode.heading(text, 5), h6: (text: string) => bbCode.heading(text, 6), + p: (text: string) => `\n${text.replace(/[\n\r]*$/, "\n\n")}`, bold: (text: string) => `[b]${text}[/b]`, italic: (text: string) => `[i]${text}[/i]`, underline: (text: string) => `[u]${text}[/u]`, @@ -57,23 +61,23 @@ export const bbCode = { color: (text: string, color: string) => color.startsWith("#") ? `[color=${color}]${text}[/color]` : `[color="${color}"]${text}[/color]`, list: (entries: string[], listType: "ordered" | "unordered" = "unordered") => { - let result = listType === "unordered" ? `[list]` : `[list=1]`; + let result = listType === "unordered" ? `\n[list]` : `\n[list=1]`; entries.forEach((entry) => { - result = `${result}\n[*]${entry}`; + result = `${result}\n[*]${replaceTrailingNewline(entry, "\n")}`; }); result = `${result}\n[/list]\n`; return result; }, table: (entries: string[][]) => { - let result = `[table]`; + let result = `\n[table]`; for (const row of entries) { - result = `${result}\n[tr]`; + result = `${result}\n[tr]\n`; for (const column of row) { - result = `${result}\n[td]${column}[/td]`; + result = `${result}\n[td]${replaceTrailingNewline(column)}[/td]\n`; } - result = `${result}\n[/tr]`; + result = `${result}\n[/tr]\n`; } - result = `${result}\n[/table]`; + result = `${result}\n[/table]\n`; return result; }, }; diff --git a/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeData.ts b/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeData.ts index cac5240bab..77608e2e4f 100644 --- a/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeData.ts +++ b/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeData.ts @@ -1,8 +1,10 @@ import { ExampleData } from "../ExampleData"; import { inlineFormatData } from "./InlineFormatData"; import { welcomeTextData } from "./WelcomeTextData"; +import { challengingData } from "./ChallengingData"; export const bbCodeData: ExampleData = { + ...challengingData, ...inlineFormatData, ...welcomeTextData, }; diff --git a/packages/ckeditor5-coremedia-example-data/src/bbcode/ChallengingData.ts b/packages/ckeditor5-coremedia-example-data/src/bbcode/ChallengingData.ts new file mode 100644 index 0000000000..60142305b8 --- /dev/null +++ b/packages/ckeditor5-coremedia-example-data/src/bbcode/ChallengingData.ts @@ -0,0 +1,50 @@ +import { ExampleData } from "../ExampleData"; +import { bbCode } from "./BBCode"; + +const introduction = `${bbCode.p(`\ +When it comes to detecting, if data need to be updated within an external \ +storage layer, there may be a challenge not to propagate semantically equal \ +data. This is at least true, if any update triggers subsequent processes, that \ +may be more or less expensive (think of publishing data, translating data, \ +etc.).\ +`)}\ +${bbCode.p(`\ +To prevent such propagation in a flow setting data and subsequently getting \ +data, a plugin ${bbCode.code("DataFacade")} exists, that will provide some \ +caching and may decide to forward the unchanged data instead to the \ +storage layer. This enables the storage layer by strict equivalence check just \ +to skip a given update.\ +`)}\ +`; + +const debuggingHint = `${bbCode.p(`\ +If debug logging is activated, then you will see, that when choosing this \ +example, not the data retrieved from CKEditor 5 are forwarded to the storage \ +layer, but the cached data.\ +`)}\ +`; + +const elementOrder = `${bbCode.h1("Challenge: Element Order")}\ +${introduction}\ +${bbCode.p(`\ +This challenge is dedicated to element order: \ +It does not (really) matter, if the order of elements is \ +${bbCode.code("<em><strong>")} or \ +${bbCode.code("<strong><em>")}. \ +CKEditor 5 will prefer one of them when transforming the model state towards \ +the data layer.\ +`)}\ +${debuggingHint}\ +${bbCode.h2("Italic, Bold")}\ +${bbCode.p(`\ +${bbCode.italic(bbCode.bold("italic, bold"))}\ +`)}\ +${bbCode.h2("Bold, Italic")}\ +${bbCode.p(`\ +${bbCode.bold(bbCode.italic("bold, italic"))}\ +`)}\ +`; + +export const challengingData: ExampleData = { + "Challenge: Element Order": elementOrder, +}; diff --git a/packages/ckeditor5-coremedia-example-data/src/bbcode/WelcomeTextData.ts b/packages/ckeditor5-coremedia-example-data/src/bbcode/WelcomeTextData.ts index 6318a4ed2a..249cf68b23 100644 --- a/packages/ckeditor5-coremedia-example-data/src/bbcode/WelcomeTextData.ts +++ b/packages/ckeditor5-coremedia-example-data/src/bbcode/WelcomeTextData.ts @@ -1,20 +1,18 @@ import { ExampleData } from "../ExampleData"; import { bbCode } from "./BBCode"; -export const welcomeText = `${bbCode.h1("CKEditor 5: CoreMedia Plugin Showcase")} - -This example instance of CKEditor 5 serves as a showcase for plugins provided -by CoreMedia. Most of these plugins are mandatory to use CKEditor 5 as editor -within CoreMedia Studio. Others are required to edit CoreMedia RichText, and -then again others provide more additional functionality. For details see -corresponding documentation. - -${bbCode.h2("Example Data")} - +export const welcomeText = `${bbCode.h1("CKEditor 5: CoreMedia Plugin Showcase")}\ +${bbCode.p(`\ +This example instance of CKEditor 5 serves as a showcase for plugins provided \ +by CoreMedia. Most of these plugins are mandatory to use CKEditor 5 as editor \ +within CoreMedia Studio. Others are required to edit CoreMedia RichText, and \ +then again others provide more additional functionality. For details see \ +corresponding documentation.`)}\ +${bbCode.h2("Example Data")}\ +${bbCode.p(` For testing several use-cases you will see buttons at the top, which load different data-sets for testing and for experimenting with this CKEditor -instance and its plugins. -`; +instance and its plugins.`)}`; export const welcomeTextData: ExampleData = { Welcome: welcomeText, From 31a9917450c79cfbd76fc66db3c0741bea4468d8 Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Mon, 2 Oct 2023 09:37:43 +0200 Subject: [PATCH 050/403] fix: No Jest, No Fail There are no Jest tests, yet, for BBCode. Add `--passWithNoTests` to Jest script. --- packages/ckeditor5-coremedia-bbcode/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-coremedia-bbcode/package.json b/packages/ckeditor5-coremedia-bbcode/package.json index b6c21a859b..bb80791669 100644 --- a/packages/ckeditor5-coremedia-bbcode/package.json +++ b/packages/ckeditor5-coremedia-bbcode/package.json @@ -19,7 +19,7 @@ "clean": "pnpm clean:src && pnpm clean:dist", "clean:src": "rimraf --glob \"src/**/*.@(js|js.map|d.ts|d.ts.map)\"", "clean:dist": "rimraf ./dist", - "jest": "jest", + "jest": "jest --passWithNoTests", "jest:coverage": "jest --collect-coverage", "npm-check-updates": "npm-check-updates --upgrade" }, From b7767e9f5abf11334888d7888776b75bf3a6345b Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Mon, 2 Oct 2023 17:07:59 +0200 Subject: [PATCH 051/403] refactor: Handle Paragraphs Major refactoring regarding BBCode processing, so that we may also handle required paragraph nodes. More to follow, as well as an API to add new rules via plugins. --- .../src/BBCodeDataProcessor.ts | 3 +- .../src/html2bbcode/html2bbcode.ts | 151 ++++++++++++------ .../src/html2bbcode/rules/Anchor.ts | 26 +++ .../src/html2bbcode/rules/Bold.ts | 60 +++++-- .../src/html2bbcode/rules/DefaultRules.ts | 11 +- .../src/html2bbcode/rules/HTML2BBCodeRule.ts | 44 +++++ .../src/html2bbcode/rules/Hyperlink.ts | 21 --- .../src/html2bbcode/rules/Italic.ts | 41 +++-- .../src/html2bbcode/rules/Paragraph.ts | 14 ++ .../src/html2bbcode/rules/TaggedElement.ts | 71 ++++++++ .../src/html2bbcode/rules/TaggingRule.ts | 3 + .../html2bbcode/rules/TransformationRule.ts | 3 + .../src/html2bbcode/rules/Underline.ts | 38 +++-- 13 files changed, 378 insertions(+), 108 deletions(-) create mode 100644 packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Anchor.ts create mode 100644 packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/HTML2BBCodeRule.ts delete mode 100644 packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Hyperlink.ts create mode 100644 packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Paragraph.ts create mode 100644 packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/TaggedElement.ts create mode 100644 packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/TaggingRule.ts create mode 100644 packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/TransformationRule.ts diff --git a/packages/ckeditor5-coremedia-bbcode/src/BBCodeDataProcessor.ts b/packages/ckeditor5-coremedia-bbcode/src/BBCodeDataProcessor.ts index 965d19411d..f4d600ba2f 100644 --- a/packages/ckeditor5-coremedia-bbcode/src/BBCodeDataProcessor.ts +++ b/packages/ckeditor5-coremedia-bbcode/src/BBCodeDataProcessor.ts @@ -9,7 +9,8 @@ import { import { bbcode2html } from "./bbcode2html/bbcode2html"; import { html2bbcode } from "./html2bbcode/html2bbcode"; -import { defaultRules, HTML2BBCodeRule } from "./html2bbcode/rules/DefaultRules"; +import { defaultRules } from "./html2bbcode/rules/DefaultRules"; +import { HTML2BBCodeRule } from "./html2bbcode/rules/HTML2BBCodeRule"; /** * Data processor for BBCode. diff --git a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/html2bbcode.ts b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/html2bbcode.ts index 79ca14ebd7..3333a49dfe 100644 --- a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/html2bbcode.ts +++ b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/html2bbcode.ts @@ -1,57 +1,116 @@ +import { HasChildren, isHTMLElement, isParentNode } from "@coremedia/ckeditor5-dom-support"; +import { HTML2BBCodeRule } from "./rules/HTML2BBCodeRule"; +import { TaggedElement } from "./rules/TaggedElement"; + /** * Parses HTML to BBCode. */ -import { HTML2BBCodeRule } from "./rules/DefaultRules"; -import { isText } from "@coremedia/ckeditor5-dom-support"; - export const html2bbcode = (domFragment: Node, rules: HTML2BBCodeRule[]): string => - convertWithChildren(domFragment, rules); + new Html2BBCodeConverter(rules).convert(domFragment); -/** - * Recursively traverses all nodes in the given dom fragment and computes a bbcode string - * from it. - * - * @param domFragment - the current node to check - * @param rules - the bbcode rules that might be applied - * @returns a bbcode string that matches the given node and its children - */ -const convertWithChildren = (domFragment: Node, rules: HTML2BBCodeRule[]): string => { - let result = ""; - - /** - * If this is a text node, there will be no children and no - * further rules need to be applied. - */ - if (isText(domFragment)) { - return domFragment.textContent ?? ""; +export class Html2BBCodeConverter { + readonly #rules: HTML2BBCodeRule[]; + + constructor(rules: HTML2BBCodeRule[] = []) { + this.#rules = rules; } - /** - * This is not a text node and therefore might have child nodes. - * If that's the case, we need to compute the resulting strings of - * the children first, before we can proceed with this node. - * - * This code block converts all children to a joined string. - */ - const children = Array.from(domFragment.childNodes); - if (children.length > 0) { - const childResults: string[] = []; - children.forEach((child) => { - childResults.push(convertWithChildren(child, rules)); - }); - result = childResults.join(""); + convert(node: Node): string { + const { content, separator } = this.#convertWithChildren(node); + const { after = "", before = "" } = separator ?? {}; + const convertedContent = `${before}${content}${after}`; + // Replace leading and trailing newlines. + convertedContent.replace(/(^[\n\r]*|[\n\r]*$)/, ""); + return convertedContent; } - /** - * Now we can check if any of the given rules apply on the - * given node. If true, the result string will be wrapped by the - * computed bbcode. Otherwise, just the result string will be returned. - */ - for (const rule of rules) { - const ruleResult = rule.toData(domFragment, result); - if (ruleResult !== undefined) { - return ruleResult; + #convertWithChildren(node: Node): { + content: string; + separator?: { + before?: string; + after?: string; + }; + } { + if (!isParentNode(node)) { + return { content: node.textContent ?? "" }; } + + const processedChildren = this.#convertChildren(node); + + if (isHTMLElement(node)) { + return this.#convertHtmlElement(node, processedChildren); + } else { + return { + content: processedChildren.content, + }; + } + } + + #convertHtmlElement( + node: HTMLElement, + processedChildren: { content: string; firstBefore: string; lastAfter: string }, + ) { + const rules = this.#rules; + const taggedElement = new TaggedElement(node); + + // Stage 1: Let all rules state their opinion about the state. + for (const rule of rules) { + rule.tag?.(taggedElement); + } + + const { separator: parentSeparator } = taggedElement; + + if (parentSeparator?.before === processedChildren.firstBefore) { + parentSeparator.before = ""; + } + + if (parentSeparator?.after === processedChildren.lastAfter) { + parentSeparator.after = ""; + } + + let content = processedChildren.content; + + for (const rule of rules) { + content = rule.transform?.(taggedElement, content) ?? content; + } + + return { + content, + separator: parentSeparator, + }; + } + + #convertChildren(node: HasChildren): { + content: string; + firstBefore: string; + lastAfter: string; + } { + const childNodes = Array.from(node.childNodes); + let childContent = ""; + let previousAfter = ""; + let firstBefore = ""; + + childNodes.forEach((value: ChildNode, index: number): void => { + const { content, separator } = this.#convertWithChildren(value); + + let before = ""; + if (separator?.before) { + before = separator.before; + if (index === 0) { + // Simple de-duplication of separators. + firstBefore = before; + } + } + + const after = separator?.after ?? ""; + + // Simple de-duplication of separators. + if (before === previousAfter) { + before = ""; + } + childContent = `${childContent}${before}${content}${after}`; + previousAfter = after; + }); + return { content: childContent, firstBefore, lastAfter: previousAfter }; } - return result; -}; +} diff --git a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Anchor.ts b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Anchor.ts new file mode 100644 index 0000000000..ddccb1dfc0 --- /dev/null +++ b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Anchor.ts @@ -0,0 +1,26 @@ +import { isHTMLAnchorElement } from "@coremedia/ckeditor5-dom-support"; +import { HTML2BBCodeRule } from "./HTML2BBCodeRule"; + +export const anchorRule: HTML2BBCodeRule = { + id: "url", + tag(taggedElement): void { + const { element } = taggedElement; + if (!isHTMLAnchorElement(element)) { + return; + } + const { href } = element; + if (href) { + taggedElement.link = href; + } + }, + transform(taggedElement, content): string { + const { link } = taggedElement; + if (!link) { + return content; + } + if (link === true) { + return `[url]${content}[/url]`; + } + return `[url=${link}]${content}[/url]`; + }, +}; diff --git a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Bold.ts b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Bold.ts index 6fe2a82df1..2436e829ff 100644 --- a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Bold.ts +++ b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Bold.ts @@ -1,16 +1,54 @@ -import { HTML2BBCodeRule } from "./DefaultRules"; +import { HTML2BBCodeRule } from "./HTML2BBCodeRule"; -export const boldRule: HTML2BBCodeRule = { - id: "Bold", - toData: (node, content: string) => { - if (!isBold(node)) { - return undefined; +const fontWeightNormal = 400; +const fontWeightBold = 700; +const boldTags = ["b", "strong"]; +const nonBoldFontWeights = ["normal", "lighter"]; + +const parseFontWeight = (fontWeight: string): number | undefined => { + if (Number.isNaN(fontWeight)) { + const normalizedFontWeight = fontWeight.trim().toLowerCase(); + if (normalizedFontWeight.startsWith("bold")) { + return fontWeightBold; + } else if (nonBoldFontWeights.includes(normalizedFontWeight)) { + return fontWeightNormal; } - return `[b]${content}[/b]`; - }, + } else { + return Number(fontWeight); + } }; -const isBold = (node: Node): boolean => { - const nodeName = node.nodeName; - return nodeName === "B" || nodeName === "STRONG"; +export const boldRule: HTML2BBCodeRule = { + id: "b", + tag(taggedElement): void { + const { element } = taggedElement; + const { + tagName, + style: { fontWeight }, + } = element; + if (fontWeight) { + const numericFontWeight = parseFontWeight(fontWeight); + if (numericFontWeight !== undefined) { + // Stick to the rule, that we should only set a _truthy_ value, so that + // we do not accidentally veto decisions on bold state made by other + // rules. + // + // Nevertheless, assuming that we have a bold HTML tag (`` or + // ``), we still do not let the below name-based rule set + // the bold state if the configured font-weight is non-bold. + if (numericFontWeight > fontWeightNormal) { + taggedElement.bold = true; + } + return; + } + } + + if (boldTags.includes(tagName.toLowerCase())) { + taggedElement.bold = true; + } + }, + transform(taggedElement, content): string { + const { bold } = taggedElement; + return bold ? `[b]${content}[/b]` : content; + }, }; diff --git a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/DefaultRules.ts b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/DefaultRules.ts index 703040e3c2..f4c41cd2f9 100644 --- a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/DefaultRules.ts +++ b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/DefaultRules.ts @@ -1,11 +1,8 @@ import { boldRule } from "./Bold"; import { italicRule } from "./Italic"; import { underlineRule } from "./Underline"; -import { hyperlinkRule } from "./Hyperlink"; +import { anchorRule } from "./Anchor"; +import { HTML2BBCodeRule } from "./HTML2BBCodeRule"; +import { paragraphRule } from "./Paragraph"; -export interface HTML2BBCodeRule { - id: string; - toData: (node: Node, content: string) => string | undefined; -} - -export const defaultRules: HTML2BBCodeRule[] = [boldRule, italicRule, underlineRule, hyperlinkRule]; +export const defaultRules: HTML2BBCodeRule[] = [boldRule, italicRule, underlineRule, anchorRule, paragraphRule]; diff --git a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/HTML2BBCodeRule.ts b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/HTML2BBCodeRule.ts new file mode 100644 index 0000000000..c23e9cc68a --- /dev/null +++ b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/HTML2BBCodeRule.ts @@ -0,0 +1,44 @@ +import { TaggingRule } from "./TaggingRule"; +import { TransformationRule } from "./TransformationRule"; + +/** + * A rule to apply during HTML to BBCode processing. + * + * The ID has no semantics, but may help during debugging. + * + * Regarding the processing order, first all `tag` rules are applied and + * afterward all `transform` rules. This allows, for example, that other rules + * toggle the `bold` tag, while there is only one rule, that creates the bold + * BBCode tag eventually. + */ +export interface HTML2BBCodeRule { + id: string; + /** + * Process the given element and apply tags accordingly. + * + * It is good practice, just to set a tag to a truthy value. `false` may + * always be regarded as the default. Exceptions to this good practice may + * exist, though. + * + * Example: The default rule for `[b]` (bold) just respects font-weight + * and/or tag name of the `HTMLElement`. It only sets `bold` to `true`, + * when it has some confidence, that the text should be marked as bold. + * It will never set the value to `false`, as other rules in undetermined + * order may consider, for example, class-list entries to toggle the format + * to bold. + */ + tag?: TaggingRule; + /** + * Transform the tagged element to BBCode. Typical rules apply to only + * one state, like if an element is considered bold. + * + * Rules should assume that there is only one rule that takes care of + * the actual transformation. If that is not the case, please check your + * rule-setup. + * + * **Example:** Only one rule should map the bold state to `[b]text[/b]`, + * while others are free to apply additional conditions to set the bold tag + * within the `tag` processing. + */ + transform?: TransformationRule; +} diff --git a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Hyperlink.ts b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Hyperlink.ts deleted file mode 100644 index cc2828532e..0000000000 --- a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Hyperlink.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { HTML2BBCodeRule } from "./DefaultRules"; -import { isElement } from "@coremedia/ckeditor5-dom-support"; - -export const hyperlinkRule: HTML2BBCodeRule = { - id: "Hyperlink", - toData: (node, content: string) => { - if (!isHyperlink(node)) { - return undefined; - } - if (!isElement(node)) { - return `[url]${content}[/url]`; - } - const href = node.getAttribute("href"); - return `[url=${href}]${content}[/url]`; - }, -}; - -const isHyperlink = (node: Node): boolean => { - const nodeName = node.nodeName; - return nodeName === "A"; -}; diff --git a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Italic.ts b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Italic.ts index b5b0702044..638e8a2db9 100644 --- a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Italic.ts +++ b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Italic.ts @@ -1,16 +1,35 @@ -import { HTML2BBCodeRule } from "./DefaultRules"; +import { HTML2BBCodeRule } from "./HTML2BBCodeRule"; + +const italicTags = ["i", "em"]; +const italicStyles = ["italic", "oblique"]; +const italicVetoStyle = "normal"; export const italicRule: HTML2BBCodeRule = { - id: "Italic", - toData: (node, content: string) => { - if (!isItalic(node)) { - return undefined; + id: "i", + tag(taggedElement): void { + const { element } = taggedElement; + const { + tagName, + style: { fontStyle }, + } = element; + if (fontStyle) { + const normalizedFontStyle = fontStyle.trim().toLowerCase(); + if (italicStyles.includes(normalizedFontStyle)) { + taggedElement.italic = true; + return; + } + // Skip decision by tag-name. + if (italicVetoStyle === normalizedFontStyle) { + return; + } } - return `[i]${content}[/i]`; - }, -}; -const isItalic = (node: Node): boolean => { - const nodeName = node.nodeName; - return nodeName === "I" || nodeName === "EM"; + if (italicTags.includes(tagName.toLowerCase())) { + taggedElement.italic = true; + } + }, + transform(taggedElement, content): string { + const { italic } = taggedElement; + return italic ? `[i]${content}[/i]` : content; + }, }; diff --git a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Paragraph.ts b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Paragraph.ts new file mode 100644 index 0000000000..298be6356b --- /dev/null +++ b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Paragraph.ts @@ -0,0 +1,14 @@ +import { HTML2BBCodeRule } from "./HTML2BBCodeRule"; + +export const paragraphRule: HTML2BBCodeRule = { + id: "p", + tag(taggedElement): void { + const { element } = taggedElement; + if (element instanceof HTMLParagraphElement) { + taggedElement.separator = { + before: "\n\n", + after: "\n\n", + }; + } + }, +}; diff --git a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/TaggedElement.ts b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/TaggedElement.ts new file mode 100644 index 0000000000..da250b5c44 --- /dev/null +++ b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/TaggedElement.ts @@ -0,0 +1,71 @@ +export class TaggedElement { + /** + * If this element requires a separator before and/or after. + */ + separator?: { + before?: string; + after?: string; + }; + /** + * Signals detected heading level. + */ + heading?: 1 | 2 | 3 | 4 | 5 | 6; + /** + * Signal, if this element is considered **bold**. + */ + bold?: boolean; + /** + * Signal, if this element is considered _italic_. + */ + italic?: boolean; + /** + * Signal, if this element is considered underlined. + */ + underline?: boolean; + /** + * Signal, if this element is considered as ~~strikethrough~~. + */ + strikethrough?: boolean; + /** + * Signal, if any alignment is to be applied. + */ + alignment?: "center" | "left" | "right"; + /** + * Signal, if any color should be set. + */ + color?: string; + /** + * Signal, if any font size should be set. + */ + size?: number; + /** + * Signal, if to create an `[url]` element, and if given as string, what + * link target value should be set. + */ + link?: boolean | string; + /** + * Signal, if to reference an image, and if, what are the properties to take + * into account. + */ + image?: { + url: string; + width?: number; + height?: number; + }; + /** + * Signal, if the element wraps list items, and if it should be considered + * ordered or unordered. + */ + list?: "unordered" | "ordered"; + /** + * Signal, if the element is considered a list item. + */ + listItem?: boolean; + code?: boolean | string = false; + preformatted?: boolean; + table?: boolean; + tableRow?: boolean; + tableCell?: "data" | "heading"; + [tagName: string]: unknown; + constructor(public readonly element: HTMLElement) {} +} diff --git a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/TaggingRule.ts b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/TaggingRule.ts new file mode 100644 index 0000000000..e6f3b60a47 --- /dev/null +++ b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/TaggingRule.ts @@ -0,0 +1,3 @@ +import { TaggedElement } from "./TaggedElement"; + +export type TaggingRule = (element: TaggedElement) => void; diff --git a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/TransformationRule.ts b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/TransformationRule.ts new file mode 100644 index 0000000000..3cc3d2e088 --- /dev/null +++ b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/TransformationRule.ts @@ -0,0 +1,3 @@ +import { TaggedElement } from "./TaggedElement"; + +export type TransformationRule = (element: TaggedElement, content: string) => string; diff --git a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Underline.ts b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Underline.ts index 66db51361a..653a4ccdda 100644 --- a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Underline.ts +++ b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Underline.ts @@ -1,16 +1,32 @@ -import { HTML2BBCodeRule } from "./DefaultRules"; +import { HTML2BBCodeRule } from "./HTML2BBCodeRule"; +const underlineTags = ["u", "ins"]; + +/** + * Maps `` to `[u]`. + * + * While `` is nowadays used to express _unarticulated annotation_, + * CKEditor's _underline_ command still uses `` in view layers. If this + * changes, mappings need to be adjusted. + */ export const underlineRule: HTML2BBCodeRule = { - id: "Underline", - toData: (node, content: string) => { - if (!isItalic(node)) { - return undefined; + id: "u", + tag(taggedElement): void { + const { element } = taggedElement; + const { + tagName, + style: { textDecoration }, + } = element; + if (textDecoration === "none") { + // Vetoes any possible underline. + return; + } + if (textDecoration.includes("underline") || underlineTags.includes(tagName.toLowerCase())) { + taggedElement.underline = true; } - return `[u]${content}[/u]`; }, -}; - -const isItalic = (node: Node): boolean => { - const nodeName = node.nodeName; - return nodeName === "U"; + transform(taggedElement, content): string { + const { underline } = taggedElement; + return underline ? `[u]${content}[/u]` : content; + }, }; From cb38981f3d12ad522ee1b123fd44e6abaffe94eb Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Mon, 2 Oct 2023 17:34:24 +0200 Subject: [PATCH 052/403] feat: Introduce Rules for Headings --- app/src/editors/bbCode.ts | 11 +++++++ .../src/html2bbcode/html2bbcode.ts | 3 +- .../src/html2bbcode/rules/DefaultRules.ts | 10 +++++- .../src/html2bbcode/rules/Heading.ts | 31 +++++++++++++++++++ .../src/html2bbcode/rules/TaggedElement.ts | 2 +- 5 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Heading.ts diff --git a/app/src/editors/bbCode.ts b/app/src/editors/bbCode.ts index a65e9310d0..adbcd3c9f9 100644 --- a/app/src/editors/bbCode.ts +++ b/app/src/editors/bbCode.ts @@ -52,5 +52,16 @@ export const createBBCodeEditor: CKEditorInstanceFactory = ( return Promise.resolve(); }, }, + heading: { + options: [ + { model: "paragraph", title: "Paragraph", class: "ck-heading_paragraph" }, + { model: "heading1", view: "h1", title: "Heading 1", class: "ck-heading_heading1" }, + { model: "heading2", view: "h2", title: "Heading 2", class: "ck-heading_heading2" }, + { model: "heading3", view: "h3", title: "Heading 3", class: "ck-heading_heading3" }, + { model: "heading4", view: "h4", title: "Heading 4", class: "ck-heading_heading4" }, + { model: "heading5", view: "h5", title: "Heading 5", class: "ck-heading_heading5" }, + { model: "heading6", view: "h6", title: "Heading 6", class: "ck-heading_heading6" }, + ], + }, }); }; diff --git a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/html2bbcode.ts b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/html2bbcode.ts index 3333a49dfe..baf26ba351 100644 --- a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/html2bbcode.ts +++ b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/html2bbcode.ts @@ -20,8 +20,7 @@ export class Html2BBCodeConverter { const { after = "", before = "" } = separator ?? {}; const convertedContent = `${before}${content}${after}`; // Replace leading and trailing newlines. - convertedContent.replace(/(^[\n\r]*|[\n\r]*$)/, ""); - return convertedContent; + return convertedContent.replace(/(^[\n\r]*|[\n\r]*$)/g, ""); } #convertWithChildren(node: Node): { diff --git a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/DefaultRules.ts b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/DefaultRules.ts index f4c41cd2f9..2a8a9ac623 100644 --- a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/DefaultRules.ts +++ b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/DefaultRules.ts @@ -4,5 +4,13 @@ import { underlineRule } from "./Underline"; import { anchorRule } from "./Anchor"; import { HTML2BBCodeRule } from "./HTML2BBCodeRule"; import { paragraphRule } from "./Paragraph"; +import { headingRule } from "./Heading"; -export const defaultRules: HTML2BBCodeRule[] = [boldRule, italicRule, underlineRule, anchorRule, paragraphRule]; +export const defaultRules: HTML2BBCodeRule[] = [ + anchorRule, + boldRule, + headingRule, + italicRule, + paragraphRule, + underlineRule, +]; diff --git a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Heading.ts b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Heading.ts new file mode 100644 index 0000000000..533003642c --- /dev/null +++ b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/Heading.ts @@ -0,0 +1,31 @@ +import { HTML2BBCodeRule } from "./HTML2BBCodeRule"; + +const headingRegEx = /^h(?\d)$/; + +export const headingRule: HTML2BBCodeRule = { + id: "p", + tag(taggedElement): void { + const { element } = taggedElement; + if (element instanceof HTMLHeadingElement) { + taggedElement.separator = { + before: "\n\n", + after: "\n\n", + }; + const match = element.localName.match(headingRegEx); + if (match) { + // @ts-expect-error: https://github.com/microsoft/TypeScript/issues/32098 + const { level }: { level: string } = match.groups; + if (!Number.isNaN(level)) { + const headingNumber = Number(level); + if (headingNumber >= 1 && headingNumber <= 6) { + taggedElement.heading = headingNumber; + } + } + } + } + }, + transform(taggedElement, content): string { + const { heading } = taggedElement; + return heading ? `[h${heading}]${content}[/h${heading}]` : content; + }, +}; diff --git a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/TaggedElement.ts b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/TaggedElement.ts index da250b5c44..c0159dc164 100644 --- a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/TaggedElement.ts +++ b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/rules/TaggedElement.ts @@ -9,7 +9,7 @@ export class TaggedElement { /** * Signals detected heading level. */ - heading?: 1 | 2 | 3 | 4 | 5 | 6; + heading?: number; /** * Signal, if this element is considered **bold**. */ From 467ff1b60fc8155f8c476784fbbda066feae3a4a Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Mon, 2 Oct 2023 17:36:36 +0200 Subject: [PATCH 053/403] chore: ESLint Fix --- packages/ckeditor5-coremedia-example-data/src/bbcode/BBCode.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCode.ts b/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCode.ts index c032d3e82a..19f7d93a53 100644 --- a/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCode.ts +++ b/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCode.ts @@ -26,8 +26,7 @@ const replaceTrailingNewline = (text: string, replacement = ""): string => text. * * https://www.bbcode.org/how-to-use-bbcode-a-complete-guide.php */ export const bbCode = { - heading: (text: string, level: 1 | 2 | 3 | 4 | 5 | 6) => - `\n[h${level}]${replaceTrailingNewline(text)}[/h${level}]\n`, + heading: (text: string, level: 1 | 2 | 3 | 4 | 5 | 6) => `\n[h${level}]${replaceTrailingNewline(text)}[/h${level}]\n`, h1: (text: string) => bbCode.heading(text, 1), h2: (text: string) => bbCode.heading(text, 2), h3: (text: string) => bbCode.heading(text, 3), From abe2eb4bb3f2f47fd99806566a45f732e3fc4c3f Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Wed, 4 Oct 2023 08:41:40 +0200 Subject: [PATCH 054/403] test: Security Challenge Data --- .../src/bbcode/BBCodeData.ts | 2 + .../src/bbcode/SecurityChallengeData.ts | 67 +++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 packages/ckeditor5-coremedia-example-data/src/bbcode/SecurityChallengeData.ts diff --git a/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeData.ts b/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeData.ts index 77608e2e4f..236d153414 100644 --- a/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeData.ts +++ b/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeData.ts @@ -2,9 +2,11 @@ import { ExampleData } from "../ExampleData"; import { inlineFormatData } from "./InlineFormatData"; import { welcomeTextData } from "./WelcomeTextData"; import { challengingData } from "./ChallengingData"; +import { securityChallengeData } from "./SecurityChallengeData"; export const bbCodeData: ExampleData = { ...challengingData, ...inlineFormatData, + ...securityChallengeData, ...welcomeTextData, }; diff --git a/packages/ckeditor5-coremedia-example-data/src/bbcode/SecurityChallengeData.ts b/packages/ckeditor5-coremedia-example-data/src/bbcode/SecurityChallengeData.ts new file mode 100644 index 0000000000..14c68ae69d --- /dev/null +++ b/packages/ckeditor5-coremedia-example-data/src/bbcode/SecurityChallengeData.ts @@ -0,0 +1,67 @@ +import { ExampleData } from "../ExampleData"; +import { bbCode } from "./BBCode"; + +const lines = (...texts: string[]): string => texts.join(""); + +const htmlEntity = { + lsqb: { + raw: `[`, + dec: `[`, + hex: `[`, + named: `[`, + }, + rsqb: { + raw: `]`, + dec: `]`, + hex: `]`, + named: `]`, + }, + gt: { + raw: `>`, + dec: `>`, + hex: `>`, + named: `>`, + }, + lt: { + raw: `<`, + dec: `<`, + hex: `<`, + named: `<`, + }, +}; + +/** + * Same possible challenges to BBCode to HTML and vice versa processing. + * + * @see [XSS Filter Evasion – OWASP Cheat Sheet Series](https://cheatsheetseries.owasp.org/cheatsheets/XSS_Filter_Evasion_Cheat_Sheet.html) + */ +export const securityChallengeData: ExampleData = { + "Security: Inline HTML in BBCode": lines( + `${bbCode.h1("Security: Inline HTML in BBCode")}`, + `${bbCode.p(`Lorem ipsum dolor`)}`, + ), + "Security: Escaped Inline HTML in BBCode (Decimal)": lines( + `${bbCode.h1("Security: Escaped Inline HTML in BBCode (Decimal)")}`, + `${bbCode.p(`Lorem ${htmlEntity.lt.dec}strong${htmlEntity.gt.dec}ipsum${htmlEntity.lt.dec}/strong${htmlEntity.gt.dec} dolor`)}`, + ), + "Security: Escaped Inline HTML in BBCode (Hexadecimal)": lines( + `${bbCode.h1("Security: Escaped Inline HTML in BBCode (Hexadecimal)")}`, + `${bbCode.p(`Lorem ${htmlEntity.lt.hex}strong${htmlEntity.gt.hex}ipsum${htmlEntity.lt.hex}/strong${htmlEntity.gt.hex} dolor`)}`, + ), + "Security: Escaped Inline HTML in BBCode (Named)": lines( + `${bbCode.h1("Security: Escaped Inline HTML in BBCode (Named)")}`, + `${bbCode.p(`Lorem ${htmlEntity.lt.named}strong${htmlEntity.gt.named}ipsum${htmlEntity.lt.named}/strong${htmlEntity.gt.named} dolor`)}`, + ), + "Security: Square Bracket HTML Entity in BBCode (Decimal)": lines( + `${bbCode.h1("Security: Square Bracket HTML Entity in BBCode (Decimal)")}`, + `Lorem ${htmlEntity.lsqb.dec}b${htmlEntity.rsqb.dec}ipsum${htmlEntity.lsqb.dec}/b${htmlEntity.rsqb.dec} dolor`, + ), + "Security: Square Bracket HTML Entity in BBCode (Hexadecimal)": lines( + `${bbCode.h1("Security: Square Bracket HTML Entity in BBCode (Hexadecimal)")}`, + `Lorem ${htmlEntity.lsqb.hex}b${htmlEntity.rsqb.hex}ipsum${htmlEntity.lsqb.hex}/b${htmlEntity.rsqb.hex} dolor`, + ), + "Security: Square Bracket HTML Entity in BBCode (Named)": lines( + `${bbCode.h1("Security: Square Bracket HTML Entity in BBCode (Named)")}`, + `Lorem ${htmlEntity.lsqb.named}b${htmlEntity.rsqb.named}ipsum${htmlEntity.lsqb.named}/b${htmlEntity.rsqb.named} dolor`, + ), +}; From c48237f017cd4af72ef2e7ba6713bc315c756582 Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Wed, 4 Oct 2023 11:40:32 +0200 Subject: [PATCH 055/403] feat: Add BBCode Security Layer We have to apply some escaping to ensure, that untrusted BBCode may not harm the application using the BBCode Plugin within CKEditor 5. For the data, we expect that the application specific BBCode parser is aware of common escaping syntax via backslash characters. --- packages/ckeditor5-coremedia-bbcode/README.md | 27 ++++++++++++++ .../ckeditor5-coremedia-bbcode/package.json | 5 ++- .../src/bbcode2html/bbcode2html.ts | 37 +++++++++++++++++-- .../src/html2bbcode/html2bbcode.ts | 16 +++++++- .../ckeditor5-coremedia-bbcode/tsconfig.json | 6 ++- .../types/@bbob/core/index.d.ts | 32 ++++++++++++++++ .../types/@bbob/html/es/index.d.ts | 7 ++++ .../types/@bbob/preset-html5/es/index.d.ts | 4 ++ .../types/@bbob/preset-html5/index.d.ts | 4 -- .../src/bbcode/SecurityChallengeData.ts | 6 +++ 10 files changed, 132 insertions(+), 12 deletions(-) create mode 100644 packages/ckeditor5-coremedia-bbcode/README.md create mode 100644 packages/ckeditor5-coremedia-bbcode/types/@bbob/core/index.d.ts create mode 100644 packages/ckeditor5-coremedia-bbcode/types/@bbob/preset-html5/es/index.d.ts delete mode 100644 packages/ckeditor5-coremedia-bbcode/types/@bbob/preset-html5/index.d.ts diff --git a/packages/ckeditor5-coremedia-bbcode/README.md b/packages/ckeditor5-coremedia-bbcode/README.md new file mode 100644 index 0000000000..ea688f0e61 --- /dev/null +++ b/packages/ckeditor5-coremedia-bbcode/README.md @@ -0,0 +1,27 @@ +# CoreMedia BBCode Plugin + +[![API Documentation][docs:api:badge]][docs:api] + +[docs:api]: "@coremedia/ckeditor5-coremedia-bbcode" +[docs:api:badge]: + +**Module:** `@coremedia/ckeditor5-coremedia-bbcode` + +TODO: Rename to ckeditor5-bbcode... there is nothing CoreMedia specific inside here. + +TODO: Documentation + +## Security Considerations + +### Escaping + +The BBCode plugin uses escaping via backslash character for parsing as well as +processing HTML to BBCode (`toData` transformation). + +Thus, expect any stored BBCode to escape square brackets in text with +backslash characters: + +```text +[b]This is bold![/b] +\[b\]This is not bold!\[/b\] +``` diff --git a/packages/ckeditor5-coremedia-bbcode/package.json b/packages/ckeditor5-coremedia-bbcode/package.json index bb80791669..9e71f71206 100644 --- a/packages/ckeditor5-coremedia-bbcode/package.json +++ b/packages/ckeditor5-coremedia-bbcode/package.json @@ -44,8 +44,9 @@ "@ckeditor/ckeditor5-engine": "^39.0.2" }, "dependencies": { - "@bbob/html": "^3.0.0", - "@bbob/preset-html5": "^3.0.0", + "@bbob/core": "^3.0.2", + "@bbob/html": "^3.0.2", + "@bbob/preset-html5": "^3.0.2", "@coremedia/ckeditor5-core-common": "16.0.1-rc.2" } } diff --git a/packages/ckeditor5-coremedia-bbcode/src/bbcode2html/bbcode2html.ts b/packages/ckeditor5-coremedia-bbcode/src/bbcode2html/bbcode2html.ts index 5f57544405..a48b441fb1 100644 --- a/packages/ckeditor5-coremedia-bbcode/src/bbcode2html/bbcode2html.ts +++ b/packages/ckeditor5-coremedia-bbcode/src/bbcode2html/bbcode2html.ts @@ -1,7 +1,38 @@ -import bbobHTML from "@bbob/html/es"; -import presetHTML5 from "@bbob/preset-html5"; +import html5Preset from "@bbob/preset-html5/es"; +import { render } from "@bbob/html/es"; +import bbob from "@bbob/core"; + +interface EscapeRule { + from: string; + to: string; +} + +/** + * Escaping applied exactly in the given order. + */ +const escapeRules: EscapeRule[] = [ + { from: "&", to: "&" }, + { from: ">", to: ">" }, + { from: "<", to: "<" }, + { from: '"', to: """ }, +]; + +const escapeForXml = (text: string): string => { + let result = text; + for (const escaping of escapeRules) { + result = result.replace(escaping.from, escaping.to); + } + return result; +}; /** * Parses BBCode to HTML. */ -export const bbcode2html = (bbcode: string): string => bbobHTML(bbcode, presetHTML5()); +export const bbcode2html = (bbcode: string): string => { + const htmlEscapedBBCode = escapeForXml(bbcode); + const processor = bbob(html5Preset()); + const processed = processor.process(htmlEscapedBBCode, { render, enableEscapeTags: true }); + // TODO: Add better logging here. + console.log("bbcode2html", processed); + return processed.html; +}; diff --git a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/html2bbcode.ts b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/html2bbcode.ts index baf26ba351..f9dc3fba45 100644 --- a/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/html2bbcode.ts +++ b/packages/ckeditor5-coremedia-bbcode/src/html2bbcode/html2bbcode.ts @@ -8,6 +8,20 @@ import { TaggedElement } from "./rules/TaggedElement"; export const html2bbcode = (domFragment: Node, rules: HTML2BBCodeRule[]): string => new Html2BBCodeConverter(rules).convert(domFragment); +/** + * Adds backslashes to any possibly existing square brackets within the given + * text so that it is not interpreted by BBCode parser. Also, a backslash + * itself will be escaped. + * + * Note that it should be ensured that corresponding BBCode parsers reading + * the data, support this type of escaping. + * + * @param text - plain text to escape + */ +const escapeText = (text: string): string => { + return text.replace(/([\][\\])/g, "\\$1"); +}; + export class Html2BBCodeConverter { readonly #rules: HTML2BBCodeRule[]; @@ -31,7 +45,7 @@ export class Html2BBCodeConverter { }; } { if (!isParentNode(node)) { - return { content: node.textContent ?? "" }; + return { content: escapeText(node.textContent ?? "") }; } const processedChildren = this.#convertChildren(node); diff --git a/packages/ckeditor5-coremedia-bbcode/tsconfig.json b/packages/ckeditor5-coremedia-bbcode/tsconfig.json index d7f6a45c01..74835e818f 100644 --- a/packages/ckeditor5-coremedia-bbcode/tsconfig.json +++ b/packages/ckeditor5-coremedia-bbcode/tsconfig.json @@ -2,12 +2,14 @@ "extends": "../../tsconfig.json", "include": [ "./__tests__", - "./src" + "./src", + "./types", ], "compilerOptions": { "paths": { + "@bbob/core": ["./types/@bbob/core/index.d.ts"], "@bbob/html/es": ["./types/@bbob/html/es/index.d.ts"], - "@bbob/preset-html5": ["./types/@bbob/preset-html5/index.d.ts"], + "@bbob/preset-html5": ["./types/@bbob/preset-html5/es/index.d.ts"], } } } diff --git a/packages/ckeditor5-coremedia-bbcode/types/@bbob/core/index.d.ts b/packages/ckeditor5-coremedia-bbcode/types/@bbob/core/index.d.ts new file mode 100644 index 0000000000..3e1f0e5af0 --- /dev/null +++ b/packages/ckeditor5-coremedia-bbcode/types/@bbob/core/index.d.ts @@ -0,0 +1,32 @@ +declare module "@bbob/core" { + export default function bbob( + // eslint-disable-next-line @typescript-eslint/ban-types + plugs?: Function | Function[], + ): { + process: ( + input: string, + opts?: { + // eslint-disable-next-line @typescript-eslint/ban-types + parser?: Function; + // eslint-disable-next-line @typescript-eslint/ban-types + render?: Function; + skipParse?: boolean; + data?: null | unknown; + onlyAllowTags?: string[]; + contextFreeTags?: string[]; + enableEscapeTags?: boolean; + }, + ) => { + readonly html: string; + tree: { + messages: unknown[]; + options: object; + walk: unknown; + // eslint-disable-next-line @typescript-eslint/ban-types + match: Function; + }; + raw: unknown; + messages: unknown[]; + }; + }; +} diff --git a/packages/ckeditor5-coremedia-bbcode/types/@bbob/html/es/index.d.ts b/packages/ckeditor5-coremedia-bbcode/types/@bbob/html/es/index.d.ts index 131c77a7a9..0d50f98e31 100644 --- a/packages/ckeditor5-coremedia-bbcode/types/@bbob/html/es/index.d.ts +++ b/packages/ckeditor5-coremedia-bbcode/types/@bbob/html/es/index.d.ts @@ -1,4 +1,10 @@ declare module "@bbob/html/es" { + interface BBNodeObject { + content: null | unknown; + tag: string; + attrs: unknown; + } + type BBNode = null | string | number | BBNodeObject | BBNode[]; export default function toHTML( source: string, // eslint-disable-next-line @typescript-eslint/ban-types @@ -9,4 +15,5 @@ declare module "@bbob/html/es" { enableEscapeTags?: boolean; }, ): string; + export const render: (nodes: BBNode, { stripTags = false } = {}) => string; } diff --git a/packages/ckeditor5-coremedia-bbcode/types/@bbob/preset-html5/es/index.d.ts b/packages/ckeditor5-coremedia-bbcode/types/@bbob/preset-html5/es/index.d.ts new file mode 100644 index 0000000000..430cd9c923 --- /dev/null +++ b/packages/ckeditor5-coremedia-bbcode/types/@bbob/preset-html5/es/index.d.ts @@ -0,0 +1,4 @@ +declare module "@bbob/preset-html5/es" { + // eslint-disable-next-line @typescript-eslint/ban-types + export default function html5Preset(): Function; +} diff --git a/packages/ckeditor5-coremedia-bbcode/types/@bbob/preset-html5/index.d.ts b/packages/ckeditor5-coremedia-bbcode/types/@bbob/preset-html5/index.d.ts deleted file mode 100644 index 0f22f991cd..0000000000 --- a/packages/ckeditor5-coremedia-bbcode/types/@bbob/preset-html5/index.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module "@bbob/preset-html5" { - // eslint-disable-next-line @typescript-eslint/ban-types - export default function presetHTML5(): Function; -} diff --git a/packages/ckeditor5-coremedia-example-data/src/bbcode/SecurityChallengeData.ts b/packages/ckeditor5-coremedia-example-data/src/bbcode/SecurityChallengeData.ts index 14c68ae69d..3b7e5316fb 100644 --- a/packages/ckeditor5-coremedia-example-data/src/bbcode/SecurityChallengeData.ts +++ b/packages/ckeditor5-coremedia-example-data/src/bbcode/SecurityChallengeData.ts @@ -36,6 +36,12 @@ const htmlEntity = { * @see [XSS Filter Evasion – OWASP Cheat Sheet Series](https://cheatsheetseries.owasp.org/cheatsheets/XSS_Filter_Evasion_Cheat_Sheet.html) */ export const securityChallengeData: ExampleData = { + "Security: Escaping in BBCode": lines( + `${bbCode.h1("Security: Escaping in BBCode")}`, + `${bbCode.p(`Lorem \\[b\\]ip\\sum\\[/b\\] dolor`)}`, + `${bbCode.h2("Remark")}`, + `${bbCode.p(`For safe processing we enable escaping by default. It is required, that parsers for stored BBCode apply the same escaping mechanism.`)}`, + ), "Security: Inline HTML in BBCode": lines( `${bbCode.h1("Security: Inline HTML in BBCode")}`, `${bbCode.p(`Lorem ipsum dolor`)}`, From ca1d3bb60f8f508e0558ace08cd615b6693af037 Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Wed, 4 Oct 2023 11:40:44 +0200 Subject: [PATCH 056/403] chore: Update pnpm-lock.yaml --- pnpm-lock.yaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f506f7a963..9eff04af07 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -435,11 +435,14 @@ importers: packages/ckeditor5-coremedia-bbcode: dependencies: + '@bbob/core': + specifier: ^3.0.2 + version: 3.0.2 '@bbob/html': - specifier: ^3.0.0 + specifier: ^3.0.2 version: 3.0.2 '@bbob/preset-html5': - specifier: ^3.0.0 + specifier: ^3.0.2 version: 3.0.2 '@coremedia/ckeditor5-core-common': specifier: 16.0.1-rc.2 From 5eac02a474a9edbb5667b6615b3f28995753a75c Mon Sep 17 00:00:00 2001 From: Mark Michaelis Date: Wed, 4 Oct 2023 13:16:27 +0200 Subject: [PATCH 057/403] doc: Documenting Supported BBCode Tags Actually, there a way more, because of naive processing of BBob, that maps `[h1]` to `

    ` just by replacing square brackets by angle brackets. Similar, a `[lorem]` will become `` without even checking, if this is a valid tag. We may need to recommend or just use `onlyAllowTags` for whitelisting supported tags. --- packages/ckeditor5-coremedia-bbcode/README.md | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/packages/ckeditor5-coremedia-bbcode/README.md b/packages/ckeditor5-coremedia-bbcode/README.md index ea688f0e61..dcb0a7a1ff 100644 --- a/packages/ckeditor5-coremedia-bbcode/README.md +++ b/packages/ckeditor5-coremedia-bbcode/README.md @@ -11,6 +11,38 @@ TODO: Rename to ckeditor5-bbcode... there is nothing CoreMedia specific inside h TODO: Documentation +## Supported BBCode + +The BBCode to HTML processing is based on +[JiLiZART/BBob](https://github.com/JiLiZART/BBob/tree/master) and its +HTML5 Preset. + +As such, the following tags are supported: + +| Tag | as HTML | +|-----------|-------------------------------------------------| +| `[h1]` | `

    ` | +| `[h2]` | `

    ` | +| `[h3]` | `

    ` | +| `[h4]` | `

    ` | +| `[h5]` | `

    ` | +| `[h6]` | `
    ` | +| `[b]` | `` | +| `[i]` | `` | +| `[u]` | `` | +| `[s]` | `` | +| `[url]` | `` | +| `[img]` | `` | +| `[quote]` | `

    ` | +| `[code]` | `

    `                                         |
    +| `[style]` | ``                            |
    +| `[list]`  | `
      ` or `