mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-07-27 01:52:28 +08:00
feat(ckeditor): fallback to GPL if license key fails
This commit is contained in:
parent
e280968271
commit
0325bee425
@ -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<EditorConfig> {
|
||||
const licenseKey = getLicenseKey();
|
||||
export async function buildConfig(opts: BuildEditorOptions): Promise<EditorConfig> {
|
||||
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<EditorConfig> {
|
||||
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<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 {
|
||||
...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() {
|
||||
|
@ -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[] };
|
||||
|
149
apps/client/src/widgets/type_widgets/ckeditor/toolbar.ts
Normal file
149
apps/client/src/widgets/type_widgets/ckeditor/toolbar.ts
Normal 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"
|
||||
]
|
||||
};
|
||||
}
|
@ -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*/`
|
||||
<div class="note-detail-editable-text note-detail-printable">
|
||||
<style>
|
||||
@ -97,24 +77,6 @@ const TPL = /*html*/`
|
||||
</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:
|
||||
*
|
||||
@ -147,7 +109,6 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
||||
|
||||
async initEditor() {
|
||||
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,
|
||||
// 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) => {
|
||||
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");
|
||||
if (contentLanguage) {
|
||||
// TODO: Wrong type?
|
||||
//@ts-ignore
|
||||
finalConfig.language = {
|
||||
ui: (typeof finalConfig.language === "string" ? finalConfig.language : "en"),
|
||||
content: contentLanguage
|
||||
}
|
||||
this.contentLanguage = contentLanguage;
|
||||
} else {
|
||||
this.contentLanguage = null;
|
||||
}
|
||||
this.contentLanguage = contentLanguage ?? null;
|
||||
|
||||
//@ts-ignore
|
||||
const editor = await editorClass.create(this.$editor[0], finalConfig);
|
||||
const opts: BuildEditorOptions = {
|
||||
contentLanguage: this.contentLanguage,
|
||||
forceGplLicense: false,
|
||||
isClassicEditor
|
||||
};
|
||||
const editor = await buildEditor(this.$editor[0], isClassicEditor, opts);
|
||||
|
||||
const notificationsPlugin = editor.plugins.get("Notification");
|
||||
notificationsPlugin.on("show:warning", (evt, data) => {
|
||||
@ -295,28 +238,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
||||
}
|
||||
|
||||
async createEditor() {
|
||||
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()
|
||||
}
|
||||
});
|
||||
await this.watchdog.create(this.$editor[0]);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
}
|
||||
|
@ -97,6 +97,9 @@ export default defineConfig(() => ({
|
||||
}
|
||||
}
|
||||
},
|
||||
test: {
|
||||
environment: "happy-dom"
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: [
|
||||
"@triliumnext/highlightjs"
|
||||
|
Loading…
x
Reference in New Issue
Block a user