diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e1d43d1da1..3052e52f92 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -145,7 +145,7 @@ jobs: env: CKEDITOR_LICENSE_KEY: ${{ secrets.CKEDITOR_LICENSE }} run: | - pnpm jest + pnpm test - id: itest if: ${{ inputs.itest }} name: "Integration Tests" diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index 00bfe26bcb..de9a1f6fe4 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -18,6 +18,7 @@ env: NODE_OPTIONS: "--max_old_space_size=4096" PNPM_VERSION: ^10.9 NPM_CONFIG_@coremedia:registry: 'https://npm.coremedia.io' + NPM_CONFIG_@coremedia-internal:registry: 'https://npm.coremedia.io' jobs: build: @@ -83,7 +84,7 @@ jobs: - name: Test env: CKEDITOR_LICENSE_KEY: ${{ secrets.CKEDITOR_LICENSE }} - run: pnpm jest + run: pnpm test - name: Playwright Tests run: pnpm playwright - name: TypeDoc diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e27e303f32..861161afc5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -126,7 +126,7 @@ jobs: - name: Test env: CKEDITOR_LICENSE_KEY: ${{ secrets.CKEDITOR_LICENSE }} - run: pnpm jest + run: pnpm test - name: Playwright Tests run: pnpm playwright - name: TypeDoc diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index fef768440b..c7ca5c47c9 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -27,7 +27,7 @@ CKEDITOR_LICENSE_KEY= To run tests use: ```text -pnpm run jest +pnpm run test ``` ## Update Process diff --git a/README.md b/README.md index 5666ec6d24..ce2626a07a 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,6 @@ workspace usage, are not published and have scope `@coremedia-internal/`. | [`ckeditor5-dom-converter`][] | Base architecture for data-processing | | [`ckeditor5-dom-support`][] | Utilities for handling DOM objects | | [`ckeditor5-font-mapper`][] | Replaces characters in given font to alternative representation on paste | -| [`ckeditor5-jest-test-helpers`][] | Support for JEST tests | | [`ckeditor5-link-common`][] | Assistive Utilities for `@ckeditor/ckeditor5-link` | | [`ckeditor5-logging`][] | Logging Facade | @@ -226,8 +225,6 @@ implements the described behavior. [`ckeditor5-font-mapper`]: <./packages/ckeditor5-font-mapper> "@coremedia/ckeditor5-font-mapper" -[`ckeditor5-jest-test-helpers`]: <./packages/ckeditor5-jest-test-helpers> "@coremedia-internal/ckeditor5-jest-test-helpers" - [`ckeditor5-link-common`]: <./packages/ckeditor5-link-common> "@coremedia/ckeditor5-link-common" [`ckeditor5-logging`]: <./packages/ckeditor5-logging> "@coremedia/ckeditor5-logging" diff --git a/app/package.json b/app/package.json index f26ec4d631..89614c9a1c 100644 --- a/app/package.json +++ b/app/package.json @@ -2,6 +2,7 @@ "name": "@coremedia/ckeditor5-app", "description": "A custom CKEditor 5 build made by the CKEditor 5 online builder.", "version": "25.0.4-rc.4", + "type": "module", "author": { "name": "CoreMedia GmbH", "email": "info@coremedia.com", @@ -9,7 +10,7 @@ "avatar": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2OTguNjY3IiBoZWlnaHQ9IjY5OC42NjciIHZlcnNpb249IjEuMCIgdmlld0JveD0iMCAwIDUyNCA1MjQiPjxwYXRoIGQ9Ik0yODIuNS42QzI0NSA0LjUgMjE1LjEgMTIuOSAxODUgMjcuOSAxMDYuNCA2Ny4yIDUzLjUgMTQyLjEgNDIuNCAyMjkuNWMtMi4xIDE3LjItMi4yIDQ4IDAgNjQuMUM0OCAzMzYuMiA2MS4zIDM3Mi4yIDg0IDQwNi4zYzYxLjkgOTMuMSAxNzUuNCAxMzYuNCAyODUgMTA4LjcgMTkuMi00LjggMzMuNy0xMC4zIDUzLjItMjAuMSAyMS0xMC41IDQyLjUtMjUuMyA1OC4zLTQwLjJsOC03LjYtMjEuNS0yMS42Yy0yMy42LTIzLjgtMjcuMS0yNi43LTM4LjYtMzIuNC0yMi40LTExLjItNDQuNy0xMS42LTcxLjctMS41LTE4LjYgNy0zMC40IDkuNC00OC43IDEwLjEtMTMgLjQtMTcuNS4yLTI3LjgtMS42LTMwLjUtNS4yLTU1LTE3LjgtNzYuOC0zOS41LTMzLjYtMzMuNi00Ny44LTgwLjQtMzguNC0xMjYuNCAxMC4yLTUwLjUgNDYuMi05MC41IDk1LjItMTA2LjEgMzEuNy0xMC4xIDY0LjItOC44IDk2LjYgMy45IDEyLjggNC45IDIyLjcgNyAzNC4xIDcgMTYuNCAwIDMxLjItNC4yIDQ1LjEtMTIuNyA1LjUtMy40IDEzLjYtMTAuNyAzMC40LTI3LjNsMjIuOC0yMi42LTkuMy04LjRjLTM5LjgtMzUuNC04NS42LTU3LTEzOC40LTY1LjEtMTIuNS0yLTQ4LjctMy4zLTU5LTIuM3oiLz48cGF0aCBkPSJNMjk2IDIxOC4xYy0yOC4zIDQuOC00NC4zIDM1LjQtMzIuMSA2MS40IDcuOSAxNyAyNy4yIDI3LjQgNDUuNCAyNC41IDI4LjktNC42IDQ1LjEtMzUuMiAzMi43LTYxLjctOC0xNy4yLTI3LjMtMjcuMy00Ni0yNC4yeiIvPjwvc3ZnPg==" }, "engines": { - "node": "22", + "node": "^22.3.0", "pnpm": "^10.9" }, "license": "Apache-2.0", @@ -31,13 +32,12 @@ "@coremedia/ckeditor5-dom-converter": "25.0.4-rc.4", "@coremedia/ckeditor5-font-mapper": "25.0.4-rc.4", "@coremedia/ckeditor5-link-common": "25.0.4-rc.4", - "@coremedia/service-agent": "^1.1.5", - "ckeditor5": "45.2.1", + "@coremedia/service-agent": "^2.1.2", + "ckeditor5": "46.1.1", "xml-formatter": "^3.6.2" }, "devDependencies": { "@babel/core": "^7.25.2", - "source-map-loader": "^1.0.0", "@babel/plugin-transform-runtime": "^7.25.4", "@ckeditor/ckeditor5-dev-translations": "^45.0.8", "@ckeditor/ckeditor5-dev-utils": "^45.0.8", @@ -54,18 +54,19 @@ "postcss-loader": "^8.1.1", "raw-loader": "^4.0.2", "rimraf": "^6.0.1", + "source-map-loader": "^1.0.0", "style-loader": "^4.0.0", "terser-webpack-plugin": "^5.3.10", "typescript": "5.4.5", - "webpack": "^5.93.0", - "webpack-cli": "^5.1.4" + "webpack": "^5.101.3", + "webpack-cli": "^6.0.1" }, "scripts": { "clean": "pnpm clean:src && pnpm clean:dist", "clean:src": "rimraf --glob \"src/**/*.@(js|js.map|d.ts|d.ts.map)\"", "clean:dist": "rimraf ./dist", "build": "webpack --mode production --stats-error-details", - "lint": "eslint \"**/*.{js,cjs,mjs,ts,tsx}\"", + "lint": "eslint \"**/*.{ts,tsx}\"", "npm-check-updates": "npm-check-updates --upgrade", "start": "http-server . --port 0 -d false -o sample" } diff --git a/app/src/CKEditorInstanceFactory.ts b/app/src/CKEditorInstanceFactory.ts index 000153fa94..d3be5d856e 100644 --- a/app/src/CKEditorInstanceFactory.ts +++ b/app/src/CKEditorInstanceFactory.ts @@ -1,4 +1,4 @@ -import { ClassicEditor } from "ckeditor5"; -import { ApplicationState } from "./ApplicationState"; +import type { ClassicEditor } from "ckeditor5"; +import type { ApplicationState } from "./ApplicationState"; export type CKEditorInstanceFactory = (sourceElement: HTMLElement, state: ApplicationState) => Promise; diff --git a/app/src/DataTypeSwitch.ts b/app/src/DataTypeSwitch.ts index 9c2a9ba06c..db909970a6 100644 --- a/app/src/DataTypeSwitch.ts +++ b/app/src/DataTypeSwitch.ts @@ -1,4 +1,5 @@ -import { SwitchButton, SwitchButtonConfig } from "./SwitchButton"; +import type { SwitchButtonConfig } from "./SwitchButton"; +import { SwitchButton } from "./SwitchButton"; export const dataTypes = { ["richtext" as const]: "Rich Text", diff --git a/app/src/PreviewSwitch.ts b/app/src/PreviewSwitch.ts index afdaef6f71..b93e301272 100644 --- a/app/src/PreviewSwitch.ts +++ b/app/src/PreviewSwitch.ts @@ -1,4 +1,5 @@ -import { SwitchButton, SwitchButtonConfig } from "./SwitchButton"; +import type { SwitchButtonConfig } from "./SwitchButton"; +import { SwitchButton } from "./SwitchButton"; export const previewStates = { ["hidden" as const]: "Hidden", diff --git a/app/src/ReadOnlySwitch.ts b/app/src/ReadOnlySwitch.ts index bd84be3422..8be069578b 100644 --- a/app/src/ReadOnlySwitch.ts +++ b/app/src/ReadOnlySwitch.ts @@ -1,4 +1,5 @@ -import { SwitchButton, SwitchButtonConfig } from "./SwitchButton"; +import type { SwitchButtonConfig } from "./SwitchButton"; +import { SwitchButton } from "./SwitchButton"; export const readOnlyStates = { ["rw" as const]: "Read Write", diff --git a/app/src/SwitchButton.ts b/app/src/SwitchButton.ts index 4daf23625f..3e35843e11 100644 --- a/app/src/SwitchButton.ts +++ b/app/src/SwitchButton.ts @@ -1,4 +1,5 @@ -import { ApplicationToolbarConfig, requireApplicationToolbar } from "./ApplicationToolbar"; +import type { ApplicationToolbarConfig } from "./ApplicationToolbar"; +import { requireApplicationToolbar } from "./ApplicationToolbar"; export interface SwitchButtonConfig extends ApplicationToolbarConfig { id?: string; diff --git a/app/src/UiLanguageSwitch.ts b/app/src/UiLanguageSwitch.ts index 0526b501df..86c9835621 100644 --- a/app/src/UiLanguageSwitch.ts +++ b/app/src/UiLanguageSwitch.ts @@ -1,4 +1,5 @@ -import { SwitchButton, SwitchButtonConfig } from "./SwitchButton"; +import type { SwitchButtonConfig } from "./SwitchButton"; +import { SwitchButton } from "./SwitchButton"; export const uiLanguages = { ["en" as const]: "English", diff --git a/app/src/createCKEditorInstance.ts b/app/src/createCKEditorInstance.ts index 988f210871..f026f39e34 100644 --- a/app/src/createCKEditorInstance.ts +++ b/app/src/createCKEditorInstance.ts @@ -1,7 +1,7 @@ -import { ClassicEditor, Command, Editor } from "ckeditor5"; +import type { ClassicEditor, Command, Editor } from "ckeditor5"; import { Differencing } from "@coremedia/ckeditor5-coremedia-differencing"; -import { ApplicationState } from "./ApplicationState"; -import { CKEditorInstanceFactory } from "./CKEditorInstanceFactory"; +import type { ApplicationState } from "./ApplicationState"; +import type { CKEditorInstanceFactory } from "./CKEditorInstanceFactory"; import { initReadOnlyToggle } from "./ReadOnlySwitch"; import { initPreview, updatePreview } from "./preview"; import { createRichTextEditor } from "./editors/richtext"; diff --git a/app/src/editors/bbCode.ts b/app/src/editors/bbCode.ts index 2272bd5077..71243b79ba 100644 --- a/app/src/editors/bbCode.ts +++ b/app/src/editors/bbCode.ts @@ -35,8 +35,8 @@ import { ImageBlockEditing, } from "ckeditor5"; import { updatePreview } from "../preview"; -import { ApplicationState } from "../ApplicationState"; -import { CKEditorInstanceFactory } from "../CKEditorInstanceFactory"; +import type { ApplicationState } from "../ApplicationState"; +import type { CKEditorInstanceFactory } from "../CKEditorInstanceFactory"; const licenseKeyErrorMessage = "Please provide a valid license key for your CKEditor5 instance. Please create a .env file in the workspace root and make your license as CKEDITOR_LICENSE_KEY variable. Please use 'GPL' if you want to use the GNU General Public License."; diff --git a/app/src/editors/richtext.ts b/app/src/editors/richtext.ts index 48af6818f7..98d815df64 100644 --- a/app/src/editors/richtext.ts +++ b/app/src/editors/richtext.ts @@ -23,6 +23,7 @@ import { CoreMediaStudioEssentials, Strictness, } from "@coremedia/ckeditor5-coremedia-studio-essentials"; +import type { PluginConstructor } from "ckeditor5"; import { Alignment, Autoformat, @@ -53,7 +54,6 @@ import { List, Paragraph, PasteFromOffice, - PluginConstructor, RemoveFormat, SourceEditing, Strikethrough, @@ -63,7 +63,7 @@ import { TableToolbar, Underline, } from "ckeditor5"; -import { RuleConfig } from "@coremedia/ckeditor5-dom-converter"; +import type { RuleConfig } from "@coremedia/ckeditor5-dom-converter"; import type { LatestCoreMediaRichTextConfig, V10CoreMediaRichTextConfig, @@ -74,13 +74,14 @@ import { stripFixedAttributes, } from "@coremedia/ckeditor5-coremedia-richtext"; import "ckeditor5/ckeditor5.css"; -import { FilterRuleSetConfiguration } from "@coremedia/ckeditor5-dataprocessor-support"; -import { LinkAttributes, LinkAttributesConfig } from "@coremedia/ckeditor5-link-common"; +import type { FilterRuleSetConfiguration } from "@coremedia/ckeditor5-dataprocessor-support"; +import type { LinkAttributesConfig } from "@coremedia/ckeditor5-link-common"; +import { LinkAttributes } from "@coremedia/ckeditor5-link-common"; import { Differencing } from "@coremedia/ckeditor5-coremedia-differencing"; import { Blocklist } from "@coremedia/ckeditor5-coremedia-blocklist"; import { DataFacade } from "@coremedia/ckeditor5-data-facade"; -import { CKEditorInstanceFactory } from "../CKEditorInstanceFactory"; -import { ApplicationState } from "../ApplicationState"; +import type { CKEditorInstanceFactory } from "../CKEditorInstanceFactory"; +import type { ApplicationState } from "../ApplicationState"; import { getHashParam } from "../HashParams"; import { initInputExampleContent } from "../inputExampleContents"; import { updatePreview } from "../preview"; diff --git a/app/src/example-data.ts b/app/src/example-data.ts index 1ae9e0557d..a15c287ce6 100644 --- a/app/src/example-data.ts +++ b/app/src/example-data.ts @@ -1,12 +1,9 @@ // noinspection HttpUrlsUsage import { PREDEFINED_MOCK_LINK_DATA } from "@coremedia/ckeditor5-coremedia-studio-integration-mock"; -import { - bbCodeData, - ExampleData, - initExamples, - richTextData, -} from "@coremedia-internal/ckeditor5-coremedia-example-data"; -import { EditingView, Editor } from "ckeditor5"; +import type { ExampleData } from "@coremedia-internal/ckeditor5-coremedia-example-data"; +import { bbCodeData, initExamples, richTextData } from "@coremedia-internal/ckeditor5-coremedia-example-data"; +import type { Editor } from "ckeditor5"; +import { EditingView } from "ckeditor5"; import { DataFacade } from "@coremedia/ckeditor5-data-facade"; const exampleData: { diff --git a/app/src/inputExampleContents.ts b/app/src/inputExampleContents.ts index 40c2635f15..255888feab 100644 --- a/app/src/inputExampleContents.ts +++ b/app/src/inputExampleContents.ts @@ -1,9 +1,6 @@ -import { - MockContentPlugin, - MockInputExamplePlugin, - InputExampleElement, -} from "@coremedia/ckeditor5-coremedia-studio-integration-mock"; -import { ClassicEditor } from "ckeditor5"; +import type { InputExampleElement } from "@coremedia/ckeditor5-coremedia-studio-integration-mock"; +import { MockContentPlugin, MockInputExamplePlugin } from "@coremedia/ckeditor5-coremedia-studio-integration-mock"; +import type { ClassicEditor } from "ckeditor5"; const INPUT_EXAMPLE_CONTENT_DIV_CLASS = "inputExampleContentDiv"; const initInputExampleContent = (editor: ClassicEditor) => { diff --git a/app/src/preview.ts b/app/src/preview.ts index 4da6a9cce6..18819e1be6 100644 --- a/app/src/preview.ts +++ b/app/src/preview.ts @@ -1,6 +1,6 @@ import { dataFormatter } from "./DataFormatter"; import { initPreviewSwitch } from "./PreviewSwitch"; -import { ApplicationState } from "./ApplicationState"; +import type { ApplicationState } from "./ApplicationState"; const previewToggleButtonId = "previewToggle"; const withPreviewClass = "with-preview"; diff --git a/app/webpack.config.mjs b/app/webpack.config.js similarity index 81% rename from app/webpack.config.mjs rename to app/webpack.config.js index 33f7a8943d..02ef43741c 100644 --- a/app/webpack.config.mjs +++ b/app/webpack.config.js @@ -1,18 +1,19 @@ -"use strict"; - /* eslint-env node */ -const { default: path } = await import("path"); -const { default: webpack } = await import("webpack"); -const { bundler, loaders } = await import("@ckeditor/ckeditor5-dev-utils"); -const { CKEditorTranslationsPlugin } = await import("@ckeditor/ckeditor5-dev-translations"); -const { default: TerserPlugin } = await import("terser-webpack-plugin"); -const { default: CircularDependencyPlugin } = await import("circular-dependency-plugin"); +import path from "path"; +import webpack from "webpack"; +import { bundler, loaders } from "@ckeditor/ckeditor5-dev-utils"; +import { CKEditorTranslationsPlugin } from "@ckeditor/ckeditor5-dev-translations"; +import TerserPlugin from "terser-webpack-plugin"; +import CircularDependencyPlugin from "circular-dependency-plugin"; import fs from "fs"; import { fileURLToPath } from "url"; import dotenv from "dotenv"; -function findEnvFile(startDir = import.meta.dirname) { +const filename = fileURLToPath(import.meta.url); +const dirname = path.dirname(filename); + +function findEnvFile(startDir = dirname) { let dir = startDir; let nothingFound = false; while (!nothingFound) { @@ -39,9 +40,6 @@ if (!envPath) { dotenv.config({ path: envPath }); -const filename = fileURLToPath(import.meta.url); -const dirname = path.dirname(filename); - export default { devtool: "source-map", performance: { hints: false }, @@ -51,7 +49,6 @@ export default { output: { // The name under which the editor will be exported. library: "ClassicEditor", - path: path.resolve(dirname, "dist"), filename: "ckeditor.js", libraryTarget: "umd", @@ -103,10 +100,20 @@ export default { enforce: "pre", use: ["source-map-loader"], }, + { + test: /\.m?js$/, + resolve: { + fullySpecified: false, + }, + }, ], }, resolve: { extensions: [".ts", ".js", ".json"], + extensionAlias: { + ".js": [".js", ".ts"], + }, + fullySpecified: false, }, }; diff --git a/itest/jest.config.cjs b/itest/jest.config.cjs index 255213e751..354706293b 100644 --- a/itest/jest.config.cjs +++ b/itest/jest.config.cjs @@ -1,7 +1,6 @@ -const jestConfig = require("@coremedia-internal/ckeditor5-jest-test-helpers/shared-jest.config.js"); +const babelConfig = require("@coremedia-internal/ckeditor5-babel-config"); module.exports = { - ...jestConfig, roots: ["/src/"], // The default timeout is 5000. This may be not enough for Jest Playwright // tests. If the test fails due to test-timeout, we will only get unspecific @@ -11,4 +10,26 @@ module.exports = { // Override from shared config. testEnvironment: require.resolve("jest-playwright-preset"), setupFilesAfterEnv: [require.resolve("expect-playwright")], + // Don't detect utility files as tests, i.e. require `test` in name. + testMatch: ["**/?(*.)+(test).[jt]s?(x)"], + moduleFileExtensions: ["js", "ts", "d.ts"], + moduleNameMapper: { + // https://www.npmjs.com/package/jest-transform-stub + "^.+\\.(css|less|sass|scss|gif|png|jpg|ttf|eot|woff|woff2|svg)$": require.resolve("jest-transform-stub"), + }, + transform: { + // '^.+\\.[tj]sx?$' to process js/ts with `ts-jest` + // '^.+\\.m?[tj]sx?$' to process js/ts/mjs/mts with `ts-jest` + "^.+\\.tsx?$": [ + "ts-jest", + { + // ts-jest configuration goes here + }, + ], + // Required, e.g., for CKEditor 5 Dependencies. + "^.+\\.jsx?$": [require.resolve("babel-jest"), babelConfig], + // https://www.npmjs.com/package/jest-transform-stub + "^.+\\.(css|less|sass|scss|gif|png|jpg|ttf|eot|woff|woff2|svg)$": require.resolve("jest-transform-stub"), + }, + transformIgnorePatterns: ["node_modules/.pnpm/(?!@ckeditor|@bbob|lodash-es|rxjs|ckeditor5|vanilla-colorful)"], }; diff --git a/itest/package.json b/itest/package.json index 49d4634c6f..3aff8a1ae0 100644 --- a/itest/package.json +++ b/itest/package.json @@ -1,6 +1,7 @@ { "name": "@coremedia/ckeditor5-coremedia-itest", "version": "25.0.4-rc.4", + "type": "module", "description": "Integration Tests for CKEditor 5", "author": { "name": "CoreMedia GmbH", @@ -12,13 +13,14 @@ "coremedia" ], "engines": { - "node": "22", + "node": "^22.3.0", "pnpm": "^10.9" }, "license": "Apache-2.0", "devDependencies": { + "@babel/core": "^7.24.4", + "@coremedia-internal/ckeditor5-babel-config": "^1.0.0", "@coremedia-internal/ckeditor5-coremedia-example-data": "^1.0.0", - "@coremedia-internal/ckeditor5-jest-test-helpers": "^1.0.0", "@coremedia/ckeditor5-coremedia-link": "25.0.4-rc.4", "@coremedia/ckeditor5-coremedia-richtext": "25.0.4-rc.4", "@coremedia/ckeditor5-coremedia-studio-integration": "25.0.4-rc.4", @@ -27,19 +29,24 @@ "@types/express": "^4.17.21", "@types/jest": "^29.5.12", "@types/node": "^20.14.10", - "ckeditor5": "45.2.1", + "babel-jest": "^29.7.0", + "ckeditor5": "46.1.1", + "enhanced-resolve": "^5.16.0", "expect-playwright": "^0.8.0", "express": "^5.1.0", "get-port": "^7.1.0", "jest": "^29.7.0", "jest-circus": "^29.7.0", "jest-config": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", "jest-environment-node": "^29.7.0", "jest-playwright-preset": "4.0.0", "jest-runner": "^29.7.0", + "jest-transform-stub": "^2.0.0", "playwright": "^1.45.1", "playwright-core": "^1.45.1", "rimraf": "^6.0.1", + "ts-jest": "^29.1.2", "tslib": "^2.6.3", "typescript": "5.4.5", "wait-for-expect": "^3.0.2" @@ -49,12 +56,10 @@ "clean:src": "rimraf --glob \"src/**/*.@(js|js.map|d.ts|d.ts.map)\"", "clean:dist": "rimraf ./dist", "build": "exit 0", - "lint": "eslint \"**/*.{js,cjs,mjs,ts,tsx}\"", + "lint": "eslint \"**/*.{ts,tsx}\"", "preplaywright": "npx playwright install chromium", "playwright": "jest", "playwright-local": "jest --config ./jest.local.js", - "jest": "exit 0", - "jest:coverage": "exit 0", "npm-check-updates": "npm-check-updates --upgrade" } } diff --git a/itest/src/BlocklistExpandedKeyboard.test.ts b/itest/src/BlocklistExpandedKeyboard.test.ts index 6518cd49db..19e604b527 100644 --- a/itest/src/BlocklistExpandedKeyboard.test.ts +++ b/itest/src/BlocklistExpandedKeyboard.test.ts @@ -1,7 +1,7 @@ import "./expect/Expectations"; import { p, richtext } from "@coremedia-internal/ckeditor5-coremedia-example-data"; import { ApplicationWrapper } from "./aut/ApplicationWrapper"; -import { MockServiceAgentPluginWrapper } from "./aut/services/MockServiceAgentPluginWrapper"; +import type { MockServiceAgentPluginWrapper } from "./aut/services/MockServiceAgentPluginWrapper"; import { ctrlOrMeta } from "./browser/UserAgent"; describe("Blocklist", () => { diff --git a/itest/src/BlocklistExpandedToolbar.test.ts b/itest/src/BlocklistExpandedToolbar.test.ts index 91198756c9..981d3df18a 100644 --- a/itest/src/BlocklistExpandedToolbar.test.ts +++ b/itest/src/BlocklistExpandedToolbar.test.ts @@ -1,6 +1,6 @@ import { p, richtext } from "@coremedia-internal/ckeditor5-coremedia-example-data"; import { ApplicationWrapper } from "./aut/ApplicationWrapper"; -import { MockServiceAgentPluginWrapper } from "./aut/services/MockServiceAgentPluginWrapper"; +import type { MockServiceAgentPluginWrapper } from "./aut/services/MockServiceAgentPluginWrapper"; import { ctrlOrMeta } from "./browser/UserAgent"; import "./expect/Expectations"; diff --git a/itest/src/ContentLink.test.ts b/itest/src/ContentLink.test.ts index 9697a67b1f..daefc449e0 100644 --- a/itest/src/ContentLink.test.ts +++ b/itest/src/ContentLink.test.ts @@ -266,6 +266,6 @@ describe("Content Link Feature", () => { await page.keyboard.press("Enter"); const contentLink = view.locator.locator(`a`); - await expect(contentLink).toHaveText("content:101"); + await expect(contentLink).toHaveText("Some Folder"); }); }); diff --git a/itest/src/Images.test.ts b/itest/src/Images.test.ts index 620ba9c752..e258d9e67f 100644 --- a/itest/src/Images.test.ts +++ b/itest/src/Images.test.ts @@ -7,15 +7,15 @@ import { richtext, } from "@coremedia-internal/ckeditor5-coremedia-example-data"; import type { MockContentConfig } from "@coremedia/ckeditor5-coremedia-studio-integration-mock"; -import { ElementHandle } from "playwright-core"; +import type { ElementHandle } from "playwright-core"; import waitForExpect from "wait-for-expect"; import { ApplicationWrapper } from "./aut/ApplicationWrapper"; -import { ClassicEditorWrapper } from "./aut/ClassicEditorWrapper"; -import ContentLinkViewWrapper from "./aut/components/balloon/ContentLinkViewWrapper"; +import type { ClassicEditorWrapper } from "./aut/ClassicEditorWrapper"; +import type ContentLinkViewWrapper from "./aut/components/balloon/ContentLinkViewWrapper"; import ImageContextualBalloonToolbar from "./aut/components/balloon/ImageContextualBalloonToolbar"; import LinkActionsViewWrapper from "./aut/components/balloon/LinkActionsViewWrapper"; import ToolbarViewWrapper from "./aut/components/ToolbarViewWrapper"; -import { MockServiceAgentPluginWrapper } from "./aut/services/MockServiceAgentPluginWrapper"; +import type { MockServiceAgentPluginWrapper } from "./aut/services/MockServiceAgentPluginWrapper"; import { PNG_EMPTY_24x24, PNG_LOCK_24x24, PNG_RED_240x135 } from "./MockFixtures"; import "./expect/Expectations"; diff --git a/itest/src/LinkUserInteraction.test.ts b/itest/src/LinkUserInteraction.test.ts index e76b2e0ddc..f92043da65 100644 --- a/itest/src/LinkUserInteraction.test.ts +++ b/itest/src/LinkUserInteraction.test.ts @@ -1,8 +1,8 @@ import { a, p, richtext } from "@coremedia-internal/ckeditor5-coremedia-example-data"; import { contentUriPath } from "@coremedia/ckeditor5-coremedia-studio-integration"; -import { Page } from "playwright"; +import type { Page } from "playwright"; import { ApplicationWrapper } from "./aut/ApplicationWrapper"; -import { MockServiceAgentPluginWrapper } from "./aut/services/MockServiceAgentPluginWrapper"; +import type { MockServiceAgentPluginWrapper } from "./aut/services/MockServiceAgentPluginWrapper"; import "./expect/Expectations"; const externalLinkUrl = "https://www.coremedia.com/"; diff --git a/itest/src/aria/AriaUtils.ts b/itest/src/aria/AriaUtils.ts index ff37e7ffd4..d62ac09b2d 100644 --- a/itest/src/aria/AriaUtils.ts +++ b/itest/src/aria/AriaUtils.ts @@ -1,5 +1,5 @@ import waitForExpect from "wait-for-expect"; -import { Locator } from "playwright"; +import type { Locator } from "playwright"; export const expectFocusedElementHasAriaText = async (ariaLabelContent: string) => waitForExpect(async () => { diff --git a/itest/src/aut/ApplicationConsole.ts b/itest/src/aut/ApplicationConsole.ts index c6fff0c946..53ea6b9707 100644 --- a/itest/src/aut/ApplicationConsole.ts +++ b/itest/src/aut/ApplicationConsole.ts @@ -1,5 +1,5 @@ -import { ConsoleMessage } from "playwright-core"; -import { Page } from "playwright"; +import type { ConsoleMessage } from "playwright-core"; +import type { Page } from "playwright"; type ConsoleMessageType = | "log" diff --git a/itest/src/aut/ApplicationWrapper.ts b/itest/src/aut/ApplicationWrapper.ts index bd8ee5a698..7db9edbe84 100644 --- a/itest/src/aut/ApplicationWrapper.ts +++ b/itest/src/aut/ApplicationWrapper.ts @@ -1,8 +1,8 @@ import path from "path"; -import { AddressInfo } from "net"; -import http from "http"; +import type { AddressInfo } from "net"; +import type http from "http"; import express from "express"; -import { Response } from "playwright"; +import type { Response } from "playwright"; import { ClassicEditorWrapper } from "./ClassicEditorWrapper"; import { ApplicationConsole } from "./ApplicationConsole"; import { MockContentPluginWrapper } from "./MockContentPluginWrapper"; diff --git a/itest/src/aut/BalloonPanelViewWrapper.ts b/itest/src/aut/BalloonPanelViewWrapper.ts index 4a53688de9..6c238a02c9 100644 --- a/itest/src/aut/BalloonPanelViewWrapper.ts +++ b/itest/src/aut/BalloonPanelViewWrapper.ts @@ -1,6 +1,7 @@ // For now, this is only a DOM related wrapper. -import { Locator } from "playwright"; -import { Locatable, visible } from "./Locatable"; +import type { Locator } from "playwright"; +import type { Locatable } from "./Locatable"; +import { visible } from "./Locatable"; import { LinkToolbarViewWrapper } from "./LinkToolbarViewWrapper"; import { LinkFormViewWrapper } from "./LinkFormViewWrapper"; import { BlocklistActionsViewWrapper } from "./BlocklistActionsViewWrapper"; diff --git a/itest/src/aut/BlocklistActionsViewWrapper.ts b/itest/src/aut/BlocklistActionsViewWrapper.ts index 0dbfc6d3a2..f08a9b07fe 100644 --- a/itest/src/aut/BlocklistActionsViewWrapper.ts +++ b/itest/src/aut/BlocklistActionsViewWrapper.ts @@ -1,5 +1,6 @@ -import { Locator } from "playwright"; -import { Locatable, visible } from "./Locatable"; +import type { Locator } from "playwright"; +import type { Locatable } from "./Locatable"; +import { visible } from "./Locatable"; export class BlocklistActionsViewWrapper implements Locatable { readonly #parent: Locatable; diff --git a/itest/src/aut/BodyCollectionWrapper.ts b/itest/src/aut/BodyCollectionWrapper.ts index 19ccc78e1f..f52d77c1a0 100644 --- a/itest/src/aut/BodyCollectionWrapper.ts +++ b/itest/src/aut/BodyCollectionWrapper.ts @@ -1,8 +1,9 @@ import type { BodyCollection } from "ckeditor5"; -import { Locator } from "playwright"; +import type { Locator } from "playwright"; import { JSWrapper } from "./JSWrapper"; -import { EditorUIViewWrapper } from "./EditorUIViewWrapper"; -import { Locatable, visible } from "./Locatable"; +import type { EditorUIViewWrapper } from "./EditorUIViewWrapper"; +import type { Locatable } from "./Locatable"; +import { visible } from "./Locatable"; import { BalloonPanelViewWrapper } from "./BalloonPanelViewWrapper"; /** diff --git a/itest/src/aut/ClassicEditorWrapper.ts b/itest/src/aut/ClassicEditorWrapper.ts index 8c681dd109..6dd9fb8f2b 100644 --- a/itest/src/aut/ClassicEditorWrapper.ts +++ b/itest/src/aut/ClassicEditorWrapper.ts @@ -1,10 +1,11 @@ -import { JSHandle, Locator, Page } from "playwright"; -import { ClassicEditor } from "ckeditor5"; +import type { JSHandle, Locator, Page } from "playwright"; +import type { ClassicEditor } from "ckeditor5"; import type { RichTextDataProcessor } from "@coremedia/ckeditor5-coremedia-richtext"; import { EditorWrapper } from "./EditorWrapper"; import { CommandCollectionWrapper } from "./CommandCollectionWrapper"; import { EditorUIWrapper } from "./EditorUIWrapper"; -import { Locatable, visible } from "./Locatable"; +import type { Locatable } from "./Locatable"; +import { visible } from "./Locatable"; import ContextualBalloonWrapper from "./components/balloon/ContextualBalloonWrapper"; /** diff --git a/itest/src/aut/CommandCollectionWrapper.ts b/itest/src/aut/CommandCollectionWrapper.ts index cf24b148e2..4fbee639f3 100644 --- a/itest/src/aut/CommandCollectionWrapper.ts +++ b/itest/src/aut/CommandCollectionWrapper.ts @@ -2,7 +2,7 @@ import type { Editor } from "ckeditor5"; import { JSWrapper } from "./JSWrapper"; import { CommandWrapper } from "./CommandWrapper"; -import { EditorWrapper } from "./EditorWrapper"; +import type { EditorWrapper } from "./EditorWrapper"; /** * Wrapper for the command collection. diff --git a/itest/src/aut/CommandWrapper.ts b/itest/src/aut/CommandWrapper.ts index 659c441a19..737b9841f0 100644 --- a/itest/src/aut/CommandWrapper.ts +++ b/itest/src/aut/CommandWrapper.ts @@ -1,6 +1,6 @@ -import { Command } from "ckeditor5"; +import type { Command } from "ckeditor5"; import { JSWrapper } from "./JSWrapper"; -import { CommandCollectionWrapper } from "./CommandCollectionWrapper"; +import type { CommandCollectionWrapper } from "./CommandCollectionWrapper"; /** * Wraps a CKEditor `Command`. diff --git a/itest/src/aut/ContentLinkViewWrapper.ts b/itest/src/aut/ContentLinkViewWrapper.ts index 0de7fa05fe..a2b2acbb35 100644 --- a/itest/src/aut/ContentLinkViewWrapper.ts +++ b/itest/src/aut/ContentLinkViewWrapper.ts @@ -1,5 +1,6 @@ -import { Locator } from "playwright"; -import { Locatable, visible } from "./Locatable"; +import type { Locator } from "playwright"; +import type { Locatable } from "./Locatable"; +import { visible } from "./Locatable"; export class ContentLinkViewWrapper implements Locatable { readonly #parent: Locatable; diff --git a/itest/src/aut/EditingControllerWrapper.ts b/itest/src/aut/EditingControllerWrapper.ts index 179cf5c05f..aa172152b8 100644 --- a/itest/src/aut/EditingControllerWrapper.ts +++ b/itest/src/aut/EditingControllerWrapper.ts @@ -1,7 +1,7 @@ -import { EditingController } from "ckeditor5"; +import type { EditingController } from "ckeditor5"; import { JSWrapper } from "./JSWrapper"; import { ViewWrapper } from "./ViewWrapper"; -import { EditorWrapper } from "./EditorWrapper"; +import type { EditorWrapper } from "./EditorWrapper"; export class EditingControllerWrapper extends JSWrapper { get view(): ViewWrapper { diff --git a/itest/src/aut/EditorUIViewWrapper.ts b/itest/src/aut/EditorUIViewWrapper.ts index 100c375361..2d31484d6a 100644 --- a/itest/src/aut/EditorUIViewWrapper.ts +++ b/itest/src/aut/EditorUIViewWrapper.ts @@ -1,10 +1,11 @@ -import { ClassicEditor } from "ckeditor5"; -import { Locator } from "playwright"; +import type { ClassicEditor } from "ckeditor5"; +import type { Locator } from "playwright"; import { JSWrapper } from "./JSWrapper"; -import { EditorUIWrapper } from "./EditorUIWrapper"; +import type { EditorUIWrapper } from "./EditorUIWrapper"; // ClassicEditorUIView: See ckeditor/ckeditor5#12027. import { BodyCollectionWrapper } from "./BodyCollectionWrapper"; -import { Locatable, visible } from "./Locatable"; +import type { Locatable } from "./Locatable"; +import { visible } from "./Locatable"; export class EditorUIViewWrapper extends JSWrapper implements Locatable { readonly #parent: EditorUIWrapper; diff --git a/itest/src/aut/EditorUIWrapper.ts b/itest/src/aut/EditorUIWrapper.ts index b12a985034..7e205edd75 100644 --- a/itest/src/aut/EditorUIWrapper.ts +++ b/itest/src/aut/EditorUIWrapper.ts @@ -1,11 +1,12 @@ -import { ElementHandle } from "playwright-core"; -import { ClassicEditor } from "ckeditor5"; -import { Locator } from "playwright"; +import type { ElementHandle } from "playwright-core"; +import type { ClassicEditor } from "ckeditor5"; +import type { Locator } from "playwright"; import { JSWrapper } from "./JSWrapper"; -import { ClassicEditorWrapper } from "./ClassicEditorWrapper"; +import type { ClassicEditorWrapper } from "./ClassicEditorWrapper"; // ClassicEditorUI: See ckeditor/ckeditor5#12027. import { EditorUIViewWrapper } from "./EditorUIViewWrapper"; -import { Locatable, visible } from "./Locatable"; +import type { Locatable } from "./Locatable"; +import { visible } from "./Locatable"; /** * Wrapper for `EditorUI`. diff --git a/itest/src/aut/EditorWrapper.ts b/itest/src/aut/EditorWrapper.ts index 6e402cad3d..9c56218a5c 100644 --- a/itest/src/aut/EditorWrapper.ts +++ b/itest/src/aut/EditorWrapper.ts @@ -1,4 +1,4 @@ -import { Editor } from "ckeditor5"; +import type { Editor } from "ckeditor5"; import { JSWrapper } from "./JSWrapper"; import { EditingControllerWrapper } from "./EditingControllerWrapper"; diff --git a/itest/src/aut/JSWrapper.ts b/itest/src/aut/JSWrapper.ts index 4fa691c2f3..14c143cb70 100644 --- a/itest/src/aut/JSWrapper.ts +++ b/itest/src/aut/JSWrapper.ts @@ -1,5 +1,5 @@ -import { JSHandle } from "playwright"; -import { PageFunctionOn, SmartHandle } from "playwright-core/types/structs"; +import type { JSHandle } from "playwright"; +import type { PageFunctionOn, SmartHandle } from "playwright-core/types/structs"; /** * General concept for JSHandle wrappers. diff --git a/itest/src/aut/LinkFormViewWrapper.ts b/itest/src/aut/LinkFormViewWrapper.ts index a939b8878c..61eb796594 100644 --- a/itest/src/aut/LinkFormViewWrapper.ts +++ b/itest/src/aut/LinkFormViewWrapper.ts @@ -1,5 +1,6 @@ -import { Locator } from "playwright"; -import { Locatable, visible } from "./Locatable"; +import type { Locator } from "playwright"; +import type { Locatable } from "./Locatable"; +import { visible } from "./Locatable"; import { ContentLinkViewWrapper } from "./ContentLinkViewWrapper"; export class LinkFormViewWrapper implements Locatable { diff --git a/itest/src/aut/LinkToolbarViewWrapper.ts b/itest/src/aut/LinkToolbarViewWrapper.ts index 99bdb95456..df53104f35 100644 --- a/itest/src/aut/LinkToolbarViewWrapper.ts +++ b/itest/src/aut/LinkToolbarViewWrapper.ts @@ -1,5 +1,6 @@ -import { Locator } from "playwright"; -import { Locatable, visible } from "./Locatable"; +import type { Locator } from "playwright"; +import type { Locatable } from "./Locatable"; +import { visible } from "./Locatable"; import { ContentLinkViewWrapper } from "./ContentLinkViewWrapper"; export class LinkToolbarViewWrapper implements Locatable { diff --git a/itest/src/aut/Locatable.ts b/itest/src/aut/Locatable.ts index ebf4f329b9..bb831c4f4c 100644 --- a/itest/src/aut/Locatable.ts +++ b/itest/src/aut/Locatable.ts @@ -1,5 +1,5 @@ -import { Locator } from "playwright"; -import { HasVisible } from "../expect/IsVisible/HasVisible"; +import type { Locator } from "playwright"; +import type { HasVisible } from "../expect/IsVisible/HasVisible"; export const visible = (locatable: Locatable): Promise => locatable.locator.isVisible(); diff --git a/itest/src/aut/MockContentPluginWrapper.ts b/itest/src/aut/MockContentPluginWrapper.ts index 9ba229a80e..a2abeb9f1d 100644 --- a/itest/src/aut/MockContentPluginWrapper.ts +++ b/itest/src/aut/MockContentPluginWrapper.ts @@ -1,6 +1,6 @@ import type { MockContentConfig, MockContentPlugin } from "@coremedia/ckeditor5-coremedia-studio-integration-mock"; import { JSWrapper } from "./JSWrapper"; -import { ClassicEditorWrapper } from "./ClassicEditorWrapper"; +import type { ClassicEditorWrapper } from "./ClassicEditorWrapper"; const PLUGIN_NAME = "MockContent"; diff --git a/itest/src/aut/MockExternalContentPluginWrapper.ts b/itest/src/aut/MockExternalContentPluginWrapper.ts index 45addd87df..93434db956 100644 --- a/itest/src/aut/MockExternalContentPluginWrapper.ts +++ b/itest/src/aut/MockExternalContentPluginWrapper.ts @@ -3,7 +3,7 @@ import type { MockExternalContent, } from "@coremedia/ckeditor5-coremedia-studio-integration-mock"; import { JSWrapper } from "./JSWrapper"; -import { ClassicEditorWrapper } from "./ClassicEditorWrapper"; +import type { ClassicEditorWrapper } from "./ClassicEditorWrapper"; const PLUGIN_NAME = "MockExternalContent"; diff --git a/itest/src/aut/MockInputExamplePluginWrapper.ts b/itest/src/aut/MockInputExamplePluginWrapper.ts index 42eee58e23..44d897a5c6 100644 --- a/itest/src/aut/MockInputExamplePluginWrapper.ts +++ b/itest/src/aut/MockInputExamplePluginWrapper.ts @@ -7,7 +7,7 @@ import type { IsLinkableEvaluationResult, } from "@coremedia/ckeditor5-coremedia-studio-integration"; import { JSWrapper } from "./JSWrapper"; -import { ClassicEditorWrapper } from "./ClassicEditorWrapper"; +import type { ClassicEditorWrapper } from "./ClassicEditorWrapper"; /** * Provides access to the `MockInputExamplePlugin`. diff --git a/itest/src/aut/ViewDocumentWrapper.ts b/itest/src/aut/ViewDocumentWrapper.ts index 4067b474cf..9afb2b34a1 100644 --- a/itest/src/aut/ViewDocumentWrapper.ts +++ b/itest/src/aut/ViewDocumentWrapper.ts @@ -1,6 +1,6 @@ -import { ViewDocument } from "ckeditor5"; +import type { ViewDocument } from "ckeditor5"; import { JSWrapper } from "./JSWrapper"; -import { ViewWrapper } from "./ViewWrapper"; +import type { ViewWrapper } from "./ViewWrapper"; export class ViewDocumentWrapper extends JSWrapper { static fromView(wrapper: ViewWrapper): ViewDocumentWrapper { diff --git a/itest/src/aut/ViewWrapper.ts b/itest/src/aut/ViewWrapper.ts index 8e5e131591..92f3cbcd07 100644 --- a/itest/src/aut/ViewWrapper.ts +++ b/itest/src/aut/ViewWrapper.ts @@ -1,7 +1,7 @@ -import { EditingView } from "ckeditor5"; +import type { EditingView } from "ckeditor5"; import { JSWrapper } from "./JSWrapper"; import { ViewDocumentWrapper } from "./ViewDocumentWrapper"; -import { EditingControllerWrapper } from "./EditingControllerWrapper"; +import type { EditingControllerWrapper } from "./EditingControllerWrapper"; export class ViewWrapper extends JSWrapper { get document(): ViewDocumentWrapper { diff --git a/itest/src/aut/components/ToolbarViewWrapper.ts b/itest/src/aut/components/ToolbarViewWrapper.ts index dde4376e97..800fb48a80 100644 --- a/itest/src/aut/components/ToolbarViewWrapper.ts +++ b/itest/src/aut/components/ToolbarViewWrapper.ts @@ -1,6 +1,6 @@ import type { ToolbarView } from "ckeditor5"; import { JSWrapper } from "../JSWrapper"; -import ViewWrapper from "./ViewWrapper"; +import type ViewWrapper from "./ViewWrapper"; export default class ToolbarViewWrapper extends JSWrapper { static fromView(viewWrapper: ViewWrapper): ToolbarViewWrapper { diff --git a/itest/src/aut/components/ViewWrapper.ts b/itest/src/aut/components/ViewWrapper.ts index 8c11712c82..c1fd8faa30 100644 --- a/itest/src/aut/components/ViewWrapper.ts +++ b/itest/src/aut/components/ViewWrapper.ts @@ -1,6 +1,6 @@ import type { View } from "ckeditor5"; import { JSWrapper } from "../JSWrapper"; -import ContextualBalloonWrapper from "./balloon/ContextualBalloonWrapper"; +import type ContextualBalloonWrapper from "./balloon/ContextualBalloonWrapper"; /** * Wraps the CKEditor 5 view. diff --git a/itest/src/aut/components/balloon/ButtonViewWrapper.ts b/itest/src/aut/components/balloon/ButtonViewWrapper.ts index 693b942367..f4c9a3cdab 100644 --- a/itest/src/aut/components/balloon/ButtonViewWrapper.ts +++ b/itest/src/aut/components/balloon/ButtonViewWrapper.ts @@ -1,9 +1,9 @@ import type { ButtonView } from "ckeditor5"; import { JSWrapper } from "../../JSWrapper"; -import { HasVisible } from "../../../expect/IsVisible/HasVisible"; -import { HasToggleable } from "../../../expect/isToggleable/HasToggleable"; -import { HasEnabled } from "../../../expect/isEnabled/HasEnabled"; -import { HasAriaLabel } from "../../../aria/AriaUtils"; +import type { HasVisible } from "../../../expect/IsVisible/HasVisible"; +import type { HasToggleable } from "../../../expect/isToggleable/HasToggleable"; +import type { HasEnabled } from "../../../expect/isEnabled/HasEnabled"; +import type { HasAriaLabel } from "../../../aria/AriaUtils"; export default class ButtonViewWrapper extends JSWrapper diff --git a/itest/src/aut/components/balloon/ContentLinkViewWrapper.ts b/itest/src/aut/components/balloon/ContentLinkViewWrapper.ts index bd8d5b0fe0..e9cf4ca76e 100644 --- a/itest/src/aut/components/balloon/ContentLinkViewWrapper.ts +++ b/itest/src/aut/components/balloon/ContentLinkViewWrapper.ts @@ -1,6 +1,6 @@ -import { HasContentName } from "../../../expect/contentName/HasContentName"; +import type { HasContentName } from "../../../expect/contentName/HasContentName"; import { JSWrapper } from "../../JSWrapper"; -import LinkActionsViewWrapper from "./LinkActionsViewWrapper"; +import type LinkActionsViewWrapper from "./LinkActionsViewWrapper"; export default class ContentLinkViewWrapper extends JSWrapper implements HasContentName { get contentName(): Promise { diff --git a/itest/src/aut/components/balloon/ContextualBalloonWrapper.ts b/itest/src/aut/components/balloon/ContextualBalloonWrapper.ts index 25fdf8aba5..541fca027f 100644 --- a/itest/src/aut/components/balloon/ContextualBalloonWrapper.ts +++ b/itest/src/aut/components/balloon/ContextualBalloonWrapper.ts @@ -1,5 +1,5 @@ import type { ContextualBalloon } from "ckeditor5"; -import { ClassicEditorWrapper } from "../../ClassicEditorWrapper"; +import type { ClassicEditorWrapper } from "../../ClassicEditorWrapper"; import { JSWrapper } from "../../JSWrapper"; import ViewWrapper from "./../ViewWrapper"; diff --git a/itest/src/aut/components/balloon/ImageContextualBalloonToolbar.ts b/itest/src/aut/components/balloon/ImageContextualBalloonToolbar.ts index c3e8f31b75..769d0cc8ed 100644 --- a/itest/src/aut/components/balloon/ImageContextualBalloonToolbar.ts +++ b/itest/src/aut/components/balloon/ImageContextualBalloonToolbar.ts @@ -1,5 +1,5 @@ -import { ButtonView, ToolbarView } from "ckeditor5"; -import ToolbarViewWrapper from "../ToolbarViewWrapper"; +import type { ButtonView, ToolbarView } from "ckeditor5"; +import type ToolbarViewWrapper from "../ToolbarViewWrapper"; import ButtonViewWrapper from "./ButtonViewWrapper"; export default class ImageContextualBalloonToolbar { diff --git a/itest/src/aut/components/balloon/LinkActionsViewWrapper.ts b/itest/src/aut/components/balloon/LinkActionsViewWrapper.ts index 5685fdc1ce..a839f3bbd2 100644 --- a/itest/src/aut/components/balloon/LinkActionsViewWrapper.ts +++ b/itest/src/aut/components/balloon/LinkActionsViewWrapper.ts @@ -1,5 +1,5 @@ -import { LinkActionsView } from "@coremedia/ckeditor5-coremedia-link"; -import ViewWrapper from "../ViewWrapper"; +import type { LinkActionsView } from "@coremedia/ckeditor5-coremedia-link"; +import type ViewWrapper from "../ViewWrapper"; import { JSWrapper } from "../../JSWrapper"; import ContentLinkViewWrapper from "./ContentLinkViewWrapper"; diff --git a/itest/src/aut/services/BlocklistServiceWrapper.ts b/itest/src/aut/services/BlocklistServiceWrapper.ts index ab9e341cb7..085a01548e 100644 --- a/itest/src/aut/services/BlocklistServiceWrapper.ts +++ b/itest/src/aut/services/BlocklistServiceWrapper.ts @@ -1,6 +1,6 @@ import type { MockBlocklistService } from "@coremedia/ckeditor5-coremedia-studio-integration-mock"; import { JSWrapper } from "../JSWrapper"; -import { MockServiceAgentPluginWrapper } from "./MockServiceAgentPluginWrapper"; +import type { MockServiceAgentPluginWrapper } from "./MockServiceAgentPluginWrapper"; export class BlocklistServiceWrapper extends JSWrapper { async addWord(word: string): Promise { diff --git a/itest/src/aut/services/ContentFormServiceWrapper.ts b/itest/src/aut/services/ContentFormServiceWrapper.ts index a6e95b944c..0b63ae9525 100644 --- a/itest/src/aut/services/ContentFormServiceWrapper.ts +++ b/itest/src/aut/services/ContentFormServiceWrapper.ts @@ -1,6 +1,6 @@ import type { MockContentFormService } from "@coremedia/ckeditor5-coremedia-studio-integration-mock"; import { JSWrapper } from "../JSWrapper"; -import { MockServiceAgentPluginWrapper } from "./MockServiceAgentPluginWrapper"; +import type { MockServiceAgentPluginWrapper } from "./MockServiceAgentPluginWrapper"; export class ContentFormServiceWrapper extends JSWrapper { /** diff --git a/itest/src/aut/services/MockServiceAgentPluginWrapper.ts b/itest/src/aut/services/MockServiceAgentPluginWrapper.ts index d3c022a21b..39436435e2 100644 --- a/itest/src/aut/services/MockServiceAgentPluginWrapper.ts +++ b/itest/src/aut/services/MockServiceAgentPluginWrapper.ts @@ -1,6 +1,6 @@ import type { MockServiceAgentPlugin } from "@coremedia/ckeditor5-coremedia-studio-integration-mock"; import { JSWrapper } from "../JSWrapper"; -import { ClassicEditorWrapper } from "../ClassicEditorWrapper"; +import type { ClassicEditorWrapper } from "../ClassicEditorWrapper"; import { ContentFormServiceWrapper } from "./ContentFormServiceWrapper"; import { BlocklistServiceWrapper } from "./BlocklistServiceWrapper"; diff --git a/itest/src/expect/ApplicationConsoleExpectations.ts b/itest/src/expect/ApplicationConsoleExpectations.ts index 8abddc6d4a..0bc2170f75 100644 --- a/itest/src/expect/ApplicationConsoleExpectations.ts +++ b/itest/src/expect/ApplicationConsoleExpectations.ts @@ -1,5 +1,5 @@ -import { ConsoleMessage } from "playwright-core"; -import { ApplicationConsole } from "../aut/ApplicationConsole"; +import type { ConsoleMessage } from "playwright-core"; +import type { ApplicationConsole } from "../aut/ApplicationConsole"; /** * Maps messages to some representation for output. Ignores arguments, as they diff --git a/itest/src/expect/ApplicationWrapperExpectations.ts b/itest/src/expect/ApplicationWrapperExpectations.ts index f011442ead..aabdc47a41 100644 --- a/itest/src/expect/ApplicationWrapperExpectations.ts +++ b/itest/src/expect/ApplicationWrapperExpectations.ts @@ -1,4 +1,4 @@ -import { ApplicationWrapper } from "../aut/ApplicationWrapper"; +import type { ApplicationWrapper } from "../aut/ApplicationWrapper"; import { extendingWaitForExpect } from "./ExpectationsBase"; /** diff --git a/itest/src/expect/ClassicEditorWrapperExpectations.ts b/itest/src/expect/ClassicEditorWrapperExpectations.ts index d6ab18ff19..441cf2d431 100644 --- a/itest/src/expect/ClassicEditorWrapperExpectations.ts +++ b/itest/src/expect/ClassicEditorWrapperExpectations.ts @@ -1,4 +1,4 @@ -import { ClassicEditorWrapper } from "../aut/ClassicEditorWrapper"; +import type { ClassicEditorWrapper } from "../aut/ClassicEditorWrapper"; import { extendingWaitForExpect } from "./ExpectationsBase"; /** diff --git a/itest/src/expect/ElementHandleExpectations.ts b/itest/src/expect/ElementHandleExpectations.ts index d71f185143..6f52dae31c 100644 --- a/itest/src/expect/ElementHandleExpectations.ts +++ b/itest/src/expect/ElementHandleExpectations.ts @@ -1,4 +1,4 @@ -import { ElementHandle } from "playwright-core"; +import type { ElementHandle } from "playwright-core"; import { extendingWaitForExpect } from "./ExpectationsBase"; /** diff --git a/itest/src/expect/Expectations.ts b/itest/src/expect/Expectations.ts index 1d3c2c5826..c88813117c 100644 --- a/itest/src/expect/Expectations.ts +++ b/itest/src/expect/Expectations.ts @@ -1,4 +1,3 @@ -/* eslint-disable import/no-duplicates */ // noinspection JSUnusedGlobalSymbols // Import expect.extend @@ -13,15 +12,15 @@ import "./contentName/WaitToHaveContentName"; import "./aria/ToHaveAriaLabel"; // Import Matcher Interfaces -import { ApplicationWrapperMatchers } from "./ApplicationWrapperExpectations"; -import { ClassicEditorWrapperMatchers } from "./ClassicEditorWrapperExpectations"; -import { ElementHandleMatchers } from "./ElementHandleExpectations"; -import { ApplicationConsoleMatchers } from "./ApplicationConsoleExpectations"; -import { WaitToBeVisible } from "./IsVisible/WaitToBeVisible"; -import { WaitToBeOn } from "./isToggleable/WaitToBeOn"; -import { WaitToBeEnabled } from "./isEnabled/WaitToBeEnabled"; -import { WaitToHaveContentName } from "./contentName/WaitToHaveContentName"; -import { WaitToHaveAriaLabel } from "./aria/ToHaveAriaLabel"; +import type { ApplicationWrapperMatchers } from "./ApplicationWrapperExpectations"; +import type { ClassicEditorWrapperMatchers } from "./ClassicEditorWrapperExpectations"; +import type { ElementHandleMatchers } from "./ElementHandleExpectations"; +import type { ApplicationConsoleMatchers } from "./ApplicationConsoleExpectations"; +import type { WaitToBeVisible } from "./IsVisible/WaitToBeVisible"; +import type { WaitToBeOn } from "./isToggleable/WaitToBeOn"; +import type { WaitToBeEnabled } from "./isEnabled/WaitToBeEnabled"; +import type { WaitToHaveContentName } from "./contentName/WaitToHaveContentName"; +import type { WaitToHaveAriaLabel } from "./aria/ToHaveAriaLabel"; /** * Tell TypeScript to know of new matchers. diff --git a/itest/src/expect/IsVisible/WaitToBeVisible.ts b/itest/src/expect/IsVisible/WaitToBeVisible.ts index 860add7e83..ab71fbd882 100644 --- a/itest/src/expect/IsVisible/WaitToBeVisible.ts +++ b/itest/src/expect/IsVisible/WaitToBeVisible.ts @@ -1,5 +1,5 @@ import { extendingWaitForExpect } from "../ExpectationsBase"; -import { HasVisible } from "./HasVisible"; +import type { HasVisible } from "./HasVisible"; expect.extend({ async waitToBeVisible(hasVisible: HasVisible): Promise { diff --git a/itest/src/expect/aria/ToHaveAriaLabel.ts b/itest/src/expect/aria/ToHaveAriaLabel.ts index 3bf3377cde..ae860adacf 100644 --- a/itest/src/expect/aria/ToHaveAriaLabel.ts +++ b/itest/src/expect/aria/ToHaveAriaLabel.ts @@ -1,5 +1,6 @@ import { extendingWaitForExpect } from "../ExpectationsBase"; -import { getAriaLabel, HasAriaLabel } from "../../aria/AriaUtils"; +import type { HasAriaLabel } from "../../aria/AriaUtils"; +import { getAriaLabel } from "../../aria/AriaUtils"; expect.extend({ async waitToHaveAriaLabel(hasAriaLabel: HasAriaLabel, expectedAriaLabel: string): Promise { diff --git a/itest/src/expect/contentName/WaitToHaveContentName.ts b/itest/src/expect/contentName/WaitToHaveContentName.ts index ec37b98b0f..42205e9dea 100644 --- a/itest/src/expect/contentName/WaitToHaveContentName.ts +++ b/itest/src/expect/contentName/WaitToHaveContentName.ts @@ -1,5 +1,5 @@ import { extendingWaitForExpect } from "../ExpectationsBase"; -import { HasContentName } from "./HasContentName"; +import type { HasContentName } from "./HasContentName"; expect.extend({ async waitToHaveContentName(hasContentName: HasContentName, expectedName: string): Promise { diff --git a/itest/src/expect/isEnabled/WaitToBeEnabled.ts b/itest/src/expect/isEnabled/WaitToBeEnabled.ts index cad678d1f0..47bb0348d1 100644 --- a/itest/src/expect/isEnabled/WaitToBeEnabled.ts +++ b/itest/src/expect/isEnabled/WaitToBeEnabled.ts @@ -1,5 +1,5 @@ import { extendingWaitForExpect } from "../ExpectationsBase"; -import { HasEnabled } from "./HasEnabled"; +import type { HasEnabled } from "./HasEnabled"; expect.extend({ async waitToBeEnabled(hasEnabled: HasEnabled): Promise { diff --git a/itest/src/expect/isToggleable/WaitToBeOn.ts b/itest/src/expect/isToggleable/WaitToBeOn.ts index 663dcc7973..3dafd598b1 100644 --- a/itest/src/expect/isToggleable/WaitToBeOn.ts +++ b/itest/src/expect/isToggleable/WaitToBeOn.ts @@ -1,5 +1,5 @@ import { extendingWaitForExpect } from "../ExpectationsBase"; -import { HasToggleable } from "./HasToggleable"; +import type { HasToggleable } from "./HasToggleable"; expect.extend({ async waitToBeOn(hasToggleable: HasToggleable): Promise { diff --git a/package.json b/package.json index d8e369b4c5..ea871f9db1 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "avatar": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2OTguNjY3IiBoZWlnaHQ9IjY5OC42NjciIHZlcnNpb249IjEuMCIgdmlld0JveD0iMCAwIDUyNCA1MjQiPjxwYXRoIGQ9Ik0yODIuNS42QzI0NSA0LjUgMjE1LjEgMTIuOSAxODUgMjcuOSAxMDYuNCA2Ny4yIDUzLjUgMTQyLjEgNDIuNCAyMjkuNWMtMi4xIDE3LjItMi4yIDQ4IDAgNjQuMUM0OCAzMzYuMiA2MS4zIDM3Mi4yIDg0IDQwNi4zYzYxLjkgOTMuMSAxNzUuNCAxMzYuNCAyODUgMTA4LjcgMTkuMi00LjggMzMuNy0xMC4zIDUzLjItMjAuMSAyMS0xMC41IDQyLjUtMjUuMyA1OC4zLTQwLjJsOC03LjYtMjEuNS0yMS42Yy0yMy42LTIzLjgtMjcuMS0yNi43LTM4LjYtMzIuNC0yMi40LTExLjItNDQuNy0xMS42LTcxLjctMS41LTE4LjYgNy0zMC40IDkuNC00OC43IDEwLjEtMTMgLjQtMTcuNS4yLTI3LjgtMS42LTMwLjUtNS4yLTU1LTE3LjgtNzYuOC0zOS41LTMzLjYtMzMuNi00Ny44LTgwLjQtMzguNC0xMjYuNCAxMC4yLTUwLjUgNDYuMi05MC41IDk1LjItMTA2LjEgMzEuNy0xMC4xIDY0LjItOC44IDk2LjYgMy45IDEyLjggNC45IDIyLjcgNyAzNC4xIDcgMTYuNCAwIDMxLjItNC4yIDQ1LjEtMTIuNyA1LjUtMy40IDEzLjYtMTAuNyAzMC40LTI3LjNsMjIuOC0yMi42LTkuMy04LjRjLTM5LjgtMzUuNC04NS42LTU3LTEzOC40LTY1LjEtMTIuNS0yLTQ4LjctMy4zLTU5LTIuM3oiLz48cGF0aCBkPSJNMjk2IDIxOC4xYy0yOC4zIDQuOC00NC4zIDM1LjQtMzIuMSA2MS40IDcuOSAxNyAyNy4yIDI3LjQgNDUuNCAyNC41IDI4LjktNC42IDQ1LjEtMzUuMiAzMi43LTYxLjctOC0xNy4yLTI3LjMtMjcuMy00Ni0yNC4yeiIvPjwvc3ZnPg==" }, "engines": { - "node": "22", + "node": "^22.3.0", "pnpm": "^10.9" }, "private": true, @@ -43,9 +43,7 @@ "doc": "typedoc", "doc:config": "typedoc --showConfig", "playwright": "pnpm --recursive --filter \"@coremedia/ckeditor5-coremedia-itest\" run playwright", - "jest": "pnpm --recursive jest", - "jest:windows:workaround:issue:4444": "pnpm --recursive jest --runInBand", - "jest:coverage": "pnpm --recursive jest:coverage", + "test": "pnpm --recursive test", "prettier": "prettier --check \"**/*.{js,cjs,mjs,ts,tsx}\"", "prettier:fix": "prettier --write \"**/*.{js,cjs,mjs,ts,tsx}\"", "preinstall": "node ./scripts/check-pnpm.mjs", @@ -61,7 +59,6 @@ "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^9.33.0", "@stylistic/eslint-plugin": "^5.2.3", - "@types/jest": "^29.5.12", "@typescript-eslint/eslint-plugin": "^8.39.1", "@typescript-eslint/parser": "^8.39.1", "eslint": "^9.33.0", diff --git a/packages/ckeditor5-bbcode/__tests__/BBCodeBold.test.ts b/packages/ckeditor5-bbcode/__tests__/BBCodeBold.test.ts deleted file mode 100644 index 587874a62d..0000000000 --- a/packages/ckeditor5-bbcode/__tests__/BBCodeBold.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { BBCodeBold, bbCodeBold } from "../src"; -import { IsBoldFontWeight } from "../src/rules/BBCodeBold"; -import { requireHTMLElement } from "./DOMUtils"; - -describe("BBCodeBold", () => { - describe("Default Configuration", () => { - const rule = bbCodeBold; - - it.each` - dataView | expected | comment - ${`TEXT`} | ${`[b]TEXT[/b]`} | ${`BBob HTML 5 Preset Result (toView)`} - ${`TEXT`} | ${`[b]TEXT[/b]`} | ${`also supporting _bolder_ to some degree`} - ${`TEXT`} | ${`[b]TEXT[/b]`} | ${`CKEditor 5 default data view representation`} - ${`TEXT`} | ${`[b]TEXT[/b]`} | ${`alternative HTML 5 representation`} - ${`TEXT`} | ${undefined} | ${`vetoed bold tag; undefined, so other rules may kick in`} - ${`TEXT`} | ${undefined} | ${`vetoed bold tag; undefined, so other rules may kick in`} - ${`TEXT`} | ${`[b]TEXT[/b]`} | ${`weight and tag agree`} - ${`TEXT`} | ${undefined} | ${`veto by numeric font-weight (400)`} - ${`TEXT`} | ${`[b]TEXT[/b]`} | ${`Bold just by font-weight.`} - ${`TEXT`} | ${undefined} | ${`Minimum font-weight.`} - ${`TEXT`} | ${`[b]TEXT[/b]`} | ${`Maximum font-weight.`} - ${`TEXT`} | ${`[b]TEXT[/b]`} | ${`corner case: "" itself will be handled by outer rules`} - `( - "[$#] Should process '$dataView' to '$expected' ($comment)", - ({ dataView, expected }: { dataView: string; expected: string | undefined }) => { - const element = requireHTMLElement(dataView); - const bbCode = rule.toData(element, element.textContent ?? ""); - expect(bbCode).toEqual(expected); - }, - ); - }); - - describe("Custom Configuration", () => { - /** - * For demonstration only: Don't judge on font-weight, just on tag name. - */ - const ignoreFontWeight: IsBoldFontWeight = (assumedBold) => assumedBold; - const rule = new BBCodeBold({ - isBold: ignoreFontWeight, - }); - - it.each` - dataView | expected | comment - ${`TEXT`} | ${undefined} | ${`Respect custom isBold rule to ignore font-weight style`} - ${`TEXT`} | ${undefined} | ${`Respect custom isBold rule to ignore font-weight style`} - ${`TEXT`} | ${`[b]TEXT[/b]`} | ${`CKEditor 5 default data view representation`} - ${`TEXT`} | ${`[b]TEXT[/b]`} | ${`Respect custom isBold rule to ignore font-weight style`} - ${`TEXT`} | ${`[b]TEXT[/b]`} | ${`Respect custom isBold rule to ignore font-weight style`} - ${`TEXT`} | ${`[b]TEXT[/b]`} | ${`Respect custom isBold rule to ignore font-weight style`} - ${`TEXT`} | ${undefined} | ${`Respect custom isBold rule to ignore font-weight style`} - `( - "[$#] Should process '$dataView' to '$expected' ($comment)", - ({ dataView, expected }: { dataView: string; expected: string | undefined }) => { - const element = requireHTMLElement(dataView); - const bbCode = rule.toData(element, element.textContent ?? ""); - expect(bbCode).toEqual(expected); - }, - ); - }); -}); diff --git a/packages/ckeditor5-bbcode/__tests__/BBCodeCode.test.ts b/packages/ckeditor5-bbcode/__tests__/BBCodeCode.test.ts deleted file mode 100644 index dc5a1039ce..0000000000 --- a/packages/ckeditor5-bbcode/__tests__/BBCodeCode.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { BBCodeCode, bbCodeCode } from "../src"; -import { requireHTMLElement } from "./DOMUtils"; - -describe("BBCodeCode", () => { - describe("Default Configuration", () => { - const rule = bbCodeCode; - - it.each` - dataView | expected | comment - ${`
TEXT
`} | ${`[code]\nTEXT\n[/code]\n`} | ${`newlines for minor pretty-printing`} - ${`
TEXT
`} | ${`[code=css]\nTEXT\n[/code]\n`} | ${`respect language`} - ${`
TEXT
`} | ${`[code]\nTEXT\n[/code]\n`} | ${`strip irrelevant plaintext language`} - ${`
\nTEXT\n
`} | ${`[code]\nTEXT\n[/code]\n`} | ${`trimming: don't pile up newlines (here: in
)`}
-      ${`
\nTEXT\n
`} | ${`[code]\nTEXT\n[/code]\n`} | ${`trimming: don't pile up newlines (here: in )`} - ${`
\n\n\n\nTEXT\n\n\n\n
`} | ${`[code]\nTEXT\n[/code]\n`} | ${`trimming: even remove extra newlines`} - ${`
\n  TEXT1\n  TEXT2\n
`} | ${`[code]\n TEXT1\n TEXT2\n[/code]\n`} | ${`trimming: must not remove indents`} - ${`
TEXT  \n
`} | ${`[code]\nTEXT\n[/code]\n`} | ${`trimming (Design Scope): We put some extra effort removing irrelevant blanks at the end.`} - ${`

TEXT

`} | ${undefined} | ${"ignore unmatched"} - ${`
TEXT
`} | ${`[code]\nTEXT\n[/code]\n`} | ${`robustness: ignore possible missing nested element`} - `( - "[$#] Should process '$dataView' to '$expected' ($comment)", - ({ dataView, expected }: { dataView: string; expected: string | undefined }) => { - const element = requireHTMLElement(dataView); - const bbCode = rule.toData(element, element.textContent ?? ""); - expect(bbCode).toEqual(expected); - }, - ); - }); - - describe("Custom Configurations", () => { - it.each` - dataView | expected | comment - ${`
TEXT
`} | ${`[code=css]\nTEXT\n[/code]\n`} | ${`respect language`} - ${`
TEXT
`} | ${`[code]\nTEXT\n[/code]\n`} | ${`strip irrelevant language by custom config`} - `( - "[$#] Custom isUnset: Should process '$dataView' to '$expected' ($comment)", - ({ dataView, expected }: { dataView: string; expected: string | undefined }) => { - const rule = new BBCodeCode({ - isUnset: (lang) => lang === "ignored", - }); - const element = requireHTMLElement(dataView); - const bbCode = rule.toData(element, element.textContent ?? ""); - expect(bbCode).toEqual(expected); - }, - ); - - it.each` - dataView | expected | comment - ${`
TEXT
`} | ${`[code=css]\nTEXT\n[/code]\n`} | ${`respect language by custom extractor`} - ${`
TEXT
`} | ${`[code]\nTEXT\n[/code]\n`} | ${`strip irrelevant plaintext language`} - `( - "[$#] Custom fromClass: Should process '$dataView' to '$expected' ($comment)", - ({ dataView, expected }: { dataView: string; expected: string | undefined }) => { - const rule = new BBCodeCode({ - fromClass: (entry) => entry, - }); - const element = requireHTMLElement(dataView); - const bbCode = rule.toData(element, element.textContent ?? ""); - expect(bbCode).toEqual(expected); - }, - ); - }); -}); diff --git a/packages/ckeditor5-bbcode/__tests__/BBCodeColor.test.ts b/packages/ckeditor5-bbcode/__tests__/BBCodeColor.test.ts deleted file mode 100644 index cf6942bc2c..0000000000 --- a/packages/ckeditor5-bbcode/__tests__/BBCodeColor.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { RgbColor, w3ExtendedColorNames } from "@coremedia/ckeditor5-dom-support"; -import { BBCodeColor, bbCodeColor, ColorMapper } from "../src/rules/BBCodeColor"; -import { requireHTMLElement } from "./DOMUtils"; - -const reverseW3CColorMap = Object.fromEntries(Object.entries(w3ExtendedColorNames).map(([key, value]) => [value, key])); - -const enforceHexRepresentation: ColorMapper = (color: string | RgbColor): string => { - if (typeof color === "string") { - return reverseW3CColorMap[color.toLowerCase()] ?? color; - } - return color.toHex(); -}; - -describe("BBCodeColor", () => { - describe("Default Configuration", () => { - const rule = bbCodeColor; - - it.each` - dataView | expected | comment - ${`TEXT`} | ${`[color=#ff0001]TEXT[/color]`} | ${`BBob HTML 5 Preset Result (toView)`} - ${`TEXT`} | ${`[color=#ff0001]TEXT[/color]`} | ${`ignore case`} - ${`TEXT`} | ${`[color=red]TEXT[/color]`} | ${`Prefer W3C Color Names`} - ${`TEXT`} | ${`[color=fuchsia]TEXT[/color]`} | ${`supported color names`} - ${`TEXT`} | ${`[color=#cccccc]TEXT[/color]`} | ${`support shortened color codes`} - ${`TEXT`} | ${`[color=red]TEXT[/color]`} | ${`also support rgb() codes`} - ${`TEXT`} | ${`[color=#ff0000a0]TEXT[/color]`} | ${`also support rgba() codes`} - `( - "[$#] Should process '$dataView' to '$expected' ($comment)", - ({ dataView, expected }: { dataView: string; expected: string | undefined }) => { - const element = requireHTMLElement(dataView); - const bbCode = rule.toData(element, element.textContent ?? ""); - expect(bbCode).toEqual(expected); - }, - ); - }); - - describe("Custom Color Mapper Configuration", () => { - const rule = new BBCodeColor({ mapper: enforceHexRepresentation }); - - it.each` - dataView | expected | comment - ${`TEXT`} | ${`[color=#ff0000]TEXT[/color]`} | ${``} - ${`TEXT`} | ${`[color=#ff00ff]TEXT[/color]`} | ${`prefer hex over color name`} - ${`TEXT`} | ${`[color=#ff00ff]TEXT[/color]`} | ${`ignore case`} - ${`TEXT`} | ${`[color=#ff0000]TEXT[/color]`} | ${`prefer hex over color name`} - `( - "[$#] Should process '$dataView' to '$expected' ($comment)", - ({ dataView, expected }: { dataView: string; expected: string | undefined }) => { - const element = requireHTMLElement(dataView); - const bbCode = rule.toData(element, element.textContent ?? ""); - expect(bbCode).toEqual(expected); - }, - ); - }); -}); diff --git a/packages/ckeditor5-bbcode/__tests__/BBCodeHeading.test.ts b/packages/ckeditor5-bbcode/__tests__/BBCodeHeading.test.ts deleted file mode 100644 index 21e4eb84ff..0000000000 --- a/packages/ckeditor5-bbcode/__tests__/BBCodeHeading.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { bbCodeHeading } from "../src"; -import { requireHTMLElement } from "./DOMUtils"; - -const prettyPrintNewlines = "\n\n"; - -describe("BBCodeHeading", () => { - describe("Default Configuration", () => { - const rule = bbCodeHeading; - - it.each` - dataView | expected - ${`

TEXT

`} | ${`[h1]TEXT[/h1]${prettyPrintNewlines}`} - ${`

TEXT

`} | ${`[h2]TEXT[/h2]${prettyPrintNewlines}`} - ${`

TEXT

`} | ${`[h3]TEXT[/h3]${prettyPrintNewlines}`} - ${`

TEXT

`} | ${`[h4]TEXT[/h4]${prettyPrintNewlines}`} - ${`
TEXT
`} | ${`[h5]TEXT[/h5]${prettyPrintNewlines}`} - ${`
TEXT
`} | ${`[h6]TEXT[/h6]${prettyPrintNewlines}`} - `( - "[$#] Should process '$dataView' to '$expected'", - ({ dataView, expected }: { dataView: string; expected: string | undefined }) => { - const element = requireHTMLElement(dataView); - const bbCode = rule.toData(element, element.textContent ?? ""); - expect(bbCode).toEqual(expected); - }, - ); - }); -}); diff --git a/packages/ckeditor5-bbcode/__tests__/BBCodeImg.test.ts b/packages/ckeditor5-bbcode/__tests__/BBCodeImg.test.ts deleted file mode 100644 index 1a042152f2..0000000000 --- a/packages/ckeditor5-bbcode/__tests__/BBCodeImg.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { bbCodeImg } from "../src/rules/BBCodeImg"; -import { requireHTMLElement } from "./DOMUtils"; - -describe("BBCodeImg", () => { - describe("Default Configuration", () => { - const rule = bbCodeImg; - const someUrl = "https://example.org/"; - - // noinspection HtmlUnknownAnchorTarget,HtmlRequiredAltAttribute,HtmlUnknownTarget - it.each` - dataView | expected | comment - ${``} | ${`[img]${someUrl}[/img]`} | ${`Default Mapping Use-Case`} - ${``} | ${`[img]${someUrl}?openBracket=%5B[/img]`} | ${`Escape Open-Bracket [`} - ${``} | ${`[img]${someUrl}?closeBracket=%5D[/img]`} | ${`Escape Close-Bracket [`} - ${``} | ${`[img]${someUrl}?quote="[/img]`} | ${`Don't escape double quote as it is rendered as content`} - ${``} | ${undefined} | ${`As there is no representation for "empty URLs" in BBCode, not mapping to [img]`} - ${``} | ${`[img]/relative[/img]`} | ${`Keep relative URLs (1)`} - ${``} | ${`[img]?search=param[/img]`} | ${`Keep relative URLs, search-param only (2)`} - ${``} | ${`[img]#hash[/img]`} | ${`Keep relative URLs, hash-param only (3)`} - `( - "[$#] Should process '$dataView' to '$expected' ($comment)", - ({ dataView, expected }: { dataView: string; expected: string | undefined }) => { - const element = requireHTMLElement(dataView); - const bbCode = rule.toData(element); - expect(bbCode).toEqual(expected); - }, - ); - }); -}); diff --git a/packages/ckeditor5-bbcode/__tests__/BBCodeItalic.test.ts b/packages/ckeditor5-bbcode/__tests__/BBCodeItalic.test.ts deleted file mode 100644 index b373473e3a..0000000000 --- a/packages/ckeditor5-bbcode/__tests__/BBCodeItalic.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { bbCodeItalic } from "../src"; -import { requireHTMLElement } from "./DOMUtils"; - -describe("BBCodeItalic", () => { - describe("Default Configuration", () => { - const rule = bbCodeItalic; - - it.each` - dataView | expected | comment - ${`TEXT`} | ${`[i]TEXT[/i]`} | ${`BBob HTML 5 Preset Result (toView)`} - ${`TEXT`} | ${`[i]TEXT[/i]`} | ${`CKEditor 5 default data view representation`} - ${`TEXT`} | ${`[i]TEXT[/i]`} | ${`alternative HTML 5 representation`} - ${`TEXT`} | ${undefined} | ${`vetoed italic tag; undefined, so other rules may kick in`} - ${`TEXT`} | ${`[i]TEXT[/i]`} | ${`Corner case: "" will be handled by outer rules.`} - `( - "[$#] Should process '$dataView' to '$expected' ($comment)", - ({ dataView, expected }: { dataView: string; expected: string | undefined }) => { - const element = requireHTMLElement(dataView); - const bbCode = rule.toData(element, element.textContent ?? ""); - expect(bbCode).toEqual(expected); - }, - ); - }); -}); diff --git a/packages/ckeditor5-bbcode/__tests__/BBCodeList.test.ts b/packages/ckeditor5-bbcode/__tests__/BBCodeList.test.ts deleted file mode 100644 index 13b7786a56..0000000000 --- a/packages/ckeditor5-bbcode/__tests__/BBCodeList.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { bbCodeList } from "../src"; -import { requireHTMLElement } from "./DOMUtils"; - -// No integration test here. Simulate, we already mapped the children. -const mockListItemsContent = (el: HTMLElement): string => - Array.from(el.children) - .map((e) => `[*] ${e.textContent ?? ""}`) - .join("\n"); - -describe("BBCodeList", () => { - describe("Default Configuration", () => { - const rule = bbCodeList; - - it.each` - dataView | expected | comment - ${`
  • TEXT
`} | ${`[list]\n[*] TEXT\n[/list]\n`} | ${""} - ${`
  1. TEXT
`} | ${`[list=1]\n[*] TEXT\n[/list]\n`} | ${""} - ${`
  1. TEXT
`} | ${`[list=1]\n[*] TEXT\n[/list]\n`} | ${""} - ${`
  1. TEXT
`} | ${`[list=a]\n[*] TEXT\n[/list]\n`} | ${""} - ${`
  1. TEXT
`} | ${`[list=A]\n[*] TEXT\n[/list]\n`} | ${""} - ${`
  1. TEXT
`} | ${`[list=i]\n[*] TEXT\n[/list]\n`} | ${""} - ${`
  1. TEXT
`} | ${`[list=I]\n[*] TEXT\n[/list]\n`} | ${""} - ${`
  1. TEXT
`} | ${`[list=1]\n[*] TEXT\n[/list]\n`} | ${"Due to BBob Preset-HTML5 Restrictions not respecting list-style-type for now."} - ${`
  • TEXT\n\n
`} | ${`[list]\n[*] TEXT\n[/list]\n`} | ${"pretty-print trimming"} - `( - "[$#] Should process '$dataView' to '$expected' ($comment)", - ({ dataView, expected }: { dataView: string; expected: string | undefined }) => { - const element = requireHTMLElement(dataView); - const content = mockListItemsContent(element); - const bbCode = rule.toData(element, content); - expect(bbCode).toEqual(expected); - }, - ); - }); -}); diff --git a/packages/ckeditor5-bbcode/__tests__/BBCodeParagraph.test.ts b/packages/ckeditor5-bbcode/__tests__/BBCodeParagraph.test.ts deleted file mode 100644 index 631d4e3ed3..0000000000 --- a/packages/ckeditor5-bbcode/__tests__/BBCodeParagraph.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { bbCodeParagraph } from "../src"; -import { requireHTMLElement } from "./DOMUtils"; - -const prettyPrintNewlines = "\n\n"; - -describe("BBCodeParagraph", () => { - describe("Default Configuration", () => { - const rule = bbCodeParagraph; - - it.each` - dataView | expected - ${`

TEXT

`} | ${`TEXT${prettyPrintNewlines}`} - `( - "[$#] Should process '$dataView' to '$expected'", - ({ dataView, expected }: { dataView: string; expected: string | undefined }) => { - const element = requireHTMLElement(dataView); - const bbCode = rule.toData(element, element.textContent ?? ""); - expect(bbCode).toEqual(expected); - }, - ); - }); -}); diff --git a/packages/ckeditor5-bbcode/__tests__/BBCodeQuote.test.ts b/packages/ckeditor5-bbcode/__tests__/BBCodeQuote.test.ts deleted file mode 100644 index 5996e2313b..0000000000 --- a/packages/ckeditor5-bbcode/__tests__/BBCodeQuote.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { bbCodeQuote } from "../src"; -import { requireHTMLElement } from "./DOMUtils"; - -describe("BBCodeQuote", () => { - describe("Default Configuration", () => { - const rule = bbCodeQuote; - - it.each` - dataView | expected | comment - ${`

TEXT

`} | ${`[quote]\nTEXT\n[/quote]\n`} | ${`newlines for minor pretty-printing`} - `( - "[$#] Should process '$dataView' to '$expected' ($comment)", - ({ dataView, expected }: { dataView: string; expected: string | undefined }) => { - const element = requireHTMLElement(dataView); - // Simple processing only applies at one level, so nested tests not possible here. - const bbCode = rule.toData(element, element.textContent ?? ""); - expect(bbCode).toEqual(expected); - }, - ); - }); -}); diff --git a/packages/ckeditor5-bbcode/__tests__/BBCodeSize.test.ts b/packages/ckeditor5-bbcode/__tests__/BBCodeSize.test.ts deleted file mode 100644 index 51acb5084b..0000000000 --- a/packages/ckeditor5-bbcode/__tests__/BBCodeSize.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { bbCodeSize } from "../src/rules/BBCodeSize"; -import { requireHTMLElement } from "./DOMUtils"; - -describe("BBCodeSize", () => { - describe("Default Configuration", () => { - const rule = bbCodeSize; - - it.each` - dataView | expected | comment - ${`T`} | ${`[size=70]T[/size]`} | ${`text-tiny mapping to "representing" number`} - ${`T`} | ${`[size=85]T[/size]`} | ${`text-small mapping to "representing" number`} - ${`T`} | ${`[size=140]T[/size]`} | ${`text-big mapping to "representing" number`} - ${`T`} | ${`[size=180]T[/size]`} | ${`text-huge mapping to "representing" number`} - `( - "[$#] Should process '$dataView' to '$expected' ($comment)", - ({ dataView, expected }: { dataView: string; expected: string | undefined }) => { - const element = requireHTMLElement(dataView); - const bbCode = rule.toData(element, element.textContent ?? ""); - expect(bbCode).toEqual(expected); - }, - ); - }); -}); diff --git a/packages/ckeditor5-bbcode/__tests__/BBCodeStrikethrough.test.ts b/packages/ckeditor5-bbcode/__tests__/BBCodeStrikethrough.test.ts deleted file mode 100644 index 48840eb4cf..0000000000 --- a/packages/ckeditor5-bbcode/__tests__/BBCodeStrikethrough.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { bbCodeStrikethrough } from "../src"; -import { requireHTMLElement } from "./DOMUtils"; - -describe("BBCodeStrikethrough", () => { - describe("Default Configuration", () => { - const rule = bbCodeStrikethrough; - - // noinspection HtmlDeprecatedTag,XmlDeprecatedElement - it.each` - dataView | expected | comment - ${`TEXT`} | ${`[s]TEXT[/s]`} | ${`BBob HTML 5 Preset Result (toView)`} - ${`TEXT`} | ${`[s]TEXT[/s]`} | ${`CKEditor 5 default data view representation`} - ${`TEXT`} | ${`[s]TEXT[/s]`} | ${`alternative HTML5 representation`} - ${`TEXT`} | ${`[s]TEXT[/s]`} | ${`alternative (deprecated) representation`} - ${`TEXT`} | ${undefined} | ${`vetoed by style; undefined, so other rules may kick in`} - ${`TEXT`} | ${`[s]TEXT[/s]`} | ${`corner case: "" itself will be handled by outer rules`} - `( - "[$#] Should process '$dataView' to '$expected' ($comment)", - ({ dataView, expected }: { dataView: string; expected: string | undefined }) => { - const element = requireHTMLElement(dataView); - const bbCode = rule.toData(element, element.textContent ?? ""); - expect(bbCode).toEqual(expected); - }, - ); - }); -}); diff --git a/packages/ckeditor5-bbcode/__tests__/BBCodeUnderline.test.ts b/packages/ckeditor5-bbcode/__tests__/BBCodeUnderline.test.ts deleted file mode 100644 index 86c43f71b8..0000000000 --- a/packages/ckeditor5-bbcode/__tests__/BBCodeUnderline.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { bbCodeUnderline } from "../src"; -import { requireHTMLElement } from "./DOMUtils"; - -describe("BBCodeUnderline", () => { - describe("Default Configuration", () => { - const rule = bbCodeUnderline; - - it.each` - dataView | expected | comment - ${`TEXT`} | ${`[u]TEXT[/u]`} | ${`BBob HTML 5 Preset Result (toView)`} - ${`TEXT`} | ${`[u]TEXT[/u]`} | ${`CKEditor 5 default data view representation`} - ${`TEXT`} | ${`[u]TEXT[/u]`} | ${`alternative HTML5 representation`} - ${`TEXT`} | ${undefined} | ${`vetoed by style; undefined, so other rules may kick in`} - ${`TEXT`} | ${`[u]TEXT[/u]`} | ${`corner case: "" itself will be handled by outer rules`} - `( - "[$#] Should process '$dataView' to '$expected' ($comment)", - ({ dataView, expected }: { dataView: string; expected: string | undefined }) => { - const element = requireHTMLElement(dataView); - const bbCode = rule.toData(element, element.textContent ?? ""); - expect(bbCode).toEqual(expected); - }, - ); - }); -}); diff --git a/packages/ckeditor5-bbcode/__tests__/BBCodeUrl.test.ts b/packages/ckeditor5-bbcode/__tests__/BBCodeUrl.test.ts deleted file mode 100644 index 7a1b8b6a9f..0000000000 --- a/packages/ckeditor5-bbcode/__tests__/BBCodeUrl.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { bbCodeUrl } from "../src"; -import { requireHTMLElement } from "./DOMUtils"; - -describe("BBCodeUrl", () => { - describe("Default Configuration", () => { - const rule = bbCodeUrl; - const someUrl = "https://example.org/"; - - // noinspection HtmlUnknownAnchorTarget - it.each` - dataView | expected | comment - ${`TEXT`} | ${`[url="${someUrl}"]TEXT[/url]`} | ${`Default Mapping Use-Case`} - ${`${someUrl}`} | ${`[url]${someUrl}[/url]`} | ${`Pretty-Print: Shorten, if applicable`} - ${`TEXT`} | ${`[url="${someUrl}?openBracket=%5B"]TEXT[/url]`} | ${`Escape Open-Bracket [`} - ${`TEXT`} | ${`[url="${someUrl}?closeBracket=%5D"]TEXT[/url]`} | ${`Escape Close-Bracket [`} - ${`TEXT`} | ${`[url="${someUrl}?quote=%22"]TEXT[/url]`} | ${`Escape Double Quote in Attribute`} - ${`${someUrl}?quote="`} | ${`[url]${someUrl}?quote="[/url]`} | ${`Don't escape double quote when rendered as content`} - ${`${someUrl}?brackets=\\]\\[`} | ${`[url="${someUrl}?brackets=%5D%5B"]${someUrl}?brackets=\\]\\[[/url]`} | ${`Escaping of text-content done by previous (outside) processing`} - ${`TEXT`} | ${undefined} | ${`As there is no representation for "empty URLs" in BBCode, not mapping to [url]`} - ${`TEXT`} | ${`[url="/relative"]TEXT[/url]`} | ${`Keep relative URLs (1)`} - ${`TEXT`} | ${`[url="?search=param"]TEXT[/url]`} | ${`Keep relative URLs, search-param only (2)`} - ${`TEXT`} | ${`[url="#hash"]TEXT[/url]`} | ${`Keep relative URLs, hash-param only (3)`} - `( - "[$#] Should process '$dataView' to '$expected' ($comment)", - ({ dataView, expected }: { dataView: string; expected: string | undefined }) => { - const element = requireHTMLElement(dataView); - const bbCode = rule.toData(element, element.textContent ?? ""); - expect(bbCode).toEqual(expected); - }, - ); - }); -}); diff --git a/packages/ckeditor5-bbcode/__tests__/BBobIntegration.test.ts b/packages/ckeditor5-bbcode/__tests__/BBobIntegration.test.ts deleted file mode 100644 index 4207f95705..0000000000 --- a/packages/ckeditor5-bbcode/__tests__/BBobIntegration.test.ts +++ /dev/null @@ -1,226 +0,0 @@ -/// - -import { bbCodeDefaultRules } from "../src"; -import { html2bbcode } from "../src/html2bbcode"; -import { bbcode2html } from "../src/bbcode2html"; -import { parseAsFragment } from "./DOMUtils"; - -const rules = bbCodeDefaultRules; -const supportedTags = rules.flatMap((r) => r.tags ?? ([] as string[])); - -const aut = { - /** - * Artificial test for consuming result of BBCode to HTML processing from - * the third-party library directly by our proprietary HTML to BBCode - * processing. - * - * This case will not happen in production, where the CKEditor layers - * serve as _mediators_ between these two. Nevertheless, testing that - * they _understand_ each other provides some confidence that nothing - * broke. - * - * @param input - BBCode input (from Data) - */ - bbcode2html2bbcode: ( - input: string, - ): { - input: string; - fromBBCode2Html: string; - fromHtml2BBCode: string; - } => { - const fromBBCode2Html = bbcode2html(input, supportedTags); - const fromHtml2BBCode = html2bbcode(parseAsFragment(fromBBCode2Html), rules); - return { - input, - fromBBCode2Html, - fromHtml2BBCode, - }; - }, - /** - * Transformation pipeline to validate, that the BBCode2HTML processing - * understands the BBCode produced by our proprietary HTML to BBCode - * processing. - * - * @param input - HTML input (from Data View) - */ - html2bbcode2html: ( - input: string, - ): { - input: string; - fromHtml2BBCode: string; - fromBBCode2Html: string; - } => { - const fromHtml2BBCode = html2bbcode(parseAsFragment(input), rules); - const fromBBCode2Html = bbcode2html(fromHtml2BBCode, supportedTags); - return { - input, - fromBBCode2Html, - fromHtml2BBCode, - }; - }, -}; - -const link = "https://example.org/"; - -/** - * We have a some slightly slanted state regarding transformation - * BBCode ↔ HTML: While for BBCode to HTML we use a third-party library, - * we have our own proprietary processing for HTML to BBCode. At least for - * the processing BBCode to HTML, we should ensure that the HTML to BBCode - * processing produces results that can be successfully parsed by the - * third-party library. The other way round is nice to have, as the - * HTML to BBCode proprietary code will never directly receive data from - * BBCode to HTML processing: It will always go through the CKEditor layers, - * too, and CKEditor is known to apply its own normalization, such as - * transforming `font-weight:bold` style to `` in the view layers. - */ -describe("BBob Integration", () => { - /** - * These use-cases are important, as they validate, that our proprietary - * HTML to BBCode mapping is understood by the third-party library when - * processing the data back to HTML. - */ - describe("Important: HTML →[toData]→ BBCode →[toView]→ HTML", () => { - it.each` - dataViewInput | expectedStoredData | expectedRestoredDataView | comment - ${""} | ${""} | ${""} | ${"empty data"} - ${`TEXT`} | ${`[b]TEXT[/b]`} | ${`TEXT`} | ${"font-weight well understood by CKEditor"} - ${`TEXT`} | ${`[b]TEXT[/b]`} | ${`TEXT`} | ${"font-weight well understood by CKEditor"} - ${`TEXT`} | ${`[i]TEXT[/i]`} | ${`TEXT`} | ${"font-style well understood by CKEditor"} - ${`TEXT`} | ${`[i]TEXT[/i]`} | ${`TEXT`} | ${"font-style well understood by CKEditor"} - ${`TEXT`} | ${`[u]TEXT[/u]`} | ${`TEXT`} | ${"text-decoration well understood by CKEditor"} - ${`TEXT`} | ${`[u]TEXT[/u]`} | ${`TEXT`} | ${"text-decoration well understood by CKEditor"} - ${`TEXT`} | ${`[s]TEXT[/s]`} | ${`TEXT`} | ${"text-decoration well understood by CKEditor"} - ${`TEXT`} | ${`[s]TEXT[/s]`} | ${`TEXT`} | ${"text-decoration well understood by CKEditor"} - ${`TEXT`} | ${`[size=70]TEXT[/size]`} | ${`TEXT`} | ${"none"} - ${`TEXT`} | ${`[size=85]TEXT[/size]`} | ${`TEXT`} | ${"none"} - ${`TEXT`} | ${`[size=140]TEXT[/size]`} | ${`TEXT`} | ${"none"} - ${`TEXT`} | ${`[size=180]TEXT[/size]`} | ${`TEXT`} | ${"none"} - ${`TEXT`} | ${`[url="${link}"]TEXT[/url]`} | ${`TEXT`} | ${"normal link"} - ${`${link}`} | ${`[url]${link}[/url]`} | ${`${link}`} | ${"pretty-print: shorten, if possible, in BBCode"} - ${`TEXT`} | ${`TEXT`} | ${`TEXT`} | ${"there is no representation in BBCode for an anchor without href attribute"} - ${`

TEXT

`} | ${`[quote]\nTEXT\n[/quote]`} | ${`

\nTEXT

`} | ${"newlines part of minimal pretty-print behavior"} - ${`
TEXT
`} | ${`[code]\nTEXT\n[/code]`} | ${`
TEXT
`} | ${"adapted to nested pre, code to work in CKEditor 5"} - ${`
TEXT
`} | ${`[code=css]\nTEXT\n[/code]`} | ${`
TEXT
`} | ${"adapted to nested pre, code to work in CKEditor 5"} - ${`
  • TEXT
`} | ${`[list]\n[*] TEXT\n[/list]`} | ${`
    \n
  • TEXT\n
`} | ${"newlines part of minimal pretty-print behavior"} - ${`
  1. TEXT
`} | ${`[list=1]\n[*] TEXT\n[/list]`} | ${`
    \n
  1. TEXT\n
`} | ${"CKEditor defaults to _no-type_, but BBob defaults to add it"} - ${`
  1. TEXT
`} | ${`[list=a]\n[*] TEXT\n[/list]`} | ${`
    \n
  1. TEXT\n
`} | ${"CKEditor may ignore type, if not configured to support this"} - ${`

TEXT

`} | ${`[h1]TEXT[/h1]`} | ${`

TEXT

`} | ${"none"} - ${`

TEXT

`} | ${`[h2]TEXT[/h2]`} | ${`

TEXT

`} | ${"none"} - ${`

TEXT

`} | ${`[h3]TEXT[/h3]`} | ${`

TEXT

`} | ${"none"} - ${`

TEXT

`} | ${`[h4]TEXT[/h4]`} | ${`

TEXT

`} | ${"none"} - ${`
TEXT
`} | ${`[h5]TEXT[/h5]`} | ${`
TEXT
`} | ${"none"} - ${`
TEXT
`} | ${`[h6]TEXT[/h6]`} | ${`
TEXT
`} | ${"none"} - ${`

TEXT1

TEXT2

`} | ${`TEXT1\n\nTEXT2`} | ${`

TEXT1

TEXT2

`} | ${"none"} - `( - "[$#] Should transform data view to data, that are well understood by subsequent `toView` mapping for: `$dataViewInput` ($comment)", - ({ - dataViewInput, - expectedStoredData, - expectedRestoredDataView, - }: { - dataViewInput: string; - expectedStoredData: string; - expectedRestoredDataView: string; - }) => { - const result = aut.html2bbcode2html(dataViewInput); - try { - // Precondition check: This is not THAT relevant, as the primary - // requirement is expressed in subsequent expectation: The stored data - // should be well understood when transforming them back to data view. - // In other words: If this fails, it may be ok, just to adjust the - // expectation. - expect(result).toHaveProperty("fromHtml2BBCode", expectedStoredData); - // When first written back in 2023, we used the default preset for - // HTML 5 for BBob library. This, for example, preferred style - // attributes for bold over corresponding tags. This is ok, as - // CKEditor's parsing from data view to model also accepts this - // to denote a bold style. It may be ok to adapt this expectation - // if the resulting HTML again is proven to be well-understood by - // CKEditor's processing. - expect(result).toHaveProperty("fromBBCode2Html", expectedRestoredDataView); - } catch (e) { - if (e instanceof Error) { - e.message = `${e.message}\n\nDebugging details:\n${JSON.stringify(result, undefined, 2)}`; - } - throw e; - } - }, - ); - }); - - /** - * These use-cases are less important, as they (only) validate, that our - * proprietary HTML to BBCode mapping produces the same result as the BBCode - * we originally parsed. This, though, may be subject to change, like, for - * example, if we decide to introduce "more pretty printing" to the resulting - * HTML to BBCode processing (like indents for lists). In these cases, it is - * expected, that we have to adapt our data. - * - * The focus of this test is more to check, that our created data are - * invariant: If we had newlines, if we had indents before, no additional - * newlines or blanks should be added on repeated iterations. Example: A - * naive mapping of `[code]` → `
` `[code]` might just always add newlines
-   * to the inner block, resulting in newlines piling up on each iteration,
-   * with results such as: `[code]\n\n\n\nlorem\n\n\n\n[/code]` eventually.
-   */
-  describe("Less important: BBCode →[toView]→ HTML →[toData]→ BBCode", () => {
-    it.each`
-      bbCode
-      ${``}
-      ${`[b]lorem[/b]`}
-      ${`[i]lorem[/i]`}
-      ${`[u]lorem[/u]`}
-      ${`[s]lorem[/s]`}
-      ${`[url="https://example.org/"]lorem[/url]`}
-      ${`[quote]\nlorem\n[/quote]`}
-      ${`[code]\nlorem\n[/code]`}
-      ${`[list]\n[*] lorem\n[/list]`}
-      ${`[list=1]\n[*] lorem\n[/list]`}
-      ${`[list=a]\n[*] lorem\n[/list]`}
-      ${`[list=I]\n[*] lorem\n[/list]`}
-      ${`[h1]lorem[/h1]`}
-      ${`[h2]lorem[/h2]`}
-      ${`[h3]lorem[/h3]`}
-      ${`[h4]lorem[/h4]`}
-      ${`[h5]lorem[/h5]`}
-      ${`[h6]lorem[/h6]`}
-      ${`[size=70]lorem[/size]`}
-      ${`[size=85]lorem[/size]`}
-      ${`[size=140]lorem[/size]`}
-      ${`[size=180]lorem[/size]`}
-    `("[$#] should process back and forth without change: $bbCode", ({ bbCode }: { bbCode: string }) => {
-      const result = aut.bbcode2html2bbcode(bbCode);
-      try {
-        expect(result).toHaveProperty("fromHtml2BBCode", bbCode);
-      } catch (e) {
-        if (e instanceof Error) {
-          e.message = `${e.message}\n\nDebugging details:\n${JSON.stringify(result, undefined, 2)}`;
-        }
-        throw e;
-      }
-    });
-
-    /**
-     * We rate these deviations "acceptable" for now. To change, we may need,
-     * for example, to provide a custom preset for bbcode2html.
-     */
-    it.each`
-      bbCode                           | expected                      | comment
-      ${"[quote=author]lorem[/quote]"} | ${"[quote]\nlorem\n[/quote]"} | ${"We have no mapping for author to HTML."}
-    `(
-      "[$#] should process back and forth with only minor change: $bbCode → $expected ($comment)",
-      ({ bbCode, expected }: { bbCode: string; expected: string }) => {
-        const result = aut.bbcode2html2bbcode(bbCode);
-        try {
-          expect(result).toHaveProperty("fromHtml2BBCode", expected);
-        } catch (e) {
-          if (e instanceof Error) {
-            e.message = `${e.message}\n\nDebugging details:\n${JSON.stringify(result, undefined, 2)}`;
-          }
-          throw e;
-        }
-      },
-    );
-  });
-});
diff --git a/packages/ckeditor5-bbcode/__tests__/bbcode2html.test.ts b/packages/ckeditor5-bbcode/__tests__/bbcode2html.test.ts
deleted file mode 100644
index 5e732ed612..0000000000
--- a/packages/ckeditor5-bbcode/__tests__/bbcode2html.test.ts
+++ /dev/null
@@ -1,402 +0,0 @@
-// noinspection HtmlUnknownTarget,SpellCheckingInspection,HtmlRequiredAltAttribute
-
-import { CoreTree } from "@bbob/core/es";
-import { bbCodeDefaultRules } from "../src";
-import { bbcode2html, processBBCode } from "../src/bbcode2html";
-
-const supportedTags = bbCodeDefaultRules.flatMap((r) => r.tags ?? ([] as string[]));
-
-const aut = {
-  expectTransformation: (
-    { data, expectedDataView }: { data: string; expectedDataView: string },
-    expectedErrors = 0,
-  ): void => {
-    const originalConsoleError = console.error;
-    const errorCache: unknown[][] = [];
-    const silencedHandler: typeof console.error = (...data: unknown[]): void => {
-      errorCache.push(data);
-    };
-
-    let actual: string;
-
-    try {
-      console.error = silencedHandler;
-      actual = bbcode2html(data, supportedTags);
-    } finally {
-      console.error = originalConsoleError;
-    }
-
-    expect(errorCache).toHaveLength(expectedErrors);
-
-    try {
-      expect(actual).toBe(expectedDataView);
-    } catch (e) {
-      console.debug("Failed expectations.", {
-        data,
-        actual,
-        expectedDataView,
-      });
-      throw e;
-    }
-  },
-  process: (data: string): ReturnType => processBBCode(data, supportedTags),
-};
-
-/**
- * These are high-level integration tests based on core processing by BBob.
- * It uses all defaults typically applied for BBCode to HTML mapping when
- * using the CKEditor 5 BBCode plugin.
- *
- * Note that all of these tests are also an implicit test of the BBob library,
- * so that changed behaviors may also be triggered by BBob update. On failed
- * tests a possible option is just to adjust the expectations.
- */
-describe("bbcode2html", () => {
-  describe("Standard Tag Processing", () => {
-    describe("Supported Inline Tags", () => {
-      it.each`
-        data                                                              | expectedDataView
-        ${`[b]T[/b]`}                                                     | ${`T`}
-        ${`[color=red]T[/color]`}                                         | ${`T`}
-        ${`[size=85]T[/size]`}                                            | ${`T`}
-        ${`[i]T[/i]`}                                                     | ${'T'}
-        ${`[s]T[/s]`}                                                     | ${`T`}
-        ${`[u]T[/u]`}                                                     | ${`T`}
-        ${`[url=https://example.org/]T[/url]`}                            | ${`T`}
-        ${`[url]https://example.org/[/url]`}                              | ${`https://example.org/`}
-        ${`[url=/relative]T[/url]`}                                       | ${`T`}
-        ${`[url]/relative[/url]`}                                         | ${`/relative`}
-        ${`[url=https://example.org/?one=1&two=2]T[/url]`}                | ${`T`}
-        ${`[url]https://example.org/?one=1&two=2[/url]`}                  | ${`https://example.org/?one=1&two=2`}
-        ${`[url]https://example.org/?[b]predicate=x%3D42[/b]#_top[/url]`} | ${`https://example.org/?predicate=x%3D42#_top`}
-        ${`[img]https://example.org/1.png[/img]`}                         | ${``}
-      `(
-        "[$#] Should process data '$data' to: $expectedDataView",
-        ({ data, expectedDataView }: { data: string; expectedDataView: string }) => {
-          aut.expectTransformation({ data, expectedDataView });
-        },
-      );
-    });
-
-    describe("Supported Block Tags", () => {
-      it.each`
-        data                    | expectedDataView
-        ${`[code]T[/code]`}     | ${`
T
`} - ${`[h1]T[/h1]`} | ${`

T

`} - ${`[h2]T[/h2]`} | ${`

T

`} - ${`[h3]T[/h3]`} | ${`

T

`} - ${`[h4]T[/h4]`} | ${`

T

`} - ${`[h5]T[/h5]`} | ${`
T
`} - ${`[h6]T[/h6]`} | ${`
T
`} - ${`[list][*] T[/list]`} | ${`
  • T
`} - ${`[quote]T[/quote]`} | ${`

T

`} - `( - "[$#] Should process data '$data' to: $expectedDataView", - ({ data, expectedDataView }: { data: string; expectedDataView: string }) => { - aut.expectTransformation({ data, expectedDataView }); - }, - ); - }); - - describe("Escaping", () => { - it.each` - data | expectedDataView | comment - ${`\\[b\\]not bold\\[/b\\]`} | ${`[b]not bold[/b]`} | ${`"normal" escape, but design-scope (as it is configurable for BBob)`} - ${`\\[i\\]not italic\\[/i\\]`} | ${`[i]not italic[/i]`} | ${`"normal" escape, but design-scope (as it is configurable for BBob)`} - ${`keep\\irrelevant\\escape`} | ${`keep\\irrelevant\\escape`} | ${`accepting BBob behavior: If irrelevant, BBob ignore an escape character.`} - `( - "[$#] Should process data '$data' to: $expectedDataView ($comment)", - ({ data, expectedDataView }: { data: string; expectedDataView: string }) => { - aut.expectTransformation({ data, expectedDataView }); - }, - ); - }); - }); - - describe("By Tag", () => { - // Some standard behaviors bundled. - describe.each` - tag | openTag | closeTag | openElement | closeElement - ${`[b]`} | ${`[b]`} | ${`[/b]`} | ${``} | ${``} - ${`[color]`} | ${`[color=red]`} | ${`[/color]`} | ${``} | ${``} - ${`[size]`} | ${`[size=85]`} | ${`[/size]`} | ${``} | ${``} - ${`[h1]`} | ${`[h1]`} | ${`[/h1]`} | ${`

`} | ${`

`} - ${`[h2]`} | ${`[h2]`} | ${`[/h2]`} | ${`

`} | ${`

`} - ${`[h3]`} | ${`[h3]`} | ${`[/h3]`} | ${`

`} | ${`

`} - ${`[h4]`} | ${`[h4]`} | ${`[/h4]`} | ${`

`} | ${`

`} - ${`[h5]`} | ${`[h5]`} | ${`[/h5]`} | ${`
`} | ${`
`} - ${`[h6]`} | ${`[h6]`} | ${`[/h6]`} | ${`
`} | ${`
`} - ${`[i]`} | ${`[i]`} | ${`[/i]`} | ${``} | ${``} - ${`[s]`} | ${`[s]`} | ${`[/s]`} | ${``} | ${``} - ${`[u]`} | ${`[u]`} | ${`[/u]`} | ${``} | ${``} - ${`[url]`} | ${`[url=https://e.org/]`} | ${`[/url]`} | ${``} | ${``} - `( - "$tag (Standard Behaviors)", - ({ - openTag, - closeTag, - openElement, - closeElement, - }: { - openTag: string; - closeTag: string; - openElement: string; - closeElement: string; - }) => { - it.each` - data | expectedDataView | comment - ${`${openTag}T${closeTag}`} | ${`${openElement}T${closeElement}`} | ${`default`} - ${`${openTag} T${closeTag}`} | ${`${openElement} T${closeElement}`} | ${`keep leading blanks`} - ${`${openTag}T ${closeTag}`} | ${`${openElement}T ${closeElement}`} | ${`keep trailing blanks`} - ${`${openTag}\nT${closeTag}`} | ${`${openElement}\nT${closeElement}`} | ${`keep leading single newlines`} - ${`${openTag}T\n${closeTag}`} | ${`${openElement}T\n${closeElement}`} | ${`keep trailing single newlines`} - ${`${openTag}T1\n\nT2${closeTag}`} | ${`${openElement}T1\n\nT2${closeElement}`} | ${`do not introduce a paragraph within element`} - `( - "[$#] Should process data '$data' to: $expectedDataView ($comment)", - ({ data, expectedDataView }: { data: string; expectedDataView: string }) => { - aut.expectTransformation({ data, expectedDataView }); - }, - ); - }, - ); - - describe("[code]", () => { - it.each` - data | expectedDataView | comment - ${`[code]T[/code]`} | ${`
T
`} | ${`Default to "plaintext" language`} - ${`[code=css]T[/code]`} | ${`
T
`} | ${`Accept language attribute`} - ${`[code=html]T[/code]`} | ${`
<i>T</i>
`} | ${`Properly encode nested HTML`} - ${`[code=bbcode]\\[i\\]T\\[/i\\][/code]`} | ${`
[i]T[/i]
`} | ${`Strip escapes`} - ${`[code=bbcode]\\[code=text\\]T\\[/code\\][/code]`} | ${`
[code=text]T[/code]
`} | ${`Strip escapes`} - ${`[code=bbcode]\\[code=bbcode\\]T\\[/code\\][/code]`} | ${`
[code=bbcode]T[/code]
`} | ${`Strip escapes`} - ${`[code=bbcode]\n\\[code=bbcode\\]\nT\n\\[/code\\]\n[/code]`} | ${`
[code=bbcode]\nT\n[/code]
`} | ${`Strip escapes and keep newlines`} - ${`[code][i]T[/i][/code]`} | ${`
T
`} | ${`Accept and parse BBCode within "code" tag`} - ${`[code][script]javascript:alert("X")[/script][/code]`} | ${`
[script]javascript:alert("X")[/script]
`} | ${`Do not transform, e.g., script-tag.`} - ${`[code]T1\n\nT2[/code]`} | ${`
T1\n\nT2
`} | ${`Don't handle duplicate newlines as paragraphs.`} - ${`[code] T1\n T2[/code]`} | ${`
  T1\n  T2
`} | ${`Keep space indents`} - ${`[code]\tT1\n\tT2[/code]`} | ${`
\tT1\n\tT2
`} | ${`Keep tab indents`} - `( - "[$#] Should process data '$data' to: $expectedDataView ($comment)", - ({ data, expectedDataView }: { data: string; expectedDataView: string }) => { - aut.expectTransformation({ data, expectedDataView }); - }, - ); - }); - - describe("[quote]", () => { - it.each` - data | expectedDataView | comment - ${`[quote]T[/quote]`} | ${`

T

`} | ${`minimal scenario`} - ${`[quote=AUTHOR]T[/quote]`} | ${`

T

`} | ${`author information not supported`} - ${`[quote]\nT\n[/quote]`} | ${`

\nT

`} | ${`strip obsolete trailing newlines`} - ${`[quote]\nP1\n\nP2\n[/quote]`} | ${`

\nP1

P2

`} | ${`paragraphs support`} - ${`[quote]\nP1\n[quote]\nP2\n[/quote]\nP3\n[/quote]`} | ${`

\nP1

\nP2

\nP3

`} | ${`nested quotes support`} - ${`[quote]\n[quote]\nT\n[/quote]\n[/quote]`} | ${`

\nT

`} | ${`directly nested quotes support`} - `( - "[$#] Should process data '$data' to: $expectedDataView ($comment)", - ({ data, expectedDataView }: { data: string; expectedDataView: string }) => { - aut.expectTransformation({ data, expectedDataView }); - }, - ); - }); - - describe("[img]", () => { - it.each` - data | expectedDataView | comment - ${`[img]https://example.org/1.png[/img]`} | ${``} | ${`minimal scenario`} - ${`[img alt="ALT"]https://example.org/1.png[/img]`} | ${`ALT`} | ${`alt text support; order of attributes irrelevant, but set expected as it is now`} - ${`[img alt=""]https://example.org/1.png[/img]`} | ${``} | ${`design-scope: may as well strip empty alt; kept for simplicity`} - ${`[img alt=1-PNG]https://example.org/1.png[/img]`} | ${`1-PNG`} | ${`alt without quotes (must not use spaces)`} - ${`A[img][/img]B`} | ${`AB`} | ${`design-scope: May as well strip irrelevant img tag; kept for simplicity`} - ${`A [img]https://example.org/1.png[/img] B`} | ${`A B`} | ${`images are meant to be inline (all known BBCode interpreters seem to expect that)`} - `( - "[$#] Should process data '$data' to: $expectedDataView ($comment)", - ({ data, expectedDataView }: { data: string; expectedDataView: string }) => { - aut.expectTransformation({ data, expectedDataView }); - }, - ); - }); - }); - - describe("Paragraphs", () => { - it.each` - data | expectedDataView | comment - ${`P1\n\nP2`} | ${`

P1

P2

`} | ${`Standard Paragraph Behavior (Only Text; LF)`} - ${`P1\r\n\r\nP2`} | ${`

P1

P2

`} | ${`Standard Paragraph Behavior (Only Text; CRLF)`} - ${`P1\r\rP2`} | ${`

P1

P2

`} | ${`Standard Paragraph Behavior (Only Text; CR)`} - ${`[b]P1[/b]\n\n[i]P2[/i]`} | ${`

P1

P2

`} | ${`Standard Paragraph Behavior (Text with inline formatting)`} - ${``} | ${``} | ${`Do not create paragraphs on empty input`} - ${`\n`} | ${`\n`} | ${`Do not create paragraphs on only single newline`} - ${`\n\n`} | ${`\n`} | ${`Trim irrelevant newlines`} - ${`\n\n\n`} | ${`\n`} | ${`Trim irrelevant newlines`} - ${`\nP1\n\nP2`} | ${`

\nP1

P2

`} | ${`Design Scope: Keep irrelevant leading newlines; simplifies processing`} - ${`P1\n\n\nP2`} | ${`

P1

P2

`} | ${`Trim obsolete newlines (trailing, 1)`} - ${`P1\n\nP2\n`} | ${`

P1

P2

`} | ${`Trim obsolete newlines (trailing, 2)`} - ${`[quote]P1\n\nP2[/quote]`} | ${`

P1

P2

`} | ${`Respect paragraphs in quote sections`} - ${`P1\n\n[quote]P2[/quote]\n\nP3`} | ${`

P1

P2

P3

`} | ${`Do not put blockquotes into paragraphs`} - ${`P1\n\n[code]P2[/code]\n\nP3`} | ${`

P1

P2

P3

`} | ${`Do not put code blocks into paragraphs`} - ${`P1\n\n[h1]P2[/h1]\n\nP3`} | ${`

P1

P2

P3

`} | ${`Do not put headings into paragraphs`} - ${`P1\n\n[list][*]P2[/list]\n\nP3`} | ${`

P1

  • P2

P3

`} | ${`Do not put lists into paragraphs`} - `( - "[$#] Should process data '$data' to: $expectedDataView ($comment)", - ({ data, expectedDataView }: { data: string; expectedDataView: string }) => { - aut.expectTransformation({ data, expectedDataView }); - }, - ); - }); - - describe("Tag Processing Challenges", () => { - /** - * We may meet unexpected formatting. This is handled (and tested) by - * BBob. These tests are mainly meant as proof-of-concept to understand - * the behavior of the BBob library. - * - * The count of expected errors is more an integration test towards BBob. - * It is perfectly fine, if the expected error count needs to be adjusted - * after BBob upgrade. Thus, the current number of expected errors only - * represents the current behavior of BBob. - */ - describe("Formatting Errors", () => { - it.each` - data | expectedDataView | expectedErrors | comment - ${`[b]T`} | ${`T`} | ${0} | ${`surprise: creates an empty element. CKEditor 5 would just remove it.`} - ${`T[/b]`} | ${`T`} | ${1} | ${`perfectly fine: Just ignore orphaned close-tag`} - ${`[b]T[/i]`} | ${`T`} | ${1} | ${`surprise: creates an empty element. CKEditor 5 would just remove it.`} - ${`[b]A[i]B[/b]C[/i]`} | ${`ABC`} | ${0} | ${`surprise: only opening tag seems to control the hierarchy`} - `( - "[$#] Should process data '$data' to: $expectedDataView ($comment)", - ({ - data, - expectedDataView, - expectedErrors, - }: { - data: string; - expectedDataView: string; - expectedErrors: number; - }) => { - aut.expectTransformation({ data, expectedDataView }, expectedErrors); - }, - ); - }); - - describe("Attribute Challenges", () => { - it.each` - data | expectedDataView | comment - ${`[url=]T[/url]`} | ${`T`} | ${`BBob: empty unique attribute handled as _not existing_`} - ${`[url=\nhttps://example.org/]T[/url]`} | ${`T`} | ${`BBob: ignores newlines within (unique) attributes`} - `( - "[$#] Should process data '$data' to: $expectedDataView ($comment)", - ({ data, expectedDataView }: { data: string; expectedDataView: string }) => { - aut.expectTransformation({ data, expectedDataView }); - }, - ); - }); - }); - - describe("Security", () => { - describe("Raw HTML Injections", () => { - it.each` - data | expectedDataView | comment - ${`A B`} | ${`A <b>B</b>`} | ${`Defaults to not supporting embedded HTML elements.`} - ${`A&B`} | ${`A&amp;B`} | ${`Defaults to not supporting entities, but to escape them.`} - `( - "[$#] Should process data '$data' to: $expectedDataView ($comment)", - ({ data, expectedDataView }: { data: string; expectedDataView: string }) => { - aut.expectTransformation({ data, expectedDataView }); - }, - ); - }); - - /** - * Testing with some XSS payloads. Note, that the BBCode plugin just - * ensures proper rendering of HTML, thus, prevents possible attack - * vectors trying to trick a simple HTML rendering based on _search & - * replace_ as it is sometimes used for BBCode to HTML processing. - * - * It is up to the editing layer, to deal with any possible other - * malicious attacks. Such as the CKEditor 5 link feature already does - * for possible malicious `data:text/html;` links. - * - * @see https://github.com/JiLiZART/BBob/issues/201 - * @see https://swarm.ptsecurity.com/fuzzing-for-xss-via-nested-parsers-condition/ - */ - describe("XSS attacks", () => { - // noinspection CssInvalidPropertyValue - it.each` - tainted | expected | comment - ${`[url=data:text/html;base64,PHNjcmlwdD5hbGVydCgiMSIpOzwvc2NyaXB0Pg==]sdfsdf[/url]`} | ${`sdfsdf`} | ${`source: https://security.snyk.io/vuln/SNYK-PYTHON-BBCODE-40502; CKEditor 5 will prohibit clicking on these.`} - ${`[url]javascript:alert('XSS');[/url]`} | ${`javascript:alert('XSS');`} | ${`source: https://github.com/dcwatson/bbcode/issues/4; Will be passed as is to CKEditor 5, which will take care not to make this clickable within the UI.`} - ${`[url=javascript:alert('XSS');]TEXT[/url]`} | ${`TEXT`} | ${`source: https://github.com/dcwatson/bbcode/issues/4; javascript:-link flavor but as attribute`} - ${`[url]javascript:alert("XSS");[/url]`} | ${`javascript:alert("XSS");`} | ${`source: https://github.com/dcwatson/bbcode/issues/4; javascript:-link flavor but with double-quotes`} - ${`[url]123" onmouseover="alert('Hacked');[/url]`} | ${`123" onmouseover="alert('Hacked');`} | ${`source: https://github.com/dcwatson/bbcode/issues/4`} - ${`[url]https://google.com?[url] onmousemove=javascript:alert(String.fromCharCode(88,83,83));//[/url][/url]`} | ${`https://google.com? onmousemove=javascript:alert(String.fromCharCode(88,83,83));//`} | ${`source: https://github.com/dcwatson/bbcode/issues/4; Slightly corrupted DOM, but attack did not pass through.`} - ${`[color="onmouseover=alert(0) style="]dare to move your mouse here[/color]`} | ${`dare to move your mouse here`} | ${`source: https://github.com/friendica/friendica/issues/9611; result of default HTML5 Preset - surprising "null" but no XSS issue: Fine!`} - `( - "[$#] Should prevent XSS-attack for: $tainted, expected: $expected ($comment)", - ({ tainted: data, expected: expectedDataView }: { tainted: string; expected: string }) => { - aut.expectTransformation({ data, expectedDataView }); - }, - ); - }); - }); - - /** - * We have to expect BBCode data, that cannot even be parsed. As such, an - * error handling should provide a predictable behavior. Nevertheless, each - * error handling is part of the design-scope: Do we accept some data-loss by - * stripping only problematic BBCode? Or do we present an empty text on any - * error in CKEditor 5 editing view (as we do for erred CoreMedia Rich Text)? - * - * Decision for now is to stick with BBob, which just strips broken BBCode - * and tries to render the rest at best effort. - */ - describe("Error Handling", () => { - it.each` - erred | expected | comment - ${`[/]`} | ${``} | ${`for "only invalid BBCode" provide empty text`} - ${`Before[/]After`} | ${`BeforeAfter`} | ${`should just ignore broken BBCode parts`} - ${`[c][/c][b]hello[/c][/b][b]`} | ${`[c]hello[b]`} | ${`example input from BBob tests`} - `( - "[$#] Should handle BBCode errors with care: $erred, expected: $expected ($comment)", - ({ erred: data, expected: expectedDataView }: { erred: string; expected: string }) => { - // We expect BBob to raise an error for all the above data. If this - // changes, feel free to adapt the number of expected errors. - aut.expectTransformation({ data, expectedDataView }, 1); - }, - ); - }); - - /** - * Tests dedicated to the BBob integration. Typically applied to some lower - * aspects to either ease debugging or to validate unchanged behavior after - * BBob upgrade. - */ - describe("BBob Integratino", () => { - /** - * Demonstrates (and validates) that the BBob parser is unaware of - * newline representations different to LF (Unix). As a result, we need - * to pre-process the incoming data to normalize newlines. - * - * If any of these tests fail, we may skip (or adapt) this extra processing. - * - * @see - */ - it.each` - newline | expectedTree | type - ${`\n`} | ${["\n"]} | ${`LF (Unix)`} - ${`\r\n`} | ${["\r", "\n"]} | ${`CRLF (Windows)`} - ${`\r`} | ${["\r"]} | ${`CR (classic MacOS)`} - ${`A\nB`} | ${["A", "\n", "B"]} | ${`LF (Unix)`} - ${`A\r\nB`} | ${["A\r", "\n", "B"]} | ${`CRLF (Windows)`} - ${`A\rB`} | ${["A\rB"]} | ${`CR (classic MacOS)`} - `( - "[$#] Should parse system dependent newline representation for $type as expected.", - ({ newline, expectedTree }: { newline: string; expectedTree: CoreTree }) => { - // deconstruct to remove extra "candy" like messages from the resulting - // array. - const tree = [...aut.process(newline).tree]; - expect(tree).toEqual(expectedTree); - }, - ); - }); -}); diff --git a/packages/ckeditor5-bbcode/__tests__/bbob/KnownIssues.test.ts b/packages/ckeditor5-bbcode/__tests__/bbob/KnownIssues.test.ts deleted file mode 100644 index 2d6a9f8703..0000000000 --- a/packages/ckeditor5-bbcode/__tests__/bbob/KnownIssues.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import bbob, { CoreRenderer } from "@bbob/core/es"; - -const render: CoreRenderer = (node) => JSON.stringify(node); - -const aut = { - /** - * Uses `JSON.stringify` as renderer and no plugins, thus, we process the - * raw tree to plain JSON. - */ - toJSONRaw: (input: string) => bbob().process(input, { render }).html, -}; - -/** - * These tests demonstrate known issues along with the BBob library. - * - * If they fail on upgrade, we may be lucky, that we may adjust our code - * accordingly. - * - * For now, they are especially meant to document known limitations we have - * rated as acceptable. - * - * The given probes may also be worth evaluating in context of alternative - * BBCode processors (like KefirBB, for example). - */ -describe("BBob Known Issues", () => { - describe("toJSONRaw", () => { - /* - * Active parameters: - * - * `bbCode`: Input data - * `expectedActual`: Flawed raw JSON data. - * - * Documentation only parameters: - * - * `expected`: What we would have expected instead. - * `issue`: If applicable, an issue reference. - * `comment`: Some comment - */ - it.each` - bbCode | expectedActual | expected | issue | comment - ${`[url=javascript:alert('XSS ME');]T[/url]`} | ${`[{"tag":"url","attrs":{"javascript:alert('XSS":"javascript:alert('XSS","ME');":"ME');"},"content":["T"]}]`} | ${`[{"tag": "url","attrs": {"javascript:alert('XSS ME');": "javascript:alert('XSS ME');"},"content": ["TEXT"]}]`} | ${`https://github.com/JiLiZART/BBob/issues/204`} | ${`Space Handling in Unique Attributes; causes "ME');" to be regarded as link`} - ${`[quote=J. D.]T[/quote]`} | ${`[{"tag":"quote","attrs":{"J.":"J.","D.":"D."},"content":["T"]}]`} | ${`[{"tag": "quote","attrs": {"J. D.": "J. D."},"content": ["T"]}]`} | ${`https://github.com/JiLiZART/BBob/issues/204`} | ${`Space Handling in Unique Attributes; simpler data`} - ${`[quote=J. "The T" D.]T[/quote]`} | ${`[{"tag":"quote","attrs":{"J.":"J.","The":"The","T":"T","D.":"D."},"content":["T"]}]`} | ${`[{"tag": "quote","attrs": {"J. \\"The T\\" D.": "J. \\"The T\\" D."},"content": ["T"]}]`} | ${`https://github.com/JiLiZART/BBob/issues/204`} | ${`Space And Quote Handling in Unique Attributes`} - `( - "[$#] $comment: $bbCode -> $expectedActual ($issue; expected: $expected)", - ({ bbCode, expectedActual }: { bbCode: string; expectedActual: string }) => { - expect(aut.toJSONRaw(bbCode)).toBe(expectedActual); - }, - ); - }); -}); diff --git a/packages/ckeditor5-bbcode/__tests__/bbob/Paragraphs.test.ts b/packages/ckeditor5-bbcode/__tests__/bbob/Paragraphs.test.ts deleted file mode 100644 index b03c90f3e8..0000000000 --- a/packages/ckeditor5-bbcode/__tests__/bbob/Paragraphs.test.ts +++ /dev/null @@ -1,245 +0,0 @@ -import { TagNode } from "@bbob/plugin-helper/es"; -import { paragraphAwareContent, ParagraphAwareContentOptions } from "../../src/bbob/Paragraphs"; - -const toNode = TagNode.create; - -type ContentFixture = NonNullable; - -const p = (content: ContentFixture): TagNode => toNode("p", {}, content); -/** - * Just any inline tag-node. - */ -const b = (content: ContentFixture): TagNode => toNode("b", {}, content); -/** - * Shortcut to add `quote` node. - */ -const q = (content: ContentFixture): TagNode => toNode("quote", {}, content); - -describe(`Paragraphs`, () => { - // ==========================================================================================[ paragraphAwareContent ] - describe(`paragraphAwareContent`, () => { - // ---------------------------------------------------------------------------------------------------[ content=[] ] - describe(`content=[]`, () => { - it(`should return "[]" with default options`, () => { - expect(paragraphAwareContent([])).toMatchObject([]); - }); - - it(`should return "[]" wrapped in paragraph with "requireParagraph=true"`, () => { - const expected = [toNode("p", {}, [])]; - expect(paragraphAwareContent([], { requireParagraph: true })).toMatchObject(expected); - }); - }); - - // ---------------------------------------------------------------------------------------------[ content=string[] ] - describe(`content=string[]: Content only containing strings without EOL characters`, () => { - it(`should skip extra paragraph (requireParagraphs=default false)`, () => { - const input = ["lorem", "ipsum"]; - expect(paragraphAwareContent(input)).toMatchObject(input); - }); - - it(`should wrap content into paragraph (requireParagraphs=true)`, () => { - const input = ["lorem", "ipsum"]; - const expected = [toNode("p", {}, input)]; - expect(paragraphAwareContent(input, { requireParagraph: true })).toMatchObject(expected); - }); - }); - - // -----------------------------------------------------------------------------------[ content=(string|TagNode)[] ] - describe(`content=(string|TagNode)[]: Content only containing strings and tag-nodes without EOL characters`, () => { - it.each` - input | comment - ${[b(["lorem"]), "ipsum", "dolor"]} | ${"tag-node at start"} - ${["lorem", b(["ipsum"]), "dolor"]} | ${"tag-node in the middle"} - ${["lorem", "ipsum", b(["dolor"])]} | ${"tag-node at the end"} - `( - `[$#] should skip extra paragraph (requireParagraphs=default false, $comment): $input`, - ({ input }: { input: ContentFixture }) => { - expect(paragraphAwareContent(input)).toMatchObject(input); - }, - ); - - it.each` - input | comment - ${[b(["lorem"]), "ipsum", "dolor"]} | ${"tag-node at start"} - ${["lorem", b(["ipsum"]), "dolor"]} | ${"tag-node in the middle"} - ${["lorem", "ipsum", b(["dolor"])]} | ${"tag-node at the end"} - `( - `[$#] should wrap content into paragraph (requireParagraphs=default true, $comment): $input`, - ({ input }: { input: ContentFixture }) => { - const expected = [toNode("p", {}, input)]; - const actual = paragraphAwareContent(input, { requireParagraph: true }); - expect(actual).toMatchObject(expected); - }, - ); - }); - - // ------------------------------------------------------------------------------------------------[ content=EOL[] ] - describe(`content=EOL[]: Content consisting of EOLs only`, () => { - it.each` - input | expected | comment - ${["\n"]} | ${["\n"]} | ${"keep single newline as is"} - ${["\n", "\n"]} | ${["\n"]} | ${"squash newlines, keep at least one"} - ${["\n", "\n", "\n"]} | ${["\n"]} | ${"squash newlines, keep at least one"} - `( - "[$#] should transform from $input to $expected (all defaults): $comment", - ({ input, expected }: { input: ContentFixture; expected: ContentFixture }) => { - const actual = paragraphAwareContent(input); - expect(actual).toMatchObject(expected); - }, - ); - - it.each` - input | expected | comment - ${["\n"]} | ${[p([])]} | ${"Design scope: Trim irrelevant newline"} - ${["\n", "\n"]} | ${[p([])]} | ${"Design scope: Trim irrelevant newline"} - ${["\n", "\n", "\n"]} | ${[p([])]} | ${"Design scope: Trim irrelevant newline"} - `( - "[$#] should transform from $input to $expected (requireParagraph=true): $comment", - ({ input, expected }: { input: ContentFixture; expected: ContentFixture }) => { - const options: ParagraphAwareContentOptions = { requireParagraph: true }; - const actual = paragraphAwareContent(input, options); - expect(actual).toMatchObject(expected); - }, - ); - }); - - // ---------------------------------------------------------------------------------------[ content=(string|EOL)[] ] - describe(`content=(string|EOL)[]: Content only containing strings (including EOL characters)`, () => { - describe("Default Options", () => { - it.each` - input | expected | comment - ${["\n", "ipsum", "dolor"]} | ${["\n", "ipsum", "dolor"]} | ${"keep single EOL at start"} - ${["lorem", "\n", "dolor"]} | ${["lorem", "\n", "dolor"]} | ${"keep single EOL in the middle"} - ${["lorem", "ipsum", "\n"]} | ${["lorem", "ipsum", "\n"]} | ${"keep single EOL at end"} - `( - `[$#] should keep single newline characters ($comment): $input`, - ({ input, expected }: { input: ContentFixture; expected: ContentFixture }) => { - const actual = paragraphAwareContent(input); - expect(actual).toMatchObject(expected); - }, - ); - - it.each` - input | expected | comment - ${["\n", "\n", "ipsum", "dolor"]} | ${["\n", "ipsum", "dolor"]} | ${"Design Scope: Squash newlines irrelevant to trigger as-paragraph-behavior."} - ${["lorem", "\n", "\n", "dolor"]} | ${[p(["lorem"]), p(["dolor"])]} | ${"respect EOL above threshold in the middle; ensure, that subsequent text-nodes must also be added to a paragraph"} - ${["lorem", "ipsum", "\n", "\n"]} | ${["lorem", "ipsum", "\n"]} | ${"Design Scope: Squash newlines at the end as irrelevant to trigger as-paragraph-behavior."} - `( - `[$#] should handle consecutive EOL at threshold ($comment): $input`, - ({ input, expected }: { input: ContentFixture; expected: ContentFixture }) => { - const actual = paragraphAwareContent(input); - expect(actual).toMatchObject(expected); - }, - ); - }); - - describe("requireParagraph=true", () => { - const options: ParagraphAwareContentOptions = { requireParagraph: true }; - - it.each` - input | expected | comment - ${["\n", "ipsum", "dolor"]} | ${[p(["\n", "ipsum", "dolor"])]} | ${"keep single EOL at start"} - ${["lorem", "\n", "dolor"]} | ${[p(["lorem", "\n", "dolor"])]} | ${"keep single EOL in the middle"} - ${["lorem", "ipsum", "\n"]} | ${[p(["lorem", "ipsum"])]} | ${"trim EOL at end"} - `( - `[$#] should keep single newline characters ($comment): $input`, - ({ input, expected }: { input: ContentFixture; expected: ContentFixture }) => { - const actual = paragraphAwareContent(input, options); - expect(actual).toMatchObject(expected); - }, - ); - - it.each` - input | expected | comment - ${["\n", "\n", "ipsum", "dolor"]} | ${[p(["\n", "ipsum", "dolor"])]} | ${"Design Scope: Squash newlines irrelevant to trigger as-paragraph-behavior."} - ${["lorem", "\n", "\n", "dolor"]} | ${[p(["lorem"]), p(["dolor"])]} | ${"respect EOL above threshold in the middle; ensure, that subsequent text-nodes must also be added to a paragraph"} - ${["lorem", "ipsum", "\n", "\n"]} | ${[p(["lorem", "ipsum"])]} | ${"trim EOL at end"} - `( - `[$#] should handle consecutive EOL at threshold ($comment): $input`, - ({ input, expected }: { input: ContentFixture; expected: ContentFixture }) => { - const actual = paragraphAwareContent(input, options); - expect(actual).toMatchObject(expected); - }, - ); - }); - }); - - // -------------------------------------------------------------------------------[ content=(string|TagNode|EOL)[] ] - describe(`content=(string|TagNode|EOL)[]: Content containing anything (including EOL characters)`, () => { - describe("Default Options", () => { - it.each` - input | expected | comment - ${["\n", b(["ipsum"]), "dolor"]} | ${["\n", b(["ipsum"]), "dolor"]} | ${"keep single EOL at start"} - ${[b(["lorem"]), "\n", "dolor"]} | ${[b(["lorem"]), "\n", "dolor"]} | ${"keep single EOL in the middle"} - ${["lorem", b(["ipsum"]), "\n"]} | ${["lorem", b(["ipsum"]), "\n"]} | ${"keep single EOL at end"} - `( - `[$#] should keep single newline characters ($comment): $input`, - ({ input, expected }: { input: ContentFixture; expected: ContentFixture }) => { - const actual = paragraphAwareContent(input); - expect(actual).toMatchObject(expected); - }, - ); - - it.each` - input | expected | comment - ${["\n", "\n", b(["ipsum"]), "dolor"]} | ${["\n", b(["ipsum"]), "dolor"]} | ${"Design Scope: Squash newlines irrelevant to trigger as-paragraph-behavior."} - ${[b(["lorem"]), "\n", "\n", "dolor"]} | ${[p([b(["lorem"])]), p(["dolor"])]} | ${"respect EOL above threshold in the middle; ensure, that subsequent text-nodes must also be added to a paragraph"} - ${["lorem", b(["ipsum"]), "\n", "\n"]} | ${["lorem", b(["ipsum"]), "\n"]} | ${"Design Scope: Squash newlines at the end as irrelevant to trigger as-paragraph-behavior."} - `( - `[$#] should handle consecutive EOL at threshold ($comment): $input`, - ({ input, expected }: { input: ContentFixture; expected: ContentFixture }) => { - const actual = paragraphAwareContent(input); - expect(actual).toMatchObject(expected); - }, - ); - }); - - describe("requireParagraph=true", () => { - const options: ParagraphAwareContentOptions = { requireParagraph: true }; - - it.each` - input | expected | comment - ${["\n", b(["ipsum"]), "dolor"]} | ${[p(["\n", b(["ipsum"]), "dolor"])]} | ${"keep single EOL at start"} - ${[b(["lorem"]), "\n", "dolor"]} | ${[p([b(["lorem"]), "\n", "dolor"])]} | ${"keep single EOL in the middle"} - ${["lorem", b(["ipsum"]), "\n"]} | ${[p(["lorem", b(["ipsum"])])]} | ${"trim EOL at end"} - `( - `[$#] should keep single newline characters ($comment): $input`, - ({ input, expected }: { input: ContentFixture; expected: ContentFixture }) => { - const actual = paragraphAwareContent(input, options); - expect(actual).toMatchObject(expected); - }, - ); - - it.each` - input | expected | comment - ${["\n", "\n", b(["ipsum"]), "dolor"]} | ${[p(["\n", b(["ipsum"]), "dolor"])]} | ${"Design Scope: Squash newlines irrelevant to trigger as-paragraph-behavior."} - ${[b(["lorem"]), "\n", "\n", "dolor"]} | ${[p([b(["lorem"])]), p(["dolor"])]} | ${"respect EOL above threshold in the middle; ensure, that subsequent text-nodes must also be added to a paragraph"} - ${["lorem", b(["ipsum"]), "\n", "\n"]} | ${[p(["lorem", b(["ipsum"])])]} | ${"trim EOL at end"} - `( - `[$#] should handle consecutive EOL at threshold ($comment): $input`, - ({ input, expected }: { input: ContentFixture; expected: ContentFixture }) => { - const actual = paragraphAwareContent(input, options); - expect(actual).toMatchObject(expected); - }, - ); - }); - }); - - // -------------------------------------------------------------------------------------------[ Block Tag Handling ] - describe("Block Tag Handling", () => { - it.each` - input | expected | comment - ${[q(["lorem"])]} | ${[q(["lorem"])]} | ${"single quote block only"} - ${["lorem", q(["ipsum"]), "dolor"]} | ${[p(["lorem"]), q(["ipsum"]), p(["dolor"])]} | ${"add paragraphs only before and after"} - ${["lorem", "\n", "\n", "ipsum", "\n", "\n", q(["dolor"]), "sit", "\n", "\n", "amet"]} | ${[p(["lorem"]), p(["ipsum"]), q(["dolor"]), p(["sit"]), p(["amet"])]} | ${"quote embedded in paragraphs"} - `( - "[$#] should not wrap (default) block tags within paragraphs: $comment", - ({ input, expected }: { input: ContentFixture; expected: ContentFixture }) => { - const options: ParagraphAwareContentOptions = { requireParagraph: true }; - const actual = paragraphAwareContent(input, options); - expect(actual).toMatchObject(expected); - }, - ); - }); - }); -}); diff --git a/packages/ckeditor5-bbcode/__tests__/bbob/ckeditor5Preset.test.ts b/packages/ckeditor5-bbcode/__tests__/bbob/ckeditor5Preset.test.ts deleted file mode 100644 index 5d4a79e96f..0000000000 --- a/packages/ckeditor5-bbcode/__tests__/bbob/ckeditor5Preset.test.ts +++ /dev/null @@ -1,247 +0,0 @@ -// noinspection HtmlRequiredAltAttribute,HttpUrlsUsage - -import html from "@bbob/html/es"; -import { ckeditor5Preset as preset } from "../../src/bbob/ckeditor5Preset"; - -type HtmlInput = Parameters[0]; -type HtmlResult = ReturnType; - -const parse = (input: HtmlInput): HtmlResult => html(input, preset()); - -describe("ckeditor5Preset", () => { - describe("Original Tests from: @bbob/preset-html5", () => { - test("[b]bolded text[/b]", () => { - const input = "[b]bolded text[/b]"; - const result = 'bolded text'; - expect(parse(input)).toBe(result); - }); - - test("[i]italicized text[/i]", () => { - const input = "[i]italicized text[/i]"; - const result = 'italicized text'; - expect(parse(input)).toBe(result); - }); - - test("[u]underlined text[/u]", () => { - const input = "[u]underlined text[/u]"; - const result = 'underlined text'; - expect(parse(input)).toBe(result); - }); - - test("[s]strikethrough text[/s]", () => { - const input = "[s]strikethrough text[/s]"; - const result = 'strikethrough text'; - expect(parse(input)).toBe(result); - }); - - test("[url]https://en.wikipedia.org[/url]", () => { - const input = "[url]https://en.wikipedia.org[/url]"; - const result = 'https://en.wikipedia.org'; - - expect(parse(input)).toBe(result); - }); - - test("[url=http://step.pgc.edu/]ECAT[/url]", () => { - const input = "[url=http://step.pgc.edu/]ECAT[/url]"; - const result = 'ECAT'; - - expect(parse(input)).toBe(result); - }); - - test("[img]https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Go-home-2.svg/100px-Go-home-2.svg.png[/img]", () => { - const input = - "[img]https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Go-home-2.svg/100px-Go-home-2.svg.png[/img]"; - const result = - ''; - - expect(parse(input)).toBe(result); - }); - - test('[quote="author"]quoted text[/quote]', () => { - const input = '[quote="author"]quoted text[/quote]'; - const result = "

quoted text

"; - - expect(parse(input)).toBe(result); - }); - - // We have overridden the behavior to also include a nested `` - // element. - test.skip("[code]monospaced text[/code]", () => { - const input = "[code]monospaced text[/code]"; - const result = "
monospaced text
"; - - expect(parse(input)).toBe(result); - }); - - test('[style size="15px"]Large Text[/style]', () => { - const input = '[style size="15px"]Large Text[/style]'; - const result = 'Large Text'; - - expect(parse(input)).toBe(result); - }); - - test('[style color="red"]Red Text[/style]', () => { - const input = '[style color="red"]Red Text[/style]'; - const result = 'Red Text'; - - expect(parse(input)).toBe(result); - }); - - test('[color="red"]Red Text[/color]', () => { - const input = '[color="red"]Red Text[/color]'; - const result = 'Red Text'; - - expect(parse(input)).toBe(result); - }); - - test(`[list][*]Entry 1[/list]`, () => { - const input = `[list][*]Entry 1[*]Entry 2[/list]`; - const result = "
  • Entry 1
  • Entry 2
"; - - expect(parse(input)).toBe(result); - }); - - test(`[list]*Entry 1[/list]`, () => { - const input = `\ - [list] - *Entry 1 - *Entry 2 - [/list]`; - const result = `\ -
    -
  • Entry 1 -
  • Entry 2 -
`; - - // Patch: Leading Whitespace is trimmed meanwhile to get rid of extra paragraph. - // See `skipEmpty´ option. Thus, trimming expected result. - expect(parse(input)).toBe(result.trim()); - }); - - test("[list=1][/list]", () => { - const input = `[list=1][/list]`; - const result = `
    `; - - expect(parse(input)).toBe(result); - }); - - test("[list=A][/list]", () => { - const input = `[list=A][/list]`; - const result = `
      `; - - expect(parse(input)).toBe(result); - }); - - test(`[table][/table]`, () => { - const input = `[table][tr][td]table 1[/td][td]table 2[/td][/tr][tr][td]table 3[/td][td]table 4[/td][/tr][/table]`; - const result = `
      table 1table 2
      table 3table 4
      `; - - expect(parse(input)).toBe(result); - }); - }); - - /** - * These tests demonstrate flawed behaviors, thus, known bugs, we may ignore - * or have to deal with. If any of these tests fail, a corresponding issue - * raised at BBob may have been fixed meanwhile. - * - * If you happen to see a test failing, search for the referenced issue ID - * within our sources and see, if corresponding actions are required. - * - * Added tests in here should reference an issue and specify a type, thus, - * if we just accepted it as known issue, or if we had to apply a workaround. - */ - describe("BBob Flawed Behaviors", () => { - // noinspection HtmlUnknownTarget,HtmlUnknownAttribute,BadExpressionStatementJS - test.each` - bbcode | expected | issue | comment - ${`[url fakeUnique=fakeUnique]T[/url]`} | ${`T`} | ${`https://github.com/JiLiZART/BBob/issues/202`} | ${`getUniqAttr flaw. This test just demonstrates the symptom.`} - ${`[unknown=https://example.org/ fakeUnique=fakeUnique]T[/unknown]`} | ${`T`} | ${`https://github.com/JiLiZART/BBob/issues/202`} | ${`getUniqAttr flaw. This demonstrates a follow-up issue regarding the default HTML renderer (for BBob Plugin we use a custom renderer with slightly better behavior)`} - ${`[url=https://example.org/ fakeUnique=fakeUnique]T[/url]`} | ${'T'} | ${`https://github.com/JiLiZART/BBob/issues/202`} | ${`getUniqAttr flaw. Demonstrates accidental override.`} - ${`[url=https://example.org/ hidden]T[/url]`} | ${`T`} | ${`https://github.com/JiLiZART/BBob/issues/202`} | ${`getUniqAttr flaw. Demonstrates accidental override, but with more realistic use-case.`} - ${`[table=onclick][tr][td]T[/td][/tr][/table]`} | ${`
      T
      `} | ${`https://github.com/JiLiZART/BBob/issues/202`} | ${`getUniqAttr flaw. Only applicable, if mapping rules do not explicitly remove unhandled attributes.`} - ${`[table onclick=onclick][tr][td]T[/td][/tr][/table]`} | ${`
      T
      `} | ${`https://github.com/JiLiZART/BBob/issues/202`} | ${`getUniqAttr flaw. Only applicable, if mapping rules do not explicitly remove unhandled attributes.`} - `( - "[$#] Expected flawed behavior: '$bbcode' to '$expected' ($issue, $comment)", - ({ bbcode, expected }: { bbcode: string; expected: string }) => { - expect(parse(bbcode)).toBe(expected); - }, - ); - }); - - describe("CKEditor 5 Data View Specific Adaptations", () => { - // We have overridden the behavior to also include a nested `` - // element. - describe("[code]", () => { - test.each` - bbcode | expected | comment - ${"[code]text[/code]"} | ${`
      text
      `} | ${"CKEditor 5 Text Part Language uses 'plaintext' as the default."} - ${"[code=css]text[/code]"} | ${`
      text
      `} | ${"CKEditor 5 Text Part Language encodes chosen languages into 'language-*' class"} - ${`[code=hack"me]text[/code]`} | ${`
      text
      `} | ${"Prevent hacking attribute by encoding."} - `( - "[$#] Should transform $bbcode to: $expected ($comment)", - ({ bbcode, expected }: { bbcode: string; expected: string }) => { - expect(parse(bbcode)).toBe(expected); - }, - ); - }); - - describe("Paragraphs (denoted by double newline)", () => { - test.each` - bbcode | expected | comment - ${`Lorem\n\nIpsum`} | ${`

      Lorem

      Ipsum

      `} | ${`standard paragraph processing`} - ${`Lorem\nIpsum`} | ${`Lorem\nIpsum`} | ${`nothing to do for single newline; Design Scope: We may have added
      here.`} - ${`Lorem\n\n`} | ${`Lorem\n`} | ${`some trimming applied, but (just) newlines at the end never trigger paragraph processing`} - ${`\n\nLorem`} | ${`\nLorem`} | ${`newlines at the beginning do not trigger paragraph processing but get trimmed`} - ${`[quote]Lorem\n\nIpsum[/quote]`} | ${`

      Lorem

      Ipsum

      `} | ${`quote: add each paragraph separately`} - ${`[quote]Lorem[quote]Ipsum[/quote][/quote]`} | ${`

      Lorem

      Ipsum

      `} | ${`quote: handle nested blockquotes properly`} - ${`[quote]Lorem[list][*]Ipsum[/list][/quote]`} | ${`

      Lorem

      • Ipsum
      `} | ${`quote: handle nested block-level elements properly`} - ${`[list][*]Lorem\n\nIpsum\n[/list]`} | ${`
      • Lorem

        Ipsum

      `} | ${`list/li: add each paragraph separately`} - ${`Lorem\n\nipsum\n[quote]dolor[/quote]\nsit`} | ${`

      Lorem

      ipsum

      dolor

      \nsit

      `} | ${`Continue with paragraphs, once we added them on a given hierarchy level. Extra newline (sit) is within design scope.`} - ${`[b]Lorem\n\nIpsum[/b]`} | ${`Lorem\n\nIpsum`} | ${`no paragraphs within inline tags`} - `( - "[$#] Should transform $bbcode to: $expected ($comment)", - ({ bbcode, expected }: { bbcode: string; expected: string }) => { - expect(parse(bbcode)).toBe(expected); - }, - ); - }); - }); - - describe("Additional Tag Support", () => { - // [size] Was supported in CKEditor 4 BBCode Plugin. The number represented - // a percentage value. As CKEditor 5 does not support percentage values in - // Font Size Feature, some enum-like mapping to classes is applied. - describe("[size]", () => { - test.each` - bbcode | expected | comment - ${`[size]T[/size]`} | ${`T`} | ${"Corner Case: Ignore Invalid (because missing) size value"} - ${`[size=lorem]T[/size]`} | ${`T`} | ${"Corner Case: Ignore Invalid (because textual) size value"} - ${`[size=42px]T[/size]`} | ${`T`} | ${"Corner Case: Ignore Invalid (because with size unit) size value"} - ${`[size=${Number.MIN_SAFE_INTEGER}]T[/size]`} | ${`T`} | ${"Corner Case: Negative (minimal safe integer) maps to text-tiny"} - ${`[size=-1]T[/size]`} | ${`T`} | ${"Corner Case: Negative maps to text-tiny"} - ${`[size=+1]T[/size]`} | ${`T`} | ${"Corner Case: '+' prefix is ignored"} - ${`[size=0]T[/size]`} | ${`T`} | ${"Lower-Bound for text-tiny"} - ${`[size=70]T[/size]`} | ${`T`} | ${"Default for text-tiny"} - ${`[size=77]T[/size]`} | ${`T`} | ${"Upper-Bound for text-tiny"} - ${`[size=78]T[/size]`} | ${`T`} | ${"Lower-Bound for text-small"} - ${`[size=85]T[/size]`} | ${`T`} | ${"Default for text-small"} - ${`[size=92]T[/size]`} | ${`T`} | ${"Upper-Bound for text-small"} - ${`[size=93]T[/size]`} | ${`T`} | ${"Lower-Bound for normal text size (no class)"} - ${`[size=100]T[/size]`} | ${`T`} | ${"Default for normal text size (no class)"} - ${`[size=119]T[/size]`} | ${`T`} | ${"Upper-Bound for normal text size (no class)"} - ${`[size=120]T[/size]`} | ${`T`} | ${"Lower-Bound for text-big"} - ${`[size=140]T[/size]`} | ${`T`} | ${"Default for text-big"} - ${`[size=159]T[/size]`} | ${`T`} | ${"Upper-Bound for text-big"} - ${`[size=160]T[/size]`} | ${`T`} | ${"Lower-Bound for text-huge"} - ${`[size=180]T[/size]`} | ${`T`} | ${"Default for text-huge"} - ${`[size=${Number.MAX_SAFE_INTEGER}]T[/size]`} | ${`T`} | ${"Upper-Bound for text-huge"} - `( - "[$#] Should transform $bbcode to: $expected ($comment)", - ({ bbcode, expected }: { bbcode: string; expected: string }) => { - expect(parse(bbcode)).toBe(expected); - }, - ); - }); - }); -}); diff --git a/packages/ckeditor5-bbcode/__tests__/html2bbcode.test.ts b/packages/ckeditor5-bbcode/__tests__/html2bbcode.test.ts deleted file mode 100644 index 313934351c..0000000000 --- a/packages/ckeditor5-bbcode/__tests__/html2bbcode.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { bbCodeDefaultRules } from "../src"; -import { html2bbcode } from "../src/html2bbcode"; -import { parseAsFragment } from "./DOMUtils"; - -const rules = bbCodeDefaultRules; - -const aut = { - expectTransformation: ({ dataView, expectedData }: { dataView: string; expectedData: string }): void => { - const actualData = html2bbcode(parseAsFragment(dataView), rules); - try { - expect(actualData).toBe(expectedData); - } catch (e) { - console.debug("Failed expectations.", { - dataView, - actualData, - expectedData, - }); - throw e; - } - }, -}; - -const url = { - absolute: "https://example.org/", - relative: "/example", -}; - -describe("html2bbcode", () => { - // noinspection HtmlDeprecatedTag,XmlDeprecatedElement - describe.each` - tag | openElement | closeElement | openTag | closeTag - ${`[b] (style)`} | ${``} | ${``} | ${`[b]`} | ${`[/b]`} - ${`[b] (strong)`} | ${``} | ${``} | ${`[b]`} | ${`[/b]`} - ${`[b] (b)`} | ${``} | ${``} | ${`[b]`} | ${`[/b]`} - ${`[color]`} | ${``} | ${``} | ${`[color=red]`} | ${`[/color]`} - ${`[size]`} | ${``} | ${``} | ${`[size=85]`} | ${`[/size]`} - ${`[h1]`} | ${`

      `} | ${`

      `} | ${`[h1]`} | ${`[/h1]`} - ${`[h2]`} | ${`

      `} | ${`

      `} | ${`[h2]`} | ${`[/h2]`} - ${`[h3]`} | ${`

      `} | ${`

      `} | ${`[h3]`} | ${`[/h3]`} - ${`[h4]`} | ${`

      `} | ${`

      `} | ${`[h4]`} | ${`[/h4]`} - ${`[h5]`} | ${`
      `} | ${`
      `} | ${`[h5]`} | ${`[/h5]`} - ${`[h6]`} | ${`
      `} | ${`
      `} | ${`[h6]`} | ${`[/h6]`} - ${`[i] (style)`} | ${``} | ${``} | ${`[i]`} | ${`[/i]`} - ${`[i] (i)`} | ${``} | ${``} | ${`[i]`} | ${`[/i]`} - ${`[i] (em)`} | ${``} | ${``} | ${`[i]`} | ${`[/i]`} - ${`[s] (style)`} | ${``} | ${``} | ${`[s]`} | ${`[/s]`} - ${`[s] (del)`} | ${``} | ${``} | ${`[s]`} | ${`[/s]`} - ${`[s] (strike)`} | ${``} | ${``} | ${`[s]`} | ${`[/s]`} - ${`[u] (style)`} | ${``} | ${``} | ${`[u]`} | ${`[/u]`} - ${`[u] (u)`} | ${``} | ${``} | ${`[u]`} | ${`[/u]`} - ${`[u] (ins)`} | ${``} | ${``} | ${`[u]`} | ${`[/u]`} - ${`[url]`} | ${``} | ${``} | ${`[url="${url.absolute}"]`} | ${`[/url]`} - `( - "$tag (Standard Behaviors)", - ({ - openElement, - closeElement, - openTag, - closeTag, - }: { - openElement: string; - closeElement: string; - openTag: string; - closeTag: string; - }) => { - it.each` - dataView | expectedData | comment - ${`${openElement}T${closeElement}`} | ${`${openTag}T${closeTag}`} | ${`default`} - `( - "[$#] Should process data view '$dataView' to: $expectedData ($comment)", - ({ dataView, expectedData }: { dataView: string; expectedData: string }) => { - aut.expectTransformation({ dataView, expectedData }); - }, - ); - }, - ); - - describe("[url]", () => { - it.each` - dataView | expectedData | comment - ${`T`} | ${`[url="${url.absolute}"]T[/url]`} | ${`default (absolute)`} - ${`${url.absolute}`} | ${`[url]${url.absolute}[/url]`} | ${`pretty print (absolute)`} - ${`T`} | ${`[url="${url.absolute}"][i]T[/i][/url]`} | ${`nested element support (inner)`} - ${`T`} | ${`[i][url="${url.absolute}"]T[/url][/i]`} | ${`nested element support (outer)`} - ${`T`} | ${`[url="${url.absolute}?brackets=%5B%5D"]T[/url]`} | ${`escape brackets in URL`} - ${`${url.absolute}?brackets=[]`} | ${`[url="${url.absolute}?brackets=%5B%5D"]${url.absolute}?brackets=\\[\\][/url]`} | ${`different escaping for different contexts`} - ${`T`} | ${`[url="${url.relative}"]T[/url]`} | ${`default (relative)`} - ${`${url.relative}`} | ${`[url]${url.relative}[/url]`} | ${`pretty print (relative)`} - `( - "[$#] Should process data view '$dataView' to: $expectedData ($comment)", - ({ dataView, expectedData }: { dataView: string; expectedData: string }) => { - aut.expectTransformation({ dataView, expectedData }); - }, - ); - }); - - describe("[img]", () => { - // noinspection HtmlRequiredAltAttribute - it.each` - dataView | expectedData | comment - ${``} | ${`[img]${url.absolute}[/img]`} | ${`default (absolute)`} - ${``} | ${`[url="${url.absolute}"][img]${url.absolute}[/img][/url]`} | ${`linked image`} - ${`BEFOREAFTER`} | ${`[i]BEFORE[img]${url.absolute}[/img]AFTER[/i]`} | ${`images are inline`} - ${``} | ${`[img]${url.absolute}?brackets=%5B%5D[/img]`} | ${`escape brackets in URL`} - ${``} | ${`[img]${url.relative}[/img]`} | ${`default (relative)`} - ${`BEFOREAFTER`} | ${`BEFOREAFTER`} | ${`design-scope: Remove irrelevant image with empty src`} - ${`ALT`} | ${`[img alt="ALT"]${url.absolute}[/img]`} | ${`alt attribute support (default)`} - ${`BEFOREALTAFTER`} | ${`BEFOREAFTER`} | ${`design-scope: Remove irrelevant image with empty src, even if alt is set`} - ${`with space`} | ${`[img alt="with space"]${url.absolute}[/img]`} | ${`attribute with spaces; latest, that we require quotes for BBob`} - ${`let's "quote"`} | ${`[img alt="let's \\"quote\\""]${url.absolute}[/img]`} | ${`alt text quote challenge (must not escape BBCode attribute)`} - ${`in [brackets]`} | ${`[img alt="in [brackets]"]${url.absolute}[/img]`} | ${`alt text with square brackets; for best robustness in BBCode parsers encoded; design-scope: This introduces entities and thus assumes BBCode is always processed to some XML format.`} - `( - "[$#] Should process data view '$dataView' to: $expectedData ($comment)", - ({ dataView, expectedData }: { dataView: string; expectedData: string }) => { - aut.expectTransformation({ dataView, expectedData }); - }, - ); - }); -}); diff --git a/packages/ckeditor5-bbcode/jest.config.cjs b/packages/ckeditor5-bbcode/jest.config.cjs deleted file mode 100644 index 208f377fa0..0000000000 --- a/packages/ckeditor5-bbcode/jest.config.cjs +++ /dev/null @@ -1,3 +0,0 @@ -const jestConfig = require("@coremedia-internal/ckeditor5-jest-test-helpers/shared-jest.config.js"); - -module.exports = { ...jestConfig }; diff --git a/packages/ckeditor5-bbcode/package.json b/packages/ckeditor5-bbcode/package.json index f7c1a622e8..3b4575e53d 100644 --- a/packages/ckeditor5-bbcode/package.json +++ b/packages/ckeditor5-bbcode/package.json @@ -1,6 +1,7 @@ { "name": "@coremedia/ckeditor5-bbcode", "version": "25.0.4-rc.4", + "type": "module", "description": "BBCode Data-Processor", "author": { "name": "CoreMedia GmbH", @@ -22,9 +23,8 @@ "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 --passWithNoTests", - "jest:coverage": "jest --collect-coverage", - "lint": "eslint \"**/*.{js,cjs,mjs,ts,tsx}\"", + "test": "test-runner-react", + "lint": "eslint \"**/*.{ts,tsx}\"", "npm-check-updates": "npm-check-updates --upgrade" }, "exports": { @@ -49,29 +49,32 @@ "/types" ], "peerDependencies": { - "ckeditor5": "45.2.1" + "ckeditor5": "46.1.1" }, "dependencies": { - "@bbob/core": "^3.0.2", - "@bbob/html": "^3.0.2", - "@bbob/plugin-helper": "^3.0.2", - "@bbob/preset": "^3.0.2", - "@bbob/preset-html5": "^3.0.2", + "@bbob/core": "4.3.1", + "@bbob/html": "4.3.1", + "@bbob/parser": "4.3.1", + "@bbob/plugin-helper": "4.3.1", + "@bbob/preset": "4.3.1", + "@bbob/preset-html5": "4.3.1", + "@bbob/types": "4.3.1", "@coremedia/ckeditor5-common": "25.0.4-rc.4", "@coremedia/ckeditor5-core-common": "25.0.4-rc.4", "@coremedia/ckeditor5-dom-support": "25.0.4-rc.4", "@coremedia/ckeditor5-logging": "25.0.4-rc.4" }, "devDependencies": { - "@coremedia-internal/ckeditor5-jest-test-helpers": "^1.0.0", - "@jest/globals": "^29.7.0", - "@types/jest": "^29.5.12", - "ckeditor5": "45.2.1", + "@coremedia-internal/studio-client.test-runner-helper": "0.2.3-alpha.050b44a", + "@types/node": "^22.0.0", + "ckeditor5": "46.1.1", "copyfiles": "^2.4.1", - "jest": "^29.7.0", - "jest-each": "^29.7.0", + "expect": "^30.2.0", + "global-jsdom": "^27.0.0", + "jsdom": "^27.0.0", "rimraf": "^6.0.1", - "ts-jest": "^29.2.4", + "ts-node": "^10.9.2", + "tsx": "^4.20.6", "typescript": "5.4.5" } } diff --git a/packages/ckeditor5-bbcode/src/BBCodeDataProcessor.ts b/packages/ckeditor5-bbcode/src/BBCodeDataProcessor.ts index c27b811dfc..335a83b0a5 100644 --- a/packages/ckeditor5-bbcode/src/BBCodeDataProcessor.ts +++ b/packages/ckeditor5-bbcode/src/BBCodeDataProcessor.ts @@ -1,14 +1,8 @@ -import { - DataProcessor, - DomConverter, - HtmlDataProcessor, - MatcherPattern, - ViewDocument, - ViewDocumentFragment, -} from "ckeditor5"; +import type { DataProcessor, ViewDomConverter, MatcherPattern, ViewDocument, ViewDocumentFragment } from "ckeditor5"; +import { HtmlDataProcessor } from "ckeditor5"; import { bbcode2html } from "./bbcode2html"; import { html2bbcode } from "./html2bbcode"; -import { BBCodeProcessingRule } from "./rules/BBCodeProcessingRule"; +import type { BBCodeProcessingRule } from "./rules/BBCodeProcessingRule"; import { bbCodeDefaultRules } from "./rules/bbCodeDefaultRules"; import { bbCodeLogger } from "./BBCodeLogger"; @@ -33,7 +27,7 @@ export class BBCodeDataProcessor implements DataProcessor { * DOM-Converter used to prepare incoming data view in `toData` processing * prior to transforming HTML to BBCode. */ - readonly #domConverter: DomConverter; + readonly #domConverter: ViewDomConverter; /** * Rules to apply in data processing. Note that, as we use a third party diff --git a/packages/ckeditor5-bbcode/src/BBCodeLogger.ts b/packages/ckeditor5-bbcode/src/BBCodeLogger.ts index 88a5ce8838..9d90904542 100644 --- a/packages/ckeditor5-bbcode/src/BBCodeLogger.ts +++ b/packages/ckeditor5-bbcode/src/BBCodeLogger.ts @@ -1,3 +1,4 @@ -import { Logger, LoggerProvider } from "@coremedia/ckeditor5-logging"; +import type { Logger } from "@coremedia/ckeditor5-logging"; +import { LoggerProvider } from "@coremedia/ckeditor5-logging"; export const bbCodeLogger: Logger = LoggerProvider.getLogger("BBCode"); diff --git a/packages/ckeditor5-bbcode/src/augmentation.ts b/packages/ckeditor5-bbcode/src/augmentation.ts index 7b134a4b50..76259451b6 100644 --- a/packages/ckeditor5-bbcode/src/augmentation.ts +++ b/packages/ckeditor5-bbcode/src/augmentation.ts @@ -1,4 +1,4 @@ -import { BBCode } from "./BBCode"; +import type { BBCode } from "./BBCode"; declare module "ckeditor5" { interface PluginsMap { diff --git a/packages/ckeditor5-bbcode/src/bbcode2html.ts b/packages/ckeditor5-bbcode/src/bbcode2html.ts index 3f0022ecd0..6d258a1618 100644 --- a/packages/ckeditor5-bbcode/src/bbcode2html.ts +++ b/packages/ckeditor5-bbcode/src/bbcode2html.ts @@ -1,4 +1,4 @@ -import bbob from "@bbob/core/es"; +import bbob from "@bbob/core"; import { bbCodeLogger } from "./BBCodeLogger"; import { ckeditor5Preset } from "./bbob/ckeditor5Preset"; import { renderHtmlDom } from "./bbob/renderHtmlDom"; @@ -20,7 +20,7 @@ export const processBBCode = (bbcode: string, allowedTags?: string[]): ReturnTyp onlyAllowTags: allowedTags, onError: (error) => bbCodeLogger.error( - `Failure while processing BBCode (${error.tagName} at ${error.lineNumber}:${error.columnNumber}): ${error.message}`, + `Failure while processing BBCode (${error.tagName} at ${error.lineNumber}:${error.columnNumber}): ${error.toString()}`, ), }); diff --git a/packages/ckeditor5-bbcode/src/bbob/Attributes.ts b/packages/ckeditor5-bbcode/src/bbob/Attributes.ts index 8b79d8ae38..4b916efb27 100644 --- a/packages/ckeditor5-bbcode/src/bbob/Attributes.ts +++ b/packages/ckeditor5-bbcode/src/bbob/Attributes.ts @@ -1,10 +1,10 @@ -import { getUniqAttr, TagAttrs } from "@bbob/plugin-helper/es"; +import { getUniqAttr } from "@bbob/plugin-helper"; import { bbCodeLogger } from "../BBCodeLogger"; /** * Consumer for an attribute name and its value. */ -export type AttributeConsumer = (attrName: string, attrValue: string) => void; +export type AttributeConsumer = (attrName: string, attrValue: unknown) => void; /** * Iterates over each attribute (key, value pair) and passes it to the @@ -13,7 +13,7 @@ export type AttributeConsumer = (attrName: string, attrValue: string) => void; * @param attrs - attributes to process * @param consumer - consumer to work an attribute's key/value pair */ -export const forEachAttribute = (attrs: TagAttrs, consumer: AttributeConsumer): void => { +export const forEachAttribute = (attrs: Record, consumer: AttributeConsumer): void => { Object.entries(attrs).forEach(([attrName, attrValue]) => consumer(attrName, attrValue)); }; @@ -26,9 +26,10 @@ export const forEachAttribute = (attrs: TagAttrs, consumer: AttributeConsumer): * @param element - element to set attributes at * @param attrs - attributes to set */ -export const setAttributesFromTagAttrs = (element: HTMLElement, attrs: TagAttrs): void => { +export const setAttributesFromTagAttrs = (element: HTMLElement, attrs: Record): void => { const trySetAttribute: AttributeConsumer = (name, value): void => { try { + // @ts-expect-error TODO must be a string here, but TagAttrs allows unknown values element.setAttribute(name, value); } catch (e) { bbCodeLogger.debug( @@ -54,7 +55,12 @@ export const setAttributesFromTagAttrs = (element: HTMLElement, attrs: TagAttrs) * @param attrs - attributes to parse */ // see https://github.com/JiLiZART/BBob/issues/202 -export const stripUniqueAttr = (attrs: TagAttrs): { uniqueAttrValue?: TagAttrs[string]; otherAttrs: TagAttrs } => { +export const stripUniqueAttr = ( + attrs: Record, +): { + uniqueAttrValue?: Record[string]; + otherAttrs: Record; +} => { const uniqueAttrValue = getUniqAttr(attrs); // Contract for _no unique attribute set_. @@ -84,14 +90,14 @@ export const stripUniqueAttr = (attrs: TagAttrs): { uniqueAttrValue?: TagAttrs[s */ export const uniqueAttrToAttr = ( uniqueAttrName: string, - attrs: TagAttrs, + attrs: Record, override = true, defaultValueSupplier?: () => string, -): TagAttrs => { +): Record => { const { uniqueAttrValue = defaultValueSupplier?.(), otherAttrs } = stripUniqueAttr(attrs); - const valueFromOtherAttrs: string | undefined = otherAttrs[uniqueAttrName]; + const valueFromOtherAttrs: unknown | undefined = otherAttrs[uniqueAttrName]; - const result: TagAttrs = otherAttrs; + const result: Record = otherAttrs; if (uniqueAttrValue !== undefined && (override || valueFromOtherAttrs === undefined)) { result[uniqueAttrName] = uniqueAttrValue; diff --git a/packages/ckeditor5-bbcode/src/bbob/Paragraphs.ts b/packages/ckeditor5-bbcode/src/bbob/Paragraphs.ts index 9e8854a296..06ef825a06 100644 --- a/packages/ckeditor5-bbcode/src/bbob/Paragraphs.ts +++ b/packages/ckeditor5-bbcode/src/bbob/Paragraphs.ts @@ -1,8 +1,9 @@ -import { isEOL, isTagNode, N, TagNode } from "@bbob/plugin-helper/es"; +import type { TagNodeTree } from "@bbob/types"; +import { TagNode, isEOL, N, isTagNode } from "@bbob/plugin-helper"; import { bbCodeLogger } from "../BBCodeLogger"; -import { Tag } from "./types"; +import type { Tag } from "./types"; -const toNode = TagNode.create; +type TagNodeType = ReturnType; /** * Options for `paragraphAwareContent`. @@ -93,9 +94,9 @@ const debug = (msg: string, state?: Record): void => { * @param options - options to respect */ export const paragraphAwareContent = ( - content: NonNullable, + content: NonNullable, options: ParagraphAwareContentOptions = {}, -): NonNullable => { +): NonNullable => { const { requireParagraph: fromConfigRequireParagraph = false, skipEmpty = true, @@ -108,11 +109,11 @@ export const paragraphAwareContent = ( options, }); - if (content.length === 0) { + if ((Array.isArray(content) || typeof content === "string") && content.length === 0) { if (fromConfigRequireParagraph) { // We were told, a paragraph is required as nested tag. Thus, // also add it for empty content. - return [toNode("p", {}, [])]; + return [TagNode.create("p", {}, [])]; } return []; } @@ -125,11 +126,11 @@ export const paragraphAwareContent = ( let requireParagraph = fromConfigRequireParagraph; // Intermediate buffer, that may need to go to a paragraph node. - const buffer: NonNullable = []; + const buffer: NonNullable = []; // Collected EOLs. They will not make it to `buffer` until flushed. const trailingNewlineBuffer: (typeof N)[] = []; // The result to return in the end. - const result: NonNullable = []; + const result: NonNullable = []; /** * Clear temporary buffers. @@ -152,8 +153,9 @@ export const paragraphAwareContent = ( /** * Signals, if the given content shall be considered empty or not. */ - const isNonEmpty = (content: NonNullable): boolean => - content.some((entry) => isTagNode(entry) || entry.trim() !== ""); + const isNonEmpty = (content: NonNullable): boolean => + Array.isArray(content) && + content.some((entry) => entry && (isTagNode(entry) || (typeof entry === "string" && entry.trim() !== ""))); /** * Adds a paragraph and triggers that all subsequent contents also @@ -164,7 +166,7 @@ export const paragraphAwareContent = ( * * @param content - content to add */ - const addCopyAsParagraph = (content: NonNullable): void => { + const addCopyAsParagraph = (content: NonNullable): void => { debug("Start: addCopyAsParagraph", { content, result, @@ -173,9 +175,9 @@ export const paragraphAwareContent = ( }); if (isNonEmpty(content)) { - result.push(toNode("p", {}, [...content])); + result.push(TagNode.create("p", {}, Array.isArray(content) ? [...content] : [content])); } else if (requireParagraph || !skipEmpty) { - result.push(toNode("p", {}, [])); + result.push(TagNode.create("p", {}, [])); } requireParagraph = true; @@ -309,7 +311,7 @@ export const paragraphAwareContent = ( * Handles a tag-node on current level. Respects block-tags that may enforce * previous buffer entries to be added as paragraph. */ - const handleTagNode = (node: TagNode): void => { + const handleTagNode = (node: TagNodeType): void => { debug("Start: handleTagNode", { node, buffer, @@ -345,7 +347,7 @@ export const paragraphAwareContent = ( }); if (isEOL(value)) { - trailingNewlineBuffer.push(value); + trailingNewlineBuffer.push(value as typeof N); } else { flushOnParagraph(); buffer.push(value); @@ -359,11 +361,12 @@ export const paragraphAwareContent = ( }; // Main processing of content items to possibly wrap into paragraphs. - for (const contentItem of content) { + const contentArray = Array.isArray(content) ? content : [content]; + for (const contentItem of contentArray) { if (isTagNode(contentItem)) { handleTagNode(contentItem); } else { - handleString(contentItem); + typeof contentItem === "string" && handleString(contentItem); } } diff --git a/packages/ckeditor5-bbcode/src/bbob/TagNodes.ts b/packages/ckeditor5-bbcode/src/bbob/TagNodes.ts index ef05f4ab97..8c3597d629 100644 --- a/packages/ckeditor5-bbcode/src/bbob/TagNodes.ts +++ b/packages/ckeditor5-bbcode/src/bbob/TagNodes.ts @@ -1,4 +1,6 @@ -import { isEOL, N, TagNode } from "@bbob/plugin-helper/es"; +import type { TagNode } from "@bbob/plugin-helper"; +import type { TagNodeTree } from "@bbob/types"; +import { isEOL, N } from "@bbob/plugin-helper"; /** * Removes EOLs at the beginning and end, that may be a result of @@ -6,14 +8,15 @@ import { isEOL, N, TagNode } from "@bbob/plugin-helper/es"; * * @param contents - contents to trim */ -export const trimEOL = (contents: TagNode["content"]): TagNode["content"] => { +export const trimEOL = (contents: TagNodeTree): TagNode["content"] => { const result: TagNode["content"] = []; const bufferedEOLs: (typeof N)[] = []; - for (const content of contents ?? []) { - if (isEOL(content)) { + const contentsArr = Array.isArray(contents) ? contents : [contents]; + for (const content of contentsArr) { + if (typeof content === "string" && isEOL(content)) { // > 0: Ignore EOLs at the beginning if (result.length > 0) { - bufferedEOLs.push(content); + bufferedEOLs.push(N); } } else { // Push any EOLs collected up to now. diff --git a/packages/ckeditor5-bbcode/src/bbob/ckeditor5Preset.ts b/packages/ckeditor5-bbcode/src/bbob/ckeditor5Preset.ts index f3c4cce81a..eeaba72b3d 100644 --- a/packages/ckeditor5-bbcode/src/bbob/ckeditor5Preset.ts +++ b/packages/ckeditor5-bbcode/src/bbob/ckeditor5Preset.ts @@ -1,18 +1,18 @@ -import { getUniqAttr, isTagNode, TagAttrs, TagNode } from "@bbob/plugin-helper/es"; -import { createPreset } from "@bbob/preset/es"; -import html5DefaultTags from "@bbob/preset-html5/es/defaultTags"; -import { CoreTree } from "@bbob/core/es"; +import type { BBobCoreTagNodeTree, ProcessorFunction } from "@bbob/types"; +import { getUniqAttr, isTagNode, TagNode } from "@bbob/plugin-helper"; +import { createPreset } from "@bbob/preset"; +import { defaultTags } from "@bbob/preset-html5/defaultTags"; import { bbCodeLogger } from "../BBCodeLogger"; import { fontSizes, normalSize } from "../utils/FontSizes"; -import { paragraphAwareContent } from "./Paragraphs"; -import { Core, DefaultTags, Options } from "./types"; import { stripUniqueAttr, uniqueAttrToAttr } from "./Attributes"; +import { paragraphAwareContent } from "./Paragraphs"; import { renderRaw } from "./renderRaw"; import { trimEOL } from "./TagNodes"; +import type { DefaultTags } from "./types"; -type DefaultTagsRule = DefaultTags[string]; +type TagNodeType = ReturnType; -const toNode = TagNode.create; +type DefaultTagsRule = DefaultTags[string]; /** * To be able to handle paragraphs also on root-level within the defined @@ -33,9 +33,9 @@ const rootNodeName = "root"; * when the tree changed in an unexpected way, i.e., does not have a singleton * node of type _root-node_ anymore. */ -const wrapInRoot = (tree: CoreTree): (() => void) => { +const wrapInRoot = (tree: BBobCoreTagNodeTree): (() => void) => { const treeContents = [...tree]; - const rootNode = toNode(rootNodeName, {}, treeContents); + const rootNode = TagNode.create(rootNodeName, {}, treeContents); tree.length = 0; tree.push(rootNode); @@ -55,7 +55,7 @@ const wrapInRoot = (tree: CoreTree): (() => void) => { ); } tree.length = 0; - tree.push(...(processedRootNode.content ?? [])); + tree.push(...(Array.isArray(processedRootNode.content) ? processedRootNode.content : [])); }; }; @@ -63,7 +63,7 @@ const wrapInRoot = (tree: CoreTree): (() => void) => { * Copy of default processor with adaptations to incorporate workaround for * https://github.com/JiLiZART/BBob/issues/125. */ -const process = (tags: DefaultTags, tree: CoreTree, core: Core, options: Options) => { +const process: ProcessorFunction = (tags, tree, core, options) => { const logger = bbCodeLogger; if (logger.isDebugEnabled()) { @@ -80,6 +80,7 @@ const process = (tags: DefaultTags, tree: CoreTree, core: Core, options: Options if (logger.isDebugEnabled()) { logger.debug(`Done processing parsed AST: ${JSON.stringify(tree)}`); } + return tree; }; /** @@ -90,19 +91,19 @@ const process = (tags: DefaultTags, tree: CoreTree, core: Core, options: Options * here (and export it as `ckeditor5Preset` directly). We keep this split-up * state to possibly revert to extending the `html5Preset` instead. */ -const basePreset: ReturnType = createPreset(html5DefaultTags, process); +const basePreset: ReturnType = createPreset(defaultTags, process); /** * Transforms the node as is, but ensures that its content respects possible * paragraph formatting. */ -const toParagraphAwareNode = (node: TagNode): TagNode => - toNode(node.tag, node.attrs, paragraphAwareContent(node.content ?? [])); +const toParagraphAwareNode = (node: TagNodeType): TagNodeType => + TagNode.create(node.tag, node.attrs, paragraphAwareContent(node.content ?? [])); -const toHtmlAnchorAttrs = (node: TagNode): TagAttrs => +const toHtmlAnchorAttrs = (node: TagNodeType): Record => uniqueAttrToAttr("href", node.attrs, false, () => renderRaw(node)); -const toHtmlImageAttrs = (node: TagNode): TagAttrs => { +const toHtmlImageAttrs = (node: TagNodeType): Record => { const { attrs } = node; // We ignore unique attributes here, but keep all others, just in case // they got defined. Some BBCode dialects use the unique attribute to @@ -127,10 +128,10 @@ const toHtmlImageAttrs = (node: TagNode): TagAttrs => { * * See also: . */ -const code: DefaultTagsRule = (node: TagNode): TagNode => { +const code = (node: TagNodeType): TagNodeType => { // Using `||` also for possibly empty string. const language = getUniqAttr(node.attrs) || "plaintext"; - return toNode("pre", {}, [toNode("htmlCode", { class: `language-${language}` }, node.content)]); + return TagNode.create("pre", {}, [TagNode.create("htmlCode", { class: `language-${language}` }, node.content)]); }; /** @@ -139,7 +140,7 @@ const code: DefaultTagsRule = (node: TagNode): TagNode => { * we must not add a `code` node within the `[code]` to `
      ` processing,
        * which again is required for proper code block support in CKEditor 5.
        */
      -const htmlCode: DefaultTagsRule = (node: TagNode): TagNode => toNode("code", node.attrs, trimEOL(node.content));
      +const htmlCode = (node: TagNodeType): TagNodeType => TagNode.create("code", node.attrs, trimEOL(node.content));
       
       /**
        * Transforms `quote` to `blockquote`. Ensures that only block-level
      @@ -147,17 +148,17 @@ const htmlCode: DefaultTagsRule = (node: TagNode): TagNode => toNode("code", nod
        * content into a paragraph node.
        */
       const quote: DefaultTagsRule = (node) =>
      -  toNode("blockquote", {}, paragraphAwareContent(node.content ?? [], { requireParagraph: true }));
      +  TagNode.create("blockquote", {}, paragraphAwareContent(node.content ?? [], { requireParagraph: true }));
       
      -const url: DefaultTagsRule = (node: TagNode): TagNode => toNode("a", toHtmlAnchorAttrs(node), node.content);
      +const url = (node: TagNodeType): TagNodeType => TagNode.create("a", toHtmlAnchorAttrs(node), node.content);
       
      -const img: DefaultTagsRule = (node: TagNode): TagNode => ({
      -  ...toNode("img", toHtmlImageAttrs(node), null),
      +const img = (node: TagNodeType) => ({
      +  ...TagNode.create("img", toHtmlImageAttrs(node), null),
         // Workaround: https://github.com/JiLiZART/BBob/issues/206
         content: null,
       });
       
      -const toFontSizeSpanAttrs = (node: TagNode): TagAttrs => {
      +const toFontSizeSpanAttrs = (node: TagNodeType): Record => {
         // Stage 1: Check if (expected) unique attribute exists; return only other attributes otherwise
         const { uniqueAttrValue, otherAttrs } = stripUniqueAttr(node.attrs);
         if (!uniqueAttrValue) {
      @@ -178,7 +179,7 @@ const toFontSizeSpanAttrs = (node: TagNode): TagAttrs => {
       
         // Stage 4: Prepare new attributes including `class` attribute (possibly merge with existing)
       
      -  const existingClass: string | undefined = otherAttrs.class;
      +  const existingClass = typeof otherAttrs.class === "string" ? otherAttrs.class : undefined;
         const classValue = existingClass ? `${matchedEntry.className} ${existingClass}` : matchedEntry.className;
         return {
           ...otherAttrs,
      @@ -190,7 +191,7 @@ const toFontSizeSpanAttrs = (node: TagNode): TagAttrs => {
        * Parses the font-size given by `[size=number]` within BBCode and transforms
        * it to a `` with a size representing class attribute.
        */
      -const size: DefaultTagsRule = (node: TagNode): TagNode => toNode("span", toFontSizeSpanAttrs(node), node.content);
      +const size = (node: TagNodeType): TagNodeType => TagNode.create("span", toFontSizeSpanAttrs(node), node.content);
       
       /**
        * Mappings for nodes, that need to be aware of internal paragraph handling
      @@ -203,15 +204,16 @@ const size: DefaultTagsRule = (node: TagNode): TagNode => toNode("span", toFontS
        *   from `*` nodes. As a subsequent step, we add support for nested paragraphs
        *   within these tags.
        */
      -const paragraphAwareTags: DefaultTags = Object.fromEntries(["root", "li"].map((tag) => [tag, toParagraphAwareNode]));
      +const paragraphAwareTags = Object.fromEntries(["root", "li"].map((tag) => [tag, toParagraphAwareNode]));
       
       /**
        * Extension of the HTML 5 Default Preset, that ships with BBob. It adapts
        * the given presets, so that they align with the expectations by CKEditor 5
        * regarding the representation in data view.
        */
      -export const ckeditor5Preset: ReturnType = basePreset.extend((tags: DefaultTags): DefaultTags => {
      -  const extendedTags: DefaultTags = {
      +// @ts-expect-error TODO just for now
      +export const ckeditor5Preset = basePreset.extend((tags) => {
      +  const extendedTags = {
           ...tags,
           ...paragraphAwareTags,
           quote,
      @@ -222,5 +224,6 @@ export const ckeditor5Preset: ReturnType = basePreset.exten
           size,
         };
         bbCodeLogger.debug(`Extended Tags to: ${Object.keys(extendedTags)}`);
      +
         return extendedTags;
       });
      diff --git a/packages/ckeditor5-bbcode/src/bbob/renderHtmlDom.ts b/packages/ckeditor5-bbcode/src/bbob/renderHtmlDom.ts
      index ff783bbc2d..deb520770d 100644
      --- a/packages/ckeditor5-bbcode/src/bbob/renderHtmlDom.ts
      +++ b/packages/ckeditor5-bbcode/src/bbob/renderHtmlDom.ts
      @@ -1,10 +1,10 @@
      -import { render as htmlRender } from "@bbob/html/es";
      -import { CoreRenderable, CoreRenderer, CoreRenderNode } from "@bbob/core/es";
      -import { isStringNode, isTagNode } from "@bbob/plugin-helper/es";
      +import { render } from "@bbob/html";
      +import type { ParseOptions, TagNodeTree } from "@bbob/types";
      +import { isStringNode, isTagNode } from "@bbob/plugin-helper";
       import { bbCodeLogger } from "../BBCodeLogger";
       import { setAttributesFromTagAttrs } from "./Attributes";
       
      -const renderDomNode = (node: CoreRenderable, options: Required): Node => {
      +const renderDomNode = (node: null | string | number | TagNodeTree, options: Required): Node => {
         if (typeof node === "number") {
           return document.createTextNode(String(node));
         }
      @@ -36,10 +36,10 @@ const renderDomNode = (node: CoreRenderable, options: Required): Node[] =>
      -  node.map((n) => renderDomNode(n, options));
      +const renderDomNodes = (node: TagNodeTree, options: Required): Node[] =>
      +  (Array.isArray(node) ? node : [node]).map((n) => renderDomNode(n, options));
       
      -const renderTree = (node: CoreRenderable[], options: Required): string => {
      +const renderTree = (node: TagNodeTree, options: Required): string => {
         const logger = bbCodeLogger;
       
         if (logger.isDebugEnabled()) {
      @@ -69,15 +69,15 @@ const defaultHtmlDomRendererOptions: Required = {
         stripTags: false,
       };
       
      -export const htmlDomRenderer = (defaultOptions: HtmlDomRendererOptions = {}): CoreRenderer => {
      +export const htmlDomRenderer = (defaultOptions: HtmlDomRendererOptions = {}) => {
         const configuredDefaultOptions: Required = {
           ...defaultHtmlDomRendererOptions,
           ...defaultOptions,
         };
      -  return (node: CoreRenderNode, options: object & HtmlDomRendererOptions = {}): string => {
      +  return (node?: TagNodeTree, options?: ParseOptions): string => {
           const optionsWithDefaults: Required = {
             ...configuredDefaultOptions,
      -      ...options,
      +      ...(options ?? {}),
           };
           let result: string;
           if (Array.isArray(node)) {
      @@ -87,10 +87,10 @@ export const htmlDomRenderer = (defaultOptions: HtmlDomRendererOptions = {}): Co
           } else {
             // Some defensive approach to fall back to an alternative rendering for
             // in-between processing.
      -      result = htmlRender(node, optionsWithDefaults);
      +      result = render(node, optionsWithDefaults);
           }
           return result;
         };
       };
       
      -export const renderHtmlDom: CoreRenderer = htmlDomRenderer();
      +export const renderHtmlDom = htmlDomRenderer();
      diff --git a/packages/ckeditor5-bbcode/src/bbob/renderRaw.ts b/packages/ckeditor5-bbcode/src/bbob/renderRaw.ts
      index cb9fdf8e7b..1243fecc1e 100644
      --- a/packages/ckeditor5-bbcode/src/bbob/renderRaw.ts
      +++ b/packages/ckeditor5-bbcode/src/bbob/renderRaw.ts
      @@ -1,4 +1,6 @@
      -import { isStringNode, isTagNode, TagNode } from "@bbob/plugin-helper/es";
      +import type { TagNode } from "@bbob/plugin-helper";
      +import type { TagNodeTree } from "@bbob/types";
      +import { isStringNode, isTagNode } from "@bbob/plugin-helper";
       
       /**
        * Renders a tag node to its raw content.
      @@ -6,20 +8,20 @@ import { isStringNode, isTagNode, TagNode } from "@bbob/plugin-helper/es";
        * @param node - node to render
        */
       export const renderRaw = (node: TagNode): string => {
      -  const render = (currentNode: NonNullable[number] | TagNode["content"]): string => {
      -    if (!currentNode) {
      -      return "";
      -    }
      -
      +  const render = (currentNode: TagNodeTree): string => {
           if (isStringNode(currentNode)) {
      -      return currentNode;
      +      return typeof currentNode === "string" ? currentNode : String(currentNode);
           }
       
           if (isTagNode(currentNode)) {
             return render(currentNode.content);
           }
       
      -    return currentNode.map((entry) => render(entry)).join("");
      +    if (Array.isArray(currentNode)) {
      +      return currentNode.map((entry) => render(entry)).join("");
      +    }
      +
      +    return "";
         };
       
         return render(node.content);
      diff --git a/packages/ckeditor5-bbcode/src/bbob/types.ts b/packages/ckeditor5-bbcode/src/bbob/types.ts
      index 19c27e4ed3..36fab667e9 100644
      --- a/packages/ckeditor5-bbcode/src/bbob/types.ts
      +++ b/packages/ckeditor5-bbcode/src/bbob/types.ts
      @@ -1,5 +1,5 @@
      -import { TagNode } from "@bbob/plugin-helper/es";
      -import { createPreset } from "@bbob/preset/es";
      +import type { TagNode } from "@bbob/plugin-helper";
      +import type { createPreset } from "@bbob/preset";
       
       export type DefaultTags = Parameters[0];
       export type TagMappingFn = DefaultTags[string];
      diff --git a/packages/ckeditor5-bbcode/src/html2bbcode.ts b/packages/ckeditor5-bbcode/src/html2bbcode.ts
      index b3ef64df4e..0a7b71cca4 100644
      --- a/packages/ckeditor5-bbcode/src/html2bbcode.ts
      +++ b/packages/ckeditor5-bbcode/src/html2bbcode.ts
      @@ -1,5 +1,6 @@
      -import { HasChildren, isHTMLElement, isParentNode } from "@coremedia/ckeditor5-dom-support";
      -import { BBCodeProcessingRule } from "./rules/BBCodeProcessingRule";
      +import type { HasChildren } from "@coremedia/ckeditor5-dom-support";
      +import { isHTMLElement, isParentNode } from "@coremedia/ckeditor5-dom-support";
      +import type { BBCodeProcessingRule } from "./rules/BBCodeProcessingRule";
       import { bbCodeDefaultRules } from "./rules/bbCodeDefaultRules";
       
       /**
      diff --git a/packages/ckeditor5-bbcode/src/rules/BBCodeBold.ts b/packages/ckeditor5-bbcode/src/rules/BBCodeBold.ts
      index 1f5de9fbf3..6196c5f71f 100644
      --- a/packages/ckeditor5-bbcode/src/rules/BBCodeBold.ts
      +++ b/packages/ckeditor5-bbcode/src/rules/BBCodeBold.ts
      @@ -1,5 +1,6 @@
      -import { FontWeightInformation, fontWeightToNumber, getFontWeight } from "@coremedia/ckeditor5-dom-support";
      -import { BBCodeProcessingRule } from "./BBCodeProcessingRule";
      +import type { FontWeightInformation } from "@coremedia/ckeditor5-dom-support";
      +import { fontWeightToNumber, getFontWeight } from "@coremedia/ckeditor5-dom-support";
      +import type { BBCodeProcessingRule } from "./BBCodeProcessingRule";
       
       /**
        * Possible HTML tags that denote a bold style.
      diff --git a/packages/ckeditor5-bbcode/src/rules/BBCodeCode.ts b/packages/ckeditor5-bbcode/src/rules/BBCodeCode.ts
      index c90ab3fdd0..28e9eb1c05 100644
      --- a/packages/ckeditor5-bbcode/src/rules/BBCodeCode.ts
      +++ b/packages/ckeditor5-bbcode/src/rules/BBCodeCode.ts
      @@ -1,5 +1,5 @@
       import { trimLeadingAndTrailingNewlines } from "../BBCodeUtils";
      -import { BBCodeProcessingRule } from "./BBCodeProcessingRule";
      +import type { BBCodeProcessingRule } from "./BBCodeProcessingRule";
       
       /**
        * The default extracted language by `HTMLElement.classList` that is considered
      diff --git a/packages/ckeditor5-bbcode/src/rules/BBCodeColor.ts b/packages/ckeditor5-bbcode/src/rules/BBCodeColor.ts
      index a83b79f8fb..41f7f7116e 100644
      --- a/packages/ckeditor5-bbcode/src/rules/BBCodeColor.ts
      +++ b/packages/ckeditor5-bbcode/src/rules/BBCodeColor.ts
      @@ -1,5 +1,6 @@
      -import { getColor, RgbColor } from "@coremedia/ckeditor5-dom-support";
      -import { BBCodeProcessingRule } from "./BBCodeProcessingRule";
      +import type { RgbColor } from "@coremedia/ckeditor5-dom-support";
      +import { getColor } from "@coremedia/ckeditor5-dom-support";
      +import type { BBCodeProcessingRule } from "./BBCodeProcessingRule";
       
       /**
        * Maps a color to a representation suitable as unique argument to the
      diff --git a/packages/ckeditor5-bbcode/src/rules/BBCodeHeading.ts b/packages/ckeditor5-bbcode/src/rules/BBCodeHeading.ts
      index d05f117692..bcc82c1f36 100644
      --- a/packages/ckeditor5-bbcode/src/rules/BBCodeHeading.ts
      +++ b/packages/ckeditor5-bbcode/src/rules/BBCodeHeading.ts
      @@ -1,4 +1,4 @@
      -import { BBCodeProcessingRule } from "./BBCodeProcessingRule";
      +import type { BBCodeProcessingRule } from "./BBCodeProcessingRule";
       
       /**
        * Regular expression to match heading levels 1 to 6.
      diff --git a/packages/ckeditor5-bbcode/src/rules/BBCodeImg.ts b/packages/ckeditor5-bbcode/src/rules/BBCodeImg.ts
      index 49c14693b0..23f50b852e 100644
      --- a/packages/ckeditor5-bbcode/src/rules/BBCodeImg.ts
      +++ b/packages/ckeditor5-bbcode/src/rules/BBCodeImg.ts
      @@ -1,5 +1,5 @@
       import { isHTMLImageElement } from "@coremedia/ckeditor5-dom-support";
      -import { BBCodeProcessingRule } from "./BBCodeProcessingRule";
      +import type { BBCodeProcessingRule } from "./BBCodeProcessingRule";
       import { toBBCodeStringAttributeValue, toBBCodeUrl } from "./EscapeUtils";
       
       /**
      diff --git a/packages/ckeditor5-bbcode/src/rules/BBCodeItalic.ts b/packages/ckeditor5-bbcode/src/rules/BBCodeItalic.ts
      index 10f4e50620..b0288eef79 100644
      --- a/packages/ckeditor5-bbcode/src/rules/BBCodeItalic.ts
      +++ b/packages/ckeditor5-bbcode/src/rules/BBCodeItalic.ts
      @@ -1,4 +1,4 @@
      -import { BBCodeProcessingRule } from "./BBCodeProcessingRule";
      +import type { BBCodeProcessingRule } from "./BBCodeProcessingRule";
       
       /**
        * Possible HTML tags that denote content rendered in italic.
      diff --git a/packages/ckeditor5-bbcode/src/rules/BBCodeList.ts b/packages/ckeditor5-bbcode/src/rules/BBCodeList.ts
      index 51bc54008a..c466f3a101 100644
      --- a/packages/ckeditor5-bbcode/src/rules/BBCodeList.ts
      +++ b/packages/ckeditor5-bbcode/src/rules/BBCodeList.ts
      @@ -1,4 +1,4 @@
      -import { BBCodeProcessingRule } from "./BBCodeProcessingRule";
      +import type { BBCodeProcessingRule } from "./BBCodeProcessingRule";
       
       /**
        * Maps `
        `/`
          to `[list]`. Respects the `type` attribute for diff --git a/packages/ckeditor5-bbcode/src/rules/BBCodeListItem.ts b/packages/ckeditor5-bbcode/src/rules/BBCodeListItem.ts index 6572ef3b0f..01119250cd 100644 --- a/packages/ckeditor5-bbcode/src/rules/BBCodeListItem.ts +++ b/packages/ckeditor5-bbcode/src/rules/BBCodeListItem.ts @@ -1,4 +1,4 @@ -import { BBCodeProcessingRule } from "./BBCodeProcessingRule"; +import type { BBCodeProcessingRule } from "./BBCodeProcessingRule"; /** * Rule that maps `
        • ` to `[*]`. diff --git a/packages/ckeditor5-bbcode/src/rules/BBCodeParagraph.ts b/packages/ckeditor5-bbcode/src/rules/BBCodeParagraph.ts index 5c3c7c9be2..bbad8cf587 100644 --- a/packages/ckeditor5-bbcode/src/rules/BBCodeParagraph.ts +++ b/packages/ckeditor5-bbcode/src/rules/BBCodeParagraph.ts @@ -1,4 +1,4 @@ -import { BBCodeProcessingRule } from "./BBCodeProcessingRule"; +import type { BBCodeProcessingRule } from "./BBCodeProcessingRule"; /** * Rule that transforms a paragraph element to its content followed by two diff --git a/packages/ckeditor5-bbcode/src/rules/BBCodeQuote.ts b/packages/ckeditor5-bbcode/src/rules/BBCodeQuote.ts index 4a99f83ca2..6697d058bc 100644 --- a/packages/ckeditor5-bbcode/src/rules/BBCodeQuote.ts +++ b/packages/ckeditor5-bbcode/src/rules/BBCodeQuote.ts @@ -1,4 +1,4 @@ -import { BBCodeProcessingRule } from "./BBCodeProcessingRule"; +import type { BBCodeProcessingRule } from "./BBCodeProcessingRule"; /** * Maps `
          ` to `[quote]`. diff --git a/packages/ckeditor5-bbcode/src/rules/BBCodeSize.ts b/packages/ckeditor5-bbcode/src/rules/BBCodeSize.ts index 469755e69c..b83b28a6d0 100644 --- a/packages/ckeditor5-bbcode/src/rules/BBCodeSize.ts +++ b/packages/ckeditor5-bbcode/src/rules/BBCodeSize.ts @@ -1,5 +1,6 @@ -import { FontSizeConfiguration, fontSizes, normalSize } from "../utils/FontSizes"; -import { BBCodeProcessingRule } from "./BBCodeProcessingRule"; +import type { FontSizeConfiguration } from "../utils/FontSizes"; +import { fontSizes, normalSize } from "../utils/FontSizes"; +import type { BBCodeProcessingRule } from "./BBCodeProcessingRule"; /** * Tries to find an applicable font-size configuration for the given element. diff --git a/packages/ckeditor5-bbcode/src/rules/BBCodeStrikethrough.ts b/packages/ckeditor5-bbcode/src/rules/BBCodeStrikethrough.ts index 959f681b5f..dd0641b5da 100644 --- a/packages/ckeditor5-bbcode/src/rules/BBCodeStrikethrough.ts +++ b/packages/ckeditor5-bbcode/src/rules/BBCodeStrikethrough.ts @@ -1,4 +1,4 @@ -import { BBCodeProcessingRule } from "./BBCodeProcessingRule"; +import type { BBCodeProcessingRule } from "./BBCodeProcessingRule"; /** * HTML Elements that may denote a "strike-through" state. diff --git a/packages/ckeditor5-bbcode/src/rules/BBCodeUnderline.ts b/packages/ckeditor5-bbcode/src/rules/BBCodeUnderline.ts index e3a36ac882..d4c9b4eb7c 100644 --- a/packages/ckeditor5-bbcode/src/rules/BBCodeUnderline.ts +++ b/packages/ckeditor5-bbcode/src/rules/BBCodeUnderline.ts @@ -1,4 +1,4 @@ -import { BBCodeProcessingRule } from "./BBCodeProcessingRule"; +import type { BBCodeProcessingRule } from "./BBCodeProcessingRule"; /** * HTML Elements that may denote a "underline" state. diff --git a/packages/ckeditor5-bbcode/src/rules/BBCodeUrl.ts b/packages/ckeditor5-bbcode/src/rules/BBCodeUrl.ts index 9cac57f3db..4c65fe8ef3 100644 --- a/packages/ckeditor5-bbcode/src/rules/BBCodeUrl.ts +++ b/packages/ckeditor5-bbcode/src/rules/BBCodeUrl.ts @@ -1,5 +1,5 @@ import { isHTMLAnchorElement } from "@coremedia/ckeditor5-dom-support"; -import { BBCodeProcessingRule } from "./BBCodeProcessingRule"; +import type { BBCodeProcessingRule } from "./BBCodeProcessingRule"; import { toBBCodeUrl } from "./EscapeUtils"; /** diff --git a/packages/ckeditor5-bbcode/src/rules/bbCodeDefaultRules.ts b/packages/ckeditor5-bbcode/src/rules/bbCodeDefaultRules.ts index 58a18eeeb1..20e1aac4fa 100644 --- a/packages/ckeditor5-bbcode/src/rules/bbCodeDefaultRules.ts +++ b/packages/ckeditor5-bbcode/src/rules/bbCodeDefaultRules.ts @@ -1,4 +1,4 @@ -import { BBCodeProcessingRule } from "./BBCodeProcessingRule"; +import type { BBCodeProcessingRule } from "./BBCodeProcessingRule"; import { bbCodeBold } from "./BBCodeBold"; import { bbCodeHeading } from "./BBCodeHeading"; import { bbCodeItalic } from "./BBCodeItalic"; diff --git a/packages/ckeditor5-bbcode/test/BBCodeBold.test.ts b/packages/ckeditor5-bbcode/test/BBCodeBold.test.ts new file mode 100644 index 0000000000..84bac521b3 --- /dev/null +++ b/packages/ckeditor5-bbcode/test/BBCodeBold.test.ts @@ -0,0 +1,144 @@ +import "global-jsdom/register"; +import type { TestContext } from "node:test"; +import test, { describe } from "node:test"; +import expect from "expect"; +import type { IsBoldFontWeight } from "../src/rules/BBCodeBold"; +import { BBCodeBold, bbCodeBold } from "../src/rules/BBCodeBold"; +import { requireHTMLElement } from "./DOMUtils"; + +void describe("BBCodeBold", () => { + void describe("Default Configuration", () => { + const rule = bbCodeBold; + + const cases = [ + { + dataView: `TEXT`, + expected: `[b]TEXT[/b]`, + comment: `BBob HTML 5 Preset Result (toView)`, + }, + { + dataView: `TEXT`, + expected: `[b]TEXT[/b]`, + comment: `also supporting _bolder_ to some degree`, + }, + { + dataView: `TEXT`, + expected: `[b]TEXT[/b]`, + comment: `CKEditor 5 default data view representation`, + }, + { + dataView: `TEXT`, + expected: `[b]TEXT[/b]`, + comment: `alternative HTML 5 representation`, + }, + { + dataView: `TEXT`, + expected: undefined, + comment: `vetoed bold tag; undefined, so other rules may kick in`, + }, + { + dataView: `TEXT`, + expected: undefined, + comment: `vetoed bold tag; undefined, so other rules may kick in`, + }, + { + dataView: `TEXT`, + expected: `[b]TEXT[/b]`, + comment: `weight and tag agree`, + }, + { + dataView: `TEXT`, + expected: undefined, + comment: `veto by numeric font-weight (400)`, + }, + { + dataView: `TEXT`, + expected: `[b]TEXT[/b]`, + comment: `Bold just by font-weight.`, + }, + { + dataView: `TEXT`, + expected: undefined, + comment: `Minimum font-weight.`, + }, + { + dataView: `TEXT`, + expected: `[b]TEXT[/b]`, + comment: `Maximum font-weight.`, + }, + { + dataView: `TEXT`, + expected: `[b]TEXT[/b]`, + comment: `corner case: "" itself will be handled by outer rules`, + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { dataView, expected, comment }] of cases.entries()) { + await t.test(`[${i}] Should process '${dataView}' to '${expected}' (${comment})`, () => { + const element = requireHTMLElement(dataView); + const bbCode = rule.toData(element, element.textContent ?? ""); + expect(bbCode).toEqual(expected); + }); + } + }); + }); + + void describe("Custom Configuration", () => { + /** + * For demonstration only: Don't judge on font-weight, just on tag name. + */ + const ignoreFontWeight: IsBoldFontWeight = (assumedBold) => assumedBold; + const rule = new BBCodeBold({ + isBold: ignoreFontWeight, + }); + + const cases = [ + { + dataView: `TEXT`, + expected: undefined, + comment: `Respect custom isBold rule to ignore font-weight style`, + }, + { + dataView: `TEXT`, + expected: undefined, + comment: `Respect custom isBold rule to ignore font-weight style`, + }, + { + dataView: `TEXT`, + expected: `[b]TEXT[/b]`, + comment: `CKEditor 5 default data view representation`, + }, + { + dataView: `TEXT`, + expected: `[b]TEXT[/b]`, + comment: `Respect custom isBold rule to ignore font-weight style`, + }, + { + dataView: `TEXT`, + expected: `[b]TEXT[/b]`, + comment: `Respect custom isBold rule to ignore font-weight style`, + }, + { + dataView: `TEXT`, + expected: `[b]TEXT[/b]`, + comment: `Respect custom isBold rule to ignore font-weight style`, + }, + { + dataView: `TEXT`, + expected: undefined, + comment: `Respect custom isBold rule to ignore font-weight style`, + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { dataView, expected, comment }] of cases.entries()) { + await t.test(`[${i}] Should process '${dataView}' to '${expected}' (${comment})`, () => { + const element = requireHTMLElement(dataView); + const bbCode = rule.toData(element, element.textContent ?? ""); + expect(bbCode).toEqual(expected); + }); + } + }); + }); +}); diff --git a/packages/ckeditor5-bbcode/test/BBCodeCode.test.ts b/packages/ckeditor5-bbcode/test/BBCodeCode.test.ts new file mode 100644 index 0000000000..52237bf503 --- /dev/null +++ b/packages/ckeditor5-bbcode/test/BBCodeCode.test.ts @@ -0,0 +1,129 @@ +import "global-jsdom/register"; +import type { TestContext } from "node:test"; +import test, { describe } from "node:test"; +import expect from "expect"; +import { BBCodeCode, bbCodeCode } from "../src/rules/BBCodeCode"; +import { requireHTMLElement } from "./DOMUtils"; + +void describe("BBCodeCode", () => { + void describe("Default Configuration", () => { + const rule = bbCodeCode; + + const cases = [ + { + dataView: `
          TEXT
          `, + expected: `[code]\nTEXT\n[/code]\n`, + comment: `newlines for minor pretty-printing`, + }, + { + dataView: `
          TEXT
          `, + expected: `[code=css]\nTEXT\n[/code]\n`, + comment: `respect language`, + }, + { + dataView: `
          TEXT
          `, + expected: `[code]\nTEXT\n[/code]\n`, + comment: `strip irrelevant plaintext language`, + }, + { + dataView: `
          \nTEXT\n
          `, + expected: `[code]\nTEXT\n[/code]\n`, + comment: `trimming: don't pile up newlines (here: in
          )`,
          +      },
          +      {
          +        dataView: `
          \nTEXT\n
          `, + expected: `[code]\nTEXT\n[/code]\n`, + comment: `trimming: don't pile up newlines (here: in )`, + }, + { + dataView: `
          \n\n\n\nTEXT\n\n\n\n
          `, + expected: `[code]\nTEXT\n[/code]\n`, + comment: `trimming: even remove extra newlines`, + }, + { + dataView: `
          \n  TEXT1\n  TEXT2\n
          `, + expected: `[code]\n TEXT1\n TEXT2\n[/code]\n`, + comment: `trimming: must not remove indents`, + }, + { + dataView: `
          TEXT  \n
          `, + expected: `[code]\nTEXT\n[/code]\n`, + comment: `trimming (Design Scope): We put some extra effort removing irrelevant blanks at the end.`, + }, + { + dataView: `

          TEXT

          `, + expected: undefined, + comment: `ignore unmatched`, + }, + { + dataView: `
          TEXT
          `, + expected: `[code]\nTEXT\n[/code]\n`, + comment: `robustness: ignore possible missing nested element`, + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { dataView, expected, comment }] of cases.entries()) { + await t.test(`[${i}] Should process '${dataView}' to '${expected}' (${comment})`, () => { + const element = requireHTMLElement(dataView); + const bbCode = rule.toData(element, element.textContent ?? ""); + expect(bbCode).toEqual(expected); + }); + } + }); + }); + + void describe("Custom Configuration", () => { + const isUnsetCases = [ + { + dataView: `
          TEXT
          `, + expected: `[code=css]\nTEXT\n[/code]\n`, + comment: `respect language`, + }, + { + dataView: `
          TEXT
          `, + expected: `[code]\nTEXT\n[/code]\n`, + comment: `strip irrelevant language by custom config`, + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { dataView, expected, comment }] of isUnsetCases.entries()) { + await t.test(`[${i}] Custom isUnset: Should process '${dataView}' to '${expected}' (${comment})`, () => { + const rule = new BBCodeCode({ + isUnset: (lang) => lang === "ignored", + }); + const element = requireHTMLElement(dataView); + const bbCode = rule.toData(element, element.textContent ?? ""); + expect(bbCode).toEqual(expected); + }); + } + }); + + const fromClassCases = [ + { + dataView: `
          TEXT
          `, + expected: `[code=css]\nTEXT\n[/code]\n`, + comment: `respect language by custom extractor`, + }, + { + dataView: `
          TEXT
          `, + expected: `[code]\nTEXT\n[/code]\n`, + comment: `strip irrelevant plaintext language`, + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { dataView, expected, comment }] of fromClassCases.entries()) { + await t.test(`[${i}] Custom fromClass: Should process '${dataView}' to '${expected}' (${comment})`, () => { + const rule = new BBCodeCode({ + fromClass: (entry) => entry, + }); + const element = requireHTMLElement(dataView); + const bbCode = rule.toData(element, element.textContent ?? ""); + expect(bbCode).toEqual(expected); + }); + } + }); + }); +}); diff --git a/packages/ckeditor5-bbcode/test/BBCodeColor.test.ts b/packages/ckeditor5-bbcode/test/BBCodeColor.test.ts new file mode 100644 index 0000000000..a49886fc43 --- /dev/null +++ b/packages/ckeditor5-bbcode/test/BBCodeColor.test.ts @@ -0,0 +1,105 @@ +import "global-jsdom/register"; +import type { TestContext } from "node:test"; +import test, { describe } from "node:test"; +import expect from "expect"; +import type { RgbColor } from "@coremedia/ckeditor5-dom-support"; +import { w3ExtendedColorNames } from "@coremedia/ckeditor5-dom-support"; +import type { ColorMapper } from "../src/rules/BBCodeColor"; +import { BBCodeColor, bbCodeColor } from "../src/rules/BBCodeColor"; +import { requireHTMLElement } from "./DOMUtils"; + +const reverseW3CColorMap = Object.fromEntries(Object.entries(w3ExtendedColorNames).map(([key, value]) => [value, key])); + +const enforceHexRepresentation: ColorMapper = (color: string | RgbColor): string => { + if (typeof color === "string") { + return reverseW3CColorMap[color.toLowerCase()] ?? color; + } + return color.toHex(); +}; + +void describe("BBCodeColor", () => { + void test("Default Configuration", async (t: TestContext) => { + const rule = bbCodeColor; + + const cases: { dataView: string; expected: string; comment: string }[] = [ + { + dataView: `TEXT`, + expected: `[color=#ff0001]TEXT[/color]`, + comment: "BBob HTML 5 Preset Result (toView)", + }, + { + dataView: `TEXT`, + expected: `[color=#ff0001]TEXT[/color]`, + comment: "ignore case", + }, + { + dataView: `TEXT`, + expected: `[color=red]TEXT[/color]`, + comment: "Prefer W3C Color Names", + }, + { + dataView: `TEXT`, + expected: `[color=fuchsia]TEXT[/color]`, + comment: "supported color names", + }, + { + dataView: `TEXT`, + expected: `[color=#cccccc]TEXT[/color]`, + comment: "support shortened color codes", + }, + { + dataView: `TEXT`, + expected: `[color=red]TEXT[/color]`, + comment: "also support rgb() codes", + }, + { + dataView: `TEXT`, + expected: `[color=#ff0000a0]TEXT[/color]`, + comment: "also support rgba() codes", + }, + ]; + + for (const { dataView, expected, comment } of cases) { + await t.test(`Should process '${dataView}' to '${expected}' (${comment})`, () => { + const element = requireHTMLElement(dataView); + const bbCode = rule.toData(element, element.textContent ?? ""); + expect(bbCode).toEqual(expected); + }); + } + }); + + void test("Custom Color Mapper Configuration", async (t: TestContext) => { + const rule = new BBCodeColor({ mapper: enforceHexRepresentation }); + + const cases: { dataView: string; expected: string; comment: string }[] = [ + { + dataView: `TEXT`, + expected: `[color=#ff0000]TEXT[/color]`, + comment: "", + }, + { + dataView: `TEXT`, + expected: `[color=#ff00ff]TEXT[/color]`, + comment: "prefer hex over color name", + }, + { + dataView: `TEXT`, + expected: `[color=#ff00ff]TEXT[/color]`, + comment: "ignore case", + }, + { + dataView: `TEXT`, + expected: `[color=#ff0000]TEXT[/color]`, + comment: "prefer hex over color name", + }, + ]; + + for (const { dataView, expected, comment } of cases) { + await t.test(`Should process '${dataView}' to '${expected}' (${comment})`, () => { + const element = requireHTMLElement(dataView); + const bbCode = rule.toData(element, element.textContent ?? ""); + expect(bbCode).toEqual(expected); + }); + } + }); +}); diff --git a/packages/ckeditor5-bbcode/test/BBCodeHeading.test.ts b/packages/ckeditor5-bbcode/test/BBCodeHeading.test.ts new file mode 100644 index 0000000000..482ad05022 --- /dev/null +++ b/packages/ckeditor5-bbcode/test/BBCodeHeading.test.ts @@ -0,0 +1,31 @@ +import "global-jsdom/register"; +import type { TestContext } from "node:test"; +import test, { describe } from "node:test"; +import expect from "expect"; +import { bbCodeHeading } from "../src/rules/BBCodeHeading"; +import { requireHTMLElement } from "./DOMUtils"; + +const prettyPrintNewlines = "\n\n"; + +void describe("BBCodeHeading", () => { + void test("Default Configuration", async (t: TestContext) => { + const rule = bbCodeHeading; + + const cases: { dataView: string; expected: string }[] = [ + { dataView: `

          TEXT

          `, expected: `[h1]TEXT[/h1]${prettyPrintNewlines}` }, + { dataView: `

          TEXT

          `, expected: `[h2]TEXT[/h2]${prettyPrintNewlines}` }, + { dataView: `

          TEXT

          `, expected: `[h3]TEXT[/h3]${prettyPrintNewlines}` }, + { dataView: `

          TEXT

          `, expected: `[h4]TEXT[/h4]${prettyPrintNewlines}` }, + { dataView: `
          TEXT
          `, expected: `[h5]TEXT[/h5]${prettyPrintNewlines}` }, + { dataView: `
          TEXT
          `, expected: `[h6]TEXT[/h6]${prettyPrintNewlines}` }, + ]; + + for (const { dataView, expected } of cases) { + await t.test(`Should process '${dataView}' to '${expected}'`, () => { + const element = requireHTMLElement(dataView); + const bbCode = rule.toData(element, element.textContent ?? ""); + expect(bbCode).toEqual(expected); + }); + } + }); +}); diff --git a/packages/ckeditor5-bbcode/test/BBCodeImg.test.ts b/packages/ckeditor5-bbcode/test/BBCodeImg.test.ts new file mode 100644 index 0000000000..73df988898 --- /dev/null +++ b/packages/ckeditor5-bbcode/test/BBCodeImg.test.ts @@ -0,0 +1,64 @@ +import "global-jsdom/register"; +import type { TestContext } from "node:test"; +import test, { describe } from "node:test"; +import expect from "expect"; +import { bbCodeImg } from "../src/rules/BBCodeImg"; +import { requireHTMLElement } from "./DOMUtils"; + +void describe("BBCodeImg", () => { + void test("Default Configuration", async (t: TestContext) => { + const rule = bbCodeImg; + const someUrl = "https://example.org/"; + + const cases: { dataView: string; expected: string | undefined; comment: string }[] = [ + { + dataView: ``, + expected: `[img]${someUrl}[/img]`, + comment: "Default Mapping Use-Case", + }, + { + dataView: ``, + expected: `[img]${someUrl}?openBracket=%5B[/img]`, + comment: "Escape Open-Bracket [", + }, + { + dataView: ``, + expected: `[img]${someUrl}?closeBracket=%5D[/img]`, + comment: "Escape Close-Bracket [", + }, + { + dataView: ``, + expected: `[img]${someUrl}?quote="[/img]`, + comment: `Don't escape double quote as it is rendered as content`, + }, + { + dataView: ``, + expected: undefined, + comment: `As there is no representation for "empty URLs" in BBCode, not mapping to [img]`, + }, + { + dataView: ``, + expected: `[img]/relative[/img]`, + comment: "Keep relative URLs (1)", + }, + { + dataView: ``, + expected: `[img]?search=param[/img]`, + comment: "Keep relative URLs, search-param only (2)", + }, + { + dataView: ``, + expected: `[img]#hash[/img]`, + comment: "Keep relative URLs, hash-param only (3)", + }, + ]; + + for (const { dataView, expected, comment } of cases) { + await t.test(`Should process '${dataView}' to '${expected}' (${comment})`, () => { + const element = requireHTMLElement(dataView); + const bbCode = rule.toData(element); + expect(bbCode).toEqual(expected); + }); + } + }); +}); diff --git a/packages/ckeditor5-bbcode/test/BBCodeItalic.test.ts b/packages/ckeditor5-bbcode/test/BBCodeItalic.test.ts new file mode 100644 index 0000000000..cac4a00231 --- /dev/null +++ b/packages/ckeditor5-bbcode/test/BBCodeItalic.test.ts @@ -0,0 +1,50 @@ +import "global-jsdom/register"; +import type { TestContext } from "node:test"; +import test, { describe } from "node:test"; +import expect from "expect"; +import { bbCodeItalic } from "../src/rules/BBCodeItalic"; +import { requireHTMLElement } from "./DOMUtils"; + +void describe("BBCodeItalic", () => { + void describe("Default Configuration", () => { + const rule = bbCodeItalic; + + const cases = [ + { + dataView: `TEXT`, + expected: `[i]TEXT[/i]`, + comment: `BBob HTML 5 Preset Result (toView)`, + }, + { + dataView: `TEXT`, + expected: `[i]TEXT[/i]`, + comment: `CKEditor 5 default data view representation`, + }, + { + dataView: `TEXT`, + expected: `[i]TEXT[/i]`, + comment: `alternative HTML 5 representation`, + }, + { + dataView: `TEXT`, + expected: undefined, + comment: `vetoed italic tag; undefined, so other rules may kick in`, + }, + { + dataView: `TEXT`, + expected: `[i]TEXT[/i]`, + comment: `Corner case: "" will be handled by outer rules.`, + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { dataView, expected, comment }] of cases.entries()) { + await t.test(`[${i}] Should process '${dataView}' to '${expected}' (${comment})`, () => { + const element = requireHTMLElement(dataView); + const bbCode = rule.toData(element, element.textContent ?? ""); + expect(bbCode).toEqual(expected); + }); + } + }); + }); +}); diff --git a/packages/ckeditor5-bbcode/test/BBCodeList.test.ts b/packages/ckeditor5-bbcode/test/BBCodeList.test.ts new file mode 100644 index 0000000000..5402679cfa --- /dev/null +++ b/packages/ckeditor5-bbcode/test/BBCodeList.test.ts @@ -0,0 +1,77 @@ +import "global-jsdom/register"; +import type { TestContext } from "node:test"; +import test, { describe } from "node:test"; +import expect from "expect"; +import { bbCodeList } from "../src/rules/BBCodeList"; +import { requireHTMLElement } from "./DOMUtils"; + +// No integration test here. Simulate, we already mapped the children. +const mockListItemsContent = (el: HTMLElement): string => + Array.from(el.children) + .map((e) => `[*] ${e.textContent ?? ""}`) + .join("\n"); + +void describe("BBCodeList", () => { + void describe("Default Configuration", () => { + const rule = bbCodeList; + + const cases = [ + { + dataView: `
          • TEXT
          `, + expected: `[list]\n[*] TEXT\n[/list]\n`, + comment: ``, + }, + { + dataView: `
          1. TEXT
          `, + expected: `[list=1]\n[*] TEXT\n[/list]\n`, + comment: ``, + }, + { + dataView: `
          1. TEXT
          `, + expected: `[list=1]\n[*] TEXT\n[/list]\n`, + comment: ``, + }, + { + dataView: `
          1. TEXT
          `, + expected: `[list=a]\n[*] TEXT\n[/list]\n`, + comment: ``, + }, + { + dataView: `
          1. TEXT
          `, + expected: `[list=A]\n[*] TEXT\n[/list]\n`, + comment: ``, + }, + { + dataView: `
          1. TEXT
          `, + expected: `[list=i]\n[*] TEXT\n[/list]\n`, + comment: ``, + }, + { + dataView: `
          1. TEXT
          `, + expected: `[list=I]\n[*] TEXT\n[/list]\n`, + comment: ``, + }, + { + dataView: `
          1. TEXT
          `, + expected: `[list=1]\n[*] TEXT\n[/list]\n`, + comment: `Due to BBob Preset-HTML5 Restrictions not respecting list-style-type for now.`, + }, + { + dataView: `
          • TEXT\n\n
          `, + expected: `[list]\n[*] TEXT\n[/list]\n`, + comment: `pretty-print trimming`, + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { dataView, expected, comment }] of cases.entries()) { + await t.test(`[${i}] Should process '${dataView}' to '${expected}' (${comment})`, () => { + const element = requireHTMLElement(dataView); + const content = mockListItemsContent(element); + const bbCode = rule.toData(element, content); + expect(bbCode).toEqual(expected); + }); + } + }); + }); +}); diff --git a/packages/ckeditor5-bbcode/__tests__/BBCodeListItem.test.ts b/packages/ckeditor5-bbcode/test/BBCodeListItem.test.ts similarity index 51% rename from packages/ckeditor5-bbcode/__tests__/BBCodeListItem.test.ts rename to packages/ckeditor5-bbcode/test/BBCodeListItem.test.ts index 0a6f8e13c8..021f9e73c6 100644 --- a/packages/ckeditor5-bbcode/__tests__/BBCodeListItem.test.ts +++ b/packages/ckeditor5-bbcode/test/BBCodeListItem.test.ts @@ -1,16 +1,18 @@ -import { bbCodeListItem } from "../src"; +import "global-jsdom/register"; +import type { TestContext } from "node:test"; +import test, { describe } from "node:test"; +import expect from "expect"; +import { bbCodeListItem } from "../src/rules/BBCodeListItem"; import { asHTMLElement, requireHTMLElement } from "./DOMUtils"; -describe("BBCodeListItem", () => { - describe("Default Configuration", () => { +void describe("BBCodeListItem", () => { + void test("Default Configuration", async (t: TestContext) => { const rule = bbCodeListItem; - it.each` - dataView | expected - ${`
        • TEXT
        • `} | ${`[*] TEXT\n`} - `( - "[$#] Should process '$dataView' to '$expected'", - ({ dataView, expected }: { dataView: string; expected: string | undefined }) => { + const cases: { dataView: string; expected: string }[] = [{ dataView: `
        • TEXT
        • `, expected: `[*] TEXT\n` }]; + + for (const { dataView, expected } of cases) { + await t.test(`Should process '${dataView}' to '${expected}'`, () => { const embeddedInListDataView = `
            ${dataView}
          `; const listElement = requireHTMLElement(embeddedInListDataView); const element = asHTMLElement(listElement.firstElementChild); @@ -20,7 +22,7 @@ describe("BBCodeListItem", () => { const content = element.textContent ?? ""; const bbCode = rule.toData(element, content); expect(bbCode).toEqual(expected); - }, - ); + }); + } }); }); diff --git a/packages/ckeditor5-bbcode/test/BBCodeParagraph.test.ts b/packages/ckeditor5-bbcode/test/BBCodeParagraph.test.ts new file mode 100644 index 0000000000..d1df8b67f3 --- /dev/null +++ b/packages/ckeditor5-bbcode/test/BBCodeParagraph.test.ts @@ -0,0 +1,26 @@ +import "global-jsdom/register"; +import type { TestContext } from "node:test"; +import test, { describe } from "node:test"; +import expect from "expect"; +import { bbCodeParagraph } from "../src/rules/BBCodeParagraph"; +import { requireHTMLElement } from "./DOMUtils"; + +const prettyPrintNewlines = "\n\n"; + +void describe("BBCodeParagraph", () => { + void test("Default Configuration", async (t: TestContext) => { + const rule = bbCodeParagraph; + + const cases: { dataView: string; expected: string }[] = [ + { dataView: `

          TEXT

          `, expected: `TEXT${prettyPrintNewlines}` }, + ]; + + for (const { dataView, expected } of cases) { + await t.test(`Should process '${dataView}' to '${expected}'`, () => { + const element = requireHTMLElement(dataView); + const bbCode = rule.toData(element, element.textContent ?? ""); + expect(bbCode).toEqual(expected); + }); + } + }); +}); diff --git a/packages/ckeditor5-bbcode/test/BBCodeQuote.test.ts b/packages/ckeditor5-bbcode/test/BBCodeQuote.test.ts new file mode 100644 index 0000000000..2731c82731 --- /dev/null +++ b/packages/ckeditor5-bbcode/test/BBCodeQuote.test.ts @@ -0,0 +1,31 @@ +import "global-jsdom/register"; +import type { TestContext } from "node:test"; +import test, { describe } from "node:test"; +import expect from "expect"; +import { bbCodeQuote } from "../src/rules/BBCodeQuote"; +import { requireHTMLElement } from "./DOMUtils"; + +void describe("BBCodeQuote", () => { + void describe("Default Configuration", () => { + const rule = bbCodeQuote; + + const cases = [ + { + dataView: `

          TEXT

          `, + expected: `[quote]\nTEXT\n[/quote]\n`, + comment: `newlines for minor pretty-printing`, + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { dataView, expected, comment }] of cases.entries()) { + await t.test(`[${i}] Should process '${dataView}' to '${expected}' (${comment})`, () => { + const element = requireHTMLElement(dataView); + // Simple processing only applies at one level, so nested tests not possible here. + const bbCode = rule.toData(element, element.textContent ?? ""); + expect(bbCode).toEqual(expected); + }); + } + }); + }); +}); diff --git a/packages/ckeditor5-bbcode/test/BBCodeSize.test.ts b/packages/ckeditor5-bbcode/test/BBCodeSize.test.ts new file mode 100644 index 0000000000..f6d08f757a --- /dev/null +++ b/packages/ckeditor5-bbcode/test/BBCodeSize.test.ts @@ -0,0 +1,45 @@ +import "global-jsdom/register"; +import type { TestContext } from "node:test"; +import test, { describe } from "node:test"; +import expect from "expect"; +import { bbCodeSize } from "../src/rules/BBCodeSize"; +import { requireHTMLElement } from "./DOMUtils"; + +void describe("BBCodeSize", () => { + void describe("Default Configuration", () => { + const rule = bbCodeSize; + + const cases = [ + { + dataView: `T`, + expected: `[size=70]T[/size]`, + comment: `text-tiny mapping to "representing" number`, + }, + { + dataView: `T`, + expected: `[size=85]T[/size]`, + comment: `text-small mapping to "representing" number`, + }, + { + dataView: `T`, + expected: `[size=140]T[/size]`, + comment: `text-big mapping to "representing" number`, + }, + { + dataView: `T`, + expected: `[size=180]T[/size]`, + comment: `text-huge mapping to "representing" number`, + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { dataView, expected, comment }] of cases.entries()) { + await t.test(`[${i}] Should process '${dataView}' to '${expected}' (${comment})`, () => { + const element = requireHTMLElement(dataView); + const bbCode = rule.toData(element, element.textContent ?? ""); + expect(bbCode).toEqual(expected); + }); + } + }); + }); +}); diff --git a/packages/ckeditor5-bbcode/test/BBCodeStrikethrough.test.ts b/packages/ckeditor5-bbcode/test/BBCodeStrikethrough.test.ts new file mode 100644 index 0000000000..040ae8fb4b --- /dev/null +++ b/packages/ckeditor5-bbcode/test/BBCodeStrikethrough.test.ts @@ -0,0 +1,55 @@ +import "global-jsdom/register"; +import type { TestContext } from "node:test"; +import test, { describe } from "node:test"; +import expect from "expect"; +import { bbCodeStrikethrough } from "../src/rules/BBCodeStrikethrough"; +import { requireHTMLElement } from "./DOMUtils"; + +void describe("BBCodeStrikethrough", () => { + void describe("Default Configuration", () => { + const rule = bbCodeStrikethrough; + + const cases = [ + { + dataView: `TEXT`, + expected: `[s]TEXT[/s]`, + comment: `BBob HTML 5 Preset Result (toView)`, + }, + { + dataView: `TEXT`, + expected: `[s]TEXT[/s]`, + comment: `CKEditor 5 default data view representation`, + }, + { + dataView: `TEXT`, + expected: `[s]TEXT[/s]`, + comment: `alternative HTML5 representation`, + }, + { + dataView: `TEXT`, + expected: `[s]TEXT[/s]`, + comment: `alternative (deprecated) representation`, + }, + { + dataView: `TEXT`, + expected: undefined, + comment: `vetoed by style; undefined, so other rules may kick in`, + }, + { + dataView: `TEXT`, + expected: `[s]TEXT[/s]`, + comment: `corner case: "" itself will be handled by outer rules`, + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { dataView, expected, comment }] of cases.entries()) { + await t.test(`[${i}] Should process '${dataView}' to '${expected}' (${comment})`, () => { + const element = requireHTMLElement(dataView); + const bbCode = rule.toData(element, element.textContent ?? ""); + expect(bbCode).toEqual(expected); + }); + } + }); + }); +}); diff --git a/packages/ckeditor5-bbcode/test/BBCodeUnderline.test.ts b/packages/ckeditor5-bbcode/test/BBCodeUnderline.test.ts new file mode 100644 index 0000000000..82bcebac8a --- /dev/null +++ b/packages/ckeditor5-bbcode/test/BBCodeUnderline.test.ts @@ -0,0 +1,50 @@ +import "global-jsdom/register"; +import type { TestContext } from "node:test"; +import test, { describe } from "node:test"; +import expect from "expect"; +import { bbCodeUnderline } from "../src/rules/BBCodeUnderline"; +import { requireHTMLElement } from "./DOMUtils"; + +void describe("BBCodeUnderline", () => { + void describe("Default Configuration", () => { + const rule = bbCodeUnderline; + + const cases = [ + { + dataView: `TEXT`, + expected: `[u]TEXT[/u]`, + comment: `BBob HTML 5 Preset Result (toView)`, + }, + { + dataView: `TEXT`, + expected: `[u]TEXT[/u]`, + comment: `CKEditor 5 default data view representation`, + }, + { + dataView: `TEXT`, + expected: `[u]TEXT[/u]`, + comment: `alternative HTML5 representation`, + }, + { + dataView: `TEXT`, + expected: undefined, + comment: `vetoed by style; undefined, so other rules may kick in`, + }, + { + dataView: `TEXT`, + expected: `[u]TEXT[/u]`, + comment: `corner case: "" itself will be handled by outer rules`, + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { dataView, expected, comment }] of cases.entries()) { + await t.test(`[${i}] Should process '${dataView}' to '${expected}' (${comment})`, () => { + const element = requireHTMLElement(dataView); + const bbCode = rule.toData(element, element.textContent ?? ""); + expect(bbCode).toEqual(expected); + }); + } + }); + }); +}); diff --git a/packages/ckeditor5-bbcode/test/BBCodeUrl.test.ts b/packages/ckeditor5-bbcode/test/BBCodeUrl.test.ts new file mode 100644 index 0000000000..369c3ac963 --- /dev/null +++ b/packages/ckeditor5-bbcode/test/BBCodeUrl.test.ts @@ -0,0 +1,81 @@ +import "global-jsdom/register"; +import type { TestContext } from "node:test"; +import test, { describe } from "node:test"; +import expect from "expect"; +import { bbCodeUrl } from "../src/rules/BBCodeUrl"; +import { requireHTMLElement } from "./DOMUtils"; + +void describe("BBCodeUnderline", () => { + void describe("Default Configuration", () => { + const rule = bbCodeUrl; + const someUrl = "https://example.org/"; + + const cases = [ + { + dataView: `TEXT`, + expected: `[url="${someUrl}"]TEXT[/url]`, + comment: `Default Mapping Use-Case`, + }, + { + dataView: `${someUrl}`, + expected: `[url]${someUrl}[/url]`, + comment: `Pretty-Print: Shorten, if applicable`, + }, + { + dataView: `TEXT`, + expected: `[url="${someUrl}?openBracket=%5B"]TEXT[/url]`, + comment: `Escape Open-Bracket [`, + }, + { + dataView: `TEXT`, + expected: `[url="${someUrl}?closeBracket=%5D"]TEXT[/url]`, + comment: `Escape Close-Bracket [`, + }, + { + dataView: `TEXT`, + expected: `[url="${someUrl}?quote=%22"]TEXT[/url]`, + comment: `Escape Double Quote in Attribute`, + }, + { + dataView: `${someUrl}?quote="`, + expected: `[url]${someUrl}?quote="[/url]`, + comment: `Don't escape double quote when rendered as content`, + }, + { + dataView: `${someUrl}?brackets=\\]\\[`, + expected: `[url="${someUrl}?brackets=%5D%5B"]${someUrl}?brackets=\\]\\[[/url]`, + comment: `Escaping of text-content done by previous (outside) processing`, + }, + { + dataView: `TEXT`, + expected: undefined, + comment: `As there is no representation for "empty URLs" in BBCode, not mapping to [url]`, + }, + { + dataView: `TEXT`, + expected: `[url="/relative"]TEXT[/url]`, + comment: `Keep relative URLs (1)`, + }, + { + dataView: `TEXT`, + expected: `[url="?search=param"]TEXT[/url]`, + comment: `Keep relative URLs, search-param only (2)`, + }, + { + dataView: `TEXT`, + expected: `[url="#hash"]TEXT[/url]`, + comment: `Keep relative URLs, hash-param only (3)`, + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { dataView, expected, comment }] of cases.entries()) { + await t.test(`[${i}] Should process '${dataView}' to '${expected}' (${comment})`, () => { + const element = requireHTMLElement(dataView); + const bbCode = rule.toData(element, element.textContent ?? ""); + expect(bbCode).toEqual(expected); + }); + } + }); + }); +}); diff --git a/packages/ckeditor5-bbcode/test/BBobIntegration.test.ts b/packages/ckeditor5-bbcode/test/BBobIntegration.test.ts new file mode 100644 index 0000000000..9b40055833 --- /dev/null +++ b/packages/ckeditor5-bbcode/test/BBobIntegration.test.ts @@ -0,0 +1,386 @@ +/// + +import "global-jsdom/register"; +import type { TestContext } from "node:test"; +import test, { describe } from "node:test"; +import expect from "expect"; +import { bbCodeDefaultRules } from "../src/rules/bbCodeDefaultRules"; +import { html2bbcode } from "../src/html2bbcode"; +import { bbcode2html } from "../src/bbcode2html"; +import { parseAsFragment } from "./DOMUtils"; + +const rules = bbCodeDefaultRules; +const supportedTags = rules.flatMap((r) => r.tags ?? ([] as string[])); + +const aut = { + /** + * Artificial test for consuming result of BBCode to HTML processing from + * the third-party library directly by our proprietary HTML to BBCode + * processing. + * + * This case will not happen in production, where the CKEditor layers + * serve as _mediators_ between these two. Nevertheless, testing that + * they _understand_ each other provides some confidence that nothing + * broke. + * + * @param input - BBCode input (from Data) + */ + bbcode2html2bbcode: ( + input: string, + ): { + input: string; + fromBBCode2Html: string; + fromHtml2BBCode: string; + } => { + const fromBBCode2Html = bbcode2html(input, supportedTags); + const fromHtml2BBCode = html2bbcode(parseAsFragment(fromBBCode2Html), rules); + return { + input, + fromBBCode2Html, + fromHtml2BBCode, + }; + }, + /** + * Transformation pipeline to validate, that the BBCode2HTML processing + * understands the BBCode produced by our proprietary HTML to BBCode + * processing. + * + * @param input - HTML input (from Data View) + */ + html2bbcode2html: ( + input: string, + ): { + input: string; + fromHtml2BBCode: string; + fromBBCode2Html: string; + } => { + const fromHtml2BBCode = html2bbcode(parseAsFragment(input), rules); + const fromBBCode2Html = bbcode2html(fromHtml2BBCode, supportedTags); + return { + input, + fromBBCode2Html, + fromHtml2BBCode, + }; + }, +}; + +const link = "https://example.org/"; + +/** + * We have a some slightly slanted state regarding transformation + * BBCode ↔ HTML: While for BBCode to HTML we use a third-party library, + * we have our own proprietary processing for HTML to BBCode. At least for + * the processing BBCode to HTML, we should ensure that the HTML to BBCode + * processing produces results that can be successfully parsed by the + * third-party library. The other way round is nice to have, as the + * HTML to BBCode proprietary code will never directly receive data from + * BBCode to HTML processing: It will always go through the CKEditor layers, + * too, and CKEditor is known to apply its own normalization, such as + * transforming `font-weight:bold` style to `` in the view layers. + */ +void describe("BBob Integration", () => { + /** + * These use-cases are important, as they validate, that our proprietary + * HTML to BBCode mapping is understood by the third-party library when + * processing the data back to HTML. + */ + describe('Important: HTML →[toData]→ BBCode →[toView]→ HTML"', () => { + const cases = [ + { + dataViewInput: ``, + expectedStoredData: ``, + expectedRestoredDataView: ``, + comment: `empty data`, + }, + { + dataViewInput: `TEXT`, + expectedStoredData: `[b]TEXT[/b]`, + expectedRestoredDataView: `TEXT`, + comment: `font-weight well understood by CKEditor`, + }, + { + dataViewInput: `TEXT`, + expectedStoredData: `[b]TEXT[/b]`, + expectedRestoredDataView: `TEXT`, + comment: `font-weight well understood by CKEditor`, + }, + { + dataViewInput: `TEXT`, + expectedStoredData: `[i]TEXT[/i]`, + expectedRestoredDataView: `TEXT`, + comment: `font-style well understood by CKEditor`, + }, + { + dataViewInput: `TEXT`, + expectedStoredData: `[i]TEXT[/i]`, + expectedRestoredDataView: `TEXT`, + comment: `font-style well understood by CKEditor`, + }, + { + dataViewInput: `TEXT`, + expectedStoredData: `[u]TEXT[/u]`, + expectedRestoredDataView: `TEXT`, + comment: `text-decoration well understood by CKEditor`, + }, + { + dataViewInput: `TEXT`, + expectedStoredData: `[u]TEXT[/u]`, + expectedRestoredDataView: `TEXT`, + comment: `text-decoration well understood by CKEditor`, + }, + { + dataViewInput: `TEXT`, + expectedStoredData: `[s]TEXT[/s]`, + expectedRestoredDataView: `TEXT`, + comment: `text-decoration well understood by CKEditor`, + }, + { + dataViewInput: `TEXT`, + expectedStoredData: `[s]TEXT[/s]`, + expectedRestoredDataView: `TEXT`, + comment: `text-decoration well understood by CKEditor`, + }, + { + dataViewInput: `TEXT`, + expectedStoredData: `[size=70]TEXT[/size]`, + expectedRestoredDataView: `TEXT`, + comment: `none`, + }, + { + dataViewInput: `TEXT`, + expectedStoredData: `[size=85]TEXT[/size]`, + expectedRestoredDataView: `TEXT`, + comment: `none`, + }, + { + dataViewInput: `TEXT`, + expectedStoredData: `[size=140]TEXT[/size]`, + expectedRestoredDataView: `TEXT`, + comment: `none`, + }, + { + dataViewInput: `TEXT`, + expectedStoredData: `[size=180]TEXT[/size]`, + expectedRestoredDataView: `TEXT`, + comment: `none`, + }, + { + dataViewInput: `TEXT`, + expectedStoredData: `[url="${link}"]TEXT[/url]`, + expectedRestoredDataView: `TEXT`, + comment: `normal link`, + }, + { + dataViewInput: `${link}`, + expectedStoredData: `[url]${link}[/url]`, + expectedRestoredDataView: `${link}`, + comment: `pretty-print: shorten, if possible, in BBCode`, + }, + { + dataViewInput: `TEXT`, + expectedStoredData: `TEXT`, + expectedRestoredDataView: `TEXT`, + comment: `there is no representation in BBCode for an anchor without href attribute`, + }, + { + dataViewInput: `

          TEXT

          `, + expectedStoredData: `[quote]\nTEXT\n[/quote]`, + expectedRestoredDataView: `

          \nTEXT

          `, + comment: `newlines part of minimal pretty-print behavior`, + }, + { + dataViewInput: `
          TEXT
          `, + expectedStoredData: `[code]\nTEXT\n[/code]`, + expectedRestoredDataView: `
          TEXT
          `, + comment: `adapted to nested pre, code to work in CKEditor 5`, + }, + { + dataViewInput: `
          TEXT
          `, + expectedStoredData: `[code=css]\nTEXT\n[/code]`, + expectedRestoredDataView: `
          TEXT
          `, + comment: `adapted to nested pre, code to work in CKEditor 5`, + }, + { + dataViewInput: `
          • TEXT
          `, + expectedStoredData: `[list]\n[*] TEXT\n[/list]`, + expectedRestoredDataView: `
            \n
          • TEXT\n
          `, + comment: `newlines part of minimal pretty-print behavior`, + }, + { + dataViewInput: `
          1. TEXT
          `, + expectedStoredData: `[list=1]\n[*] TEXT\n[/list]`, + expectedRestoredDataView: `
            \n
          1. TEXT\n
          `, + comment: `CKEditor defaults to _no-type_, but BBob defaults to add it`, + }, + { + dataViewInput: `
          1. TEXT
          `, + expectedStoredData: `[list=a]\n[*] TEXT\n[/list]`, + expectedRestoredDataView: `
            \n
          1. TEXT\n
          `, + comment: `CKEditor may ignore type, if not configured to support this`, + }, + { + dataViewInput: `

          TEXT

          `, + expectedStoredData: `[h1]TEXT[/h1]`, + expectedRestoredDataView: `

          TEXT

          `, + comment: `none`, + }, + { + dataViewInput: `

          TEXT

          `, + expectedStoredData: `[h2]TEXT[/h2]`, + expectedRestoredDataView: `

          TEXT

          `, + comment: `none`, + }, + { + dataViewInput: `

          TEXT

          `, + expectedStoredData: `[h3]TEXT[/h3]`, + expectedRestoredDataView: `

          TEXT

          `, + comment: `none`, + }, + { + dataViewInput: `

          TEXT

          `, + expectedStoredData: `[h4]TEXT[/h4]`, + expectedRestoredDataView: `

          TEXT

          `, + comment: `none`, + }, + { + dataViewInput: `
          TEXT
          `, + expectedStoredData: `[h5]TEXT[/h5]`, + expectedRestoredDataView: `
          TEXT
          `, + comment: `none`, + }, + { + dataViewInput: `
          TEXT
          `, + expectedStoredData: `[h6]TEXT[/h6]`, + expectedRestoredDataView: `
          TEXT
          `, + comment: `none`, + }, + { + dataViewInput: `

          TEXT1

          TEXT2

          `, + expectedStoredData: `TEXT1\n\nTEXT2`, + expectedRestoredDataView: `

          TEXT1

          TEXT2

          `, + comment: `none`, + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { dataViewInput, expectedStoredData, expectedRestoredDataView, comment }] of cases.entries()) { + await t.test( + `[${i}] Should transform data view to data, that are well understood by subsequent 'toView' mapping for: ${dataViewInput} (${comment})`, + () => { + const result = aut.html2bbcode2html(dataViewInput); + try { + // Precondition check: This is not THAT relevant, as the primary + // requirement is expressed in subsequent expectation: The stored data + // should be well understood when transforming them back to data view. + // In other words: If this fails, it may be ok, just to adjust the + // expectation. + expect(result).toHaveProperty("fromHtml2BBCode", expectedStoredData); + // When first written back in 2023, we used the default preset for + // HTML 5 for BBob library. This, for example, preferred style + // attributes for bold over corresponding tags. This is ok, as + // CKEditor's parsing from data view to model also accepts this + // to denote a bold style. It may be ok to adapt this expectation + // if the resulting HTML again is proven to be well-understood by + // CKEditor's processing. + expect(result).toHaveProperty("fromBBCode2Html", expectedRestoredDataView); + } catch (e) { + if (e instanceof Error) { + e.message = `${e.message}\n\nDebugging details:\n${JSON.stringify(result, undefined, 2)}`; + } + throw e; + } + }, + ); + } + }); + }); + + /** + * These use-cases are less important, as they (only) validate, that our + * proprietary HTML to BBCode mapping produces the same result as the BBCode + * we originally parsed. This, though, may be subject to change, like, for + * example, if we decide to introduce "more pretty printing" to the resulting + * HTML to BBCode processing (like indents for lists). In these cases, it is + * expected, that we have to adapt our data. + * + * The focus of this test is more to check, that our created data are + * invariant: If we had newlines, if we had indents before, no additional + * newlines or blanks should be added on repeated iterations. Example: A + * naive mapping of `[code]` → `
          ` `[code]` might just always add newlines
          +   * to the inner block, resulting in newlines piling up on each iteration,
          +   * with results such as: `[code]\n\n\n\nlorem\n\n\n\n[/code]` eventually.
          +   */
          +  void describe("Less important: BBCode →[toView]→ HTML →[toData]→ BBCode", () => {
          +    const cases = [
          +      { bbCode: `` },
          +      { bbCode: `[b]lorem[/b]` },
          +      { bbCode: `[i]lorem[/i]` },
          +      { bbCode: `[u]lorem[/u]` },
          +      { bbCode: `[s]lorem[/s]` },
          +      { bbCode: `[url="https://example.org/"]lorem[/url]` },
          +      { bbCode: `[quote]\nlorem\n[/quote]` },
          +      { bbCode: `[code]\nlorem\n[/code]` },
          +      { bbCode: `[list]\n[*] lorem\n[/list]` },
          +      { bbCode: `[list=1]\n[*] lorem\n[/list]` },
          +      { bbCode: `[list=a]\n[*] lorem\n[/list]` },
          +      { bbCode: `[list=I]\n[*] lorem\n[/list]` },
          +      { bbCode: `[h1]lorem[/h1]` },
          +      { bbCode: `[h2]lorem[/h2]` },
          +      { bbCode: `[h3]lorem[/h3]` },
          +      { bbCode: `[h4]lorem[/h4]` },
          +      { bbCode: `[h5]lorem[/h5]` },
          +      { bbCode: `[h6]lorem[/h6]` },
          +      { bbCode: `[size=70]lorem[/size]` },
          +      { bbCode: `[size=85]lorem[/size]` },
          +      { bbCode: `[size=140]lorem[/size]` },
          +      { bbCode: `[size=180]lorem[/size]` },
          +    ] as const;
          +
          +    void test("cases", async (t: TestContext) => {
          +      for (const [i, { bbCode }] of cases.entries()) {
          +        await t.test(`[${i}] Should process back and forth without change: ${bbCode}`, () => {
          +          const result = aut.bbcode2html2bbcode(bbCode);
          +          try {
          +            expect(result).toHaveProperty("fromHtml2BBCode", bbCode);
          +          } catch (e) {
          +            if (e instanceof Error) {
          +              e.message = `${e.message}\n\nDebugging details:\n${JSON.stringify(result, undefined, 2)}`;
          +            }
          +            throw e;
          +          }
          +        });
          +      }
          +    });
          +
          +    const backAndForthCases = [
          +      {
          +        bbCode: "[quote=author]lorem[/quote]",
          +        expected: "[quote]\nlorem\n[/quote]",
          +        comment: "We have no mapping for author to HTML.",
          +      },
          +    ] as const;
          +
          +    /**
          +     * We rate these deviations "acceptable" for now. To change, we may need,
          +     * for example, to provide a custom preset for bbcode2html.
          +     */
          +    void test("cases", async (t: TestContext) => {
          +      for (const [i, { bbCode, expected, comment }] of backAndForthCases.entries()) {
          +        await t.test(
          +          `[${i}] Should process back and forth with only minor change: ${bbCode} → ${expected} (${comment})`,
          +          () => {
          +            const result = aut.bbcode2html2bbcode(bbCode);
          +            try {
          +              expect(result).toHaveProperty("fromHtml2BBCode", expected);
          +            } catch (e) {
          +              if (e instanceof Error) {
          +                e.message = `${e.message}\n\nDebugging details:\n${JSON.stringify(result, undefined, 2)}`;
          +              }
          +              throw e;
          +            }
          +          },
          +        );
          +      }
          +    });
          +  });
          +});
          diff --git a/packages/ckeditor5-bbcode/__tests__/DOMUtils.ts b/packages/ckeditor5-bbcode/test/DOMUtils.ts
          similarity index 100%
          rename from packages/ckeditor5-bbcode/__tests__/DOMUtils.ts
          rename to packages/ckeditor5-bbcode/test/DOMUtils.ts
          diff --git a/packages/ckeditor5-bbcode/__tests__/README.md b/packages/ckeditor5-bbcode/test/README.md
          similarity index 100%
          rename from packages/ckeditor5-bbcode/__tests__/README.md
          rename to packages/ckeditor5-bbcode/test/README.md
          diff --git a/packages/ckeditor5-bbcode/test/bbcode2html.test.ts b/packages/ckeditor5-bbcode/test/bbcode2html.test.ts
          new file mode 100644
          index 0000000000..227c44bab3
          --- /dev/null
          +++ b/packages/ckeditor5-bbcode/test/bbcode2html.test.ts
          @@ -0,0 +1,837 @@
          +// noinspection HtmlUnknownTarget,SpellCheckingInspection,HtmlRequiredAltAttribute
          +import "global-jsdom/register";
          +import type { TestContext } from "node:test";
          +import test, { describe } from "node:test";
          +import expect from "expect";
          +import { bbCodeDefaultRules } from "../src";
          +import { bbcode2html, processBBCode } from "../src/bbcode2html";
          +
          +const supportedTags = bbCodeDefaultRules.flatMap((r) => r.tags ?? ([] as string[]));
          +
          +const aut = {
          +  expectTransformation: (
          +    { data, expectedDataView }: { data: string; expectedDataView: string },
          +    expectedErrors = 0,
          +  ): void => {
          +    const originalConsoleError = console.error;
          +    const errorCache: unknown[][] = [];
          +    const silencedHandler: typeof console.error = (...data: unknown[]): void => {
          +      errorCache.push(data);
          +    };
          +
          +    let actual: string;
          +
          +    try {
          +      console.error = silencedHandler;
          +      actual = bbcode2html(data, supportedTags);
          +    } finally {
          +      console.error = originalConsoleError;
          +    }
          +
          +    expect(errorCache).toHaveLength(expectedErrors);
          +
          +    try {
          +      expect(actual).toBe(expectedDataView);
          +    } catch (e) {
          +      console.debug("Failed expectations.", {
          +        data,
          +        actual,
          +        expectedDataView,
          +      });
          +      throw e;
          +    }
          +  },
          +  process: (data: string): ReturnType => processBBCode(data, supportedTags),
          +};
          +
          +/**
          + * These are high-level integration tests based on core processing by BBob.
          + * It uses all defaults typically applied for BBCode to HTML mapping when
          + * using the CKEditor 5 BBCode plugin.
          + *
          + * Note that all of these tests are also an implicit test of the BBob library,
          + * so that changed behaviors may also be triggered by BBob update. On failed
          + * tests a possible option is just to adjust the expectations.
          + */
          +void describe("bbcode2html", () => {
          +  void describe("Standard Tag Processing", () => {
          +    void describe("Supported Inline Tags", () => {
          +      const cases = [
          +        {
          +          data: `[b]T[/b]`,
          +          expectedDataView: `T`,
          +        },
          +        {
          +          data: `[color=red]T[/color]`,
          +          expectedDataView: `T`,
          +        },
          +        {
          +          data: `[size=85]T[/size]`,
          +          expectedDataView: `T`,
          +        },
          +        {
          +          data: `[i]T[/i]`,
          +          expectedDataView: `T`,
          +        },
          +        {
          +          data: `[s]T[/s]`,
          +          expectedDataView: `T`,
          +        },
          +        {
          +          data: `[u]T[/u]`,
          +          expectedDataView: `T`,
          +        },
          +        {
          +          data: `[url=https://example.org/]T[/url]`,
          +          expectedDataView: `T`,
          +        },
          +        {
          +          data: `[url]https://example.org/[/url]`,
          +          expectedDataView: `https://example.org/`,
          +        },
          +        {
          +          data: `[url=/relative]T[/url]`,
          +          expectedDataView: `T`,
          +        },
          +        {
          +          data: `[url]/relative[/url]`,
          +          expectedDataView: `/relative`,
          +        },
          +        {
          +          data: `[url=https://example.org/?one=1&two=2]T[/url]`,
          +          expectedDataView: `T`,
          +        },
          +        {
          +          data: `[url]https://example.org/?one=1&two=2[/url]`,
          +          expectedDataView: `https://example.org/?one=1&two=2`,
          +        },
          +        {
          +          data: `[url]https://example.org/?[b]predicate=x%3D42[/b]#_top[/url]`,
          +          expectedDataView: `https://example.org/?predicate=x%3D42#_top`,
          +        },
          +        {
          +          data: `[img]https://example.org/1.png[/img]`,
          +          expectedDataView: ``,
          +        },
          +      ] as const;
          +
          +      void test("cases", async (t: TestContext) => {
          +        for (const [i, { data, expectedDataView }] of cases.entries()) {
          +          await t.test(`[${i}] Should process data '${data}' to: ${expectedDataView}`, () => {
          +            aut.expectTransformation({ data, expectedDataView });
          +          });
          +        }
          +      });
          +    });
          +
          +    void describe("Supported Block Tags", () => {
          +      const cases = [
          +        {
          +          data: `[code]T[/code]`,
          +          expectedDataView: `
          T
          `, + }, + { + data: `[h1]T[/h1]`, + expectedDataView: `

          T

          `, + }, + { + data: `[h2]T[/h2]`, + expectedDataView: `

          T

          `, + }, + { + data: `[h3]T[/h3]`, + expectedDataView: `

          T

          `, + }, + { + data: `[h4]T[/h4]`, + expectedDataView: `

          T

          `, + }, + { + data: `[h5]T[/h5]`, + expectedDataView: `
          T
          `, + }, + { + data: `[h6]T[/h6]`, + expectedDataView: `
          T
          `, + }, + { + data: `[list][*] T[/list]`, + expectedDataView: `
          • T
          `, + }, + { + data: `[quote]T[/quote]`, + expectedDataView: `

          T

          `, + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { data, expectedDataView }] of cases.entries()) { + await t.test(`[${i}] Should process data '${data}' to: ${expectedDataView}`, () => { + aut.expectTransformation({ data, expectedDataView }); + }); + } + }); + }); + + void describe("Escaping", () => { + const cases = [ + { + data: `\\[b\\]not bold\\[/b\\]`, + expectedDataView: `[b]not bold[/b]`, + comment: `"normal" escape, but design-scope (as it is configurable for BBob)`, + }, + { + data: `\\[i\\]not italic\\[/i\\]`, + expectedDataView: `[i]not italic[/i]`, + comment: `"normal" escape, but design-scope (as it is configurable for BBob)`, + }, + { + data: `keep\\irrelevant\\escape`, + expectedDataView: `keep\\irrelevant\\escape`, + comment: `accepting BBob behavior: If irrelevant, BBob ignore an escape character.`, + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { data, expectedDataView, comment }] of cases.entries()) { + await t.test(`[${i}] Should process data '${data}' to: ${expectedDataView} (${comment})`, () => { + aut.expectTransformation({ data, expectedDataView }); + }); + } + }); + }); + }); + + void describe("By Tag", () => { + type BbcodeCase = { + tag: string; + openTag: string; + closeTag: string; + openElement: string; + closeElement: string; + }; + + // Some standard behaviors bundled. + const bbcodeCases: BbcodeCase[] = [ + { + tag: "[b]", + openTag: "[b]", + closeTag: "[/b]", + openElement: '', + closeElement: "", + }, + { + tag: "[color]", + openTag: "[color=red]", + closeTag: "[/color]", + openElement: '', + closeElement: "", + }, + { + tag: "[size]", + openTag: "[size=85]", + closeTag: "[/size]", + openElement: '', + closeElement: "", + }, + { tag: "[h1]", openTag: "[h1]", closeTag: "[/h1]", openElement: "

          ", closeElement: "

          " }, + { tag: "[h2]", openTag: "[h2]", closeTag: "[/h2]", openElement: "

          ", closeElement: "

          " }, + { tag: "[h3]", openTag: "[h3]", closeTag: "[/h3]", openElement: "

          ", closeElement: "

          " }, + { tag: "[h4]", openTag: "[h4]", closeTag: "[/h4]", openElement: "

          ", closeElement: "

          " }, + { tag: "[h5]", openTag: "[h5]", closeTag: "[/h5]", openElement: "
          ", closeElement: "
          " }, + { tag: "[h6]", openTag: "[h6]", closeTag: "[/h6]", openElement: "
          ", closeElement: "
          " }, + { + tag: "[i]", + openTag: "[i]", + closeTag: "[/i]", + openElement: '', + closeElement: "", + }, + { + tag: "[s]", + openTag: "[s]", + closeTag: "[/s]", + openElement: '', + closeElement: "", + }, + { + tag: "[u]", + openTag: "[u]", + closeTag: "[/u]", + openElement: '', + closeElement: "", + }, + { + tag: "[url]", + openTag: "[url=https://e.org/]", + closeTag: "[/url]", + openElement: '', + closeElement: "", + }, + ]; + + for (const { tag, openTag, closeTag, openElement, closeElement } of bbcodeCases) { + describe(`${tag} (Standard Behaviors)`, () => { + const cases = [ + { + data: `${openTag}T${closeTag}`, + expectedDataView: `${openElement}T${closeElement}`, + comment: `default`, + }, + { + data: `${openTag} T${closeTag}`, + expectedDataView: `${openElement} T${closeElement}`, + comment: `keep leading blanks`, + }, + { + data: `${openTag}T ${closeTag}`, + expectedDataView: `${openElement}T ${closeElement}`, + comment: `keep trailing blanks`, + }, + { + data: `${openTag}\nT${closeTag}`, + expectedDataView: `${openElement}\nT${closeElement}`, + comment: `keep leading single newlines`, + }, + { + data: `${openTag}T\n${closeTag}`, + expectedDataView: `${openElement}T\n${closeElement}`, + comment: `keep trailing single newlines`, + }, + { + data: `${openTag}T1\n\nT2${closeTag}`, + expectedDataView: `${openElement}T1\n\nT2${closeElement}`, + comment: `do not introduce a paragraph within element`, + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { data, expectedDataView, comment }] of cases.entries()) { + await t.test(`[${i}] Should process data '${data}' to: ${expectedDataView} (${comment})`, () => { + aut.expectTransformation({ data, expectedDataView }); + }); + } + }); + }); + } + + void describe("[code]", () => { + const cases = [ + { + data: `[code]T[/code]`, + expectedDataView: `
          T
          `, + comment: `Default to "plaintext" language`, + }, + { + data: `[code=css]T[/code]`, + expectedDataView: `
          T
          `, + comment: `Accept language attribute`, + }, + { + data: `[code=html]T[/code]`, + expectedDataView: `
          <i>T</i>
          `, + comment: `Properly encode nested HTML`, + }, + { + data: `[code=bbcode]\\[i\\]T\\[/i\\][/code]`, + expectedDataView: `
          [i]T[/i]
          `, + comment: `Strip escapes`, + }, + { + data: `[code=bbcode]\\[code=text\\]T\\[/code\\][/code]`, + expectedDataView: `
          [code=text]T[/code]
          `, + comment: `Strip escapes`, + }, + { + data: `[code=bbcode]\\[code=bbcode\\]T\\[/code\\][/code]`, + expectedDataView: `
          [code=bbcode]T[/code]
          `, + comment: `Strip escapes`, + }, + { + data: `[code=bbcode]\n\\[code=bbcode\\]\nT\n\\[/code\\]\n[/code]`, + expectedDataView: `
          [code=bbcode]\nT\n[/code]
          `, + comment: `Strip escapes and keep newlines`, + }, + { + data: `[code][i]T[/i][/code]`, + expectedDataView: `
          T
          `, + comment: `Accept and parse BBCode within "code" tag`, + }, + { + data: `[code][script]javascript:alert("X")[/script][/code]`, + expectedDataView: `
          [script]javascript:alert("X")[/script]
          `, + comment: `Do not transform, e.g., script-tag.`, + }, + { + data: `[code]T1\n\nT2[/code]`, + expectedDataView: `
          T1\n\nT2
          `, + comment: `Don't handle duplicate newlines as paragraphs.`, + }, + { + data: `[code] T1\n T2[/code]`, + expectedDataView: `
            T1\n  T2
          `, + comment: `Keep space indents`, + }, + { + data: `[code]\tT1\n\tT2[/code]`, + expectedDataView: `
          \tT1\n\tT2
          `, + comment: `Keep tab indents`, + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { data, expectedDataView, comment }] of cases.entries()) { + await t.test(`[${i}] Should process data '${data}' to: ${expectedDataView} (${comment})`, () => { + aut.expectTransformation({ data, expectedDataView }); + }); + } + }); + }); + + void describe("[quote]", () => { + const cases = [ + { + data: `[quote]T[/quote]`, + expectedDataView: `

          T

          `, + comment: `minimal scenario`, + }, + { + data: `[quote=AUTHOR]T[/quote]`, + expectedDataView: `

          T

          `, + comment: `author information not supported`, + }, + { + data: `[quote]\nT\n[/quote]`, + expectedDataView: `

          \nT

          `, + comment: `strip obsolete trailing newlines`, + }, + { + data: `[quote]\nP1\n\nP2\n[/quote]`, + expectedDataView: `

          \nP1

          P2

          `, + comment: `paragraphs support`, + }, + { + data: `[quote]\nP1\n[quote]\nP2\n[/quote]\nP3\n[/quote]`, + expectedDataView: `

          \nP1

          \nP2

          \nP3

          `, + comment: `nested quotes support`, + }, + { + data: `[quote]\n[quote]\nT\n[/quote]\n[/quote]`, + expectedDataView: `

          \nT

          `, + comment: `directly nested quotes support`, + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { data, expectedDataView, comment }] of cases.entries()) { + await t.test(`[${i}] Should process data '${data}' to: ${expectedDataView} (${comment})`, () => { + aut.expectTransformation({ data, expectedDataView }); + }); + } + }); + }); + + void describe("[img]", () => { + const cases = [ + { + data: `[img]https://example.org/1.png[/img]`, + expectedDataView: ``, + comment: `minimal scenario`, + }, + { + data: `[img alt="ALT"]https://example.org/1.png[/img]`, + expectedDataView: `ALT`, + comment: `alt text support; order of attributes irrelevant, but set expected as it is now`, + }, + { + data: `[img alt=""]https://example.org/1.png[/img]`, + expectedDataView: ``, + comment: `design-scope: may as well strip empty alt; kept for simplicity`, + }, + { + data: `[img alt=1-PNG]https://example.org/1.png[/img]`, + expectedDataView: `1-PNG`, + comment: `alt without quotes (must not use spaces)`, + }, + { + data: `A[img][/img]B`, + expectedDataView: `AB`, + comment: `design-scope: May as well strip irrelevant img tag; kept for simplicity`, + }, + { + data: `A [img]https://example.org/1.png[/img] B`, + expectedDataView: `A B`, + comment: `images are meant to be inline (all known BBCode interpreters seem to expect that)`, + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { data, expectedDataView, comment }] of cases.entries()) { + await t.test(`[${i}] Should process data '${data}' to: ${expectedDataView} (${comment})`, () => { + aut.expectTransformation({ data, expectedDataView }); + }); + } + }); + }); + }); + + void describe("Paragraphs", () => { + const cases = [ + { + data: `P1\n\nP2`, + expectedDataView: `

          P1

          P2

          `, + comment: `Standard Paragraph Behavior (Only Text; LF)`, + }, + { + data: `P1\r\n\r\nP2`, + expectedDataView: `

          P1

          P2

          `, + comment: `Standard Paragraph Behavior (Only Text; CRLF)`, + }, + { + data: `P1\r\rP2`, + expectedDataView: `

          P1

          P2

          `, + comment: `Standard Paragraph Behavior (Only Text; CR)`, + }, + { + data: `[b]P1[/b]\n\n[i]P2[/i]`, + expectedDataView: `

          P1

          P2

          `, + comment: `Standard Paragraph Behavior (Text with inline formatting)`, + }, + { + data: ``, + expectedDataView: ``, + comment: `Do not create paragraphs on empty input`, + }, + { + data: `\n`, + expectedDataView: `\n`, + comment: `Do not create paragraphs on only single newline`, + }, + { + data: `\n\n`, + expectedDataView: `\n`, + comment: `Trim irrelevant newlines`, + }, + { + data: `\n\n\n`, + expectedDataView: `\n`, + comment: `Trim irrelevant newlines`, + }, + { + data: `\nP1\n\nP2`, + expectedDataView: `

          \nP1

          P2

          `, + comment: `Design Scope: Keep irrelevant leading newlines; simplifies processing`, + }, + { + data: `P1\n\n\nP2`, + expectedDataView: `

          P1

          P2

          `, + comment: `Trim obsolete newlines (trailing, 1)`, + }, + { + data: `P1\n\nP2\n`, + expectedDataView: `

          P1

          P2

          `, + comment: `Trim obsolete newlines (trailing, 2)`, + }, + { + data: `[quote]P1\n\nP2[/quote]`, + expectedDataView: `

          P1

          P2

          `, + comment: `Respect paragraphs in quote sections`, + }, + { + data: `P1\n\n[quote]P2[/quote]\n\nP3`, + expectedDataView: `

          P1

          P2

          P3

          `, + comment: `Do not put blockquotes into paragraphs`, + }, + { + data: `P1\n\n[code]P2[/code]\n\nP3`, + expectedDataView: `

          P1

          P2

          P3

          `, + comment: `Do not put code blocks into paragraphs`, + }, + { + data: `P1\n\n[h1]P2[/h1]\n\nP3`, + expectedDataView: `

          P1

          P2

          P3

          `, + comment: `Do not put headings into paragraphs`, + }, + { + data: `P1\n\n[list][*]P2[/list]\n\nP3`, + expectedDataView: `

          P1

          • P2

          P3

          `, + comment: `Do not put lists into paragraphs`, + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { data, expectedDataView, comment }] of cases.entries()) { + await t.test(`[${i}] Should process data '${data}' to: ${expectedDataView} (${comment})`, () => { + aut.expectTransformation({ data, expectedDataView }); + }); + } + }); + }); + + void describe("Tag Processing Challenges", () => { + /** + * We may meet unexpected formatting. This is handled (and tested) by + * BBob. These tests are mainly meant as proof-of-concept to understand + * the behavior of the BBob library. + * + * The count of expected errors is more an integration test towards BBob. + * It is perfectly fine, if the expected error count needs to be adjusted + * after BBob upgrade. Thus, the current number of expected errors only + * represents the current behavior of BBob. + */ + void describe("Formatting Errors", () => { + const cases = [ + { + data: `[b]T`, + expectedDataView: `T`, + expectedErrors: 0, + comment: `surprise: creates an empty element. CKEditor 5 would just remove it.`, + }, + { + data: `T[/b]`, + expectedDataView: `T`, + expectedErrors: 1, + comment: `perfectly fine: Just ignore orphaned close-tag`, + }, + { + data: `[b]T[/i]`, + expectedDataView: `T`, + expectedErrors: 1, + comment: `surprise: creates an empty element. CKEditor 5 would just remove it.`, + }, + { + data: `[b]A[i]B[/b]C[/i]`, + expectedDataView: `ABC`, + expectedErrors: 0, + comment: `surprise: only opening tag seems to control the hierarchy`, + }, + ] as const; + + // TODO reactivate tests / verified + void test.skip("cases", async (t: TestContext) => { + for (const [i, { data, expectedDataView, expectedErrors, comment }] of cases.entries()) { + await t.test(`[${i}] Should process data '${data}' to: ${expectedDataView} (${comment})`, () => { + aut.expectTransformation({ data, expectedDataView }, expectedErrors); + }); + } + }); + }); + + void describe("Attribute Challenges", () => { + const cases = [ + { + data: `[url=]T[/url]`, + expectedDataView: `T`, + comment: "BBob: empty unique attribute handled as _not existing_", + }, + { + data: `[url=\nhttps://example.org/]T[/url]`, + expectedDataView: `T`, + comment: "BBob: ignores newlines within (unique) attributes", + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { data, expectedDataView, comment }] of cases.entries()) { + await t.test(`[${i}] Should process data '${data}' to: ${expectedDataView} (${comment})`, () => { + aut.expectTransformation({ data, expectedDataView }); + }); + } + }); + }); + }); + + void describe("Security", () => { + void describe("Raw HTML Injections", () => { + const cases = [ + { + data: `A B`, + expectedDataView: `A <b>B</b>`, + comment: `Defaults to not supporting embedded HTML elements.`, + }, + { + data: `A&B`, + expectedDataView: `A&amp;B`, + comment: `Defaults to not supporting entities, but to escape them.`, + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { data, expectedDataView, comment }] of cases.entries()) { + await t.test(`[${i}] Should process data '${data}' to: ${expectedDataView} (${comment})`, () => { + aut.expectTransformation({ data, expectedDataView }); + }); + } + }); + }); + + /** + * Testing with some XSS payloads. Note, that the BBCode plugin just + * ensures proper rendering of HTML, thus, prevents possible attack + * vectors trying to trick a simple HTML rendering based on _search & + * replace_ as it is sometimes used for BBCode to HTML processing. + * + * It is up to the editing layer, to deal with any possible other + * malicious attacks. Such as the CKEditor 5 link feature already does + * for possible malicious `data:text/html;` links. + * + * @see https://github.com/JiLiZART/BBob/issues/201 + * @see https://swarm.ptsecurity.com/fuzzing-for-xss-via-nested-parsers-condition/ + */ + void describe("XSS attacks", () => { + // noinspection CssInvalidPropertyValue + const cases = [ + { + tainted: `[url=data:text/html;base64,PHNjcmlwdD5hbGVydCgiMSIpOzwvc2NyaXB0Pg==]sdfsdf[/url]`, + expected: `sdfsdf`, + comment: `source: https://security.snyk.io/vuln/SNYK-PYTHON-BBCODE-40502; CKEditor 5 will prohibit clicking on these.`, + }, + { + tainted: `[url]javascript:alert('XSS');[/url]`, + expected: `javascript:alert('XSS');`, + comment: `source: https://github.com/dcwatson/bbcode/issues/4; Will be passed as is to CKEditor 5, which will take care not to make this clickable within the UI.`, + }, + { + tainted: `[url=javascript:alert('XSS');]TEXT[/url]`, + expected: `TEXT`, + comment: `source: https://github.com/dcwatson/bbcode/issues/4; javascript:-link flavor but as attribute`, + }, + { + tainted: `[url]javascript:alert("XSS");[/url]`, + expected: `javascript:alert("XSS");`, + comment: `source: https://github.com/dcwatson/bbcode/issues/4; javascript:-link flavor but with double-quotes`, + }, + { + tainted: `[url]123" onmouseover="alert('Hacked');[/url]`, + expected: `123" onmouseover="alert('Hacked');`, + comment: `source: https://github.com/dcwatson/bbcode/issues/4`, + }, + { + tainted: `[url]https://google.com?[url] onmousemove=javascript:alert(String.fromCharCode(88,83,83));//[/url][/url]`, + expected: `https://google.com? onmousemove=javascript:alert(String.fromCharCode(88,83,83));//`, + comment: `source: https://github.com/dcwatson/bbcode/issues/4; Slightly corrupted DOM, but attack did not pass through.`, + }, + { + tainted: `[color="onmouseover=alert(0) style="]dare to move your mouse here[/color]`, + expected: 'dare to move your mouse here', + comment: ``, + }, + ] as const; + + // TODO reactivate tests / verified + void test.skip("cases", async (t: TestContext) => { + for (const [i, { tainted, expected, comment }] of cases.entries()) { + await t.test(`[${i}] Should prevent XSS-attack for: ${tainted}, expected: ${expected} (${comment})`, () => { + aut.expectTransformation({ data: tainted, expectedDataView: expected }); + }); + } + }); + }); + }); + + /** + * We have to expect BBCode data, that cannot even be parsed. As such, an + * error handling should provide a predictable behavior. Nevertheless, each + * error handling is part of the design-scope: Do we accept some data-loss by + * stripping only problematic BBCode? Or do we present an empty text on any + * error in CKEditor 5 editing view (as we do for erred CoreMedia Rich Text)? + * + * Decision for now is to stick with BBob, which just strips broken BBCode + * and tries to render the rest at best effort. + */ + void describe("Error Handling", () => { + const cases = [ + { + erred: `[/]`, + expected: `[/]`, + comment: `for "only invalid BBCode" provide empty text`, + expectedErrors: 0, + }, + { + erred: `Before[/]After`, + expected: `Before[/]After`, + comment: `should just ignore broken BBCode parts`, + expectedErrors: 0, + }, + { + erred: `[c][/c][b]hello[/c][/b][b]`, + expected: `[c]hello[b]`, + comment: `example input from BBob tests`, + expectedErrors: 1, + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { erred: data, expectedErrors, expected: expectedDataView, comment }] of cases.entries()) { + await t.test( + `[${i}] Should handle BBCode errors with care: ${data}, expected: ${expectedDataView} (${comment})`, + () => { + // We expect BBob to raise an error for all the above data. If this + // changes, feel free to adapt the number of expected errors. + aut.expectTransformation({ data, expectedDataView }, expectedErrors); + }, + ); + } + }); + }); + + /** + * Tests dedicated to the BBob integration. Typically applied to some lower + * aspects to either ease debugging or to validate unchanged behavior after + * BBob upgrade. + */ + void describe("BBob Integratino", () => { + /** + * Demonstrates (and validates) that the BBob parser is unaware of + * newline representations different to LF (Unix). As a result, we need + * to pre-process the incoming data to normalize newlines. + * + * If any of these tests fail, we may skip (or adapt) this extra processing. + * + * @see + */ + const cases = [ + { + newline: `\n`, + expectedTree: ["\n"], + type: `LF (Unix)`, + }, + { + newline: `\r\n`, + expectedTree: ["\r", "\n"], + type: `CRLF (Windows)`, + }, + { + newline: `\r`, + expectedTree: ["\r"], + type: `CR (classic MacOS)`, + }, + { + newline: `A\nB`, + expectedTree: ["A", "\n", "B"], + type: `LF (Unix)`, + }, + { + newline: `A\r\nB`, + expectedTree: ["A\r", "\n", "B"], + type: `CRLF (Windows)`, + }, + { + newline: `A\rB`, + expectedTree: ["A\rB"], + type: `CR (classic MacOS)`, + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { newline, expectedTree, type }] of cases.entries()) { + await t.test(`[${i}] Should parse system dependent newline representation for ${type} as expected.`, () => { + // deconstruct to remove extra "candy" like messages from the resulting + // array. + const tree = [...aut.process(newline).tree]; + expect(tree).toEqual(expectedTree); + }); + } + }); + }); +}); diff --git a/packages/ckeditor5-bbcode/__tests__/bbob/Attributes.test.ts b/packages/ckeditor5-bbcode/test/bbob/Attributes.test.ts similarity index 51% rename from packages/ckeditor5-bbcode/__tests__/bbob/Attributes.test.ts rename to packages/ckeditor5-bbcode/test/bbob/Attributes.test.ts index bfec91b064..c6723029d1 100644 --- a/packages/ckeditor5-bbcode/__tests__/bbob/Attributes.test.ts +++ b/packages/ckeditor5-bbcode/test/bbob/Attributes.test.ts @@ -1,4 +1,6 @@ -import { TagAttrs } from "@bbob/plugin-helper/es"; +import type { TestContext } from "node:test"; +import test, { describe } from "node:test"; +import expect from "expect"; import { forEachAttribute, setAttributesFromTagAttrs, @@ -6,38 +8,38 @@ import { uniqueAttrToAttr, } from "../../src/bbob/Attributes"; -describe("Attributes", () => { - describe("forEachAttribute", () => { - it("should do nothing on empty record", () => { +void describe("Attributes", () => { + void describe("forEachAttribute", () => { + void test("should do nothing on empty record", () => { let called = false; forEachAttribute({}, () => (called = true)); expect(called).toBe(false); }); - it("should process expected entries", () => { - const probe: TagAttrs = { + void test("should process expected entries", () => { + const probe: Record = { src: "SRC", otherSrc: "SRC", lorem: "LOREM", empty: "", }; - const processedEntries: [string, string][] = []; + const processedEntries: [string, unknown][] = []; forEachAttribute(probe, (name, value) => processedEntries.push([name, value])); expect(Object.fromEntries(processedEntries)).toMatchObject(probe); }); }); - describe("setAttributesFromTagAttrs", () => { - it("should not set any attribute on empty attributes", () => { + void describe("setAttributesFromTagAttrs", () => { + void test("should not set any attribute on empty attributes", () => { const el = document.createElement("div"); const originalLength = el.attributes.length; setAttributesFromTagAttrs(el, {}); expect(el.attributes.length).toBe(originalLength); }); - it("should set normal attributes as given", () => { + void test("should set normal attributes as given", () => { const el = document.createElement("div"); - const attrs: TagAttrs = { + const attrs: Record = { class: "CLASS", style: "STYLE", title: "", @@ -48,10 +50,10 @@ describe("Attributes", () => { }); }); - it("should ignore invalid attributes, but process others", () => { + void test("should ignore invalid attributes, but process others", () => { const el = document.createElement("div"); const invalidKey = "[invalid key]"; - const attrs: TagAttrs = { + const attrs: Record = { class: "CLASS", [invalidKey]: "INVALID", style: "STYLE", @@ -71,14 +73,14 @@ describe("Attributes", () => { * tests may break, and we may need to investigate how to deal with the * results. */ - describe("stripUniqueAttr (BBob integration)", () => { - it("should get only 'otherAttrs' for empty attributes", () => { + void describe("stripUniqueAttr (BBob integration)", () => { + void test("should get only 'otherAttrs' for empty attributes", () => { const attr = stripUniqueAttr({}); expect(attr.uniqueAttrValue).toBeUndefined(); expect(attr.otherAttrs).toMatchObject({}); }); - it("should get only 'otherAttrs' for attributes not having a unique attribute", () => { + void test("should get only 'otherAttrs' for attributes not having a unique attribute", () => { const attrs = { one: "1", two: "2", @@ -88,7 +90,7 @@ describe("Attributes", () => { expect(attr.otherAttrs).toMatchObject(attrs); }); - it("should extract unique attribute, if it is the only attribute", () => { + void test("should extract unique attribute, if it is the only attribute", () => { const uniqueAttr = "https://example.org/"; const attrs = { // Typical representation of a URL, for example, in [url=https://example.org/]. @@ -99,7 +101,7 @@ describe("Attributes", () => { expect(attr.otherAttrs).toMatchObject({}); }); - it("should extract unique attribute, and separate from others", () => { + void test("should extract unique attribute, and separate from others", () => { const uniqueAttr = "https://example.org/"; const otherAttrs = { one: "1", @@ -118,29 +120,48 @@ describe("Attributes", () => { }); }); - describe("uniqueAttrToAttr", () => { - type AutCall = (uniqueAttrName: string, attrs: TagAttrs, uniqueDefault: string) => TagAttrs; + void describe("uniqueAttrToAttr", () => { + type AutCall = ( + uniqueAttrName: string, + attrs: Record, + uniqueDefault: string, + ) => Record; const aut = { - callWithDefaults: (uniqueAttrName: string, attrs: TagAttrs) => uniqueAttrToAttr(uniqueAttrName, attrs), - callWithOverrideEnabled: (uniqueAttrName: string, attrs: TagAttrs) => + callWithDefaults: (uniqueAttrName: string, attrs: Record) => + uniqueAttrToAttr(uniqueAttrName, attrs), + callWithOverrideEnabled: (uniqueAttrName: string, attrs: Record) => uniqueAttrToAttr(uniqueAttrName, attrs, true), - callWithOverrideDisabled: (uniqueAttrName: string, attrs: TagAttrs) => + callWithOverrideDisabled: (uniqueAttrName: string, attrs: Record) => uniqueAttrToAttr(uniqueAttrName, attrs, false), - callWithDefaultSupplied: (uniqueAttrName: string, attrs: TagAttrs, uniqueDefault: string) => + callWithDefaultSupplied: (uniqueAttrName: string, attrs: Record, uniqueDefault: string) => uniqueAttrToAttr(uniqueAttrName, attrs, true, () => uniqueDefault), }; - it.each` - autCall | callType - ${aut.callWithDefaults} | ${"call with defaults"} - ${aut.callWithOverrideEnabled} | ${"call with override enabled"} - ${aut.callWithOverrideDisabled} | ${"call with override disabled"} - `("[$#] should return empty attributes unchanged: $callType", ({ autCall }: { autCall: AutCall }) => { - const result = autCall("unique", {}, "uniqueDefault"); - expect(result).toMatchObject({}); + const emptyAttrCases: { autCall: AutCall; callType: string }[] = [ + { + autCall: aut.callWithDefaults, + callType: "call with defaults", + }, + { + autCall: aut.callWithOverrideEnabled, + callType: "call with override enabled", + }, + { + autCall: aut.callWithOverrideDisabled, + callType: "call with override disabled", + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { autCall, callType }] of emptyAttrCases.entries()) { + await t.test(`[${i}] should return empty attributes unchanged: ${callType}`, () => { + const result = autCall("unique", {}, "uniqueDefault"); + expect(result).toMatchObject({}); + }); + } }); - it("should use default unique attribute for empty attributes", () => { + void test("should use default unique attribute for empty attributes", () => { const autCall = aut.callWithDefaultSupplied; const uniqueKey = "unique"; const uniqueDefault = "uniqueDefault"; @@ -148,21 +169,39 @@ describe("Attributes", () => { expect(result).toMatchObject({ [uniqueKey]: uniqueDefault }); }); - it.each` - autCall | callType - ${aut.callWithDefaults} | ${"call with defaults"} - ${aut.callWithOverrideEnabled} | ${"call with override enabled"} - ${aut.callWithDefaultSupplied} | ${"call with supplied default"} - `("[$#] should override from unique attributes: $callType", ({ autCall }: { autCall: AutCall }) => { - const uniqueKey = "unique"; - const uniqueValueInAttrs = "uniqueValueInAttrs"; - const uniqueValue = "uniqueValue"; - const uniqueDefault = "uniqueDefault"; - const result = autCall(uniqueKey, { [uniqueKey]: uniqueValueInAttrs, [uniqueValue]: uniqueValue }, uniqueDefault); - expect(result).toMatchObject({ [uniqueKey]: uniqueValue }); + const uniqueAttrCases = [ + { + autCall: aut.callWithDefaults, + callType: "call with defaults", + }, + { + autCall: aut.callWithOverrideEnabled, + callType: "call with override enabled", + }, + { + autCall: aut.callWithDefaultSupplied, + callType: "call with supplied default", + }, + ]; + + void test("cases", async (t: TestContext) => { + for (const [i, { autCall, callType }] of uniqueAttrCases.entries()) { + await t.test(`[${i}] should override from unique attributes: ${callType}`, () => { + const uniqueKey = "unique"; + const uniqueValueInAttrs = "uniqueValueInAttrs"; + const uniqueValue = "uniqueValue"; + const uniqueDefault = "uniqueDefault"; + const result = autCall( + uniqueKey, + { [uniqueKey]: uniqueValueInAttrs, [uniqueValue]: uniqueValue }, + uniqueDefault, + ); + expect(result).toMatchObject({ [uniqueKey]: uniqueValue }); + }); + } }); - it("should prefer existing attribute, when override is disabled", () => { + void test("should prefer existing attribute, when override is disabled", () => { const autCall = aut.callWithOverrideDisabled; const uniqueKey = "unique"; const uniqueValueInAttrs = "uniqueValueInAttrs"; diff --git a/packages/ckeditor5-bbcode/test/bbob/KnownIssues.test.ts b/packages/ckeditor5-bbcode/test/bbob/KnownIssues.test.ts new file mode 100644 index 0000000000..7c55599a41 --- /dev/null +++ b/packages/ckeditor5-bbcode/test/bbob/KnownIssues.test.ts @@ -0,0 +1,80 @@ +import type { TestContext } from "node:test"; +import test, { describe } from "node:test"; +import type { TagNodeTree } from "@bbob/types"; +import expect from "expect"; +import bbob from "@bbob/core"; + +const render = (node: TagNodeTree | undefined) => { + return JSON.stringify(node); +}; + +const aut = { + /** + * Uses `JSON.stringify` as renderer and no plugins, thus, we process the + * raw tree to plain JSON. + */ + toJSONRaw: (input: string) => { + return bbob().process(input, { render }); + }, +}; + +/** + * These tests demonstrate known issues along with the BBob library. + * + * If they fail on upgrade, we may be lucky, that we may adjust our code + * accordingly. + * + * For now, they are especially meant to document known limitations we have + * rated as acceptable. + * + * The given probes may also be worth evaluating in context of alternative + * BBCode processors (like KefirBB, for example). + */ +void describe("BBob Known Issues", () => { + void describe("toJSONRaw", () => { + /* + * Active parameters: + * + * `bbCode`: Input data + * `expectedActual`: Flawed raw JSON data. + * + * Documentation only parameters: + * + * `expected`: What we would have expected instead. + * `issue`: If applicable, an issue reference. + * `comment`: Some comment + */ + const cases = [ + { + bbCode: `[url=javascript:alert('XSS ME');]T[/url]`, + expectedActual: `[{"tag":"url","attrs":{"javascript:alert('XSS ME');":"javascript:alert('XSS ME');"},"content":["T"]}]`, + expected: `[{"tag":"url","attrs":{"javascript:alert('XSS ME');":"javascript:alert('XSS ME');"},"content":["T"],"start":{"from":0,"to":33},"end":{"from":34,"to":40}}]`, + issue: "https://github.com/JiLiZART/BBob/issues/204", + comment: `Space Handling in Unique Attributes; causes "ME');" to be regarded as link`, + }, + { + bbCode: `[quote=J. D.]T[/quote]`, + expectedActual: `[{"tag":"quote","attrs":{"J. D.":"J. D."},"content":["T"]}]`, + expected: `[{"tag":"quote","attrs":{"J. D.":"J. D."},"content":["T"],"start":{"from":0,"to":13},"end":{"from":14,"to":22}}]`, + issue: "https://github.com/JiLiZART/BBob/issues/204", + comment: "Space Handling in Unique Attributes; simpler data", + }, + { + bbCode: `[quote=J. "The T" D.]T[/quote]`, + expectedActual: `[{"tag":"quote","attrs":{"J. \\"The T\\" D.":"J. \\"The T\\" D."},"content":["T"]}]`, + expected: `[{"tag":"quote","attrs":{"J. \\"The T\\" D.":"J. \\"The T\\" D."},"content":["T"],"start":{"from":0,"to":21},"end":{"from":22,"to":30}}]`, + issue: "https://github.com/JiLiZART/BBob/issues/204", + comment: "Space And Quote Handling in Unique Attributes", + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { bbCode, expectedActual, expected, issue, comment }] of cases.entries()) { + await t.test(`[${i}] ${comment}: ${bbCode} -> ${expectedActual} (${issue}; expected: ${expected})`, () => { + const result = aut.toJSONRaw(bbCode); + expect(result.html).toBe(expected); + }); + } + }); + }); +}); diff --git a/packages/ckeditor5-bbcode/test/bbob/Paragraphs.test.ts b/packages/ckeditor5-bbcode/test/bbob/Paragraphs.test.ts new file mode 100644 index 0000000000..9c87938055 --- /dev/null +++ b/packages/ckeditor5-bbcode/test/bbob/Paragraphs.test.ts @@ -0,0 +1,401 @@ +import type { TestContext } from "node:test"; +import test, { describe } from "node:test"; +import expect from "expect"; +import { TagNode } from "@bbob/plugin-helper"; +import type { ParagraphAwareContentOptions } from "../../src/bbob/Paragraphs"; +import { paragraphAwareContent } from "../../src/bbob/Paragraphs"; + +type TagNodeType = ReturnType; + +type ContentFixture = NonNullable; + +const p = (content: ContentFixture): TagNodeType => TagNode.create("p", {}, content); +/** + * Just any inline tag-node. + */ +const b = (content: ContentFixture): TagNodeType => TagNode.create("b", {}, content); +/** + * Shortcut to add `quote` node. + */ +const q = (content: ContentFixture): TagNodeType => TagNode.create("quote", {}, content); + +describe(`Paragraphs`, () => { + // ==========================================================================================[ paragraphAwareContent ] + describe(`paragraphAwareContent`, () => { + // ---------------------------------------------------------------------------------------------------[ content=[] ] + describe(`content=[]`, () => { + void test(`should return "[]" with default options`, () => { + expect({ expected: paragraphAwareContent([]) }).toMatchObject({ expected: [] }); + }); + + void test(`should return "[]" wrapped in paragraph with "requireParagraph=true"`, () => { + const expected = [TagNode.create("p", {}, [])]; + expect({ expected: paragraphAwareContent([], { requireParagraph: true }) }).toMatchObject({ expected }); + }); + }); + + // ---------------------------------------------------------------------------------------------[ content=string[] ] + describe(`content=string[]: Content only containing strings without EOL characters`, () => { + void test(`should skip extra paragraph (requireParagraphs=default false)`, () => { + const input = ["lorem", "ipsum"]; + expect({ expected: paragraphAwareContent(input) }).toMatchObject({ expected: input }); + }); + + void test(`should wrap content into paragraph (requireParagraphs=true)`, () => { + const input = ["lorem", "ipsum"]; + const expected = [TagNode.create("p", {}, input)]; + expect({ expected: paragraphAwareContent(input, { requireParagraph: true }) }).toMatchObject({ expected }); + }); + }); + + // -----------------------------------------------------------------------------------[ content=(string|TagNodeType)[] ] + describe(`content=(string|TagNode)[]: Content only containing strings and tag-nodes without EOL characters`, () => { + const cases: { input: ContentFixture; comment: string }[] = [ + { + input: [b(["lorem"]), "ipsum", "dolor"], + comment: "tag-node at start", + }, + { + input: ["lorem", b(["ipsum"]), "dolor"], + comment: "tag-node in the middle", + }, + { + input: ["lorem", "ipsum", b(["dolor"])], + comment: "tag-node at the end", + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { input, comment }] of cases.entries()) { + await t.test( + `[${i}] should skip extra paragraph (requireParagraphs=default false, ${comment}): ${input}`, + () => { + expect({ expected: paragraphAwareContent(input) }).toMatchObject({ expected: input }); + }, + ); + } + }); + + const wrapParagraphCases: { input: (TagNodeType | string)[]; comment: string }[] = [ + { input: [b(["lorem"]), "ipsum", "dolor"], comment: "tag-node at start" }, + { input: ["lorem", b(["ipsum"]), "dolor"], comment: "tag-node in the middle" }, + { input: ["lorem", "ipsum", b(["dolor"])], comment: "tag-node at the end" }, + ]; + + void test("cases", async (t: TestContext) => { + for (const [i, { input, comment }] of wrapParagraphCases.entries()) { + await t.test( + `[${i}] should wrap content into paragraph (requireParagraphs=default true, ${comment}): ${input}`, + () => { + const expected = [TagNode.create("p", {}, input)]; + const actual = paragraphAwareContent(input, { requireParagraph: true }); + expect({ expected: actual }).toMatchObject({ expected }); + }, + ); + } + }); + }); + + // ------------------------------------------------------------------------------------------------[ content=EOL[] ] + describe(`content=EOL[]: Content consisting of EOLs only`, () => { + const cases: { input: ContentFixture; expected: ContentFixture; comment: string }[] = [ + { input: ["\n"], expected: ["\n"], comment: "keep single newline as is" }, + { input: ["\n", "\n"], expected: ["\n"], comment: "squash newlines, keep at least one" }, + { input: ["\n", "\n", "\n"], expected: ["\n"], comment: "squash newlines, keep at least one" }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { input, expected, comment }] of cases.entries()) { + await t.test(`[${i}] should transform from ${input} to ${expected} (all defaults): ${comment}`, () => { + const actual = paragraphAwareContent(input); + expect({ expected: actual }).toMatchObject({ expected }); + }); + } + }); + + const trimNewlineCases: { input: ContentFixture; expected: ContentFixture; comment: string }[] = [ + { input: ["\n"], expected: [p([])], comment: "Design scope: Trim irrelevant newline" }, + { input: ["\n", "\n"], expected: [p([])], comment: "Design scope: Trim irrelevant newline" }, + { input: ["\n", "\n", "\n"], expected: [p([])], comment: "Design scope: Trim irrelevant newline" }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { input, expected, comment }] of trimNewlineCases.entries()) { + await t.test( + `[${i}] should transform from ${input} to ${expected} (requireParagraph=true): ${comment}`, + () => { + const options: ParagraphAwareContentOptions = { requireParagraph: true }; + const actual = paragraphAwareContent(input, options); + expect({ expected: actual }).toMatchObject({ expected }); + }, + ); + } + }); + + // ---------------------------------------------------------------------------------------[ content=(string|EOL)[] ] + describe(`content=(string|EOL)[]: Content only containing strings (including EOL characters)`, () => { + void describe("Default Options", () => { + const singleEOLCases: { input: ContentFixture; expected: ContentFixture; comment: string }[] = [ + { + input: ["\n", "ipsum", "dolor"], + expected: ["\n", "ipsum", "dolor"], + comment: "keep single EOL at start", + }, + { + input: ["lorem", "\n", "dolor"], + expected: ["lorem", "\n", "dolor"], + comment: "keep single EOL in the middle", + }, + { input: ["lorem", "ipsum", "\n"], expected: ["lorem", "ipsum", "\n"], comment: "keep single EOL at end" }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { input, expected, comment }] of singleEOLCases.entries()) { + await t.test(`[${i}] should keep single newline characters (${comment}): ${input}`, () => { + const actual = paragraphAwareContent(input); + expect({ expected: actual }).toMatchObject({ expected }); + }); + } + }); + + const squashNewlinesCases: { input: ContentFixture; expected: ContentFixture; comment: string }[] = [ + { + input: ["\n", "\n", "ipsum", "dolor"], + expected: ["\n", "ipsum", "dolor"], + comment: "Design Scope: Squash newlines irrelevant to trigger as-paragraph-behavior.", + }, + { + input: ["lorem", "\n", "\n", "dolor"], + expected: [p(["lorem"]), p(["dolor"])], + comment: + "respect EOL above threshold in the middle; ensure, that subsequent text-nodes must also be added to a paragraph", + }, + { + input: ["lorem", "ipsum", "\n", "\n"], + expected: ["lorem", "ipsum", "\n"], + comment: "Design Scope: Squash newlines at the end as irrelevant to trigger as-paragraph-behavior.", + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { input, expected, comment }] of squashNewlinesCases.entries()) { + await t.test(`[${i}] should handle consecutive EOL at threshold (${comment}): ${input}`, () => { + const actual = paragraphAwareContent(input); + expect({ expected: actual }).toMatchObject({ expected }); + }); + } + }); + }); + + void describe("requireParagraph=true", () => { + const options: ParagraphAwareContentOptions = { requireParagraph: true }; + + const paragraphizeSingleEOLCases: { input: ContentFixture; expected: ContentFixture; comment: string }[] = [ + { + input: ["\n", "ipsum", "dolor"], + expected: [p(["\n", "ipsum", "dolor"])], + comment: "keep single EOL at start", + }, + { + input: ["lorem", "\n", "dolor"], + expected: [p(["lorem", "\n", "dolor"])], + comment: "keep single EOL in the middle", + }, + { + input: ["lorem", "ipsum", "\n"], + expected: [p(["lorem", "ipsum"])], + comment: "trim EOL at end", + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { input, expected, comment }] of paragraphizeSingleEOLCases.entries()) { + await t.test(`[${i}] should keep single newline characters (${comment}): ${input}`, () => { + const actual = paragraphAwareContent(input, options); + expect({ expected: actual }).toMatchObject({ expected }); + }); + } + }); + + const paragraphizeMultipleEOLCases: { input: ContentFixture; expected: ContentFixture; comment: string }[] = [ + { + input: ["\n", "\n", "ipsum", "dolor"], + expected: [p(["\n", "ipsum", "dolor"])], + comment: "Design Scope: Squash newlines irrelevant to trigger as-paragraph-behavior.", + }, + { + input: ["lorem", "\n", "\n", "dolor"], + expected: [p(["lorem"]), p(["dolor"])], + comment: + "respect EOL above threshold in the middle; ensure, that subsequent text-nodes must also be added to a paragraph", + }, + { + input: ["lorem", "ipsum", "\n", "\n"], + expected: [p(["lorem", "ipsum"])], + comment: "trim EOL at end", + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { input, expected, comment }] of paragraphizeMultipleEOLCases.entries()) { + await t.test(`[${i}] should handle consecutive EOL at threshold (${comment}): ${input}`, () => { + const actual = paragraphAwareContent(input, options); + expect({ expected: actual }).toMatchObject({ expected }); + }); + } + }); + }); + }); + + // -------------------------------------------------------------------------------[ content=(string|TagNode|EOL)[] ] + describe(`content=(string|TagNode|EOL)[]: Content containing anything (including EOL characters)`, () => { + void describe("Default Options", () => { + const singleEOLWithTagNodes: { input: ContentFixture; expected: ContentFixture; comment: string }[] = [ + { + input: ["\n", b(["ipsum"]), "dolor"], + expected: ["\n", b(["ipsum"]), "dolor"], + comment: "keep single EOL at start", + }, + { + input: [b(["lorem"]), "\n", "dolor"], + expected: [b(["lorem"]), "\n", "dolor"], + comment: "keep single EOL in the middle", + }, + { + input: ["lorem", b(["ipsum"]), "\n"], + expected: ["lorem", b(["ipsum"]), "\n"], + comment: "keep single EOL at end", + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { input, expected, comment }] of singleEOLWithTagNodes.entries()) { + await t.test(`[${i}] should keep single newline characters (${comment}): ${input}`, () => { + const actual = paragraphAwareContent(input); + expect({ expected: actual }).toMatchObject({ expected }); + }); + } + }); + + const multipleEOLWithTagNodes: { input: ContentFixture; expected: ContentFixture; comment: string }[] = [ + { + input: ["\n", "\n", b(["ipsum"]), "dolor"], + expected: ["\n", b(["ipsum"]), "dolor"], + comment: "Design Scope: Squash newlines irrelevant to trigger as-paragraph-behavior.", + }, + { + input: [b(["lorem"]), "\n", "\n", "dolor"], + expected: [p([b(["lorem"])]), p(["dolor"])], + comment: + "respect EOL above threshold in the middle; ensure, that subsequent text-nodes must also be added to a paragraph", + }, + { + input: ["lorem", b(["ipsum"]), "\n", "\n"], + expected: ["lorem", b(["ipsum"]), "\n"], + comment: "Design Scope: Squash newlines at the end as irrelevant to trigger as-paragraph-behavior.", + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { input, expected, comment }] of multipleEOLWithTagNodes.entries()) { + await t.test(`[${i}] should handle consecutive EOL at threshold (${comment}): ${input}`, () => { + const actual = paragraphAwareContent(input); + expect({ expected: actual }).toMatchObject({ expected }); + }); + } + }); + }); + + void describe("requireParagraph=true", () => { + const options: ParagraphAwareContentOptions = { requireParagraph: true }; + + const singleEOLWithTagNodes: { input: ContentFixture; expected: ContentFixture; comment: string }[] = [ + { + input: ["\n", b(["ipsum"]), "dolor"], + expected: [p(["\n", b(["ipsum"]), "dolor"])], + comment: "keep single EOL at start", + }, + { + input: [b(["lorem"]), "\n", "dolor"], + expected: [p([b(["lorem"]), "\n", "dolor"])], + comment: "keep single EOL in the middle", + }, + { + input: ["lorem", b(["ipsum"]), "\n"], + expected: [p(["lorem", b(["ipsum"])])], + comment: "trim EOL at end", + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { input, expected, comment }] of singleEOLWithTagNodes.entries()) { + await t.test(`[${i}] should keep single newline characters (${comment}): ${input}`, () => { + const actual = paragraphAwareContent(input, options); + expect({ expected: actual }).toMatchObject({ expected }); + }); + } + }); + + const multipleEOLWithTagNodes: { input: ContentFixture; expected: ContentFixture; comment: string }[] = [ + { + input: ["\n", "\n", b(["ipsum"]), "dolor"], + expected: [p(["\n", b(["ipsum"]), "dolor"])], + comment: "Design Scope: Squash newlines irrelevant to trigger as-paragraph-behavior.", + }, + { + input: [b(["lorem"]), "\n", "\n", "dolor"], + expected: [p([b(["lorem"])]), p(["dolor"])], + comment: + "respect EOL above threshold in the middle; ensure, that subsequent text-nodes must also be added to a paragraph", + }, + { + input: ["lorem", b(["ipsum"]), "\n", "\n"], + expected: [p(["lorem", b(["ipsum"])])], + comment: "trim EOL at end", + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { input, expected, comment }] of multipleEOLWithTagNodes.entries()) { + await t.test(`[${i}] should handle consecutive EOL at threshold (${comment}): ${input}`, () => { + const actual = paragraphAwareContent(input, options); + expect({ expected: actual }).toMatchObject({ expected }); + }); + } + }); + }); + }); + + // -------------------------------------------------------------------------------------------[ Block Tag Handling ] + void describe("Block Tag Handling", () => { + const quoteBlocksWithParagraphs: { input: ContentFixture; expected: ContentFixture; comment: string }[] = [ + { + input: [q(["lorem"])], + expected: [q(["lorem"])], + comment: "single quote block only", + }, + { + input: ["lorem", q(["ipsum"]), "dolor"], + expected: [p(["lorem"]), q(["ipsum"]), p(["dolor"])], + comment: "add paragraphs only before and after", + }, + { + input: ["lorem", "\n", "\n", "ipsum", "\n", "\n", q(["dolor"]), "sit", "\n", "\n", "amet"], + expected: [p(["lorem"]), p(["ipsum"]), q(["dolor"]), p(["sit"]), p(["amet"])], + comment: "quote embedded in paragraphs", + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { input, expected, comment }] of quoteBlocksWithParagraphs.entries()) { + await t.test(`[${i}] should not wrap (default) block tags within paragraphs: ${comment}`, () => { + const options: ParagraphAwareContentOptions = { requireParagraph: true }; + const actual = paragraphAwareContent(input, options); + expect({ expected: actual }).toMatchObject({ expected }); + }); + } + }); + }); + }); + }); +}); diff --git a/packages/ckeditor5-bbcode/test/bbob/ckeditor5Preset.test.ts b/packages/ckeditor5-bbcode/test/bbob/ckeditor5Preset.test.ts new file mode 100644 index 0000000000..e69618c514 --- /dev/null +++ b/packages/ckeditor5-bbcode/test/bbob/ckeditor5Preset.test.ts @@ -0,0 +1,417 @@ +// noinspection HtmlRequiredAltAttribute,HttpUrlsUsage + +import type { TestContext } from "node:test"; +import test, { describe } from "node:test"; +import expect from "expect"; +import html from "@bbob/html"; +import { ckeditor5Preset as preset } from "../../src/bbob/ckeditor5Preset"; + +type HtmlInput = Parameters[0]; +type HtmlResult = ReturnType; + +const parse = (input: HtmlInput): HtmlResult => html(input, preset()); + +void describe("ckeditor5Preset", () => { + void describe("Original Tests from: @bbob/preset-html5", () => { + void test("[b]bolded text[/b]", () => { + const input = "[b]bolded text[/b]"; + const result = 'bolded text'; + expect(parse(input)).toBe(result); + }); + + void test("[i]italicized text[/i]", () => { + const input = "[i]italicized text[/i]"; + const result = 'italicized text'; + expect(parse(input)).toBe(result); + }); + + void test("[u]underlined text[/u]", () => { + const input = "[u]underlined text[/u]"; + const result = 'underlined text'; + expect(parse(input)).toBe(result); + }); + + void test("[s]strikethrough text[/s]", () => { + const input = "[s]strikethrough text[/s]"; + const result = 'strikethrough text'; + expect(parse(input)).toBe(result); + }); + + void test("[url]https://en.wikipedia.org[/url]", () => { + const input = "[url]https://en.wikipedia.org[/url]"; + const result = 'https://en.wikipedia.org'; + + expect(parse(input)).toBe(result); + }); + + void test("[url=http://step.pgc.edu/]ECAT[/url]", () => { + const input = "[url=http://step.pgc.edu/]ECAT[/url]"; + const result = 'ECAT'; + + expect(parse(input)).toBe(result); + }); + + void test("[img]https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Go-home-2.svg/100px-Go-home-2.svg.png[/img]", () => { + const input = + "[img]https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Go-home-2.svg/100px-Go-home-2.svg.png[/img]"; + const result = + ''; + + expect(parse(input)).toBe(result); + }); + + test('[quote="author"]quoted text[/quote]', () => { + const input = '[quote="author"]quoted text[/quote]'; + const result = "

          quoted text

          "; + + expect(parse(input)).toBe(result); + }); + + // We have overridden the behavior to also include a nested `` + // element. + test.skip("[code]monospaced text[/code]", () => { + const input = "[code]monospaced text[/code]"; + const result = "
          monospaced text
          "; + + expect(parse(input)).toBe(result); + }); + + test('[style size="15px"]Large Text[/style]', () => { + const input = '[style size="15px"]Large Text[/style]'; + const result = 'Large Text'; + + expect(parse(input)).toBe(result); + }); + + test('[style color="red"]Red Text[/style]', () => { + const input = '[style color="red"]Red Text[/style]'; + const result = 'Red Text'; + + expect(parse(input)).toBe(result); + }); + + test('[color="red"]Red Text[/color]', () => { + const input = '[color="red"]Red Text[/color]'; + const result = 'Red Text'; + + expect(parse(input)).toBe(result); + }); + + void test(`[list][*]Entry 1[/list]`, () => { + const input = `[list][*]Entry 1[*]Entry 2[/list]`; + const result = "
          • Entry 1
          • Entry 2
          "; + + expect(parse(input)).toBe(result); + }); + + void test(`[list]*Entry 1[/list]`, () => { + const input = `\ + [list] + *Entry 1 + *Entry 2 + [/list]`; + const result = `\ +
            +
          • Entry 1 +
          • Entry 2 +
          `; + + // Patch: Leading Whitespace is trimmed meanwhile to get rid of extra paragraph. + // See `skipEmpty´ option. Thus, trimming expected result. + expect(parse(input)).toBe(result.trim()); + }); + + void test("[list=1][/list]", () => { + const input = `[list=1][/list]`; + const result = `
            `; + + expect(parse(input)).toBe(result); + }); + + void test("[list=A][/list]", () => { + const input = `[list=A][/list]`; + const result = `
              `; + + expect(parse(input)).toBe(result); + }); + + void test(`[table][/table]`, () => { + const input = `[table][tr][td]table 1[/td][td]table 2[/td][/tr][tr][td]table 3[/td][td]table 4[/td][/tr][/table]`; + const result = `
              table 1table 2
              table 3table 4
              `; + + expect(parse(input)).toBe(result); + }); + }); + + /** + * These tests demonstrate flawed behaviors, thus, known bugs, we may ignore + * or have to deal with. If any of these tests fail, a corresponding issue + * raised at BBob may have been fixed meanwhile. + * + * If you happen to see a test failing, search for the referenced issue ID + * within our sources and see, if corresponding actions are required. + * + * Added tests in here should reference an issue and specify a type, thus, + * if we just accepted it as known issue, or if we had to apply a workaround. + */ + void describe("BBob Flawed Behaviors", () => { + // noinspection HtmlUnknownTarget,HtmlUnknownAttribute,BadExpressionStatementJS + const cases = [ + { + bbcode: `[url fakeUnique=fakeUnique]T[/url]`, + expected: `T`, + issue: "https://github.com/JiLiZART/BBob/issues/202", + comment: "getUniqAttr flaw. This test just demonstrates the symptom.", + }, + { + bbcode: `[unknown=https://example.org/ fakeUnique=fakeUnique]T[/unknown]`, + expected: `T`, + issue: "https://github.com/JiLiZART/BBob/issues/202", + comment: + "getUniqAttr flaw. This demonstrates a follow-up issue regarding the default HTML renderer (for BBob Plugin we use a custom renderer with slightly better behavior)", + }, + { + bbcode: `[url=https://example.org/ fakeUnique=fakeUnique]T[/url]`, + expected: `T`, + issue: "https://github.com/JiLiZART/BBob/issues/202", + comment: "getUniqAttr flaw. Demonstrates accidental override.", + }, + { + bbcode: `[url=https://example.org/ hidden]T[/url]`, + expected: `T`, + issue: "https://github.com/JiLiZART/BBob/issues/202", + comment: "getUniqAttr flaw. Demonstrates accidental override, but with more realistic use-case.", + }, + { + bbcode: `[table=onclick][tr][td]T[/td][/tr][/table]`, + expected: `
              T
              `, + issue: "https://github.com/JiLiZART/BBob/issues/202", + comment: "getUniqAttr flaw. Only applicable, if mapping rules do not explicitly remove unhandled attributes.", + }, + { + bbcode: `[table onclick=onclick][tr][td]T[/td][/tr][/table]`, + expected: `
              T
              `, + issue: "https://github.com/JiLiZART/BBob/issues/202", + comment: "getUniqAttr flaw. Only applicable, if mapping rules do not explicitly remove unhandled attributes.", + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { bbcode, expected, issue, comment }] of cases.entries()) { + await t.test(`[${i}] Expected flawed behavior: '${bbcode}' to '${expected}' (${issue}, ${comment})`, () => { + expect(parse(bbcode)).toBe(expected); + }); + } + }); + }); + + void describe("CKEditor 5 Data View Specific Adaptations", () => { + // We have overridden the behavior to also include a nested `` + // element. + void describe("[code]", () => { + const cases = [ + { + bbcode: "[code]text[/code]", + expected: `
              text
              `, + comment: "CKEditor 5 Text Part Language uses 'plaintext' as the default.", + }, + { + bbcode: "[code=css]text[/code]", + expected: `
              text
              `, + comment: "CKEditor 5 Text Part Language encodes chosen languages into 'language-*' class", + }, + { + bbcode: `[code=hack"me]text[/code]`, + expected: `
              text
              `, + comment: "Prevent hacking attribute by encoding.", + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { bbcode, expected, comment }] of cases.entries()) { + await t.test(`[${i}] Should transform $bbcode to: ${expected} (${comment})`, () => { + expect(parse(bbcode)).toBe(expected); + }); + } + }); + }); + + void describe("Paragraphs (denoted by double newline)", () => { + const cases = [ + { + bbcode: `Lorem\n\nIpsum`, + expected: `

              Lorem

              Ipsum

              `, + comment: `standard paragraph processing`, + }, + { + bbcode: `Lorem\nIpsum`, + expected: `Lorem\nIpsum`, + comment: `nothing to do for single newline; Design Scope: We may have added
              here.`, + }, + { + bbcode: `Lorem\n\n`, + expected: `Lorem\n`, + comment: `some trimming applied, but (just) newlines at the end never trigger paragraph processing`, + }, + { + bbcode: `\n\nLorem`, + expected: `\nLorem`, + comment: `newlines at the beginning do not trigger paragraph processing but get trimmed`, + }, + { + bbcode: `[quote]Lorem\n\nIpsum[/quote]`, + expected: `

              Lorem

              Ipsum

              `, + comment: `quote: add each paragraph separately`, + }, + { + bbcode: `[quote]Lorem[quote]Ipsum[/quote][/quote]`, + expected: `

              Lorem

              Ipsum

              `, + comment: `quote: handle nested blockquotes properly`, + }, + { + bbcode: `[quote]Lorem[list][*]Ipsum[/list][/quote]`, + expected: `

              Lorem

              • Ipsum
              `, + comment: `quote: handle nested block-level elements properly`, + }, + { + bbcode: `[list][*]Lorem\n\nIpsum\n[/list]`, + expected: `
              • Lorem

                Ipsum

              `, + comment: `list/li: add each paragraph separately`, + }, + { + bbcode: `Lorem\n\nipsum\n[quote]dolor[/quote]\nsit`, + expected: `

              Lorem

              ipsum

              dolor

              \nsit

              `, + comment: `Continue with paragraphs, once we added them on a given hierarchy level. Extra newline (sit) is within design scope.`, + }, + { + bbcode: `[b]Lorem\n\nIpsum[/b]`, + expected: `Lorem\n\nIpsum`, + comment: `no paragraphs within inline tags`, + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { bbcode, expected, comment }] of cases.entries()) { + await t.test(`[${i}] Should transform $bbcode to: ${expected} (${comment})`, () => { + expect(parse(bbcode)).toBe(expected); + }); + } + }); + }); + }); + + void describe("Additional Tag Support", () => { + // [size] Was supported in CKEditor 4 BBCode Plugin. The number represented + // a percentage value. As CKEditor 5 does not support percentage values in + // Font Size Feature, some enum-like mapping to classes is applied. + void describe("[size]", () => { + const cases = [ + { + bbcode: `[size]T[/size]`, + expected: `T`, + comment: "Corner Case: Ignore Invalid (because missing) size value", + }, + { + bbcode: `[size=lorem]T[/size]`, + expected: `T`, + comment: "Corner Case: Ignore Invalid (because textual) size value", + }, + { + bbcode: `[size=42px]T[/size]`, + expected: `T`, + comment: "Corner Case: Ignore Invalid (because with size unit) size value", + }, + { + bbcode: `[size=${Number.MIN_SAFE_INTEGER}]T[/size]`, + expected: `T`, + comment: "Corner Case: Negative (minimal safe integer) maps to text-tiny", + }, + { + bbcode: `[size=-1]T[/size]`, + expected: `T`, + comment: "Corner Case: Negative maps to text-tiny", + }, + { + bbcode: `[size=+1]T[/size]`, + expected: `T`, + comment: "Corner Case: '+' prefix is ignored", + }, + { + bbcode: `[size=0]T[/size]`, + expected: `T`, + comment: "Lower-Bound for text-tiny", + }, + { bbcode: `[size=70]T[/size]`, expected: `T`, comment: "Default for text-tiny" }, + { + bbcode: `[size=77]T[/size]`, + expected: `T`, + comment: "Upper-Bound for text-tiny", + }, + { + bbcode: `[size=78]T[/size]`, + expected: `T`, + comment: "Lower-Bound for text-small", + }, + { + bbcode: `[size=85]T[/size]`, + expected: `T`, + comment: "Default for text-small", + }, + { + bbcode: `[size=92]T[/size]`, + expected: `T`, + comment: "Upper-Bound for text-small", + }, + { + bbcode: `[size=93]T[/size]`, + expected: `T`, + comment: "Lower-Bound for normal text size (no class)", + }, + { + bbcode: `[size=100]T[/size]`, + expected: `T`, + comment: "Default for normal text size (no class)", + }, + { + bbcode: `[size=119]T[/size]`, + expected: `T`, + comment: "Upper-Bound for normal text size (no class)", + }, + { + bbcode: `[size=120]T[/size]`, + expected: `T`, + comment: "Lower-Bound for text-big", + }, + { bbcode: `[size=140]T[/size]`, expected: `T`, comment: "Default for text-big" }, + { + bbcode: `[size=159]T[/size]`, + expected: `T`, + comment: "Upper-Bound for text-big", + }, + { + bbcode: `[size=160]T[/size]`, + expected: `T`, + comment: "Lower-Bound for text-huge", + }, + { + bbcode: `[size=180]T[/size]`, + expected: `T`, + comment: "Default for text-huge", + }, + { + bbcode: `[size=${Number.MAX_SAFE_INTEGER}]T[/size]`, + expected: `T`, + comment: "Upper-Bound for text-huge", + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { bbcode, expected, comment }] of cases.entries()) { + await t.test(`[${i}] Should transform $bbcode to: ${expected} (${comment})`, () => { + expect(parse(bbcode)).toBe(expected); + }); + } + }); + }); + }); +}); diff --git a/packages/ckeditor5-bbcode/test/html2bbcode.test.ts b/packages/ckeditor5-bbcode/test/html2bbcode.test.ts new file mode 100644 index 0000000000..0be9d9d19f --- /dev/null +++ b/packages/ckeditor5-bbcode/test/html2bbcode.test.ts @@ -0,0 +1,240 @@ +import type { TestContext } from "node:test"; +import { describe, test } from "node:test"; +import expect from "expect"; +import { bbCodeDefaultRules } from "../src"; +import { html2bbcode } from "../src/html2bbcode"; +import { parseAsFragment } from "./DOMUtils"; + +const rules = bbCodeDefaultRules; + +const aut = { + expectTransformation: ({ dataView, expectedData }: { dataView: string; expectedData: string }): void => { + const actualData = html2bbcode(parseAsFragment(dataView), rules); + try { + expect(actualData).toBe(expectedData); + } catch (e) { + console.debug("Failed expectations.", { + dataView, + actualData, + expectedData, + }); + throw e; + } + }, +}; + +const url = { + absolute: "https://example.org/", + relative: "/example", +}; + +void describe("html2bbcode", () => { + const bbcodeCases = [ + { + tag: "[b] (style)", + openElement: '', + closeElement: "", + openTag: "[b]", + closeTag: "[/b]", + }, + { tag: "[b] (strong)", openElement: "", closeElement: "", openTag: "[b]", closeTag: "[/b]" }, + { tag: "[b] (b)", openElement: "", closeElement: "", openTag: "[b]", closeTag: "[/b]" }, + { + tag: "[color]", + openElement: '', + closeElement: "", + openTag: "[color=red]", + closeTag: "[/color]", + }, + { + tag: "[size]", + openElement: '', + closeElement: "", + openTag: "[size=85]", + closeTag: "[/size]", + }, + { tag: "[h1]", openElement: "

              ", closeElement: "

              ", openTag: "[h1]", closeTag: "[/h1]" }, + { tag: "[h2]", openElement: "

              ", closeElement: "

              ", openTag: "[h2]", closeTag: "[/h2]" }, + { tag: "[h3]", openElement: "

              ", closeElement: "

              ", openTag: "[h3]", closeTag: "[/h3]" }, + { tag: "[h4]", openElement: "

              ", closeElement: "

              ", openTag: "[h4]", closeTag: "[/h4]" }, + { tag: "[h5]", openElement: "
              ", closeElement: "
              ", openTag: "[h5]", closeTag: "[/h5]" }, + { tag: "[h6]", openElement: "
              ", closeElement: "
              ", openTag: "[h6]", closeTag: "[/h6]" }, + { + tag: "[i] (style)", + openElement: '', + closeElement: "", + openTag: "[i]", + closeTag: "[/i]", + }, + { tag: "[i] (i)", openElement: "", closeElement: "", openTag: "[i]", closeTag: "[/i]" }, + { tag: "[i] (em)", openElement: "", closeElement: "", openTag: "[i]", closeTag: "[/i]" }, + { + tag: "[s] (style)", + openElement: '', + closeElement: "", + openTag: "[s]", + closeTag: "[/s]", + }, + { tag: "[s] (del)", openElement: "", closeElement: "", openTag: "[s]", closeTag: "[/s]" }, + { tag: "[s] (strike)", openElement: "", closeElement: "", openTag: "[s]", closeTag: "[/s]" }, + { + tag: "[u] (style)", + openElement: '', + closeElement: "", + openTag: "[u]", + closeTag: "[/u]", + }, + { tag: "[u] (u)", openElement: "", closeElement: "", openTag: "[u]", closeTag: "[/u]" }, + { tag: "[u] (ins)", openElement: "", closeElement: "", openTag: "[u]", closeTag: "[/u]" }, + { + tag: "[url]", + openElement: ``, + closeElement: "", + openTag: `[url="${url.absolute}"]`, + closeTag: "[/url]", + }, + ] as const; + + for (const { tag, openTag, closeTag, openElement, closeElement } of bbcodeCases) { + describe(`${tag} (Standard Behaviors)`, () => { + const cases = [ + { + dataView: `${openElement}T${closeElement}`, + expectedData: `${openTag}T${closeTag}`, + comment: `default`, + }, + ]; + + void test("cases", async (t: TestContext) => { + for (const [i, { dataView, expectedData, comment }] of cases.entries()) { + await t.test(`[${i}] Should process data view '${dataView}' to: '${expectedData}' (${comment})`, () => { + aut.expectTransformation({ dataView, expectedData }); + }); + } + }); + }); + } + + void describe("[url]", () => { + const cases = [ + { + dataView: `T`, + expectedData: `[url="${url.absolute}"]T[/url]`, + comment: "default (absolute)", + }, + { + dataView: `${url.absolute}`, + expectedData: `[url]${url.absolute}[/url]`, + comment: "pretty print (absolute)", + }, + { + dataView: `T`, + expectedData: `[url="${url.absolute}"][i]T[/i][/url]`, + comment: "nested element support (inner)", + }, + { + dataView: `T`, + expectedData: `[i][url="${url.absolute}"]T[/url][/i]`, + comment: "nested element support (outer)", + }, + { + dataView: `T`, + expectedData: `[url="${url.absolute}?brackets=%5B%5D"]T[/url]`, + comment: "escape brackets in URL", + }, + { + dataView: `${url.absolute}?brackets=[]`, + expectedData: `[url="${url.absolute}?brackets=%5B%5D"]${url.absolute}?brackets=\\[\\][/url]`, + comment: "different escaping for different contexts", + }, + { + dataView: `T`, + expectedData: `[url="${url.relative}"]T[/url]`, + comment: "default (relative)", + }, + { + dataView: `${url.relative}`, + expectedData: `[url]${url.relative}[/url]`, + comment: "pretty print (relative)", + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { dataView, expectedData, comment }] of cases.entries()) { + await t.test(`[${i}] Should process data view '${dataView}' to: '${expectedData}' (${comment})`, () => { + aut.expectTransformation({ dataView, expectedData }); + }); + } + }); + }); + + void describe("[img]", () => { + // noinspection HtmlRequiredAltAttribute + const cases = [ + { + dataView: ``, + expectedData: `[img]${url.absolute}[/img]`, + comment: "default (absolute)", + }, + { + dataView: ``, + expectedData: `[url="${url.absolute}"][img]${url.absolute}[/img][/url]`, + comment: "linked image", + }, + { + dataView: `BEFOREAFTER`, + expectedData: `[i]BEFORE[img]${url.absolute}[/img]AFTER[/i]`, + comment: "images are inline", + }, + { + dataView: ``, + expectedData: `[img]${url.absolute}?brackets=%5B%5D[/img]`, + comment: "escape brackets in URL", + }, + { + dataView: ``, + expectedData: `[img]${url.relative}[/img]`, + comment: "default (relative)", + }, + { + dataView: `BEFOREAFTER`, + expectedData: `BEFOREAFTER`, + comment: "design-scope: Remove irrelevant image with empty src", + }, + { + dataView: `ALT`, + expectedData: `[img alt="ALT"]${url.absolute}[/img]`, + comment: "alt attribute support (default)", + }, + { + dataView: `BEFOREALTAFTER`, + expectedData: `BEFOREAFTER`, + comment: "design-scope: Remove irrelevant image with empty src, even if alt is set", + }, + { + dataView: `with space`, + expectedData: `[img alt="with space"]${url.absolute}[/img]`, + comment: "attribute with spaces; latest, that we require quotes for BBob", + }, + { + dataView: `let's "quote"`, + expectedData: `[img alt="let's \\"quote\\""]${url.absolute}[/img]`, + comment: "alt text quote challenge (must not escape BBCode attribute)", + }, + { + dataView: `in [brackets]`, + expectedData: `[img alt="in [brackets]"]${url.absolute}[/img]`, + comment: + "alt text with square brackets; for best robustness in BBCode parsers encoded; design-scope: This introduces entities and thus assumes BBCode is always processed to some XML format.", + }, + ] as const; + + void test("cases", async (t: TestContext) => { + for (const [i, { dataView, expectedData, comment }] of cases.entries()) { + await t.test(`[${i}] Should process data view '${dataView}' to: '${expectedData}' (${comment})`, () => { + aut.expectTransformation({ dataView, expectedData }); + }); + } + }); + }); +}); diff --git a/packages/ckeditor5-bbcode/tsconfig.json b/packages/ckeditor5-bbcode/tsconfig.json index 23fd955189..edd3689c42 100644 --- a/packages/ckeditor5-bbcode/tsconfig.json +++ b/packages/ckeditor5-bbcode/tsconfig.json @@ -1,28 +1,13 @@ { "extends": "../../tsconfig.json", "include": [ - "./__tests__", - "./src", - "./types" + "./test", + "./src" ], "compilerOptions": { "outDir": "./dist", - "paths": { - "@bbob/core/*": [ - "./types/@bbob/core/*" - ], - "@bbob/html/*": [ - "./types/@bbob/html/*" - ], - "@bbob/plugin-helper/*": [ - "./types/@bbob/plugin-helper/*" - ], - "@bbob/preset/*": [ - "./types/@bbob/preset/*" - ], - "@bbob/preset-html5/*": [ - "./types/@bbob/preset-html5/*" - ] - } + "types": [ + "node" + ] } } diff --git a/packages/ckeditor5-bbcode/tsconfig.release.json b/packages/ckeditor5-bbcode/tsconfig.release.json index 5b48a1de22..b94c2a597d 100644 --- a/packages/ckeditor5-bbcode/tsconfig.release.json +++ b/packages/ckeditor5-bbcode/tsconfig.release.json @@ -1,6 +1,6 @@ { "extends": "./tsconfig.json", "exclude": [ - "./__tests__/", + "./test/" ] } diff --git a/packages/ckeditor5-bbcode/types/@bbob/core/es/index.d.ts b/packages/ckeditor5-bbcode/types/@bbob/core/es/index.d.ts deleted file mode 100644 index 073b89965c..0000000000 --- a/packages/ckeditor5-bbcode/types/@bbob/core/es/index.d.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { TagNode } from "@bbob/plugin-helper/es"; - -export type CoreWalk = (callback: (node: string | TagNode) => string | TagNode) => void; - -export type CoreParser = () => unknown; -export type CoreRenderable = null | string | number | TagNode; -export type CoreRenderNode = CoreRenderable | CoreRenderable[]; -export type CoreRenderer = (node: CoreRenderNode, options?: object) => string; -export type CoreData = unknown | null; - -export interface CoreOptions { - parser?: CoreParser; - render?: CoreRenderer; - skipParse?: boolean; - data?: CoreData; - onlyAllowTags?: string[]; - contextFreeTags?: string[]; - enableEscapeTags?: boolean; - openTag?: string; - closeTag?: string; - onError?: (info: { message: string; tagName: string; lineNumber: number; columnNumber: number }) => void; - - [key: string]: unknown; -} - -export type CoreMatcher = (expression: unknown, callback: (node: string | TagNode) => string | TagNode) => unknown; - -export type CoreTree = (string | TagNode)[] & { - messages: string[]; - options: CoreOptions; - walk: CoreWalk; - match: CoreMatcher; -}; - -export type CoreIterator = (tree: CoreTree, callback: (entry: CoreTree[number]) => CoreTree[number]) => CoreTree; - -export type CorePlugin = ( - tree: CoreTree, - options: { parse: CoreParser; render: CoreRenderer; iterate: CoreIterator; match: CoreMatcher; data: CoreData }, -) => CoreTree | CoreTree[number] | undefined | null; - -// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type -export default function bbob(plugs?: Function | Function[]): { - process: ( - input: string | undefined, - opts?: CoreOptions, - ) => { - readonly html: string; - tree: CoreTree; - raw: unknown; - messages: CoreTree["messages"]; - }; -}; diff --git a/packages/ckeditor5-bbcode/types/@bbob/html/es/index.d.ts b/packages/ckeditor5-bbcode/types/@bbob/html/es/index.d.ts deleted file mode 100644 index c913a92dfa..0000000000 --- a/packages/ckeditor5-bbcode/types/@bbob/html/es/index.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { TagNode } from "@bbob/plugin-helper/es"; - -type BBNode = null | string | number | TagNode | BBNode[]; -export default function toHTML( - source: string, - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type - plugins?: Function | Function[], - options?: { - onlyAllowTags?: string[]; - contextFreeTags?: string[]; - enableEscapeTags?: boolean; - }, -): string; -export const render: (nodes: BBNode, { stripTags = false } = {}) => string; diff --git a/packages/ckeditor5-bbcode/types/@bbob/plugin-helper/es/index.d.ts b/packages/ckeditor5-bbcode/types/@bbob/plugin-helper/es/index.d.ts deleted file mode 100644 index 2bcbd6d7f2..0000000000 --- a/packages/ckeditor5-bbcode/types/@bbob/plugin-helper/es/index.d.ts +++ /dev/null @@ -1,31 +0,0 @@ -type TagAttrs = Record; - -export declare class TagNode { - readonly tag: string; - attrs: TagAttrs; - content: (string | TagNode)[] | null; - attr: (name: string, value?: string) => string | undefined; - append: (value: string) => void; - readonly length: number; - static create: (tag: string, attrs: TagAttrs = {}, content: (string | TagNode)[] | null = []) => TagNode; - static isOf: (node: TagNode, type: string) => node is TagNode & { tag: typeof type }; -} - -export declare function isTagNode(el: unknown): el is TagNode; -export declare function isStringNode(el: unknown): el is string; -export declare function isEOL(el: unknown): el is "\n"; -export declare function escapeHTML(value: string): string; - -export declare function getUniqAttr(attrs: Record): string | null; - -export declare const N: "\n"; -export declare const TAB: "\t"; -export declare const F: "\f"; -export declare const R: "\r"; -export declare const EQ: "="; -export declare const QUOTEMARK: `"`; -export declare const SPACE: " "; -export declare const OPEN_BRAKET: "["; -export declare const CLOSE_BRAKET: "]"; -export declare const SLASH: "/"; -export declare const BACKSLASH: "\\"; diff --git a/packages/ckeditor5-bbcode/types/@bbob/preset-html5/es/defaultTags.d.ts b/packages/ckeditor5-bbcode/types/@bbob/preset-html5/es/defaultTags.d.ts deleted file mode 100644 index 8548383d1a..0000000000 --- a/packages/ckeditor5-bbcode/types/@bbob/preset-html5/es/defaultTags.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { createPreset } from "@bbob/preset/es"; - -type DefaultTags = Parameters[0]; -type TagMappingFn = DefaultTags[string]; -type SupportedBBCodeTag = "b" | "i" | "u" | "s" | "url" | "img" | "quote" | "code" | "style" | "list" | "color"; - -declare const defaultTags: Record; -export default defaultTags; diff --git a/packages/ckeditor5-bbcode/types/@bbob/preset-html5/es/index.d.ts b/packages/ckeditor5-bbcode/types/@bbob/preset-html5/es/index.d.ts deleted file mode 100644 index 11dc440b17..0000000000 --- a/packages/ckeditor5-bbcode/types/@bbob/preset-html5/es/index.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { createPreset } from "@bbob/preset/es"; - -declare const html5Preset: ReturnType; -export default html5Preset; diff --git a/packages/ckeditor5-bbcode/types/@bbob/preset/es/index.d.ts b/packages/ckeditor5-bbcode/types/@bbob/preset/es/index.d.ts deleted file mode 100644 index e92d20054b..0000000000 --- a/packages/ckeditor5-bbcode/types/@bbob/preset/es/index.d.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { TagNode } from "@bbob/plugin-helper/es"; - -type Content = TagNode["content"] | TagNode; - -type RenderFn = (content: Content, options?: Options) => string; - -interface Options { - skipParse?: boolean; - parser?: unknown; - render: RenderFn; - data?: unknown; - - [key: string]: unknown; -} - -/** - * Creates preset for @bbob/core - * @param defTags - default tags - * @param processor - a processor function of tree - * @returns preset function - */ -declare function createPreset( - defTags: Record string | TagNode>, - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type - processor?: Function, -): { - extend: ( - callback: (defTags: Parameters[0], options: object) => Parameters[0], - ) => ReturnType; - options?: object; - // Function signature. - (options: object = {}): { - options?: object; - (tree: unknown, core: { render: RenderFn }): unknown; - }; -}; - -export { createPreset }; -export default createPreset; diff --git a/packages/ckeditor5-common/jest.config.cjs b/packages/ckeditor5-common/jest.config.cjs deleted file mode 100644 index 208f377fa0..0000000000 --- a/packages/ckeditor5-common/jest.config.cjs +++ /dev/null @@ -1,3 +0,0 @@ -const jestConfig = require("@coremedia-internal/ckeditor5-jest-test-helpers/shared-jest.config.js"); - -module.exports = { ...jestConfig }; diff --git a/packages/ckeditor5-common/package.json b/packages/ckeditor5-common/package.json index 86883ac878..467c5f1178 100644 --- a/packages/ckeditor5-common/package.json +++ b/packages/ckeditor5-common/package.json @@ -1,6 +1,7 @@ { "name": "@coremedia/ckeditor5-common", "version": "25.0.4-rc.4", + "type": "module", "description": "Common Utilities, independent from CKEditor", "author": { "name": "CoreMedia GmbH", @@ -33,19 +34,20 @@ ], "license": "Apache-2.0", "devDependencies": { - "@coremedia-internal/ckeditor5-jest-test-helpers": "^1.0.0", - "@jest/globals": "^29.7.0", - "@types/jest": "^29.5.12", - "ckeditor5": "45.2.1", + "@coremedia-internal/studio-client.test-runner-helper": "0.2.3-alpha.050b44a", + "@types/node": "^22.0.0", + "ckeditor5": "46.1.1", "copyfiles": "^2.4.1", - "jest": "^29.7.0", - "jest-each": "^29.7.0", + "expect": "^30.2.0", + "global-jsdom": "^27.0.0", + "jsdom": "^27.0.0", "rimraf": "^6.0.1", - "ts-jest": "^29.2.4", + "ts-node": "^10.9.2", + "tsx": "^4.20.6", "typescript": "5.4.5" }, "peerDependencies": { - "ckeditor5": "45.2.1" + "ckeditor5": "46.1.1" }, "dependencies": { "@coremedia/ckeditor5-logging": "25.0.4-rc.4" @@ -56,9 +58,8 @@ "clean:dist": "rimraf ./dist", "build": "tsc --project ./tsconfig.release.json", "postbuild": "node prepare-package.cjs && copyfiles ./README.md dist", - "jest": "jest --passWithNoTests", - "jest:coverage": "jest --collect-coverage --passWithNoTests", - "lint": "eslint \"**/*.{js,cjs,mjs,ts,tsx}\"", - "npm-check-updates": "npm-check-updates --upgrade" + "npm-check-updates": "npm-check-updates --upgrade", + "lint": "eslint \"**/*.{ts,tsx}\"", + "test": "test-runner-react" } } diff --git a/packages/ckeditor5-common/__tests__/AdvancedTypes.test.ts b/packages/ckeditor5-common/test/AdvancedTypes.test.ts similarity index 55% rename from packages/ckeditor5-common/__tests__/AdvancedTypes.test.ts rename to packages/ckeditor5-common/test/AdvancedTypes.test.ts index 97039b390f..7340b94ce1 100644 --- a/packages/ckeditor5-common/__tests__/AdvancedTypes.test.ts +++ b/packages/ckeditor5-common/test/AdvancedTypes.test.ts @@ -1,8 +1,12 @@ +import "global-jsdom/register"; +import type { TestContext } from "node:test"; +import test, { describe } from "node:test"; +import expect from "expect"; import { isRaw } from "../src/AdvancedTypes"; -describe("AdvancedTypes", () => { - describe("isRaw", () => { - it("Demonstrate Use Case", () => { +void describe("AdvancedTypes", () => { + void describe("isRaw", () => { + void test("Demonstrate Use Case", () => { const value = "some value"; const obj: unknown = { value }; @@ -26,26 +30,18 @@ describe("AdvancedTypes", () => { expect(actual1).toStrictEqual(actual2); }); - it.each` - value - ${"string"} - ${["string"]} - ${""} - ${null} - ${undefined} - ${0} - ${42} - ${[0, 42]} - ${true} - ${false} - ${{}} - ${{ key: "value" }} - `("[$#] Should signal `true` for existing property having value: `$value`.", ({ value }) => { - const obj = { value }; - expect(isRaw(obj, "value")).toStrictEqual(true); + const cases = ["string", ["string"], "", null, undefined, 0, 42, [0, 42], true, false, {}, { key: "value" }]; + + void test("cases", async (t: TestContext) => { + for (const [i, value] of cases.entries()) { + await t.test(`[${i}] Should signal 'true' for existing property having value: '${value}'.`, () => { + const obj = { value }; + expect(isRaw(obj, "value")).toStrictEqual(true); + }); + } }); - it("Should signal `false for missing property", () => { + void test("Should signal `false for missing property", () => { const value = "some value"; const obj: unknown = { value }; expect(isRaw(obj, "notExisting")).toStrictEqual(false); diff --git a/packages/ckeditor5-common/__tests__/RequiredNonNull.test.ts b/packages/ckeditor5-common/test/RequiredNonNull.test.ts similarity index 79% rename from packages/ckeditor5-common/__tests__/RequiredNonNull.test.ts rename to packages/ckeditor5-common/test/RequiredNonNull.test.ts index 7b089b33e0..a013599498 100644 --- a/packages/ckeditor5-common/__tests__/RequiredNonNull.test.ts +++ b/packages/ckeditor5-common/test/RequiredNonNull.test.ts @@ -1,4 +1,8 @@ -import { RequiredNonNull, RequiredNonNullPropertiesMissingError, requireNonNulls } from "../src/RequiredNonNull"; +import "global-jsdom/register"; +import test, { describe } from "node:test"; +import expect from "expect"; +import type { RequiredNonNull } from "../src/RequiredNonNull"; +import { RequiredNonNullPropertiesMissingError, requireNonNulls } from "../src/RequiredNonNull"; interface WithOptionalNullableValues { optionalNullable?: number | null; @@ -12,8 +16,8 @@ class WithOptionalNullableValuesImpl implements WithOptionalNullableValues { ) {} } -describe("RequiredNonNull", () => { - it("Use Case: RequiredNonNull", () => { +void describe("RequiredNonNull", () => { + void test("Use Case: RequiredNonNull", () => { // Only required needs to be set and `null` is a valid option. const defaultProbe: WithOptionalNullableValues = { requiredNullable: null, @@ -38,14 +42,14 @@ describe("RequiredNonNull", () => { ); }); - describe("requireNonNulls", () => { - it("should pass for all unset, but none required to be non-null", () => { + void describe("requireNonNulls", () => { + void test("should pass for all unset, but none required to be non-null", () => { const probe: WithOptionalNullableValues = { requiredNullable: null }; const probeFn = () => requireNonNulls(probe); expect(probeFn).not.toThrow(); }); - it("should fail for unset optional property", () => { + void test("should fail for unset optional property", () => { const probe: WithOptionalNullableValues = new WithOptionalNullableValuesImpl(null); const probeFn = () => requireNonNulls(probe, "optionalNullable"); expect(probeFn).toThrow(RequiredNonNullPropertiesMissingError); @@ -53,7 +57,7 @@ describe("RequiredNonNull", () => { expect(probeFn).toThrow(/property.*WithOptionalNullableValuesImpl.*optionalNullable/); }); - it("should fail for optional property set to null", () => { + void test("should fail for optional property set to null", () => { const probe: WithOptionalNullableValues = new WithOptionalNullableValuesImpl(null, null); const probeFn = () => requireNonNulls(probe, "optionalNullable"); expect(probeFn).toThrow(RequiredNonNullPropertiesMissingError); @@ -61,7 +65,7 @@ describe("RequiredNonNull", () => { expect(probeFn).toThrow(/property.*WithOptionalNullableValuesImpl.*optionalNullable/); }); - it("should fail for required property set to null", () => { + void test("should fail for required property set to null", () => { const probe: WithOptionalNullableValues = new WithOptionalNullableValuesImpl(null); const probeFn = () => requireNonNulls(probe, "requiredNullable"); expect(probeFn).toThrow(RequiredNonNullPropertiesMissingError); @@ -69,7 +73,7 @@ describe("RequiredNonNull", () => { expect(probeFn).toThrow(/property.*WithOptionalNullableValuesImpl.*requiredNullable/); }); - it("should fail for both properties set to null", () => { + void test("should fail for both properties set to null", () => { const probe: WithOptionalNullableValues = new WithOptionalNullableValuesImpl(null); const probeFn = () => requireNonNulls(probe, "requiredNullable", "optionalNullable"); expect(probeFn).toThrow(RequiredNonNullPropertiesMissingError); @@ -77,7 +81,7 @@ describe("RequiredNonNull", () => { expect(probeFn).toThrow(/properties.*WithOptionalNullableValuesImpl.*((requiredNullable|optionalNullable).*){2}/); }); - it("should pass for all set to non-null", () => { + void test("should pass for all set to non-null", () => { const probe: WithOptionalNullableValues = new WithOptionalNullableValuesImpl(21, 42); const probeFn = () => requireNonNulls(probe); expect(probeFn).not.toThrow(); diff --git a/packages/ckeditor5-common/tsconfig.json b/packages/ckeditor5-common/tsconfig.json index 40d1484330..bc71e18f70 100644 --- a/packages/ckeditor5-common/tsconfig.json +++ b/packages/ckeditor5-common/tsconfig.json @@ -1,10 +1,13 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "outDir": "./dist" + "outDir": "./dist", + "types": [ + "node" + ] }, "include": [ - "./__tests__", + "./test", "./src" ] } diff --git a/packages/ckeditor5-common/tsconfig.release.json b/packages/ckeditor5-common/tsconfig.release.json index c4486a896a..6f82b8a4bf 100644 --- a/packages/ckeditor5-common/tsconfig.release.json +++ b/packages/ckeditor5-common/tsconfig.release.json @@ -1,6 +1,6 @@ { "extends": "./tsconfig.json", "exclude": [ - "./__tests__" + "./test" ] } diff --git a/packages/ckeditor5-core-common/jest.config.cjs b/packages/ckeditor5-core-common/jest.config.cjs deleted file mode 100644 index 208f377fa0..0000000000 --- a/packages/ckeditor5-core-common/jest.config.cjs +++ /dev/null @@ -1,3 +0,0 @@ -const jestConfig = require("@coremedia-internal/ckeditor5-jest-test-helpers/shared-jest.config.js"); - -module.exports = { ...jestConfig }; diff --git a/packages/ckeditor5-core-common/package.json b/packages/ckeditor5-core-common/package.json index 7741af710a..0e5e8dd4d8 100644 --- a/packages/ckeditor5-core-common/package.json +++ b/packages/ckeditor5-core-common/package.json @@ -1,6 +1,7 @@ { "name": "@coremedia/ckeditor5-core-common", "version": "25.0.4-rc.4", + "type": "module", "description": "Assistive Utilities for `@ckeditor/ckeditor5-core`.", "author": { "name": "CoreMedia GmbH", @@ -36,17 +37,13 @@ ], "license": "Apache-2.0", "devDependencies": { - "@coremedia-internal/ckeditor5-jest-test-helpers": "^1.0.0", - "@types/jest": "^29.5.12", - "ckeditor5": "45.2.1", + "ckeditor5": "46.1.1", "copyfiles": "^2.4.1", - "jest": "^29.7.0", - "jest-config": "^29.7.0", "rimraf": "^6.0.1", "typescript": "5.4.5" }, "peerDependencies": { - "ckeditor5": "45.2.1" + "ckeditor5": "46.1.1" }, "dependencies": { "@coremedia/ckeditor5-common": "25.0.4-rc.4", @@ -58,9 +55,7 @@ "clean:dist": "rimraf ./dist", "build": "tsc --project ./tsconfig.release.json", "postbuild": "node prepare-package.cjs && copyfiles ./README.md dist", - "jest": "jest --passWithNoTests", - "jest:coverage": "jest --collect-coverage --passWithNoTests", - "lint": "eslint \"**/*.{js,cjs,mjs,ts,tsx}\"", + "lint": "eslint \"**/*.{ts,tsx}\"", "npm-check-updates": "npm-check-updates --upgrade" } } diff --git a/packages/ckeditor5-core-common/src/Commands.ts b/packages/ckeditor5-core-common/src/Commands.ts index 8cf3f06d09..63b2b52387 100644 --- a/packages/ckeditor5-core-common/src/Commands.ts +++ b/packages/ckeditor5-core-common/src/Commands.ts @@ -1,5 +1,6 @@ -import { Editor, Command } from "ckeditor5"; -import { Logger, LoggerProvider } from "@coremedia/ckeditor5-logging"; +import type { Editor, Command } from "ckeditor5"; +import type { Logger } from "@coremedia/ckeditor5-logging"; +import { LoggerProvider } from "@coremedia/ckeditor5-logging"; const commandsLogger: Logger = LoggerProvider.getLogger("Commands"); diff --git a/packages/ckeditor5-core-common/src/Plugins.ts b/packages/ckeditor5-core-common/src/Plugins.ts index ab466b1d9a..356b830432 100644 --- a/packages/ckeditor5-core-common/src/Plugins.ts +++ b/packages/ckeditor5-core-common/src/Plugins.ts @@ -1,5 +1,5 @@ import { type Logger, LoggerProvider } from "@coremedia/ckeditor5-logging"; -import { Editor, Plugin, PluginCollection, PluginConstructor, PluginsMap } from "ckeditor5"; +import type { Editor, Plugin, PluginCollection, PluginConstructor, PluginsMap } from "ckeditor5"; const pluginsLogger: Logger = LoggerProvider.getLogger("Plugins"); diff --git a/packages/ckeditor5-core-common/src/index.ts b/packages/ckeditor5-core-common/src/index.ts index 43c6916ff1..25db045edb 100644 --- a/packages/ckeditor5-core-common/src/index.ts +++ b/packages/ckeditor5-core-common/src/index.ts @@ -11,11 +11,5 @@ export { type OnMissingPlugin, } from "./Plugins"; export { addTranslations, openLink } from "./utils"; -export { - CommandHandler, - disableCommand, - enableCommand, - ifCommand, - optionalCommandNotFound, - recommendCommand, -} from "./Commands"; +export type { CommandHandler } from "./Commands"; +export { disableCommand, enableCommand, ifCommand, optionalCommandNotFound, recommendCommand } from "./Commands"; diff --git a/packages/ckeditor5-coremedia-blocklist/jest.config.cjs b/packages/ckeditor5-coremedia-blocklist/jest.config.cjs deleted file mode 100644 index 208f377fa0..0000000000 --- a/packages/ckeditor5-coremedia-blocklist/jest.config.cjs +++ /dev/null @@ -1,3 +0,0 @@ -const jestConfig = require("@coremedia-internal/ckeditor5-jest-test-helpers/shared-jest.config.js"); - -module.exports = { ...jestConfig }; diff --git a/packages/ckeditor5-coremedia-blocklist/package.json b/packages/ckeditor5-coremedia-blocklist/package.json index 39285c0767..8bb4a07de0 100644 --- a/packages/ckeditor5-coremedia-blocklist/package.json +++ b/packages/ckeditor5-coremedia-blocklist/package.json @@ -1,6 +1,7 @@ { "name": "@coremedia/ckeditor5-coremedia-blocklist", "version": "25.0.4-rc.4", + "type": "module", "description": "Lets users add and remove words to a list in the editor and highlights them in the text.", "author": { "name": "CoreMedia GmbH", @@ -22,9 +23,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 --passWithNoTests", - "jest:coverage": "jest --collect-coverage --passWithNoTests", - "lint": "eslint \"**/*.{js,cjs,mjs,ts,tsx}\"", + "lint": "eslint \"**/*.{ts,tsx}\"", "npm-check-updates": "npm-check-updates --upgrade" }, "exports": { @@ -49,19 +48,18 @@ "/theme" ], "devDependencies": { - "@coremedia-internal/ckeditor5-jest-test-helpers": "^1.0.0", - "@coremedia/service-agent": "^1.1.5", - "@types/jest": "^29.5.12", - "ckeditor5": "45.2.1", + "@coremedia-internal/studio-client.test-runner-helper": "0.2.3-alpha.050b44a", + "@coremedia/service-agent": "^2.1.2", + "@types/node": "^22.0.0", + "ckeditor5": "46.1.1", "copyfiles": "^2.4.1", - "jest": "^29.7.0", "rimraf": "^6.0.1", "rxjs": "^7.8.1", "typescript": "5.4.5" }, "peerDependencies": { - "@coremedia/service-agent": "^1.1.5 || ^2.0.1", - "ckeditor5": "45.2.1" + "@coremedia/service-agent": "^2.1.2", + "ckeditor5": "46.1.1" }, "dependencies": { "@coremedia/ckeditor5-core-common": "25.0.4-rc.4", diff --git a/packages/ckeditor5-coremedia-blocklist/src/augmentation.ts b/packages/ckeditor5-coremedia-blocklist/src/augmentation.ts index f935df02ed..c41a05c849 100644 --- a/packages/ckeditor5-coremedia-blocklist/src/augmentation.ts +++ b/packages/ckeditor5-coremedia-blocklist/src/augmentation.ts @@ -1,5 +1,6 @@ -import Blocklist from "./blocklist"; -import BlocklistCommand, { BLOCKLIST_COMMAND_NAME } from "./blocklistCommand"; +import type Blocklist from "./blocklist"; +import type { BLOCKLIST_COMMAND_NAME } from "./blocklistCommand"; +import type BlocklistCommand from "./blocklistCommand"; declare module "ckeditor5" { interface PluginsMap { diff --git a/packages/ckeditor5-coremedia-blocklist/src/blocklistChangesUtils.ts b/packages/ckeditor5-coremedia-blocklist/src/blocklistChangesUtils.ts index a73613a4d2..1400bfbe1b 100644 --- a/packages/ckeditor5-coremedia-blocklist/src/blocklistChangesUtils.ts +++ b/packages/ckeditor5-coremedia-blocklist/src/blocklistChangesUtils.ts @@ -1,16 +1,15 @@ -import { - Collection, +import type { Editor, - DiffItem, - DiffItemAttribute, - Element, - Item, + DifferItem, + DifferItemAttribute, + ModelElement, + ModelItem, Marker, Model, - Node, - Range, - FindAndReplaceUtils, + ModelNode, + ModelRange, } from "ckeditor5"; +import { Collection, FindAndReplaceUtils } from "ckeditor5"; import { createMarkerNameAndStoreWord } from "./blocklistMarkerUtils"; // copied from @ckeditor/ckeditor5-find-and-replace/src/findandreplace.d.ts since not exported in index.js @@ -26,10 +25,10 @@ export interface ResultType { * Reacts to document changes in order to update search list. */ export const onDocumentChange = (results: Collection, editor: Editor, blockedWordsList: string[]) => { - const changedNodes = new Set(); + const changedNodes = new Set(); const removedMarkers = new Set(); const model = editor.model; - const changes = model.document.differ.getChanges() as Exclude[]; + const changes = model.document.differ.getChanges() as Exclude[]; // Get nodes in which changes happened to re-run a search callback on them. changes.forEach((change) => { @@ -39,7 +38,7 @@ export const onDocumentChange = (results: Collection, editor: Editor return; } if (change.name === "$text" || (change.position.nodeAfter && model.schema.isInline(change.position.nodeAfter))) { - changedNodes.add(change.position.parent as Element); + changedNodes.add(change.position.parent as ModelElement); [...model.markers.getMarkersAtPosition(change.position)].forEach((markerAtChange) => { removedMarkers.add(markerAtChange.name); }); @@ -59,7 +58,7 @@ export const onDocumentChange = (results: Collection, editor: Editor // Get markers from the updated nodes and remove all (search will be re-run on these nodes). changedNodes.forEach((node) => { - const markersInNode = [...model.markers.getMarkersIntersectingRange(model.createRangeIn(node as Element))]; + const markersInNode = [...model.markers.getMarkersIntersectingRange(model.createRangeIn(node as ModelElement))]; markersInNode.forEach((marker) => removedMarkers.add(marker.name)); }); @@ -114,9 +113,9 @@ export const createSearchCallback = (word: string) => { export const updateFindResultFromRange = ( blockedWord: string, editor: Editor, - range: Range, + range: ModelRange, model: Model, - findCallback: ({ item, text }: { item: Item; text: string }) => ResultType[], + findCallback: ({ item, text }: { item: ModelItem; text: string }) => ResultType[], startResults: Collection | null, ): Collection => { const results = startResults ?? new Collection(); @@ -127,7 +126,7 @@ export const updateFindResultFromRange = ( if (model.schema.checkChild(item, "$text")) { const foundItems = findCallback({ item, - text: findAndReplaceUtils.rangeToText(model.createRangeIn(item as Element)), + text: findAndReplaceUtils.rangeToText(model.createRangeIn(item as ModelElement)), }); if (!foundItems) { return; diff --git a/packages/ckeditor5-coremedia-blocklist/src/blocklistCommand.ts b/packages/ckeditor5-coremedia-blocklist/src/blocklistCommand.ts index 2c6b74a5ec..abdc477121 100644 --- a/packages/ckeditor5-coremedia-blocklist/src/blocklistCommand.ts +++ b/packages/ckeditor5-coremedia-blocklist/src/blocklistCommand.ts @@ -1,4 +1,5 @@ -import { Command, Editor } from "ckeditor5"; +import type { Editor } from "ckeditor5"; +import { Command } from "ckeditor5"; export const BLOCKLIST_COMMAND_NAME = "Blocklist"; diff --git a/packages/ckeditor5-coremedia-blocklist/src/blocklistediting.ts b/packages/ckeditor5-coremedia-blocklist/src/blocklistediting.ts index 4e0df012eb..d697b42332 100644 --- a/packages/ckeditor5-coremedia-blocklist/src/blocklistediting.ts +++ b/packages/ckeditor5-coremedia-blocklist/src/blocklistediting.ts @@ -1,9 +1,12 @@ import { Collection, Plugin } from "ckeditor5"; import { serviceAgent } from "@coremedia/service-agent"; -import { Subscription } from "rxjs"; -import { BlocklistService, createBlocklistServiceDescriptor } from "@coremedia/ckeditor5-coremedia-studio-integration"; -import { Logger, LoggerProvider } from "@coremedia/ckeditor5-logging"; -import { createSearchCallback, onDocumentChange, ResultType, updateFindResultFromRange } from "./blocklistChangesUtils"; +import type { Subscription } from "rxjs"; +import type { BlocklistService } from "@coremedia/ckeditor5-coremedia-studio-integration"; +import { createBlocklistServiceDescriptor } from "@coremedia/ckeditor5-coremedia-studio-integration"; +import type { Logger } from "@coremedia/ckeditor5-logging"; +import { LoggerProvider } from "@coremedia/ckeditor5-logging"; +import type { ResultType } from "./blocklistChangesUtils"; +import { createSearchCallback, onDocumentChange, updateFindResultFromRange } from "./blocklistChangesUtils"; import BlocklistCommand, { BLOCKLIST_COMMAND_NAME } from "./blocklistCommand"; import { getMarkerDetails, removeMarkerDetails } from "./blocklistMarkerUtils"; diff --git a/packages/ckeditor5-coremedia-blocklist/src/blocklistui.ts b/packages/ckeditor5-coremedia-blocklist/src/blocklistui.ts index 8bddcf4099..4646a82d4b 100644 --- a/packages/ckeditor5-coremedia-blocklist/src/blocklistui.ts +++ b/packages/ckeditor5-coremedia-blocklist/src/blocklistui.ts @@ -1,22 +1,20 @@ import { ifCommand } from "@coremedia/ckeditor5-core-common"; -import { - Plugin, - ButtonView, - clickOutsideHandler, - ContextualBalloon, - TextProxy, +import type { + ModelTextProxy, ViewAttributeElement, ViewDocumentClickEvent, ViewDocumentFragment, ViewNode, ViewPosition, - PositionOptions, + DomOptimalPositionOptions, } from "ckeditor5"; +import { Plugin, ButtonView, clickOutsideHandler, ContextualBalloon } from "ckeditor5"; import blocklistIcon from "../theme/icons/blocklist.svg"; -import BlocklistCommand, { BLOCKLIST_COMMAND_NAME } from "./blocklistCommand"; +import type BlocklistCommand from "./blocklistCommand"; +import { BLOCKLIST_COMMAND_NAME } from "./blocklistCommand"; import BlocklistActionsView from "./ui/blocklistActionsView"; import "./lang/blocklist"; -import { UnblockEvent } from "./ui/blockedWordView"; +import type { UnblockEvent } from "./ui/blockedWordView"; import BlocklistEditing from "./blocklistediting"; const BLOCKLIST_KEYSTROKE = "Ctrl+Shift+B"; @@ -56,7 +54,10 @@ export default class Blocklistui extends Plugin { } #getBlocklistActionsView(): BlocklistActionsView { - return this.blocklistActionsView ?? (this.blocklistActionsView = this.#createBlocklistActionsView()); + if (!this.blocklistActionsView) { + this.blocklistActionsView = this.#createBlocklistActionsView(); + } + return this.blocklistActionsView; } /** @@ -233,12 +234,12 @@ export default class Blocklistui extends Plugin { }); } - #getBalloonPositionData(): Partial { + #getBalloonPositionData(): Partial { const view = this.editor.editing.view; const viewDocument = view.document; const selection = view.document.selection; const selectedElement = selection.getSelectedElement(); - const target: PositionOptions["target"] = () => { + const target: DomOptimalPositionOptions["target"] = () => { const targetWord = selectedElement; if (targetWord) { const targetWordViewElement = view.domConverter.mapViewToDom(targetWord); @@ -389,7 +390,7 @@ export default class Blocklistui extends Plugin { #getSelectedText(): string { const selection = this.editor.model.document.selection; const range = selection.getFirstRange(); - const isTextProxy = (node: unknown): node is TextProxy => + const isTextProxy = (node: unknown): node is ModelTextProxy => typeof node === "object" && node !== null && "data" in node; if (range) { return Array.from(range.getItems()) diff --git a/packages/ckeditor5-coremedia-blocklist/src/ui/blockedWordView.ts b/packages/ckeditor5-coremedia-blocklist/src/ui/blockedWordView.ts index 738d8564c9..534662da0a 100644 --- a/packages/ckeditor5-coremedia-blocklist/src/ui/blockedWordView.ts +++ b/packages/ckeditor5-coremedia-blocklist/src/ui/blockedWordView.ts @@ -1,4 +1,5 @@ -import { ButtonView, KeystrokeHandler, Locale, submitHandler, View, ViewCollection } from "ckeditor5"; +import type { Locale } from "ckeditor5"; +import { ButtonView, KeystrokeHandler, submitHandler, View, ViewCollection } from "ckeditor5"; import trashbinIcon from "../../theme/icons/trashbin.svg"; import "../../theme/blockedwordview.css"; import "../lang/blocklist"; diff --git a/packages/ckeditor5-coremedia-blocklist/src/ui/blocklistActionsView.ts b/packages/ckeditor5-coremedia-blocklist/src/ui/blocklistActionsView.ts index ecd4db2cb6..128ad890ef 100644 --- a/packages/ckeditor5-coremedia-blocklist/src/ui/blocklistActionsView.ts +++ b/packages/ckeditor5-coremedia-blocklist/src/ui/blocklistActionsView.ts @@ -2,17 +2,8 @@ * @module blocklist/ui/blocklistactionsview */ -import { - View, - ViewCollection, - FocusCycler, - ListView, - FocusableView, - FocusTracker, - KeystrokeHandler, - Locale, - Editor, -} from "ckeditor5"; +import type { FocusableView, Locale, Editor } from "ckeditor5"; +import { View, ViewCollection, FocusCycler, ListView, FocusTracker, KeystrokeHandler } from "ckeditor5"; import BlocklistInputView from "./blocklistInputView"; import BlockedWordView from "./blockedWordView"; diff --git a/packages/ckeditor5-coremedia-blocklist/src/ui/blocklistInputView.ts b/packages/ckeditor5-coremedia-blocklist/src/ui/blocklistInputView.ts index ed42857059..24b7dd681f 100644 --- a/packages/ckeditor5-coremedia-blocklist/src/ui/blocklistInputView.ts +++ b/packages/ckeditor5-coremedia-blocklist/src/ui/blocklistInputView.ts @@ -1,13 +1,11 @@ +import type { ViewCollection, InputTextView, Locale } from "ckeditor5"; import { ButtonView, LabeledFieldView, View, - ViewCollection, createLabeledInputText, submitHandler, - InputTextView, KeystrokeHandler, - Locale, IconCheck, } from "ckeditor5"; import "../lang/blocklist"; diff --git a/packages/ckeditor5-coremedia-blocklist/tsconfig.json b/packages/ckeditor5-coremedia-blocklist/tsconfig.json index b98d0d6782..288ecb8b40 100644 --- a/packages/ckeditor5-coremedia-blocklist/tsconfig.json +++ b/packages/ckeditor5-coremedia-blocklist/tsconfig.json @@ -1,11 +1,14 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "outDir": "./dist" + "outDir": "./dist", + "types": [ + "node" + ] }, "include": [ "./__mocks__", - "./__tests__", + "./test", "./src", "../../typings" ] diff --git a/packages/ckeditor5-coremedia-content-clipboard/jest.config.cjs b/packages/ckeditor5-coremedia-content-clipboard/jest.config.cjs deleted file mode 100644 index 208f377fa0..0000000000 --- a/packages/ckeditor5-coremedia-content-clipboard/jest.config.cjs +++ /dev/null @@ -1,3 +0,0 @@ -const jestConfig = require("@coremedia-internal/ckeditor5-jest-test-helpers/shared-jest.config.js"); - -module.exports = { ...jestConfig }; diff --git a/packages/ckeditor5-coremedia-content-clipboard/package.json b/packages/ckeditor5-coremedia-content-clipboard/package.json index fd070de97e..0fddb4b3d7 100644 --- a/packages/ckeditor5-coremedia-content-clipboard/package.json +++ b/packages/ckeditor5-coremedia-content-clipboard/package.json @@ -14,6 +14,7 @@ "clipboard" ], "version": "25.0.4-rc.4", + "type": "module", "license": "Apache-2.0", "scripts": { "build": "tsc --project ./tsconfig.release.json && copyfiles -u 1 theme/* theme/**/* dist/theme", @@ -21,20 +22,15 @@ "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 --passWithNoTests", - "jest:coverage": "jest --collect-coverage", - "lint": "eslint \"**/*.{js,cjs,mjs,ts,tsx}\"", + "lint": "eslint \"**/*.{ts,tsx}\"", "npm-check-updates": "npm-check-updates --upgrade" }, "devDependencies": { - "@coremedia-internal/ckeditor5-jest-test-helpers": "^1.0.0", - "@coremedia/service-agent": "^1.1.5", - "@types/jest": "^29.5.12", - "ckeditor5": "45.2.1", + "@coremedia-internal/studio-client.test-runner-helper": "0.2.3-alpha.050b44a", + "@coremedia/service-agent": "^2.1.2", + "@types/node": "^22.0.0", + "ckeditor5": "46.1.1", "copyfiles": "^2.4.1", - "jest": "^29.7.0", - "jest-each": "^29.7.0", - "jest-xml-matcher": "^1.2.0", "rimraf": "^6.0.1", "rxjs": "^7.8.1", "typescript": "5.4.5" @@ -61,8 +57,8 @@ "/theme" ], "peerDependencies": { - "@coremedia/service-agent": "^1.1.5 || ^2.0.1", - "ckeditor5": "45.2.1" + "@coremedia/service-agent": "^2.1.2", + "ckeditor5": "46.1.1" }, "dependencies": { "@coremedia/ckeditor5-common": "25.0.4-rc.4", diff --git a/packages/ckeditor5-coremedia-content-clipboard/src/ContentClipboard.ts b/packages/ckeditor5-coremedia-content-clipboard/src/ContentClipboard.ts index c3d3826c6f..84a88c7e66 100644 --- a/packages/ckeditor5-coremedia-content-clipboard/src/ContentClipboard.ts +++ b/packages/ckeditor5-coremedia-content-clipboard/src/ContentClipboard.ts @@ -1,30 +1,34 @@ -import { Logger, LoggerProvider } from "@coremedia/ckeditor5-logging"; -import { - Clipboard, +import type { Logger } from "@coremedia/ckeditor5-logging"; +import { LoggerProvider } from "@coremedia/ckeditor5-logging"; +import type { ClipboardContentInsertionEvent, ClipboardEventData, ClipboardInputTransformationData, ClipboardInputTransformationEvent, - ClipboardPipeline, - DocumentFragment as ModelDocumentFragment, - DomEventData, + ViewDocumentDomEventData, Editor, EventInfo, GetCallback, + ModelRange, + ViewDocumentClipboardInputEvent, + ViewRange, +} from "ckeditor5"; +import { + Clipboard, + ClipboardPipeline, + ModelDocumentFragment, Plugin, - Range as ModelRange, StylesProcessor, ViewDocument, - ViewDocumentClipboardInputEvent, ViewDocumentFragment, - ViewRange, } from "ckeditor5"; -import { InitInformation, reportInitEnd, reportInitStart } from "@coremedia/ckeditor5-core-common"; +import type { InitInformation } from "@coremedia/ckeditor5-core-common"; +import { reportInitEnd, reportInitStart } from "@coremedia/ckeditor5-core-common"; import { isRaw } from "@coremedia/ckeditor5-common"; +import type { IsDroppableEvaluationResult } from "@coremedia/ckeditor5-coremedia-studio-integration"; import { getEvaluationResult, isDroppable, - IsDroppableEvaluationResult, receiveDraggedItemsFromDataTransfer, } from "@coremedia/ckeditor5-coremedia-studio-integration"; import ContentClipboardEditing from "./ContentClipboardEditing"; @@ -107,7 +111,10 @@ export default class ContentClipboard extends Plugin { * @param _evt - event information * @param data - clipboard data */ - static readonly #dragOverHandler = (_evt: unknown, data: DomEventData & ClipboardEventData) => { + static readonly #dragOverHandler = ( + _evt: unknown, + data: ViewDocumentDomEventData & ClipboardEventData, + ) => { // The listener already processed the clipboard content on the // higher priority (for example, while pasting into the code block). if (isContentEventData(data) && !!data.content) { diff --git a/packages/ckeditor5-coremedia-content-clipboard/src/ContentClipboardEditing.ts b/packages/ckeditor5-coremedia-content-clipboard/src/ContentClipboardEditing.ts index 13136af29f..84d9c7adfc 100644 --- a/packages/ckeditor5-coremedia-content-clipboard/src/ContentClipboardEditing.ts +++ b/packages/ckeditor5-coremedia-content-clipboard/src/ContentClipboardEditing.ts @@ -1,10 +1,13 @@ import "../theme/loadmask.css"; -import { Plugin, Editor, DowncastDispatcher } from "ckeditor5"; +import type { Editor, DowncastDispatcher } from "ckeditor5"; +import { Plugin } from "ckeditor5"; import { reportInitEnd, reportInitStart } from "@coremedia/ckeditor5-core-common"; -import { ContentClipboardMarkerDataUtils, MarkerData } from "./ContentClipboardMarkerDataUtils"; +import type { MarkerData } from "./ContentClipboardMarkerDataUtils"; +import { ContentClipboardMarkerDataUtils } from "./ContentClipboardMarkerDataUtils"; import { addContentMarkerConversion, removeContentMarkerConversion } from "./converters"; import DataToModelMechanism from "./DataToModelMechanism"; -import ContentToModelRegistry, { CreateModelFunctionCreator } from "./ContentToModelRegistry"; +import type { CreateModelFunctionCreator } from "./ContentToModelRegistry"; +import ContentToModelRegistry from "./ContentToModelRegistry"; import { UndoSupport } from "./integrations/Undo"; const PLUGIN_NAME = "ContentClipboardEditing"; diff --git a/packages/ckeditor5-coremedia-content-clipboard/src/ContentInputDataCache.ts b/packages/ckeditor5-coremedia-content-clipboard/src/ContentInputDataCache.ts index a7653fd561..af8142c1ce 100644 --- a/packages/ckeditor5-coremedia-content-clipboard/src/ContentInputDataCache.ts +++ b/packages/ckeditor5-coremedia-content-clipboard/src/ContentInputDataCache.ts @@ -1,4 +1,4 @@ -import { Batch } from "ckeditor5"; +import type { Batch } from "ckeditor5"; /** * A cache to store data about content input. diff --git a/packages/ckeditor5-coremedia-content-clipboard/src/ContentMarkers.ts b/packages/ckeditor5-coremedia-content-clipboard/src/ContentMarkers.ts index 4670537619..cf4b8cf29a 100644 --- a/packages/ckeditor5-coremedia-content-clipboard/src/ContentMarkers.ts +++ b/packages/ckeditor5-coremedia-content-clipboard/src/ContentMarkers.ts @@ -1,12 +1,11 @@ import { LoggerProvider } from "@coremedia/ckeditor5-logging"; import { serviceAgent } from "@coremedia/service-agent"; -import { - createRichtextConfigurationServiceDescriptor, - RichtextConfigurationService, -} from "@coremedia/ckeditor5-coremedia-studio-integration"; -import type { Editor, Model, Range as ModelRange, Writer } from "ckeditor5"; +import type { RichtextConfigurationService } from "@coremedia/ckeditor5-coremedia-studio-integration"; +import { createRichtextConfigurationServiceDescriptor } from "@coremedia/ckeditor5-coremedia-studio-integration"; +import type { Editor, Model, ModelRange, ModelWriter } from "ckeditor5"; +import ContentInputDataCache from "./ContentInputDataCache"; +import type { ContentInputData, InsertionContext } from "./ContentInputDataCache"; import { ContentClipboardMarkerDataUtils } from "./ContentClipboardMarkerDataUtils"; -import ContentInputDataCache, { ContentInputData, InsertionContext } from "./ContentInputDataCache"; const logger = LoggerProvider.getLogger("ContentMarkers"); @@ -30,7 +29,7 @@ export const insertContentMarkers = (editor: Editor, targetRange: ModelRange, co { isUndoable: false, }, - (writer: Writer) => { + (writer: ModelWriter) => { writer.setSelection(targetRange); }, ); @@ -93,7 +92,7 @@ const handleExpandedRange = (model: Model, range: ModelRange): ModelRange => { { isUndoable: false, }, - (writer: Writer) => { + (writer: ModelWriter) => { writer.remove(range); }, ); @@ -142,7 +141,7 @@ const addContentInputMarker = (editor: Editor, markerRange: ModelRange, contentI { isUndoable: false, }, - (writer: Writer) => { + (writer: ModelWriter) => { writer.addMarker(markerName, { usingOperation: true, range: markerRange, diff --git a/packages/ckeditor5-coremedia-content-clipboard/src/ContentToModelRegistry.ts b/packages/ckeditor5-coremedia-content-clipboard/src/ContentToModelRegistry.ts index 82101ead76..4b4bc8e938 100644 --- a/packages/ckeditor5-coremedia-content-clipboard/src/ContentToModelRegistry.ts +++ b/packages/ckeditor5-coremedia-content-clipboard/src/ContentToModelRegistry.ts @@ -1,6 +1,6 @@ -import { Writer, Node } from "ckeditor5"; +import type { ModelWriter, ModelNode } from "ckeditor5"; -export type CreateModelFunction = (writer: Writer) => Node; +export type CreateModelFunction = (writer: ModelWriter) => ModelNode; export type CreateModelFunctionCreator = (contentUri: string) => Promise; /** diff --git a/packages/ckeditor5-coremedia-content-clipboard/src/DataToModelMechanism.ts b/packages/ckeditor5-coremedia-content-clipboard/src/DataToModelMechanism.ts index 8e4be240a2..478a1ce176 100644 --- a/packages/ckeditor5-coremedia-content-clipboard/src/DataToModelMechanism.ts +++ b/packages/ckeditor5-coremedia-content-clipboard/src/DataToModelMechanism.ts @@ -1,20 +1,27 @@ import { serviceAgent } from "@coremedia/service-agent"; -import { Editor, Node, PendingActions, Position, Range, Writer } from "ckeditor5"; -import { Logger, LoggerProvider } from "@coremedia/ckeditor5-logging"; -import { +import type { Editor, ModelNode, ModelPosition, ModelRange, ModelWriter } from "ckeditor5"; +import { PendingActions } from "ckeditor5"; +import type { Logger } from "@coremedia/ckeditor5-logging"; +import { LoggerProvider } from "@coremedia/ckeditor5-logging"; +import type { ContentImportService, + IContentReferenceService, + RichtextConfigurationService, +} from "@coremedia/ckeditor5-coremedia-studio-integration"; +import { COREMEDIA_CONTEXT_KEY, createContentImportServiceDescriptor, createContentReferenceServiceDescriptor, createRichtextConfigurationServiceDescriptor, - IContentReferenceService, - RichtextConfigurationService, } from "@coremedia/ckeditor5-coremedia-studio-integration"; import { getOptionalPlugin } from "@coremedia/ckeditor5-core-common"; -import { ContentClipboardMarkerDataUtils, MarkerData } from "./ContentClipboardMarkerDataUtils"; -import ContentInputDataCache, { ContentInputData } from "./ContentInputDataCache"; +import type { MarkerData } from "./ContentClipboardMarkerDataUtils"; +import { ContentClipboardMarkerDataUtils } from "./ContentClipboardMarkerDataUtils"; +import type { ContentInputData } from "./ContentInputDataCache"; +import ContentInputDataCache from "./ContentInputDataCache"; import MarkerRepositionUtil from "./MarkerRepositionUtil"; -import ContentToModelRegistry, { CreateModelFunction } from "./ContentToModelRegistry"; +import type { CreateModelFunction } from "./ContentToModelRegistry"; +import ContentToModelRegistry from "./ContentToModelRegistry"; import { enableUndo, UndoSupport } from "./integrations/Undo"; const UTILITY_NAME = "DataToModelMechanism"; @@ -223,15 +230,15 @@ export default class DataToModelMechanism { pendingMarkerNames: string[], contentInputData: ContentInputData, markerData: MarkerData, - createItemFunction: (writer: Writer) => Node, + createItemFunction: (writer: ModelWriter) => ModelNode, ): void { - editor.model.enqueueChange(contentInputData.insertionContext.batch, (writer: Writer): void => { - const item: Node = createItemFunction(writer); + editor.model.enqueueChange(contentInputData.insertionContext.batch, (writer: ModelWriter): void => { + const item: ModelNode = createItemFunction(writer); const marker = writer.model.markers.get(ContentClipboardMarkerDataUtils.toMarkerNameFromData(markerData)); if (!marker) { return; } - const markerPosition: Position | undefined = marker.getStart(); + const markerPosition: ModelPosition | undefined = marker.getStart(); if (!markerPosition) { ContentInputDataCache.removeData(marker.name); return; @@ -257,7 +264,7 @@ export default class DataToModelMechanism { // Split is necessary if the link is not rendered inline, and if we are not // at the end of a container/document. This prevents empty paragraphs // after the inserted element. - let finalAfterInsertPosition: Position = range.end; + let finalAfterInsertPosition: ModelPosition = range.end; // Parent Check: Required precondition for split(). if (!range.end.isAtEnd && !contentInputData.itemContext.isInline && range.end.parent?.parent) { finalAfterInsertPosition = writer.split(range.end).range.end; @@ -268,7 +275,7 @@ export default class DataToModelMechanism { { isUndoable: false, }, - (writer: Writer): void => { + (writer: ModelWriter): void => { writer.removeSelectionAttribute("linkHref"); }, ); @@ -288,7 +295,7 @@ export default class DataToModelMechanism { { isUndoable: false, }, - (writer: Writer): void => { + (writer: ModelWriter): void => { const marker = writer.model.markers.get(ContentClipboardMarkerDataUtils.toMarkerNameFromData(markerData)); if (!marker) { return; @@ -310,7 +317,7 @@ export default class DataToModelMechanism { * @param textRanges - ranges to apply attributes to * @param attributes - attributes to apply */ - static #applyAttributes(writer: Writer, textRanges: Range[], attributes: [string, unknown][]): void { + static #applyAttributes(writer: ModelWriter, textRanges: ModelRange[], attributes: [string, unknown][]): void { for (const attribute of attributes) { for (const range of textRanges) { writer.setAttribute(attribute[0], attribute[1], range); diff --git a/packages/ckeditor5-coremedia-content-clipboard/src/MarkerRepositionUtil.ts b/packages/ckeditor5-coremedia-content-clipboard/src/MarkerRepositionUtil.ts index cd5da61069..de77c1dec5 100644 --- a/packages/ckeditor5-coremedia-content-clipboard/src/MarkerRepositionUtil.ts +++ b/packages/ckeditor5-coremedia-content-clipboard/src/MarkerRepositionUtil.ts @@ -1,5 +1,6 @@ -import { Editor, Position as ModelPosition, Writer } from "ckeditor5"; -import { ContentClipboardMarkerDataUtils, MarkerData } from "./ContentClipboardMarkerDataUtils"; +import type { Editor, ModelPosition, ModelWriter } from "ckeditor5"; +import type { MarkerData } from "./ContentClipboardMarkerDataUtils"; +import { ContentClipboardMarkerDataUtils } from "./ContentClipboardMarkerDataUtils"; import ContentInputDataCache from "./ContentInputDataCache"; type MarkerFilterFunction = (markerData: MarkerData, otherMarkerData: MarkerData) => boolean; @@ -59,7 +60,7 @@ export default class MarkerRepositionUtil { { isUndoable: false, }, - (writer: Writer): void => { + (writer: ModelWriter): void => { const newRange = writer.createRange(position, position); writer.updateMarker(moveMarkerName, { range: newRange, diff --git a/packages/ckeditor5-coremedia-content-clipboard/src/augmentation.ts b/packages/ckeditor5-coremedia-content-clipboard/src/augmentation.ts index c56256ea24..39b186ba0c 100644 --- a/packages/ckeditor5-coremedia-content-clipboard/src/augmentation.ts +++ b/packages/ckeditor5-coremedia-content-clipboard/src/augmentation.ts @@ -1,10 +1,10 @@ -import ContentClipboard from "./ContentClipboard"; -import ContentClipboardEditing from "./ContentClipboardEditing"; -import { UndoSupport } from "./integrations/Undo"; -import { PasteContentCommand } from "./paste/PasteContentCommand"; -import PasteContentEditing from "./paste/PasteContentEditing"; -import PasteContentPlugin from "./paste/PasteContentPlugin"; -import PasteContentUI from "./paste/PasteContentUI"; +import type ContentClipboard from "./ContentClipboard"; +import type ContentClipboardEditing from "./ContentClipboardEditing"; +import type { UndoSupport } from "./integrations/Undo"; +import type { PasteContentCommand } from "./paste/PasteContentCommand"; +import type PasteContentEditing from "./paste/PasteContentEditing"; +import type PasteContentPlugin from "./paste/PasteContentPlugin"; +import type PasteContentUI from "./paste/PasteContentUI"; declare module "ckeditor5" { interface PluginsMap { diff --git a/packages/ckeditor5-coremedia-content-clipboard/src/converters.ts b/packages/ckeditor5-coremedia-content-clipboard/src/converters.ts index 747126fa22..281589411c 100644 --- a/packages/ckeditor5-coremedia-content-clipboard/src/converters.ts +++ b/packages/ckeditor5-coremedia-content-clipboard/src/converters.ts @@ -1,6 +1,7 @@ -import { EventInfo, DowncastConversionApi, Item as ModelItem, Range as ModelRange } from "ckeditor5"; +import type { EventInfo, DowncastConversionApi, ModelItem, ModelRange } from "ckeditor5"; import ContentInputDataCache from "./ContentInputDataCache"; -import { ContentClipboardMarkerDataUtils, MarkerData } from "./ContentClipboardMarkerDataUtils"; +import type { MarkerData } from "./ContentClipboardMarkerDataUtils"; +import { ContentClipboardMarkerDataUtils } from "./ContentClipboardMarkerDataUtils"; export interface AddMarkerEventData { markerName: string; diff --git a/packages/ckeditor5-coremedia-content-clipboard/src/index-doc.ts b/packages/ckeditor5-coremedia-content-clipboard/src/index-doc.ts index b046b072a2..5f33cd15c3 100644 --- a/packages/ckeditor5-coremedia-content-clipboard/src/index-doc.ts +++ b/packages/ckeditor5-coremedia-content-clipboard/src/index-doc.ts @@ -10,7 +10,7 @@ export * as integrations from "./integrations/index-doc"; */ export * as paste from "./paste/index-doc"; -export * as lang from "./lang/index-doc"; +export type * as lang from "./lang/index-doc"; export * from "./ContentClipboard"; export { default as ContentClipboard } from "./ContentClipboard"; diff --git a/packages/ckeditor5-coremedia-content-clipboard/src/index.ts b/packages/ckeditor5-coremedia-content-clipboard/src/index.ts index 4fb97861a0..da9a3b411c 100644 --- a/packages/ckeditor5-coremedia-content-clipboard/src/index.ts +++ b/packages/ckeditor5-coremedia-content-clipboard/src/index.ts @@ -9,6 +9,6 @@ export { default as PasteContentEditing } from "./paste/PasteContentEditing"; export { default as PasteContentPlugin } from "./paste/PasteContentPlugin"; export { default as PasteContentUI } from "./paste/PasteContentUI"; export { PasteContentCommand } from "./paste/PasteContentCommand"; -export { CreateModelFunction, CreateModelFunctionCreator } from "./ContentToModelRegistry"; +export type { CreateModelFunction, CreateModelFunctionCreator } from "./ContentToModelRegistry"; import "./augmentation"; diff --git a/packages/ckeditor5-coremedia-content-clipboard/src/integrations/Undo.ts b/packages/ckeditor5-coremedia-content-clipboard/src/integrations/Undo.ts index c5cc83fc53..3124a8a769 100644 --- a/packages/ckeditor5-coremedia-content-clipboard/src/integrations/Undo.ts +++ b/packages/ckeditor5-coremedia-content-clipboard/src/integrations/Undo.ts @@ -1,6 +1,6 @@ import { Plugin, UndoEditing } from "ckeditor5"; +import type { CommandHandler } from "@coremedia/ckeditor5-core-common"; import { - CommandHandler, disableCommand, enableCommand, ifCommand, diff --git a/packages/ckeditor5-coremedia-content-clipboard/src/lang/index-doc.ts b/packages/ckeditor5-coremedia-content-clipboard/src/lang/index-doc.ts index 3b144a8024..8c79e382d2 100644 --- a/packages/ckeditor5-coremedia-content-clipboard/src/lang/index-doc.ts +++ b/packages/ckeditor5-coremedia-content-clipboard/src/lang/index-doc.ts @@ -5,4 +5,4 @@ * @category Virtual */ -export * as contentlink from "./paste"; +export type * as contentlink from "./paste"; diff --git a/packages/ckeditor5-coremedia-content-clipboard/src/paste/PasteContentCommand.ts b/packages/ckeditor5-coremedia-content-clipboard/src/paste/PasteContentCommand.ts index f7b59ffc6d..d4b92ee83d 100644 --- a/packages/ckeditor5-coremedia-content-clipboard/src/paste/PasteContentCommand.ts +++ b/packages/ckeditor5-coremedia-content-clipboard/src/paste/PasteContentCommand.ts @@ -1,8 +1,8 @@ -import { Command, Editor } from "ckeditor5"; +import type { Editor } from "ckeditor5"; +import { Command } from "ckeditor5"; import { serviceAgent } from "@coremedia/service-agent"; +import type { ClipboardItemRepresentation, ClipboardService } from "@coremedia/ckeditor5-coremedia-studio-integration"; import { - ClipboardItemRepresentation, - ClipboardService, createClipboardServiceDescriptor, createRichtextConfigurationServiceDescriptor, isUriPath, diff --git a/packages/ckeditor5-coremedia-content-clipboard/src/paste/PasteContentUI.ts b/packages/ckeditor5-coremedia-content-clipboard/src/paste/PasteContentUI.ts index 65d56f1b38..ed978e2750 100644 --- a/packages/ckeditor5-coremedia-content-clipboard/src/paste/PasteContentUI.ts +++ b/packages/ckeditor5-coremedia-content-clipboard/src/paste/PasteContentUI.ts @@ -1,5 +1,6 @@ import { reportInitEnd, reportInitStart } from "@coremedia/ckeditor5-core-common"; -import { ButtonView, Editor, Plugin } from "ckeditor5"; +import type { Editor } from "ckeditor5"; +import { ButtonView, Plugin } from "ckeditor5"; import pasteIcon from "../../theme/icons/paste.svg"; import "../lang/paste"; diff --git a/packages/ckeditor5-coremedia-content-clipboard/tsconfig.json b/packages/ckeditor5-coremedia-content-clipboard/tsconfig.json index e239fc8fc4..ed71dab96a 100644 --- a/packages/ckeditor5-coremedia-content-clipboard/tsconfig.json +++ b/packages/ckeditor5-coremedia-content-clipboard/tsconfig.json @@ -1,7 +1,10 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "outDir": "./dist" + "outDir": "./dist", + "types": [ + "node" + ] }, "include": [ "./src", diff --git a/packages/ckeditor5-coremedia-content/jest.config.cjs b/packages/ckeditor5-coremedia-content/jest.config.cjs deleted file mode 100644 index 208f377fa0..0000000000 --- a/packages/ckeditor5-coremedia-content/jest.config.cjs +++ /dev/null @@ -1,3 +0,0 @@ -const jestConfig = require("@coremedia-internal/ckeditor5-jest-test-helpers/shared-jest.config.js"); - -module.exports = { ...jestConfig }; diff --git a/packages/ckeditor5-coremedia-content/package.json b/packages/ckeditor5-coremedia-content/package.json index 1956c87fb9..9abc1defbc 100644 --- a/packages/ckeditor5-coremedia-content/package.json +++ b/packages/ckeditor5-coremedia-content/package.json @@ -1,6 +1,7 @@ { "name": "@coremedia/ckeditor5-coremedia-content", "version": "25.0.4-rc.4", + "type": "module", "description": "CKEditor components and utilities related to CoreMedia content.", "author": { "name": "CoreMedia GmbH", @@ -35,13 +36,11 @@ ], "license": "Apache-2.0", "devDependencies": { - "@coremedia-internal/ckeditor5-jest-test-helpers": "^1.0.0", - "@coremedia/service-agent": "^1.1.5", - "@types/jest": "^29.5.12", - "ckeditor5": "45.2.1", + "@coremedia-internal/studio-client.test-runner-helper": "0.2.3-alpha.050b44a", + "@coremedia/service-agent": "^2.1.2", + "@types/node": "^22.0.0", + "ckeditor5": "46.1.1", "copyfiles": "^2.4.1", - "jest": "^29.7.0", - "jest-config": "^29.7.0", "rimraf": "^6.0.1", "typescript": "5.4.5" }, @@ -50,8 +49,8 @@ "@coremedia/ckeditor5-logging": "25.0.4-rc.4" }, "peerDependencies": { - "@coremedia/service-agent": "^1.1.5 || ^2.0.1", - "ckeditor5": "45.2.1" + "@coremedia/service-agent": "^2.1.2", + "ckeditor5": "46.1.1" }, "scripts": { "clean": "pnpm clean:src && pnpm clean:dist", @@ -59,9 +58,7 @@ "clean:dist": "rimraf ./dist", "build": "tsc --project ./tsconfig.release.json", "postbuild": "node prepare-package.cjs && copyfiles ./README.md dist", - "jest": "jest --passWithNoTests", - "jest:coverage": "jest --collect-coverage --passWithNoTests", - "lint": "eslint \"**/*.{js,cjs,mjs,ts,tsx}\"", + "lint": "eslint \"**/*.{ts,tsx}\"", "npm-check-updates": "npm-check-updates --upgrade" } } diff --git a/packages/ckeditor5-coremedia-content/src/commands/OpenInTabCommand.ts b/packages/ckeditor5-coremedia-content/src/commands/OpenInTabCommand.ts index 3c300f8928..3f8b92af04 100644 --- a/packages/ckeditor5-coremedia-content/src/commands/OpenInTabCommand.ts +++ b/packages/ckeditor5-coremedia-content/src/commands/OpenInTabCommand.ts @@ -1,12 +1,10 @@ -import { Command, Editor } from "ckeditor5"; -import { - isModelUriPath, - isUriPath, - requireContentUriPath, - UriPath, -} from "@coremedia/ckeditor5-coremedia-studio-integration"; +import type { Editor } from "ckeditor5"; +import { Command } from "ckeditor5"; +import type { UriPath } from "@coremedia/ckeditor5-coremedia-studio-integration"; +import { isModelUriPath, isUriPath, requireContentUriPath } from "@coremedia/ckeditor5-coremedia-studio-integration"; import { LoggerProvider } from "@coremedia/ckeditor5-logging"; -import { canBeOpenedInTab, openEntityInTab, OpenEntityInTabResult } from "../OpenInTab"; +import type { OpenEntityInTabResult } from "../OpenInTab"; +import { canBeOpenedInTab, openEntityInTab } from "../OpenInTab"; // noinspection JSConstantReassignment diff --git a/packages/ckeditor5-coremedia-content/tsconfig.json b/packages/ckeditor5-coremedia-content/tsconfig.json index 8439751c6a..8f181444bd 100644 --- a/packages/ckeditor5-coremedia-content/tsconfig.json +++ b/packages/ckeditor5-coremedia-content/tsconfig.json @@ -1,7 +1,10 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "outDir": "./dist" + "outDir": "./dist", + "types": [ + "node" + ] }, "include": [ "./src" diff --git a/packages/ckeditor5-coremedia-differencing/__tests__/integrations/XDiffElements.test.ts b/packages/ckeditor5-coremedia-differencing/__tests__/integrations/XDiffElements.test.ts deleted file mode 100644 index a43024a7b8..0000000000 --- a/packages/ckeditor5-coremedia-differencing/__tests__/integrations/XDiffElements.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { blockquote, richtext } from "@coremedia-internal/ckeditor5-coremedia-example-data"; -import * as aut from "../../src/integrations/XDiffElements"; -import { TestDirection, toData, toView } from "./TestDirection"; -import { RulesTester } from "./RulesTester"; - -describe("XDiffElements", () => { - const ruleConfigurations = [aut.xDiffElements]; - - describe.each` - data | direction | view - ${`

              AB

              `} | ${toView} | ${`

              AB

              `} - ${`

              A

              `} | ${toView} | ${`

              A

              `} - ${`

              A

              `} | ${toView} | ${`

              A

              `} - ${`

              A

              `} | ${toView} | ${`

              A

              `} - ${`

              A

              `} | ${toView} | ${`

              A

              `} - ${`

              A

              `} | ${toData} | ${`

              A

              `} - ${`

              A

              B

              `} | ${toView} | ${`

              A

              B

              `} - ${`

              A

              B

              `} | ${toData} | ${`

              A

              B

              `} - `( - "[$#] Should provide mapping from data $direction view: $data $direction $view", - ({ data, direction, view }: { data: string; direction: TestDirection; view: string }) => { - // Using blockquote as it may contain multiple paragraphs, and we need one central - // element to do the comparison. - const dataString = richtext(blockquote(data)); - const htmlString = `
              ${view}
              `; - const tester = new RulesTester(ruleConfigurations, "blockquote"); - - tester.executeTests({ - dataString, - direction, - htmlString, - }); - }, - ); -}); diff --git a/packages/ckeditor5-coremedia-differencing/jest.config.cjs b/packages/ckeditor5-coremedia-differencing/jest.config.cjs deleted file mode 100644 index 208f377fa0..0000000000 --- a/packages/ckeditor5-coremedia-differencing/jest.config.cjs +++ /dev/null @@ -1,3 +0,0 @@ -const jestConfig = require("@coremedia-internal/ckeditor5-jest-test-helpers/shared-jest.config.js"); - -module.exports = { ...jestConfig }; diff --git a/packages/ckeditor5-coremedia-differencing/package.json b/packages/ckeditor5-coremedia-differencing/package.json index 0cde62ec1b..b81ab6b21b 100644 --- a/packages/ckeditor5-coremedia-differencing/package.json +++ b/packages/ckeditor5-coremedia-differencing/package.json @@ -1,6 +1,7 @@ { "name": "@coremedia/ckeditor5-coremedia-differencing", "version": "25.0.4-rc.4", + "type": "module", "description": "Server-side differencing support in CoreMedia Studio.", "author": { "name": "CoreMedia GmbH", @@ -37,20 +38,20 @@ "license": "Apache-2.0", "devDependencies": { "@coremedia-internal/ckeditor5-coremedia-example-data": "^1.0.0", - "@coremedia-internal/ckeditor5-jest-test-helpers": "^1.0.0", - "@jest/globals": "^29.7.0", - "@types/jest": "^29.5.12", - "ckeditor5": "45.2.1", + "@coremedia-internal/studio-client.test-runner-helper": "0.2.3-alpha.050b44a", + "@types/node": "^22.0.0", + "ckeditor5": "46.1.1", "copyfiles": "^2.4.1", - "jest": "^29.7.0", - "jest-config": "^29.7.0", - "jest-xml-matcher": "^1.2.0", + "expect": "^30.2.0", + "global-jsdom": "^27.0.0", + "jsdom": "^27.0.0", "rimraf": "^6.0.1", - "ts-jest": "^29.2.4", + "ts-node": "^10.9.2", + "tsx": "^4.20.6", "typescript": "5.4.5" }, "peerDependencies": { - "ckeditor5": "45.2.1" + "ckeditor5": "46.1.1" }, "dependencies": { "@coremedia/ckeditor5-common": "25.0.4-rc.4", @@ -66,9 +67,8 @@ "clean:dist": "rimraf ./dist", "build": "tsc --project ./tsconfig.release.json", "postbuild": "node prepare-package.cjs && copyfiles ./README.md dist", - "jest": "jest --passWithNoTests", - "jest:coverage": "jest --collect-coverage", - "lint": "eslint \"**/*.{js,cjs,mjs,ts,tsx}\"", + "test": "test-runner-react", + "lint": "eslint \"**/*.{ts,tsx}\"", "npm-check-updates": "npm-check-updates --upgrade" } } diff --git a/packages/ckeditor5-coremedia-differencing/src/Differencing.ts b/packages/ckeditor5-coremedia-differencing/src/Differencing.ts index 5eefa6a413..4289fdc06e 100644 --- a/packages/ckeditor5-coremedia-differencing/src/Differencing.ts +++ b/packages/ckeditor5-coremedia-differencing/src/Differencing.ts @@ -1,6 +1,7 @@ import { Plugin } from "ckeditor5"; import { reportInitEnd, reportInitStart } from "@coremedia/ckeditor5-core-common"; -import { Logger, LoggerProvider } from "@coremedia/ckeditor5-logging"; +import type { Logger } from "@coremedia/ckeditor5-logging"; +import { LoggerProvider } from "@coremedia/ckeditor5-logging"; import { ImageElementSupport } from "./integrations/Image"; import { HtmlImageElementSupport } from "./integrations/HtmlSupportImage"; import { XDIFF_ATTRIBUTES, XDIFF_BREAK_ELEMENT_CONFIG, XDIFF_SPAN_ELEMENT_CONFIG } from "./Xdiff"; diff --git a/packages/ckeditor5-coremedia-differencing/src/PluginIntegrationHook.ts b/packages/ckeditor5-coremedia-differencing/src/PluginIntegrationHook.ts index 30e9cee24c..e57cd836e0 100644 --- a/packages/ckeditor5-coremedia-differencing/src/PluginIntegrationHook.ts +++ b/packages/ckeditor5-coremedia-differencing/src/PluginIntegrationHook.ts @@ -1,5 +1,6 @@ import { Plugin, priorities } from "ckeditor5"; -import { Logger, LoggerProvider } from "@coremedia/ckeditor5-logging"; +import type { Logger } from "@coremedia/ckeditor5-logging"; +import { LoggerProvider } from "@coremedia/ckeditor5-logging"; import { reportInitEnd, reportInitStart } from "@coremedia/ckeditor5-core-common"; /** diff --git a/packages/ckeditor5-coremedia-differencing/src/augmentation.ts b/packages/ckeditor5-coremedia-differencing/src/augmentation.ts index 78c93af697..d17e147786 100644 --- a/packages/ckeditor5-coremedia-differencing/src/augmentation.ts +++ b/packages/ckeditor5-coremedia-differencing/src/augmentation.ts @@ -1,8 +1,8 @@ -import Differencing from "./Differencing"; -import { HtmlImageElementSupport } from "./integrations/HtmlSupportImage"; -import { ImageElementSupport } from "./integrations/Image"; -import { RichTextDataProcessorIntegration } from "./integrations/RichTextDataProcessorIntegration"; -import { PluginIntegrationHook } from "./PluginIntegrationHook"; +import type Differencing from "./Differencing"; +import type { HtmlImageElementSupport } from "./integrations/HtmlSupportImage"; +import type { ImageElementSupport } from "./integrations/Image"; +import type { RichTextDataProcessorIntegration } from "./integrations/RichTextDataProcessorIntegration"; +import type { PluginIntegrationHook } from "./PluginIntegrationHook"; declare module "ckeditor5" { interface PluginsMap { diff --git a/packages/ckeditor5-coremedia-differencing/src/integrations/HtmlSupportImage.ts b/packages/ckeditor5-coremedia-differencing/src/integrations/HtmlSupportImage.ts index 63af52d328..3101ac665d 100644 --- a/packages/ckeditor5-coremedia-differencing/src/integrations/HtmlSupportImage.ts +++ b/packages/ckeditor5-coremedia-differencing/src/integrations/HtmlSupportImage.ts @@ -1,5 +1,6 @@ import { Plugin } from "ckeditor5"; -import { Logger, LoggerProvider } from "@coremedia/ckeditor5-logging"; +import type { Logger } from "@coremedia/ckeditor5-logging"; +import { LoggerProvider } from "@coremedia/ckeditor5-logging"; import { reportInitEnd, reportInitStart } from "@coremedia/ckeditor5-core-common"; import { XDIFF_ATTRIBUTES } from "../Xdiff"; diff --git a/packages/ckeditor5-coremedia-differencing/src/integrations/Image.ts b/packages/ckeditor5-coremedia-differencing/src/integrations/Image.ts index a046ac7514..baa81983be 100644 --- a/packages/ckeditor5-coremedia-differencing/src/integrations/Image.ts +++ b/packages/ckeditor5-coremedia-differencing/src/integrations/Image.ts @@ -1,5 +1,7 @@ -import { Plugin, Model } from "ckeditor5"; -import { Logger, LoggerProvider } from "@coremedia/ckeditor5-logging"; +import type { Model } from "ckeditor5"; +import { Plugin } from "ckeditor5"; +import type { Logger } from "@coremedia/ckeditor5-logging"; +import { LoggerProvider } from "@coremedia/ckeditor5-logging"; import { reportInitEnd, reportInitStart } from "@coremedia/ckeditor5-core-common"; import { XDIFF_ATTRIBUTES } from "../Xdiff"; import { PluginIntegrationHook } from "../PluginIntegrationHook"; diff --git a/packages/ckeditor5-coremedia-differencing/src/integrations/XDiffElements.ts b/packages/ckeditor5-coremedia-differencing/src/integrations/XDiffElements.ts index 839ad2fd7a..04ed599bd4 100644 --- a/packages/ckeditor5-coremedia-differencing/src/integrations/XDiffElements.ts +++ b/packages/ckeditor5-coremedia-differencing/src/integrations/XDiffElements.ts @@ -1,4 +1,4 @@ -import { RuleConfig } from "@coremedia/ckeditor5-dom-converter"; +import type { RuleConfig } from "@coremedia/ckeditor5-dom-converter"; import { isElement, copyAttributesFrom } from "@coremedia/ckeditor5-dom-support"; import { namespaces } from "@coremedia/ckeditor5-coremedia-richtext"; diff --git a/packages/ckeditor5-coremedia-differencing/__tests__/integrations/RuleBasedHtmlDomConverters.ts b/packages/ckeditor5-coremedia-differencing/test/integrations/RuleBasedHtmlDomConverters.ts similarity index 93% rename from packages/ckeditor5-coremedia-differencing/__tests__/integrations/RuleBasedHtmlDomConverters.ts rename to packages/ckeditor5-coremedia-differencing/test/integrations/RuleBasedHtmlDomConverters.ts index 7d2895003a..5f29de4eec 100644 --- a/packages/ckeditor5-coremedia-differencing/__tests__/integrations/RuleBasedHtmlDomConverters.ts +++ b/packages/ckeditor5-coremedia-differencing/test/integrations/RuleBasedHtmlDomConverters.ts @@ -1,8 +1,7 @@ +import type { RuleConfig, RuleSection } from "@coremedia/ckeditor5-dom-converter"; import { byPriority, parseRule, - RuleConfig, - RuleSection, RuleBasedConversionListener, HtmlDomConverter, } from "@coremedia/ckeditor5-dom-converter"; diff --git a/packages/ckeditor5-coremedia-differencing/__tests__/integrations/RulesTester.ts b/packages/ckeditor5-coremedia-differencing/test/integrations/RulesTester.ts similarity index 83% rename from packages/ckeditor5-coremedia-differencing/__tests__/integrations/RulesTester.ts rename to packages/ckeditor5-coremedia-differencing/test/integrations/RulesTester.ts index 0252498cbe..082b6e4006 100644 --- a/packages/ckeditor5-coremedia-differencing/__tests__/integrations/RulesTester.ts +++ b/packages/ckeditor5-coremedia-differencing/test/integrations/RulesTester.ts @@ -1,8 +1,10 @@ -import { RuleConfig } from "@coremedia/ckeditor5-dom-converter"; +import "global-jsdom/register"; +import test from "node:test"; +import expect from "expect"; +import type { RuleConfig } from "@coremedia/ckeditor5-dom-converter"; import { RuleBasedHtmlDomConverterFactory } from "./RuleBasedHtmlDomConverters"; -import { isToData, isToView, TestDirection } from "./TestDirection"; - -import "jest-xml-matcher"; +import type { TestDirection } from "./TestDirection"; +import { isToData, isToView } from "./TestDirection"; /** * Class to help writing data driven tests for `RuleConfig` objects. @@ -47,22 +49,22 @@ export class RulesTester { }; if (isToView(direction)) { - it("toView", () => { + void test("toView", () => { const { toViewConverter, xmlElement, htmlElementSerialized } = setUp(); const result = toViewConverter.convert(xmlElement) as HTMLElement; // Unfortunately, does not ignore order of attributes. If we struggle // with this, we may want to search for alternative approaches. - expect(result.outerHTML).toEqualXML(htmlElementSerialized); + expect(result.outerHTML).toEqual(htmlElementSerialized); }); } if (isToData(direction)) { - it("toData", () => { + test("toData", () => { const { toDataConverter, htmlElement, xmlElementSerialized } = setUp(); const result = toDataConverter.convert(htmlElement) as Element; // Unfortunately, does not ignore order of attributes. If we struggle // with this, we may want to search for alternative approaches. - expect(xmlSerializer.serializeToString(result)).toEqualXML(xmlElementSerialized); + expect(xmlSerializer.serializeToString(result)).toEqual(xmlElementSerialized); }); } } diff --git a/packages/ckeditor5-coremedia-differencing/__tests__/integrations/TestDirection.ts b/packages/ckeditor5-coremedia-differencing/test/integrations/TestDirection.ts similarity index 100% rename from packages/ckeditor5-coremedia-differencing/__tests__/integrations/TestDirection.ts rename to packages/ckeditor5-coremedia-differencing/test/integrations/TestDirection.ts diff --git a/packages/ckeditor5-coremedia-differencing/test/integrations/XDiffElements.test.ts b/packages/ckeditor5-coremedia-differencing/test/integrations/XDiffElements.test.ts new file mode 100644 index 0000000000..81063d3911 --- /dev/null +++ b/packages/ckeditor5-coremedia-differencing/test/integrations/XDiffElements.test.ts @@ -0,0 +1,71 @@ +import { describe } from "node:test"; +import { blockquote, richtext } from "@coremedia-internal/ckeditor5-coremedia-example-data"; +import * as aut from "../../src/integrations/XDiffElements"; +import { RulesTester } from "./RulesTester"; +import type { TestDirection } from "./TestDirection"; +import { toData, toView } from "./TestDirection"; + +void describe("XDiffElements", () => { + const ruleConfigurations = [aut.xDiffElements]; + + const cases: { data: string; direction: TestDirection; view: string }[] = [ + { + data: `

              AB

              `, + direction: toView, + view: `

              AB

              `, + }, + { + data: `

              A

              `, + direction: toView, + view: `

              A

              `, + }, + { + data: `

              A

              `, + direction: toView, + view: `

              A

              `, + }, + { + data: `

              A

              `, + direction: toView, + view: `

              A

              `, + }, + { + data: `

              A

              `, + direction: toView, + view: `

              A

              `, + }, + { + data: `

              A

              `, + direction: toData, + view: `

              A

              `, + }, + { + data: `

              A

              B

              `, + direction: toView, + view: `

              A

              B

              `, + }, + { + data: `

              A

              B

              `, + direction: toData, + view: `

              A

              B

              `, + }, + ]; + + void describe("cases", () => { + for (const { data, direction, view } of cases) { + void describe(`Should provide mapping from data ${direction} view: ${data} ${direction} ${view}`, () => { + // Using blockquote as it may contain multiple paragraphs, and we need one central + // element to do the comparison. + const dataString = richtext(blockquote(data)); + const htmlString = `
              ${view}
              `; + const tester = new RulesTester(ruleConfigurations, "blockquote"); + + tester.executeTests({ + dataString, + direction, + htmlString, + }); + }); + } + }); +}); diff --git a/packages/ckeditor5-coremedia-differencing/tsconfig.json b/packages/ckeditor5-coremedia-differencing/tsconfig.json index 40d1484330..bc71e18f70 100644 --- a/packages/ckeditor5-coremedia-differencing/tsconfig.json +++ b/packages/ckeditor5-coremedia-differencing/tsconfig.json @@ -1,10 +1,13 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "outDir": "./dist" + "outDir": "./dist", + "types": [ + "node" + ] }, "include": [ - "./__tests__", + "./test", "./src" ] } diff --git a/packages/ckeditor5-coremedia-differencing/tsconfig.release.json b/packages/ckeditor5-coremedia-differencing/tsconfig.release.json index c4486a896a..6f82b8a4bf 100644 --- a/packages/ckeditor5-coremedia-differencing/tsconfig.release.json +++ b/packages/ckeditor5-coremedia-differencing/tsconfig.release.json @@ -1,6 +1,6 @@ { "extends": "./tsconfig.json", "exclude": [ - "./__tests__" + "./test" ] } diff --git a/packages/ckeditor5-coremedia-example-data/jest.config.cjs b/packages/ckeditor5-coremedia-example-data/jest.config.cjs deleted file mode 100644 index 208f377fa0..0000000000 --- a/packages/ckeditor5-coremedia-example-data/jest.config.cjs +++ /dev/null @@ -1,3 +0,0 @@ -const jestConfig = require("@coremedia-internal/ckeditor5-jest-test-helpers/shared-jest.config.js"); - -module.exports = { ...jestConfig }; diff --git a/packages/ckeditor5-coremedia-example-data/package.json b/packages/ckeditor5-coremedia-example-data/package.json index 383f923a65..fddd6ea563 100644 --- a/packages/ckeditor5-coremedia-example-data/package.json +++ b/packages/ckeditor5-coremedia-example-data/package.json @@ -1,6 +1,7 @@ { "name": "@coremedia-internal/ckeditor5-coremedia-example-data", "version": "1.0.0", + "type": "module", "private": true, "description": "Example Data for Testing", "author": { @@ -26,13 +27,16 @@ ], "license": "Apache-2.0", "devDependencies": { - "@coremedia-internal/ckeditor5-jest-test-helpers": "^1.0.0", - "@jest/globals": "^29.7.0", - "@types/jest": "^29.5.12", + "@coremedia-internal/studio-client.test-runner-helper": "0.2.3-alpha.050b44a", + "@types/node": "^22.0.0", "@types/webpack": "^5.28.5", "copyfiles": "^2.4.1", - "jest": "^29.7.0", + "expect": "^30.2.0", + "global-jsdom": "^27.0.0", + "jsdom": "^27.0.0", "rimraf": "^6.0.1", + "ts-node": "^10.9.2", + "tsx": "^4.20.6", "typescript": "5.4.5" }, "dependencies": { @@ -44,9 +48,8 @@ "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 --passWithNoTests", - "jest:coverage": "jest --collect-coverage --passWithNoTests", - "lint": "eslint \"**/*.{js,cjs,mjs,ts,tsx}\"", + "test": "test-runner-react", + "lint": "eslint \"**/*.{ts,tsx}\"", "npm-check-updates": "npm-check-updates --upgrade" } } diff --git a/packages/ckeditor5-coremedia-example-data/src/Differencing.ts b/packages/ckeditor5-coremedia-example-data/src/Differencing.ts index ba79b5f376..91da513357 100644 --- a/packages/ckeditor5-coremedia-example-data/src/Differencing.ts +++ b/packages/ckeditor5-coremedia-example-data/src/Differencing.ts @@ -1,7 +1,8 @@ /** * Supported attributes in XDIFF Namespace. */ -import { emptyElement, ImageAttributes } from "./RichText"; +import type { ImageAttributes } from "./RichText"; +import { emptyElement } from "./RichText"; export type XDiffAttribute = "class" | "id" | "previous" | "next" | "changetype" | "changes"; diff --git a/packages/ckeditor5-coremedia-example-data/src/InitExamples.ts b/packages/ckeditor5-coremedia-example-data/src/InitExamples.ts index c9229cfcf4..25aac94a87 100644 --- a/packages/ckeditor5-coremedia-example-data/src/InitExamples.ts +++ b/packages/ckeditor5-coremedia-example-data/src/InitExamples.ts @@ -1,4 +1,4 @@ -import { ExampleData } from "./ExampleData"; +import type { ExampleData } from "./ExampleData"; const createLabelFor = (inputElement: HTMLInputElement): HTMLLabelElement => { const { id: inputId } = inputElement; diff --git a/packages/ckeditor5-coremedia-example-data/src/RichTextConvenience.ts b/packages/ckeditor5-coremedia-example-data/src/RichTextConvenience.ts index 87d68c2e36..c5f2fe176a 100644 --- a/packages/ckeditor5-coremedia-example-data/src/RichTextConvenience.ts +++ b/packages/ckeditor5-coremedia-example-data/src/RichTextConvenience.ts @@ -1,4 +1,5 @@ -import { Attributes, Content, p, span, strong } from "./RichTextBase"; +import type { Attributes, Content } from "./RichTextBase"; +import { p, span, strong } from "./RichTextBase"; /** * Convenience to create a `` element in view and model layer. diff --git a/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeCodeData.ts b/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeCodeData.ts index b17bde883b..0764cefb1a 100644 --- a/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeCodeData.ts +++ b/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeCodeData.ts @@ -1,4 +1,4 @@ -import { ExampleData } from "../ExampleData"; +import type { ExampleData } from "../ExampleData"; const text = `\ [h1]\\[code\\]: CKEditor 5 Code Blocks in BBCode[/h1] diff --git a/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeColorData.ts b/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeColorData.ts index d08322cdda..da72f40592 100644 --- a/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeColorData.ts +++ b/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeColorData.ts @@ -1,4 +1,4 @@ -import { ExampleData } from "../ExampleData"; +import type { ExampleData } from "../ExampleData"; const rainbow = ["red", "orange", "yellow", "green", "blue", "indigo", "violet"]; const bbCodeRainbow = Array.from("rainbow") diff --git a/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeData.ts b/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeData.ts index 87b048ec2e..de68ec170c 100644 --- a/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeData.ts +++ b/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeData.ts @@ -1,4 +1,4 @@ -import { ExampleData } from "../ExampleData"; +import type { ExampleData } from "../ExampleData"; import { inlineFormatData } from "./InlineFormatData"; import { welcomeTextData } from "./WelcomeTextData"; import { challengingData } from "./ChallengingData"; diff --git a/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeHeadingData.ts b/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeHeadingData.ts index eba57f7989..54de88a49a 100644 --- a/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeHeadingData.ts +++ b/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeHeadingData.ts @@ -1,4 +1,4 @@ -import { ExampleData } from "../ExampleData"; +import type { ExampleData } from "../ExampleData"; const rainbow = ["red", "orange", "yellow", "green", "blue", "indigo", "violet"]; const bbCodeRainbow = rainbow diff --git a/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeImgData.ts b/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeImgData.ts index 30a9f6c89b..9a73e054ec 100644 --- a/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeImgData.ts +++ b/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeImgData.ts @@ -1,4 +1,4 @@ -import { ExampleData } from "../ExampleData"; +import type { ExampleData } from "../ExampleData"; import { pngBlue10x10, pngGreen10x10, pngRed10x10 } from "../media/Base64Images"; const text = `\ diff --git a/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeListData.ts b/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeListData.ts index 4244670e32..2923c6f175 100644 --- a/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeListData.ts +++ b/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeListData.ts @@ -1,4 +1,4 @@ -import { ExampleData } from "../ExampleData"; +import type { ExampleData } from "../ExampleData"; const introduction = `\ [h1]\\[list\\]: CKEditor 5 Document Lists in BBCode[/h1] diff --git a/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeParagraphData.ts b/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeParagraphData.ts index c22272d55d..209899531f 100644 --- a/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeParagraphData.ts +++ b/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeParagraphData.ts @@ -1,4 +1,4 @@ -import { ExampleData } from "../ExampleData"; +import type { ExampleData } from "../ExampleData"; const text = `\ [h1]Paragraphs in BBCode[/h1] diff --git a/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeQuoteData.ts b/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeQuoteData.ts index db6ce7c327..fc9fe7705d 100644 --- a/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeQuoteData.ts +++ b/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeQuoteData.ts @@ -1,4 +1,4 @@ -import { ExampleData } from "../ExampleData"; +import type { ExampleData } from "../ExampleData"; const introduction = `\ [h1]\\[quote\\]: CKEditor 5 Block Quotes in BBCode[/h1] diff --git a/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeSizeData.ts b/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeSizeData.ts index 58d8a302ed..e6969dde08 100644 --- a/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeSizeData.ts +++ b/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeSizeData.ts @@ -1,4 +1,4 @@ -import { ExampleData } from "../ExampleData"; +import type { ExampleData } from "../ExampleData"; enum FontSize { tiny = 70, diff --git a/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeUrlData.ts b/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeUrlData.ts index d213b78517..3cef3a7390 100644 --- a/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeUrlData.ts +++ b/packages/ckeditor5-coremedia-example-data/src/bbcode/BBCodeUrlData.ts @@ -1,4 +1,4 @@ -import { ExampleData } from "../ExampleData"; +import type { ExampleData } from "../ExampleData"; const text = `\ [h1]\\[url\\]: CKEditor 5 Links in BBCode[/h1] diff --git a/packages/ckeditor5-coremedia-example-data/src/bbcode/ChallengingData.ts b/packages/ckeditor5-coremedia-example-data/src/bbcode/ChallengingData.ts index 4bbade403e..28bdfa0809 100644 --- a/packages/ckeditor5-coremedia-example-data/src/bbcode/ChallengingData.ts +++ b/packages/ckeditor5-coremedia-example-data/src/bbcode/ChallengingData.ts @@ -1,4 +1,4 @@ -import { ExampleData } from "../ExampleData"; +import type { ExampleData } from "../ExampleData"; const introduction = `\ When it comes to detecting, if data need to be updated within an external diff --git a/packages/ckeditor5-coremedia-example-data/src/bbcode/InlineFormatData.ts b/packages/ckeditor5-coremedia-example-data/src/bbcode/InlineFormatData.ts index be3384dfed..53cff80aa6 100644 --- a/packages/ckeditor5-coremedia-example-data/src/bbcode/InlineFormatData.ts +++ b/packages/ckeditor5-coremedia-example-data/src/bbcode/InlineFormatData.ts @@ -1,4 +1,4 @@ -import { ExampleData } from "../ExampleData"; +import type { ExampleData } from "../ExampleData"; const inlineExamples = [ { diff --git a/packages/ckeditor5-coremedia-example-data/src/bbcode/SecurityChallengeData.ts b/packages/ckeditor5-coremedia-example-data/src/bbcode/SecurityChallengeData.ts index ed188271d8..161c9be79e 100644 --- a/packages/ckeditor5-coremedia-example-data/src/bbcode/SecurityChallengeData.ts +++ b/packages/ckeditor5-coremedia-example-data/src/bbcode/SecurityChallengeData.ts @@ -1,4 +1,4 @@ -import { ExampleData } from "../ExampleData"; +import type { ExampleData } from "../ExampleData"; // The following text uses `#` to represent a backslash character in BBCode. // This makes it easier to write the BBCode without struggling with escaping diff --git a/packages/ckeditor5-coremedia-example-data/src/bbcode/SimpleData.ts b/packages/ckeditor5-coremedia-example-data/src/bbcode/SimpleData.ts index 40b8d9e284..d1380f8925 100644 --- a/packages/ckeditor5-coremedia-example-data/src/bbcode/SimpleData.ts +++ b/packages/ckeditor5-coremedia-example-data/src/bbcode/SimpleData.ts @@ -1,4 +1,4 @@ -import { ExampleData } from "../ExampleData"; +import type { ExampleData } from "../ExampleData"; export const simpleData: ExampleData = { Empty: "", diff --git a/packages/ckeditor5-coremedia-example-data/src/bbcode/WelcomeTextData.ts b/packages/ckeditor5-coremedia-example-data/src/bbcode/WelcomeTextData.ts index 7a8512adfc..2f7afee173 100644 --- a/packages/ckeditor5-coremedia-example-data/src/bbcode/WelcomeTextData.ts +++ b/packages/ckeditor5-coremedia-example-data/src/bbcode/WelcomeTextData.ts @@ -1,4 +1,4 @@ -import { ExampleData } from "../ExampleData"; +import type { ExampleData } from "../ExampleData"; export const welcomeText = `\ [h1]CKEditor 5: CoreMedia BBCode Plugin Showcase[/h1] diff --git a/packages/ckeditor5-coremedia-example-data/src/richtext/ChallengingData.ts b/packages/ckeditor5-coremedia-example-data/src/richtext/ChallengingData.ts index 0d53b1e84d..6bb9987c6b 100644 --- a/packages/ckeditor5-coremedia-example-data/src/richtext/ChallengingData.ts +++ b/packages/ckeditor5-coremedia-example-data/src/richtext/ChallengingData.ts @@ -1,4 +1,4 @@ -import { ExampleData } from "../ExampleData"; +import type { ExampleData } from "../ExampleData"; import { h1, p, em, strong, richtext, h2, code } from "../RichText"; const introduction = `${p(`\ diff --git a/packages/ckeditor5-coremedia-example-data/src/richtext/ContentLinkData.ts b/packages/ckeditor5-coremedia-example-data/src/richtext/ContentLinkData.ts index 1a40f1328d..f52dca8e5a 100644 --- a/packages/ckeditor5-coremedia-example-data/src/richtext/ContentLinkData.ts +++ b/packages/ckeditor5-coremedia-example-data/src/richtext/ContentLinkData.ts @@ -1,4 +1,4 @@ -import { ExampleData } from "../ExampleData"; +import type { ExampleData } from "../ExampleData"; import { richTextDocument } from "../RichTextDOM"; import { h1 } from "../RichTextConvenience"; diff --git a/packages/ckeditor5-coremedia-example-data/src/richtext/DifferencingData.ts b/packages/ckeditor5-coremedia-example-data/src/richtext/DifferencingData.ts index 263c1bc697..828150420d 100644 --- a/packages/ckeditor5-coremedia-example-data/src/richtext/DifferencingData.ts +++ b/packages/ckeditor5-coremedia-example-data/src/richtext/DifferencingData.ts @@ -1,4 +1,4 @@ -import { ExampleData } from "../ExampleData"; +import type { ExampleData } from "../ExampleData"; import { Differencing } from "../Differencing"; import { h1, sectionHeading, richtext } from "../RichText"; diff --git a/packages/ckeditor5-coremedia-example-data/src/richtext/EntitiesData.ts b/packages/ckeditor5-coremedia-example-data/src/richtext/EntitiesData.ts index ada046186d..1c3fe81d2f 100644 --- a/packages/ckeditor5-coremedia-example-data/src/richtext/EntitiesData.ts +++ b/packages/ckeditor5-coremedia-example-data/src/richtext/EntitiesData.ts @@ -1,4 +1,4 @@ -import { ExampleData } from "../ExampleData"; +import type { ExampleData } from "../ExampleData"; import { h1, h2, richtext } from "../RichText"; /** diff --git a/packages/ckeditor5-coremedia-example-data/src/richtext/GrsData.ts b/packages/ckeditor5-coremedia-example-data/src/richtext/GrsData.ts index f01fc01f24..ed386b46c0 100644 --- a/packages/ckeditor5-coremedia-example-data/src/richtext/GrsData.ts +++ b/packages/ckeditor5-coremedia-example-data/src/richtext/GrsData.ts @@ -1,12 +1,17 @@ -import { ExampleData } from "../ExampleData"; +import type { ExampleData } from "../ExampleData"; +import type { + CoreAttributes, + DefaultBlock, + DefaultInline, + InternationalizationAttributions, + List, + TableDataAttributes, +} from "../RichText"; import { a, blockquote, br, code, - CoreAttributes, - DefaultBlock, - DefaultInline, em, h1, h2, @@ -15,9 +20,7 @@ import { h5, h6, img, - InternationalizationAttributions, li, - List, ol, p, pre, @@ -27,7 +30,6 @@ import { sub, sup, table, - TableDataAttributes, tbody, td, tr, diff --git a/packages/ckeditor5-coremedia-example-data/src/richtext/ImageData.ts b/packages/ckeditor5-coremedia-example-data/src/richtext/ImageData.ts index 26f725871b..661285e53b 100644 --- a/packages/ckeditor5-coremedia-example-data/src/richtext/ImageData.ts +++ b/packages/ckeditor5-coremedia-example-data/src/richtext/ImageData.ts @@ -1,4 +1,4 @@ -import { ExampleData } from "../ExampleData"; +import type { ExampleData } from "../ExampleData"; import { h1, p, richtext, img, a, h2 } from "../RichText"; const documentReference = "content/100"; diff --git a/packages/ckeditor5-coremedia-example-data/src/richtext/InvalidData.ts b/packages/ckeditor5-coremedia-example-data/src/richtext/InvalidData.ts index 1bfc8c4b92..1164ad82a2 100644 --- a/packages/ckeditor5-coremedia-example-data/src/richtext/InvalidData.ts +++ b/packages/ckeditor5-coremedia-example-data/src/richtext/InvalidData.ts @@ -1,4 +1,4 @@ -import { ExampleData } from "../ExampleData"; +import type { ExampleData } from "../ExampleData"; import { richtext } from "../RichTextBase"; import { h1 } from "../RichTextConvenience"; diff --git a/packages/ckeditor5-coremedia-example-data/src/richtext/LinkTargetData.ts b/packages/ckeditor5-coremedia-example-data/src/richtext/LinkTargetData.ts index ff62b9b28d..095bcbeef7 100644 --- a/packages/ckeditor5-coremedia-example-data/src/richtext/LinkTargetData.ts +++ b/packages/ckeditor5-coremedia-example-data/src/richtext/LinkTargetData.ts @@ -1,7 +1,7 @@ import { lorem } from "../LoremIpsum"; import { em, h1, richtext } from "../RichText"; import { richTextDocument } from "../RichTextDOM"; -import { ExampleData } from "../ExampleData"; +import type { ExampleData } from "../ExampleData"; const SOME_TARGET = "somewhere"; const EVIL_TARGET = `