feat(ckeditor): fallback to GPL if license key fails

This commit is contained in:
Elian Doran 2025-06-19 19:38:10 +03:00
parent e280968271
commit 0325bee425
No known key found for this signature in database
5 changed files with 253 additions and 234 deletions

View File

@ -4,25 +4,64 @@ import { buildExtraCommands, type EditorConfig } from "@triliumnext/ckeditor5";
import { getHighlightJsNameForMime } from "../../../services/mime_types.js"; import { getHighlightJsNameForMime } from "../../../services/mime_types.js";
import options from "../../../services/options.js"; import options from "../../../services/options.js";
import { ensureMimeTypesForHighlighting, isSyntaxHighlightEnabled } from "../../../services/syntax_highlight.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 emojiDefinitionsUrl from "@triliumnext/ckeditor5/emoji_definitions/en.json?url";
import { copyTextWithToast } from "../../../services/clipboard_ext.js"; import { copyTextWithToast } from "../../../services/clipboard_ext.js";
import getTemplates from "./snippets.js"; import getTemplates from "./snippets.js";
import { PREMIUM_PLUGINS } from "../../../../../../packages/ckeditor5/src/plugins.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 = { export interface BuildEditorOptions {
label: "Text formatting", forceGplLicense: boolean;
icon: "text" isClassicEditor: boolean;
}; contentLanguage: string | null;
}
export async function buildConfig(): Promise<EditorConfig> { export async function buildConfig(opts: BuildEditorOptions): Promise<EditorConfig> {
const licenseKey = getLicenseKey(); const licenseKey = (opts.forceGplLicense ? OPEN_SOURCE_LICENSE_KEY : getLicenseKey());
const hasPremiumLicense = (licenseKey !== OPEN_SOURCE_LICENSE_KEY); const hasPremiumLicense = (licenseKey !== OPEN_SOURCE_LICENSE_KEY);
const config: EditorConfig = { const config: EditorConfig = {
licenseKey, 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: { image: {
styles: { styles: {
options: [ options: [
@ -137,10 +176,22 @@ export async function buildConfig(): Promise<EditorConfig> {
template: { template: {
definitions: await getTemplates() 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. // This value must be kept in sync with the language defined in webpack.config.js.
language: "en" 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. // Enable premium plugins.
if (hasPremiumLicense) { if (hasPremiumLicense) {
config.extraPlugins = [ config.extraPlugins = [
@ -148,149 +199,28 @@ export async function buildConfig(): Promise<EditorConfig> {
]; ];
} }
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 { return {
...classicConfig, ...config,
toolbar: { ...buildToolbarConfig(opts.isClassicEditor)
...classicConfig.toolbar,
items
}
}; };
} }
export function buildClassicToolbar(multilineToolbar: boolean) { function buildListOfLanguages() {
// For nested toolbars, refer to https://ckeditor.com/docs/ckeditor5/latest/getting-started/setup/toolbar.html#grouping-toolbar-items-in-dropdowns-nested-toolbars. const userLanguages = mimeTypesService
return { .getMimeTypes()
toolbar: { .filter((mt) => mt.enabled)
items: [ .map((mt) => ({
"heading", language: normalizeMimeTypeForCKEditor(mt.mime),
"fontSize", label: mt.title
"|", }));
"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 [
return { {
toolbar: { language: mimeTypesService.MIME_TYPE_AUTO,
items: [ label: t("editable-text.auto-detect-language")
"fontSize",
"bold",
"italic",
"underline",
{
...TEXT_FORMATTING_GROUP,
items: [ "strikethrough", "|", "superscript", "subscript", "|", "kbd" ]
},
"|",
"fontColor",
"fontBackgroundColor",
"|",
"code",
"link",
"bookmark",
"removeFormat",
"internallink",
"cuttonote"
]
}, },
...userLanguages
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"
]
};
} }
function getLicenseKey() { function getLicenseKey() {

View File

@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { buildClassicToolbar, buildFloatingToolbar } from "./config.js"; import { buildClassicToolbar, buildFloatingToolbar } from "./toolbar.js";
type ToolbarConfig = string | "|" | { items: ToolbarConfig[] }; type ToolbarConfig = string | "|" | { items: ToolbarConfig[] };

View File

@ -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"
]
};
}

View File

@ -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 utils, { hasTouchBar } from "../../services/utils.js";
import keyboardActionService from "../../services/keyboard_actions.js"; import keyboardActionService from "../../services/keyboard_actions.js";
import froca from "../../services/froca.js"; import froca from "../../services/froca.js";
@ -12,29 +9,12 @@ import dialogService from "../../services/dialog.js";
import options from "../../services/options.js"; import options from "../../services/options.js";
import toast from "../../services/toast.js"; import toast from "../../services/toast.js";
import { buildSelectedBackgroundColor } from "../../components/touch_bar.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 type FNote from "../../entities/fnote.js";
import { getMermaidConfig } from "../../services/mermaid.js"; import { PopupEditor, ClassicEditor, EditorWatchdog, type CKTextEditor, type MentionFeed, type WatchdogConfig, EditorConfig } from "@triliumnext/ckeditor5";
import { PopupEditor, ClassicEditor, EditorWatchdog, type CKTextEditor, type MentionFeed, type WatchdogConfig } from "@triliumnext/ckeditor5";
import "@triliumnext/ckeditor5/index.css"; import "@triliumnext/ckeditor5/index.css";
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
import { updateTemplateCache } from "./ckeditor/snippets.js"; 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*/` const TPL = /*html*/`
<div class="note-detail-editable-text note-detail-printable"> <div class="note-detail-editable-text note-detail-printable">
<style> <style>
@ -97,24 +77,6 @@ const TPL = /*html*/`
</div> </div>
`; `;
function buildListOfLanguages() {
const userLanguages = mimeTypesService
.getMimeTypes()
.filter((mt) => mt.enabled)
.map((mt) => ({
language: normalizeMimeTypeForCKEditor(mt.mime),
label: mt.title
}));
return [
{
language: mimeTypesService.MIME_TYPE_AUTO,
label: t("editable-text.auto-detect-language")
},
...userLanguages
];
}
/** /**
* The editor can operate into two distinct modes: * The editor can operate into two distinct modes:
* *
@ -147,7 +109,6 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
async initEditor() { async initEditor() {
const isClassicEditor = utils.isMobile() || options.get("textNoteEditorType") === "ckeditor-classic"; const isClassicEditor = utils.isMobile() || options.get("textNoteEditorType") === "ckeditor-classic";
const editorClass = isClassicEditor ? ClassicEditor : PopupEditor;
// CKEditor since version 12 needs the element to be visible before initialization. At the same time, // CKEditor since version 12 needs the element to be visible before initialization. At the same time,
// we want to avoid flicker - i.e., show editor only once everything is ready. That's why we have separate // we want to avoid flicker - i.e., show editor only once everything is ready. That's why we have separate
@ -192,33 +153,15 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
this.watchdog.setCreator(async (_, editorConfig) => { this.watchdog.setCreator(async (_, editorConfig) => {
logInfo("Creating new CKEditor"); logInfo("Creating new CKEditor");
const finalConfig = {
...editorConfig,
...(await buildConfig()),
...buildToolbarConfig(isClassicEditor),
htmlSupport: {
allow: JSON.parse(options.get("allowedHtmlTags")),
styles: true,
classes: true,
attributes: true
}
};
const contentLanguage = this.note?.getLabelValue("language"); const contentLanguage = this.note?.getLabelValue("language");
if (contentLanguage) { this.contentLanguage = contentLanguage ?? null;
// TODO: Wrong type?
//@ts-ignore
finalConfig.language = {
ui: (typeof finalConfig.language === "string" ? finalConfig.language : "en"),
content: contentLanguage
}
this.contentLanguage = contentLanguage;
} else {
this.contentLanguage = null;
}
//@ts-ignore const opts: BuildEditorOptions = {
const editor = await editorClass.create(this.$editor[0], finalConfig); contentLanguage: this.contentLanguage,
forceGplLicense: false,
isClassicEditor
};
const editor = await buildEditor(this.$editor[0], isClassicEditor, opts);
const notificationsPlugin = editor.plugins.get("Notification"); const notificationsPlugin = editor.plugins.get("Notification");
notificationsPlugin.on("show:warning", (evt, data) => { notificationsPlugin.on("show:warning", (evt, data) => {
@ -295,28 +238,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
} }
async createEditor() { async createEditor() {
await this.watchdog.create(this.$editor[0], { await this.watchdog.create(this.$editor[0]);
placeholder: t("editable_text.placeholder"),
mention: {
feeds: mentionSetup,
},
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()
}
});
} }
async doRefresh(note: FNote) { async doRefresh(note: FNote) {
@ -655,3 +577,18 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
} }
async function buildEditor(element: HTMLElement, isClassicEditor: boolean, opts: BuildEditorOptions) {
const editorClass = isClassicEditor ? ClassicEditor : PopupEditor;
let config = await buildConfig(opts);
let editor = await editorClass.create(element, config);
if (editor.isReadOnly) {
editor.destroy();
opts.forceGplLicense = true;
config = await buildConfig(opts);
editor = await editorClass.create(element, config);
}
return editor;
}

View File

@ -97,6 +97,9 @@ export default defineConfig(() => ({
} }
} }
}, },
test: {
environment: "happy-dom"
},
optimizeDeps: { optimizeDeps: {
exclude: [ exclude: [
"@triliumnext/highlightjs" "@triliumnext/highlightjs"