From 0325bee425e8315b7522b1ea454f6ef24254807f Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 19 Jun 2025 19:38:10 +0300 Subject: [PATCH] feat(ckeditor): fallback to GPL if license key fails --- .../widgets/type_widgets/ckeditor/config.ts | 220 ++++++------------ .../{config.spec.ts => toolbar.spec.ts} | 2 +- .../widgets/type_widgets/ckeditor/toolbar.ts | 149 ++++++++++++ .../src/widgets/type_widgets/editable_text.ts | 113 ++------- apps/client/vite.config.mts | 3 + 5 files changed, 253 insertions(+), 234 deletions(-) rename apps/client/src/widgets/type_widgets/ckeditor/{config.spec.ts => toolbar.spec.ts} (94%) create mode 100644 apps/client/src/widgets/type_widgets/ckeditor/toolbar.ts diff --git a/apps/client/src/widgets/type_widgets/ckeditor/config.ts b/apps/client/src/widgets/type_widgets/ckeditor/config.ts index 809354e22..4d4e1a27c 100644 --- a/apps/client/src/widgets/type_widgets/ckeditor/config.ts +++ b/apps/client/src/widgets/type_widgets/ckeditor/config.ts @@ -4,25 +4,64 @@ import { buildExtraCommands, type EditorConfig } from "@triliumnext/ckeditor5"; import { getHighlightJsNameForMime } from "../../../services/mime_types.js"; import options from "../../../services/options.js"; import { ensureMimeTypesForHighlighting, isSyntaxHighlightEnabled } from "../../../services/syntax_highlight.js"; -import utils from "../../../services/utils.js"; import emojiDefinitionsUrl from "@triliumnext/ckeditor5/emoji_definitions/en.json?url"; import { copyTextWithToast } from "../../../services/clipboard_ext.js"; import getTemplates from "./snippets.js"; import { PREMIUM_PLUGINS } from "../../../../../../packages/ckeditor5/src/plugins.js"; +import { t } from "../../../services/i18n.js"; +import { getMermaidConfig } from "../../../services/mermaid.js"; +import noteAutocompleteService, { type Suggestion } from "../../../services/note_autocomplete.js"; +import mimeTypesService from "../../../services/mime_types.js"; +import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons"; +import { buildToolbarConfig } from "./toolbar.js"; -const OPEN_SOURCE_LICENSE_KEY = "GPL"; +export const OPEN_SOURCE_LICENSE_KEY = "GPL"; -const TEXT_FORMATTING_GROUP = { - label: "Text formatting", - icon: "text" -}; +export interface BuildEditorOptions { + forceGplLicense: boolean; + isClassicEditor: boolean; + contentLanguage: string | null; +} -export async function buildConfig(): Promise { - const licenseKey = getLicenseKey(); +export async function buildConfig(opts: BuildEditorOptions): Promise { + const licenseKey = (opts.forceGplLicense ? OPEN_SOURCE_LICENSE_KEY : getLicenseKey()); const hasPremiumLicense = (licenseKey !== OPEN_SOURCE_LICENSE_KEY); const config: EditorConfig = { licenseKey, + placeholder: t("editable_text.placeholder"), + mention: { + feeds: [ + { + marker: "@", + feed: (queryText: string) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText), + itemRenderer: (item) => { + const itemElement = document.createElement("button"); + + itemElement.innerHTML = `${(item as Suggestion).highlightedNotePathTitle} `; + + return itemElement; + }, + minimumCharacters: 0 + } + ], + }, + codeBlock: { + languages: buildListOfLanguages() + }, + math: { + engine: "katex", + outputType: "span", // or script + lazyLoad: async () => { + (window as any).katex = (await import("../../../services/math.js")).default + }, + forceOutputType: false, // forces output to use outputType + enablePreview: true // Enable preview view + }, + mermaid: { + lazyLoad: async () => (await import("mermaid")).default, // FIXME + config: getMermaidConfig() + }, image: { styles: { options: [ @@ -137,10 +176,22 @@ export async function buildConfig(): Promise { template: { definitions: await getTemplates() }, + htmlSupport: { + allow: JSON.parse(options.get("allowedHtmlTags")) + }, // This value must be kept in sync with the language defined in webpack.config.js. language: "en" }; + // Set up content language. + const { contentLanguage } = opts; + if (contentLanguage) { + config.language = { + ui: (typeof config.language === "string" ? config.language : "en"), + content: contentLanguage + } + } + // Enable premium plugins. if (hasPremiumLicense) { config.extraPlugins = [ @@ -148,149 +199,28 @@ export async function buildConfig(): Promise { ]; } - return config; -} - -export function buildToolbarConfig(isClassicToolbar: boolean) { - if (utils.isMobile()) { - return buildMobileToolbar(); - } else if (isClassicToolbar) { - const multilineToolbar = utils.isDesktop() && options.get("textNoteEditorMultilineToolbar") === "true"; - return buildClassicToolbar(multilineToolbar); - } else { - return buildFloatingToolbar(); - } -} - -export function buildMobileToolbar() { - const classicConfig = buildClassicToolbar(false); - const items: string[] = []; - - for (const item of classicConfig.toolbar.items) { - if (typeof item === "object" && "items" in item) { - for (const subitem of item.items) { - items.push(subitem); - } - } else { - items.push(item); - } - } - return { - ...classicConfig, - toolbar: { - ...classicConfig.toolbar, - items - } + ...config, + ...buildToolbarConfig(opts.isClassicEditor) }; } -export function buildClassicToolbar(multilineToolbar: boolean) { - // For nested toolbars, refer to https://ckeditor.com/docs/ckeditor5/latest/getting-started/setup/toolbar.html#grouping-toolbar-items-in-dropdowns-nested-toolbars. - return { - toolbar: { - items: [ - "heading", - "fontSize", - "|", - "bold", - "italic", - { - ...TEXT_FORMATTING_GROUP, - items: ["underline", "strikethrough", "|", "superscript", "subscript", "|", "kbd"] - }, - "|", - "fontColor", - "fontBackgroundColor", - "removeFormat", - "|", - "bulletedList", - "numberedList", - "todoList", - "|", - "blockQuote", - "admonition", - "insertTable", - "|", - "code", - "codeBlock", - "|", - "footnote", - { - label: "Insert", - icon: "plus", - items: ["imageUpload", "|", "link", "bookmark", "internallink", "includeNote", "|", "specialCharacters", "emoji", "math", "mermaid", "horizontalLine", "pageBreak", "dateTime"] - }, - "|", - "alignment", - "outdent", - "indent", - "|", - "insertTemplate", - "markdownImport", - "cuttonote", - "findAndReplace" - ], - shouldNotGroupWhenFull: multilineToolbar - } - }; -} +function buildListOfLanguages() { + const userLanguages = mimeTypesService + .getMimeTypes() + .filter((mt) => mt.enabled) + .map((mt) => ({ + language: normalizeMimeTypeForCKEditor(mt.mime), + label: mt.title + })); -export function buildFloatingToolbar() { - return { - toolbar: { - items: [ - "fontSize", - "bold", - "italic", - "underline", - { - ...TEXT_FORMATTING_GROUP, - items: [ "strikethrough", "|", "superscript", "subscript", "|", "kbd" ] - }, - "|", - "fontColor", - "fontBackgroundColor", - "|", - "code", - "link", - "bookmark", - "removeFormat", - "internallink", - "cuttonote" - ] + return [ + { + language: mimeTypesService.MIME_TYPE_AUTO, + label: t("editable-text.auto-detect-language") }, - - blockToolbar: [ - "heading", - "|", - "bulletedList", - "numberedList", - "todoList", - "|", - "blockQuote", - "admonition", - "codeBlock", - "insertTable", - "footnote", - { - label: "Insert", - icon: "plus", - items: ["link", "bookmark", "internallink", "includeNote", "|", "math", "mermaid", "horizontalLine", "pageBreak", "dateTime"] - }, - "|", - "alignment", - "outdent", - "indent", - "|", - "insertTemplate", - "imageUpload", - "markdownImport", - "specialCharacters", - "emoji", - "findAndReplace" - ] - }; + ...userLanguages + ]; } function getLicenseKey() { diff --git a/apps/client/src/widgets/type_widgets/ckeditor/config.spec.ts b/apps/client/src/widgets/type_widgets/ckeditor/toolbar.spec.ts similarity index 94% rename from apps/client/src/widgets/type_widgets/ckeditor/config.spec.ts rename to apps/client/src/widgets/type_widgets/ckeditor/toolbar.spec.ts index 3bce30d9b..0f17ce348 100644 --- a/apps/client/src/widgets/type_widgets/ckeditor/config.spec.ts +++ b/apps/client/src/widgets/type_widgets/ckeditor/toolbar.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { buildClassicToolbar, buildFloatingToolbar } from "./config.js"; +import { buildClassicToolbar, buildFloatingToolbar } from "./toolbar.js"; type ToolbarConfig = string | "|" | { items: ToolbarConfig[] }; diff --git a/apps/client/src/widgets/type_widgets/ckeditor/toolbar.ts b/apps/client/src/widgets/type_widgets/ckeditor/toolbar.ts new file mode 100644 index 000000000..ad83baab6 --- /dev/null +++ b/apps/client/src/widgets/type_widgets/ckeditor/toolbar.ts @@ -0,0 +1,149 @@ +import utils from "../../../services/utils.js"; +import options from "../../../services/options.js"; + +const TEXT_FORMATTING_GROUP = { + label: "Text formatting", + icon: "text" +}; + +export function buildToolbarConfig(isClassicToolbar: boolean) { + if (utils.isMobile()) { + return buildMobileToolbar(); + } else if (isClassicToolbar) { + const multilineToolbar = utils.isDesktop() && options.get("textNoteEditorMultilineToolbar") === "true"; + return buildClassicToolbar(multilineToolbar); + } else { + return buildFloatingToolbar(); + } +} + +export function buildMobileToolbar() { + const classicConfig = buildClassicToolbar(false); + const items: string[] = []; + + for (const item of classicConfig.toolbar.items) { + if (typeof item === "object" && "items" in item) { + for (const subitem of item.items) { + items.push(subitem); + } + } else { + items.push(item); + } + } + + return { + ...classicConfig, + toolbar: { + ...classicConfig.toolbar, + items + } + }; +} + +export function buildClassicToolbar(multilineToolbar: boolean) { + // For nested toolbars, refer to https://ckeditor.com/docs/ckeditor5/latest/getting-started/setup/toolbar.html#grouping-toolbar-items-in-dropdowns-nested-toolbars. + return { + toolbar: { + items: [ + "heading", + "fontSize", + "|", + "bold", + "italic", + { + ...TEXT_FORMATTING_GROUP, + items: ["underline", "strikethrough", "|", "superscript", "subscript", "|", "kbd"] + }, + "|", + "fontColor", + "fontBackgroundColor", + "removeFormat", + "|", + "bulletedList", + "numberedList", + "todoList", + "|", + "blockQuote", + "admonition", + "insertTable", + "|", + "code", + "codeBlock", + "|", + "footnote", + { + label: "Insert", + icon: "plus", + items: ["imageUpload", "|", "link", "bookmark", "internallink", "includeNote", "|", "specialCharacters", "emoji", "math", "mermaid", "horizontalLine", "pageBreak", "dateTime"] + }, + "|", + "alignment", + "outdent", + "indent", + "|", + "insertTemplate", + "markdownImport", + "cuttonote", + "findAndReplace" + ], + shouldNotGroupWhenFull: multilineToolbar + } + }; +} + +export function buildFloatingToolbar() { + return { + toolbar: { + items: [ + "fontSize", + "bold", + "italic", + "underline", + { + ...TEXT_FORMATTING_GROUP, + items: [ "strikethrough", "|", "superscript", "subscript", "|", "kbd" ] + }, + "|", + "fontColor", + "fontBackgroundColor", + "|", + "code", + "link", + "bookmark", + "removeFormat", + "internallink", + "cuttonote" + ] + }, + + blockToolbar: [ + "heading", + "|", + "bulletedList", + "numberedList", + "todoList", + "|", + "blockQuote", + "admonition", + "codeBlock", + "insertTable", + "footnote", + { + label: "Insert", + icon: "plus", + items: ["link", "bookmark", "internallink", "includeNote", "|", "math", "mermaid", "horizontalLine", "pageBreak", "dateTime"] + }, + "|", + "alignment", + "outdent", + "indent", + "|", + "insertTemplate", + "imageUpload", + "markdownImport", + "specialCharacters", + "emoji", + "findAndReplace" + ] + }; +} diff --git a/apps/client/src/widgets/type_widgets/editable_text.ts b/apps/client/src/widgets/type_widgets/editable_text.ts index 0b047d54c..8db9f3164 100644 --- a/apps/client/src/widgets/type_widgets/editable_text.ts +++ b/apps/client/src/widgets/type_widgets/editable_text.ts @@ -1,6 +1,3 @@ -import { t } from "../../services/i18n.js"; -import noteAutocompleteService, { type Suggestion } from "../../services/note_autocomplete.js"; -import mimeTypesService from "../../services/mime_types.js"; import utils, { hasTouchBar } from "../../services/utils.js"; import keyboardActionService from "../../services/keyboard_actions.js"; import froca from "../../services/froca.js"; @@ -12,29 +9,12 @@ import dialogService from "../../services/dialog.js"; import options from "../../services/options.js"; import toast from "../../services/toast.js"; import { buildSelectedBackgroundColor } from "../../components/touch_bar.js"; -import { buildConfig, buildToolbarConfig } from "./ckeditor/config.js"; +import { buildConfig, BuildEditorOptions, OPEN_SOURCE_LICENSE_KEY } from "./ckeditor/config.js"; import type FNote from "../../entities/fnote.js"; -import { getMermaidConfig } from "../../services/mermaid.js"; -import { PopupEditor, ClassicEditor, EditorWatchdog, type CKTextEditor, type MentionFeed, type WatchdogConfig } from "@triliumnext/ckeditor5"; +import { PopupEditor, ClassicEditor, EditorWatchdog, type CKTextEditor, type MentionFeed, type WatchdogConfig, EditorConfig } from "@triliumnext/ckeditor5"; import "@triliumnext/ckeditor5/index.css"; -import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons"; import { updateTemplateCache } from "./ckeditor/snippets.js"; -const mentionSetup: MentionFeed[] = [ - { - marker: "@", - feed: (queryText: string) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText), - itemRenderer: (item) => { - const itemElement = document.createElement("button"); - - itemElement.innerHTML = `${(item as Suggestion).highlightedNotePathTitle} `; - - return itemElement; - }, - minimumCharacters: 0 - } -]; - const TPL = /*html*/`